Software Architecture & Design Principles
SOLID Principles
- SRP — a module should have one reason to change: one responsibility, one actor it answers to
- OCP — open for extension, closed for modification: add behavior without editing working code
- LSP — subtypes must be substitutable for their base type without surprising callers
- ISP — many small, client-specific interfaces beat one fat interface nobody fully needs
- DIP — depend on abstractions, not concretions (see Dependency Inversion And Injection)
- Applied dogmatically to code that never changes, SOLID is pure overhead — it earns its keep on code under active change
| Principle | Rule | Smell it prevents |
|---|---|---|
| SRP | One reason to change | Divergent Change — unrelated edits keep colliding in one class |
| OCP | Extend without modifying | Every new case means editing (and re-testing) old, working code |
| LSP | Subtypes honor the base contract | Callers riddled with instanceof checks to work around a subtype |
| ISP | Slim, client-specific interfaces | Implementers forced to stub methods they have no use for |
| DIP | Depend on abstractions | High-level policy wired directly to low-level, volatile detail |
SRP is the most misquoted: it does not mean "a class should do one thing" in some atomic sense, but that it should answer to one actor — one stakeholder or reason to change. A class that both formats a report for Finance and validates it for Compliance has two actors; a change requested by one can break the other's expectations without anyone noticing until it ships.
class Invoice {
double calculateTotal() { /* business rule Finance owns */ }
String toHtml() { /* presentation Marketing owns */ }
void saveToDatabase() { /* persistence Ops owns */ }
}
// Three actors, three reasons to change → three classes:
class Invoice { double calculateTotal() { /* ... */ } }
class InvoiceHtmlView { String render(Invoice invoice) { /* ... */ } }
class InvoiceRepository { void save(Invoice invoice) { /* ... */ } }OCP is achieved through abstraction, not foresight: you don't need to predict every future case, just make sure new cases can be added as new code (a new subclass, a new Strategy) rather than edits to existing, tested code. A switch on a type code that keeps growing new cases is the opposite of OCP — see Composing Methods And Conditionals for the refactoring that replaces it with polymorphism.