SOLID vs Pragmatism
When to Bend the Rules
SOLID principles are guidelines, not laws. Dogmatic adherence creates over-engineered, unreadable code. Apply pragmatism.
Decision: SOLID vs YAGNI Tension
| Principle | YAGNI Conflict | Resolution |
|---|---|---|
| SRP | Splitting classes prematurely | Split when class has actual multiple reasons to change, not hypothetical ones |
| OCP | Creating abstractions "just in case" | Wait for second variation before abstracting |
| LSP | Deep inheritance hierarchies | Prefer composition; inheritance only for true "is-a" relationships |
| ISP | Interfaces with one method | Split only when clients actually need different subsets |
| DIP | Abstracting every dependency | Abstract at architectural boundaries, not internal implementation details |
YAGNI Rule: "You Aren't Gonna Need It" -- don't implement something until it's necessary, not "potentially useful."
Pattern: When SOLID Hurts
Violation of KISS (Keep It Simple, Stupid):
// Over-engineered (SOLID taken too far)
interface IStringFormatter {
format(input: string): string;
}
class UpperCaseFormatter implements IStringFormatter {
format(input: string): string {
return input.toUpperCase();
}
}
class FormatterFactory {
create(type: string): IStringFormatter {
if (type === 'uppercase') return new UpperCaseFormatter();
throw new Error('Unknown formatter');
}
}
const formatter = new FormatterFactory().create('uppercase');
console.log(formatter.format('hello')); // "HELLO"
// Simple, pragmatic solution
console.log('hello'.toUpperCase()); // "HELLO"
When complexity outweighs benefit: If you have one formatter, used in one place, SOLID is over-engineering.
Decision Framework: Apply SOLID When...
| Scenario | Apply SOLID? | Why |
|---|---|---|
| Building a library/framework | Yes | Consumers need extensibility |
| Team > 5 developers | Yes | Prevents stepping on each other |
| Domain is complex | Yes | SOLID manages complexity |
| Requirements change frequently | Yes | Flexibility is valuable |
| Small script/prototype | No | Simplicity > flexibility |
| Stable, well-understood domain | No | Over-engineering |
| Solo project, < 1000 lines | No | Overhead not worth it |
| One-off data migration | No | Throw-away code |
Pattern: Pragmatic SOLID
Start simple, refactor toward SOLID:
- Write the simplest code that works
- When you add the second variation, apply OCP
- When class has two change reasons, apply SRP
- When tests are hard, apply DIP
- When clients use different subsets, apply ISP
- When inheritance breaks, check LSP
Example (Python):
# Initial implementation (simple)
class OrderProcessor:
def process(self, order):
# Calculate total
total = sum(item.price for item in order.items)
# Apply discount
if order.customer_type == 'premium':
total *= 0.9
# Save to database
db.execute("INSERT INTO orders ...")
# Send email
send_email(order.customer.email, "Order confirmed")
# After growth (refactored toward SOLID)
class OrderProcessor:
def __init__(self, repository: IOrderRepository, notifier: INotifier):
self.repository = repository
self.notifier = notifier
def process(self, order: Order):
self.repository.save(order)
self.notifier.notify(order.customer)
Don't prematurely abstract: If you only have regular and premium customers, a simple if is fine. Wait until you have 3-4 types before abstracting to a strategy pattern.
Common Mistakes
- Applying SOLID everywhere -- Small scripts don't need DIP. Why: over-engineering kills productivity
- Premature abstraction -- "We might need to swap databases someday." Why: YAGNI violation
- Ignoring team experience -- Junior team struggling with advanced patterns. Why: code becomes unreadable
- Forgetting readability -- 10 classes for simple logic. Why: destroys comprehension
- SOLID as a checklist -- "Did I apply all 5?" Why: principles are context-dependent
- Refactoring stable code -- "Let's make this more SOLID." Why: if it works and doesn't change, leave it alone
The Sunk Cost Fallacy
Bad reason to apply SOLID: "We already started abstracting, so we must finish." Good reason: "This abstraction is paying off; let's continue."
If SOLID refactoring isn't improving testability, flexibility, or readability -- stop.
Trade-offs: SOLID Costs
Benefits: - Testable code - Flexible architecture - Easier to change
Costs: - More classes/files - Indirection (harder to trace) - Upfront design time - Learning curve
Decision: Apply SOLID when benefits > costs.