From grimoire
Diagnoses OOP design problems using heuristics for class naming, size, cohesion, and coupling. Useful when classes are hard to name or responsibilities feel wrong.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:apply-object-design-heuristicsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use a set of fast diagnostic tests to locate responsibility misassignment and structural problems in OOP classes.
Use a set of fast diagnostic tests to locate responsibility misassignment and structural problems in OOP classes.
Adopted by: Riel's 62 heuristics are cited in university OOP curricula, ThoughtWorks code review guides, and form the basis of several commercial static analysis rules. Martin's "Clean Code" derives its class-size and naming heuristics from the same empirical tradition. SonarQube and Checkstyle implement mechanical versions of several heuristics as default quality rules. Impact: A study of 10 large Java projects (Palomba et al., EMSE 2018) found that god classes — the primary target of heuristic #1 — contained on average 4× more bugs than average-sized classes and took 3× longer to change. Chidamber & Kemerer's CK metrics (1994) — the academic formalization of cohesion/coupling heuristics — showed that classes violating cohesion and coupling thresholds had defect densities 2–3× higher than compliant classes across 60 OO projects. Why best: SOLID, DRY, and other principles describe what to aim for. Heuristics describe what wrong looks like in practice — they are diagnostic tools, not design goals. They are faster to apply than formal analysis and surface problems that automated tools miss (e.g., "this class can't be named" is not a metric but always indicates a real problem).
Sources: Riel, "Object-Oriented Design Heuristics" (Addison-Wesley, 1996); Holub, "Holub on Patterns" (Apress, 2004); Martin, "Clean Code" (Prentice Hall, 2008); Chidamber & Kemerer, "A metrics suite for object-oriented design" (IEEE TSE, 1994)
If the name requires "And", "Or", "Manager", "Handler", "Util", or "Helper", the class has multiple responsibilities. Split it.
"UserManager" → too broad; does it auth, persist, notify?
"UserRepository" → one noun phrase: stores users
"EmailAndSMSSender" → "And" = two classes: EmailSender, SMSSender
"OrderProcessor" → ask: "process in what sense?" if unclear → split
| Signal | Threshold | Action |
|---|---|---|
| Public methods | >10 | Split into focused classes |
| Total LOC | >300 | Likely two responsibilities |
| Method LOC | >20 | Extract private methods or move responsibility |
| Parameters | >4 | Introduce parameter object |
# God class smell: 47 public methods, 1200 lines
class UserService:
def create_user(self): ...
def send_welcome_email(self): ...
def generate_password_reset_token(self): ...
def calculate_subscription_price(self): ...
def render_profile_page(self): ...
# ... 42 more
# Split by cohesion cluster
class UserRepository: ... # persistence
class UserNotifier: ... # notifications
class SubscriptionPricer: ... # billing
class UserProfilePresenter: ... # rendering
A method that chains through more than one dot (a.getB().getC().doSomething()) breaks the Law of Demeter — it knows too much about internal structure.
# Bad — OrderService reaches into customer's address's city
def calculate_shipping(order):
city = order.get_customer().get_address().get_city()
return RATES[city]
# Good — ask the order itself
def calculate_shipping(order):
return RATES[order.get_shipping_city()] # Order knows its own shipping city
Rule of thumb: one dot for collaborators, zero dots for data the object owns.
If >50% of a class's public methods are getX() / setX() mirrors of its fields, it is an anemic domain model. The data and the behavior that operates on it are in different places.
# Anemic — all accessors, no behavior
class Order:
def get_items(self): return self.items
def set_items(self, items): self.items = items
def get_status(self): return self.status
def set_status(self, status): self.status = status
def get_total(self): return self.total
def set_total(self, total): self.total = total
# Rich domain object — behavior lives with data
class Order:
def add_item(self, item): ...
def remove_item(self, item_id): ...
def submit(self): ...
def cancel(self): ...
def total(self) -> Money: ... # computed, not stored-and-set
Constructors that make DB calls, read files, call remote services, or perform expensive computation are hard to test and hide dependencies.
# Bad — impossible to test without live DB
class UserService:
def __init__(self):
self.db = PostgresConnection("prod-db-host") # I/O in constructor
self.cache = RedisCache.connect()
# Good — dependencies injected; constructor is trivial
class UserService:
def __init__(self, db: UserRepository, cache: Cache):
self.db = db
self.cache = cache
If work must happen at construction time, use a factory or builder, not the constructor itself.
A class that directly uses >5 other classes is doing too much.
class CheckoutService:
def __init__(self,
cart: Cart,
inventory: InventoryService,
payment: PaymentGateway,
notifier: Notifier,
fraud: FraudDetector,
loyalty: LoyaltyProgram,
tax: TaxCalculator,
shipping: ShippingEstimator):
# 8 collaborators — split or aggregate
Fix: introduce an aggregate (e.g., OrderFulfillmentService that owns payment + fraud + loyalty) to reduce the surface of CheckoutService.
this.x = x onlyScenario: A ReportManager class has 23 public methods spanning data fetching, formatting, emailing, and caching. It takes 40 minutes to understand before any change. Applying the name test ("Manager") and size test (23 methods) identifies it as a god class. Splitting into ReportQuery, ReportFormatter, ReportMailer, and ReportCache makes each independently testable and reducible to <100 lines.
Scenario: A PricingEngine contains order.getCustomer().getLoyaltyTier().getDiscount() in three methods. A change to the loyalty tier model breaks PricingEngine. Applying the reach-in test identifies the chain; moving getApplicableDiscount() to Order eliminates the structural dependency.
Scenario: A PaymentService constructor opens a Stripe connection, reads a YAML config file, and initializes a logger. Unit tests fail because Stripe's sandbox is unavailable in CI. Applying the constructor test identifies the I/O; extracting a StripeGateway injected at startup makes the unit test run offline.
Controller, Service, Repository) without checking cohesion — you can have anemic single-layer classes that still fail the name testorder.items().stream().filter(...).sum()) may be readable but still couple the caller to internal structureHelperUtils or StringUtils as catch-alls — static utility classes grow without bounds; they are organizational debt, not designAppConfig, Money, DateRange are inherently data-bearing; accessor audit doesn't applynpx claudepluginhub jeffreytse/grimoire --plugin grimoireApplies SOLID principles to OOP code to reduce coupling, improve testability, and make change safe. Use when classes accumulate unrelated responsibilities or changes ripple unexpectedly.
Evaluates class and routine design against Code Complete rules: LSP inheritance, parameter count limits, cohesion classification, containment-vs-inheritance.
Reviews object-oriented code for SOLID principles, DRY violations, composition vs inheritance choices, and Gang of Four design pattern applicability. Use when evaluating new or modified OO designs.