Software Architecture & Design Principles

SOLID Principles

Five guidelines for object-oriented design — SRP, OCP, LSP, ISP, DIP — aimed at one goal: let the system change without every change rippling outward. They are heuristics to weigh, not laws to obey blindly.
  • 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
The five principles, and the smell each one prevents
PrincipleRuleSmell it prevents
SRPOne reason to changeDivergent Change — unrelated edits keep colliding in one class
OCPExtend without modifyingEvery new case means editing (and re-testing) old, working code
LSPSubtypes honor the base contractCallers riddled with instanceof checks to work around a subtype
ISPSlim, client-specific interfacesImplementers forced to stub methods they have no use for
DIPDepend on abstractionsHigh-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.

SRP: two actors, one class → split by reason to change
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.

Sources
  • Clean ArchitecturePart III — Design Principles (Ch. 7–11)
  • Head First Design Patterns (2nd ed.)Ch. 1 — Intro to Design Patterns (OO principles)