From grimoire
Applies SOLID principles to OOP code to reduce coupling, improve testability, and make change safe. Use when classes accumulate unrelated responsibilities or changes ripple unexpectedly.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:apply-solid-principlesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Apply the five SOLID design principles to OOP code to reduce coupling, improve testability, and make change safe.
Apply the five SOLID design principles to OOP code to reduce coupling, improve testability, and make change safe.
Adopted by: Google (internal code review guidelines), Microsoft (.NET Framework Design Guidelines), Amazon (principal engineer design standards), ThoughtWorks Technology Radar — majority-adoption standard across OOP-heavy engineering organizations. Impact: Dependency Inversion — the D in SOLID — is the primary enabler of unit-testable code; Google's Engineering Practices documentation and ThoughtWorks Technology Radar cite DI as foundational for testable design. Yamashita & Moonen (ICSM 2012) found high coupling and low cohesion — the exact defects SRP and DIP address — are the strongest predictors of maintainability degradation across 74 Java projects. SonarQube ships SOLID violation rules as default quality gates, directly connecting SOLID compliance to automated defect prevention pipelines. Why best: Competing alternatives — anemic domain models (pure data + service layers with no design rules), procedural decomposition, or ad-hoc OOP with no explicit principles — either defer coupling problems to runtime failures or leave them unaddressed entirely. SOLID is the only OOP design framework with a published acronym, universal naming, tooling support (SonarQube, IDE refactoring tools), and a formal specification — making it auditable and enforceable in code review rather than relying on individual judgment.
Sources: Martin, "Agile Software Development, Principles, Patterns, and Practices" (Prentice Hall, 2003); Martin, "Clean Code" (Prentice Hall, 2008); Google Engineering Practices; Microsoft .NET Design Guidelines
Rule: A class should have one reason to change — one actor whose needs it serves.
Signal it's violated: Class name contains "And", "Manager", "Handler", or "Util" with >200 lines. Method saveUserAndSendEmail() is one method doing two jobs.
Fix: Split by actor. If the finance team and the HR team both need Employee to change for different reasons, that's two classes.
# Bad — mixes persistence, business logic, and formatting
class Employee:
def calculate_pay(self): ...
def save_to_db(self): ... # persistence actor
def generate_report(self): ... # reporting actor
# Good — one reason to change each
class Employee:
def calculate_pay(self): ...
class EmployeeRepository:
def save(self, employee): ...
class EmployeeReporter:
def generate_report(self, employee): ...
Rule: Open for extension, closed for modification. Add behavior by adding code, not editing existing code.
Signal it's violated: Adding a new feature requires editing an if/elif chain or switch statement inside an existing class. Every new payment type requires editing PaymentProcessor.
Fix: Extract the varying behavior to an abstraction. New variants implement the abstraction; core logic never changes.
# Bad — every new shape requires editing area()
def area(shape):
if shape.type == "circle":
return 3.14 * shape.radius ** 2
elif shape.type == "square":
return shape.side ** 2
# adding triangle means editing this function
# Good — new shapes extend without modifying existing code
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def area(self): return 3.14 * self.radius ** 2
class Square(Shape):
def area(self): return self.side ** 2
class Triangle(Shape): # new shape, zero edits to existing code
def area(self): return 0.5 * self.base * self.height
Rule: Subtypes must be substitutable for their base type without breaking program correctness. Callers must not need to know which subtype they have.
Signal it's violated: isinstance() / typeof checks in calling code. Overridden methods throw NotImplementedException. Square extends Rectangle but breaks set_width() behavior.
Fix: If a subtype must restrict or ignore inherited behavior, it's not a true subtype — redesign the hierarchy or use composition.
# Bad — Square breaks Rectangle's contract
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # silently breaks caller assumptions
# Good — model separately; use a common abstraction only if behavior is truly shared
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def area(self): return self.width * self.height
class Square(Shape):
def area(self): return self.side ** 2
Rule: Clients should not depend on methods they don't use. Split fat interfaces into focused ones.
Signal it's violated: Implementing class has empty, stub, or raise NotImplementedException methods. Interface has >7 methods serving callers with different needs.
Fix: Split by client. Printer only needs print(); Scanner only needs scan(). Don't force Printer to implement scan().
# Bad — all implementors forced to implement all methods
class Machine(ABC):
def print(self): ...
def scan(self): ...
def fax(self): ...
class SimplePrinter(Machine):
def print(self): ...
def scan(self): raise NotImplementedError # forced stub
def fax(self): raise NotImplementedError # forced stub
# Good — implement only what you use
class Printer(ABC):
def print(self): ...
class Scanner(ABC):
def scan(self): ...
class SimplePrinter(Printer):
def print(self): ... # only what it needs
class AllInOne(Printer, Scanner):
def print(self): ...
def scan(self): ...
Rule: High-level modules should not depend on low-level modules. Both should depend on abstractions. Pass dependencies in, don't construct them inside.
Signal it's violated: new ConcreteService() inside a class. import MySQLDatabase in business logic. Tests require a live database or HTTP connection.
Fix: Define an interface in the high-level module; pass the implementation via constructor injection. Tests inject fakes; production injects real implementations.
# Bad — OrderService is hardwired to MySQL; impossible to unit test
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # concrete dependency baked in
def place_order(self, order):
self.db.save(order)
# Good — depend on abstraction; inject the implementation
class OrderRepository(ABC):
@abstractmethod
def save(self, order): ...
class OrderService:
def __init__(self, repo: OrderRepository): # injected
self.repo = repo
def place_order(self, order):
self.repo.save(order)
# Test injects fake; production injects real
class FakeRepository(OrderRepository):
def save(self, order): self.saved = order
service = OrderService(FakeRepository()) # no DB needed
Check each principle as a dedicated pass:
| Principle | Code review question |
|---|---|
| S | Does this class have more than one reason to change? |
| O | Does adding behavior require editing existing logic? |
| L | Can every subtype be used everywhere the base type is used? |
| I | Does any implementor have empty or stub methods? |
| D | Does any class construct its own dependencies? |
SOLID has a cost: more files, more abstractions, more indirection. Skip it for:
The test: "Will this code be changed by multiple people for different reasons over 6+ months?" If yes, apply SOLID. If no, skip.
npx claudepluginhub jeffreytse/grimoire --plugin grimoireSOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) for object-oriented design.
Reviews and refactors object-oriented code for SOLID compliance across PHP, Java, Python, TypeScript, and C++. Detects violations, suggests fixes, and explains trade-offs.
Checks Java code for SOLID principles violations using patterns like multi-duty managers, if/else type chains, concrete dependencies, and empty interface methods. Suggests refactors.