SOLID Principles Code Review
Review Python code for adherence to SOLID principles (SRP, OCP, LSP, ISP, DIP). Use when reviewing Python code for maintainability, testability, and design quality.
$ インストール
git clone https://github.com/jack-michaud/faire /tmp/faire && cp -r /tmp/faire/jack-software/skills/code-review/solid-principles-review ~/.claude/skills/faire// tip: Run this command in your terminal to install the skill
name: SOLID Principles Code Review description: Review Python code for adherence to SOLID principles (SRP, OCP, LSP, ISP, DIP). Use when reviewing Python code for maintainability, testability, and design quality.
SOLID Principles Code Review
Overview
This skill evaluates Python code against the five SOLID principles of object-oriented design. SOLID principles guide the creation of maintainable, flexible, and testable code.
Scope: This skill focuses exclusively on SOLID principle violations and design patterns, not general code quality, performance, or formatting issues.
The Five Principles:
- Single Responsibility Principle (SRP): One class, one responsibility
- Open/Closed Principle (OCP): Open for extension, closed for modification
- Liskov Substitution Principle (LSP): Subclasses must be substitutable for parent classes
- Interface Segregation Principle (ISP): Many small interfaces > one large interface
- Dependency Inversion Principle (DIP): Depend on abstractions, not concrete implementations
When to Use
- Reviewing new features or refactoring code
- Evaluating code maintainability and testability
- Identifying design issues that make code hard to change
- Before merging significant architectural changes
- When code is becoming difficult to test or extend
Process
Focus only on SOLID principle violations.
Step 1: Single Responsibility Principle (SRP)
Look for:
- Classes with multiple reasons to change
- Methods that do more than one thing
- Mixed concerns (e.g., business logic + database access)
- Boolean flags that change method behavior
- Long classes (>200 lines often indicates multiple responsibilities)
Red Flags:
# BAD: Multiple responsibilities
class Order:
def calculate_total(self): pass # Calculation
def save_to_database(self): pass # Persistence
def send_email(self): pass # Notification
Good Pattern:
# GOOD: Single responsibilities
class Order:
pass # Just data
class OrderCalculator:
def calculate_total(self, order): pass
class OrderRepository:
def save(self, order): pass
class OrderNotifier:
def send_confirmation(self, order): pass
Step 2: Open/Closed Principle (OCP)
Look for:
- if-elif chains checking types or categories
- Type checking with
isinstance()ortype() - Methods that must be modified to add new functionality
- Hard-coded lists of types
Red Flags:
# BAD: Must modify for new shapes
def calculate_area(shape):
if shape.type == "circle":
return 3.14 * shape.radius ** 2
elif shape.type == "rectangle":
return shape.width * shape.height
# Adding triangle requires modifying this function
Good Pattern:
# GOOD: Extend via inheritance or protocol
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def calculate_area(self): pass
class Circle(Shape):
def calculate_area(self):
return 3.14 * self.radius ** 2
class Rectangle(Shape):
def calculate_area(self):
return self.width * self.height
Step 3: Liskov Substitution Principle (LSP)
Look for:
- Subclasses throwing exceptions that parent doesn't throw
- Empty method implementations or
NotImplementedError - Subclasses requiring more restrictive inputs than parent
- Type checking before calling methods (indicates substitution doesn't work)
- Subclasses that weaken postconditions or strengthen preconditions
Red Flags:
# BAD: Square can't substitute for Rectangle
class Rectangle:
def set_width(self, width):
self._width = width
def set_height(self, height):
self._height = height
class Square(Rectangle):
def set_width(self, width):
self._width = width
self._height = width # Unexpected side effect!
Good Pattern:
# GOOD: Proper abstraction hierarchy
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def get_area(self): pass
class Rectangle(Shape):
def get_area(self):
return self._width * self._height
class Square(Shape): # Not a Rectangle!
def get_area(self):
return self._side ** 2
Step 4: Interface Segregation Principle (ISP)
Look for:
- Large abstract classes with many methods (>5-7 methods)
- Implementations with empty methods or
NotImplementedError - Classes implementing interfaces but only using a subset
- Comments like "not applicable" or "not supported"
Red Flags:
# BAD: Fat interface forces all implementations
from abc import ABC, abstractmethod
class MultiFunctionDevice(ABC):
@abstractmethod
def print_document(self, doc): pass
@abstractmethod
def scan_document(self): pass
@abstractmethod
def fax_document(self, doc): pass
class SimplePrinter(MultiFunctionDevice):
def print_document(self, doc):
print(doc)
def scan_document(self):
raise NotImplementedError # Forced to implement!
def fax_document(self, doc):
raise NotImplementedError # Forced to implement!
Good Pattern:
# GOOD: Small, focused interfaces
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print_document(self, doc): pass
class Scanner(ABC):
@abstractmethod
def scan_document(self): pass
class SimplePrinter(Printer):
def print_document(self, doc):
print(doc)
class MultiFunctionPrinter(Printer, Scanner):
def print_document(self, doc):
print(doc)
def scan_document(self):
return "scanned"
Step 5: Dependency Inversion Principle (DIP)
Look for:
- Direct imports and instantiation of concrete classes in business logic
- Hard-coded dependencies created inside
__init__ - Difficult-to-test code requiring real databases/files/network
- High-level modules depending on low-level implementation details
Red Flags:
# BAD: Direct dependency on concrete implementation
import sqlite3
class UserService:
def __init__(self):
# Hard-coded dependency
self.db = sqlite3.connect('users.db')
def register_user(self, name, email):
# Business logic coupled to SQLite
self.db.execute("INSERT INTO users VALUES (?, ?)", (name, email))
Good Pattern:
# GOOD: Depend on abstraction via injection
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def save(self, user_data): pass
class SQLiteUserRepository(UserRepository):
def __init__(self, db_path):
self.db = sqlite3.connect(db_path)
def save(self, user_data):
self.db.execute("INSERT INTO users VALUES (?, ?)",
(user_data['name'], user_data['email']))
class UserService:
def __init__(self, repository: UserRepository):
self.repository = repository # Injected dependency
def register_user(self, name, email):
self.repository.save({'name': name, 'email': email})
Step 6: Python-Specific Considerations
Prefer:
ProtocoloverABCfor structural subtyping (when runtime enforcement not needed)- Type hints to make dependencies explicit
- Dataclasses for data structures (supports SRP)
- Decorators for cross-cutting concerns (supports OCP)
- Constructor injection for dependency injection (simplest DIP approach)
Example with Protocol:
from typing import Protocol
class Repository(Protocol):
def save(self, data: dict) -> None: ...
def find(self, id: str) -> dict | None: ...
# No inheritance needed - duck typing with type hints
class InMemoryRepository:
def save(self, data: dict) -> None: pass
def find(self, id: str) -> dict | None: pass
Output Format
For each SOLID violation found:
File: [file path] Lines: [line range, e.g., "45-52" or "78"] Principle: [SRP|OCP|LSP|ISP|DIP] Issue: [brief description of the violation] Impact: [why this matters - testability, maintainability, flexibility] Suggestion: [specific refactoring approach or pattern to apply] Example: [code snippet showing the improved design, if helpful]
Decision Rules
When to Flag Issues
Always flag:
NotImplementedErrorin production code (ISP/LSP violation)- Multiple
isinstance()checks for behavior switching (OCP violation) - Can't write unit tests without real database/network (DIP violation)
- Classes over 500 lines (likely SRP violation)
- Subclasses throwing exceptions parent doesn't throw (LSP violation)
Flag if significant:
- Classes with 5+ distinct responsibilities (SRP)
- Abstract classes with 7+ methods (ISP)
- Long parameter lists (might indicate missing abstractions - DIP)
- Type-based conditionals (OCP)
Don't flag:
- Simple scripts (<100 lines total) - SOLID overkill for simple code
- Prototypes or proof-of-concept code
- Business rule conditionals (e.g.,
if total > 100:) - these are logic, not type switching - One-time migration scripts
- Proper use of Python idioms (duck typing, EAFP style)
Balancing Pragmatism
SOLID principles are guidelines, not absolute rules. Consider:
- Code lifecycle: Is this code actively maintained or rarely changed?
- Team context: Will abstractions help or confuse the team?
- Actual pain points: Is the code currently causing problems?
- Project size: Small projects need less abstraction than large systems
Suggest refactoring when:
- Code is hard to test
- Code changes frequently break unrelated features
- Adding new features requires modifying existing code
- Team struggles to understand or modify the code
Defer refactoring when:
- Code works and rarely changes
- Refactoring is purely academic
- Risk of bugs outweighs benefits
- Code is scheduled for deprecation
Anti-patterns
-
❌ Don't: Flag every conditional as an OCP violation
- ✅ Do: Only flag type-based conditionals that require modification for new types
-
❌ Don't: Suggest SOLID refactoring for simple scripts
- ✅ Do: Consider code context and lifecycle before suggesting abstractions
-
❌ Don't: Recommend ABC when Protocol would work better
- ✅ Do: Prefer Protocol for flexibility unless runtime enforcement needed
-
❌ Don't: Focus only on violations without explaining impact
- ✅ Do: Explain why the violation matters (testability, maintainability, etc.)
-
❌ Don't: Suggest over-engineering with excessive abstraction layers
- ✅ Do: Recommend the simplest solution that solves the actual problem
Examples
Example 1: SRP Violation
Context: Order class doing too much
Code:
class Order:
def calculate_total(self):
return sum(item.price for item in self.items)
def save_to_database(self, db):
db.execute("INSERT INTO orders VALUES (?)", (self.total,))
def send_confirmation_email(self, email_service):
email_service.send("Order confirmed", self.customer_email)
Review Output:
---
**File**: order.py
**Lines**: 1-8
**Principle**: SRP
**Issue**: Order class has three responsibilities: calculation, persistence, and notification
**Impact**: Changes to email format require modifying Order class; difficult to test calculation logic without mocking database and email
**Suggestion**: Extract into separate classes: OrderCalculator, OrderRepository, OrderNotificationService
**Example**:
class Order:
pass # Data only
class OrderCalculator:
def calculate_total(self, order):
return sum(item.price for item in order.items)
class OrderRepository:
def save(self, order, db):
db.execute("INSERT INTO orders VALUES (?)", (order.total,))
class OrderNotifier:
def send_confirmation(self, order, email_service):
email_service.send("Order confirmed", order.customer_email)
---
Example 2: DIP Violation
Context: Service depends on concrete implementation
Code:
import sqlite3
class UserService:
def __init__(self):
self.db = sqlite3.connect('users.db')
def create_user(self, name, email):
self.db.execute("INSERT INTO users VALUES (?, ?)", (name, email))
Review Output:
---
**File**: user_service.py
**Lines**: 3-8
**Principle**: DIP
**Issue**: UserService directly depends on SQLite implementation; cannot test without real database
**Impact**: Impossible to write unit tests with mocks; cannot switch to different database without modifying UserService
**Suggestion**: Introduce repository abstraction and inject via constructor
**Example**:
from typing import Protocol
class UserRepository(Protocol):
def save(self, name: str, email: str) -> None: ...
class SQLiteUserRepository:
def __init__(self, db_path: str):
self.db = sqlite3.connect(db_path)
def save(self, name: str, email: str) -> None:
self.db.execute("INSERT INTO users VALUES (?, ?)", (name, email))
class UserService:
def __init__(self, repository: UserRepository):
self.repository = repository
def create_user(self, name: str, email: str) -> None:
self.repository.save(name, email)
# Easy to test with mock
mock_repo = Mock(spec=UserRepository)
service = UserService(mock_repo)
---
Testing This Skill (TDD Validation)
Test cases are located in skills/collaboration/solid-principles-review/examples/:
-
example-1-srp-violation: Order class with multiple responsibilities
- Expected: Flag calculation + persistence + notification in one class
-
example-2-ocp-violation: Type-based conditionals requiring modification
- Expected: Flag if-elif chain checking shape types
-
example-3-dip-violation: Hard-coded database dependency
- Expected: Flag direct SQLite instantiation in business logic
-
example-4-complex-good: Well-designed code following SOLID
- Expected: Find zero violations or only minor improvements
Run tests by launching subagents with:
Apply skills/collaboration/solid-principles-review/SKILL.md to review [test file path]
Repository
