Use when designing or reviewing classes and modules in OOP code — especially when tests are hard to write, changes ripple unexpectedly, or classes accumulate unrelated responsibilities.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add jeffreytse/grimoire-core --skill apply-solid-principles --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Apply Solid Principles?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeffreytse-apply-solid-principles)More formats (shields.io, HTML) on the badges page.
---
name: apply-solid-principles
description: Use when designing or reviewing classes and modules in OOP code — especially when tests are hard to write, changes ripple unexpectedly, or classes accumulate unrelated responsibilities.
source: Robert C. Martin, "Agile Software Development, Principles, Patterns, and Practices" (2003); Martin, "Clean Code" (2008); Martin "Design Principles and Design Patterns" (2000)
tags: [solid, oop, design-principles, coupling, testability, developer, defect-reduction, maintainability]
---
# Apply SOLID Principles
Apply the five SOLID design principles to OOP code to reduce coupling, improve testability, and make change safe.
## Why This Is Best Practice
**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
## Steps
### 1. S — Single Responsibility Principle
**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.
```python
# 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): ...
```
---
### 2. O — Open/Closed Principle
**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.
```python
# 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
```
---
### 3. L — Liskov Substitution Principle
**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.
```python
# 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
```
---
### 4. I — Interface Segregation Principle
**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()`.
```python
# 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): ...
```
---
### 5. D — Dependency Inversion Principle
**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.
```python
# 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
```
---
### 6. Apply in code review
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? |
## When NOT to Use
SOLID has a cost: more files, more abstractions, more indirection. Skip it for:
- **Scripts and one-off tools** — 50-line script that runs once; no extension needed
- **Data classes / value objects** — pure data holders with no behavior; SRP doesn't apply
- **Prototypes** — defer until the design stabilizes; premature abstraction is waste
- **Small functions** — SOLID is OOP class design guidance; pure functions have different rules
The test: "Will this code be changed by multiple people for different reasons over 6+ months?" If yes, apply SOLID. If no, skip.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!