The design-principles course, taught the useful way: each principle is a diagnosis — a named force that makes code expensive to change — not a compliance rule. The underlying quantity is always the same: how far does a change ripple? Good design keeps ripples local; every principle here is a tactic for that.
Cohesion — how much a module's parts belong together; coupling — how much modules depend on each other's internals. High cohesion, low coupling is the whole game; SOLID is five refinements of it. The operational tests: cohesion — "can I state this class's job in one sentence without 'and'?"; coupling — "if I change this class's internals, what recompiles, what breaks, who needs to know?"
S — Single Responsibility. A module should have one reason to change — reason meaning a stakeholder or axis of change, not "does one thing." Smell it cures: the class you're afraid to touch because billing, formatting, and persistence all live there and every feature request lands in it. Overdose symptom: confetti — fifty two-line classes whose behavior lives nowhere.
O — Open/Closed. Extendable without modification: new behavior arrives as new code (a new strategy, subclass, or plugin), not edits to a working switch statement. The honest modern reading: apply it where variation is proven (a second case exists or is certain), not speculatively — speculative extension points are their own debt. Sum types + exhaustive matching are the FP counter-pattern that makes the trade-off explicit: closed variants, open operations.
L — Liskov Substitution. Subtypes must honor the base type's contract — no strengthened preconditions, no weakened postconditions, no surprise exceptions. Smell it cures: instanceof checks scattered through client code because some subclass "mostly" works. The classic square-extends-rectangle failure teaches the real lesson: behavioral substitutability, not conceptual is-a, decides inheritance.
I — Interface Segregation. Clients shouldn't depend on methods they don't use. Smell: implementing an interface by throwing UnsupportedOperationException from half of it. Fix: several small role interfaces over one fat one — which also shrinks test doubles to the role actually exercised.
D — Dependency Inversion. High-level policy should depend on abstractions, not concrete details; details depend on the abstractions. This is the principle that makes code testable and infrastructures swappable — realized in practice as constructor-injected interfaces with a composition root (Dependency Injection). Note the distinction: dependency inversion is the principle; injection is one mechanism.
Inheritance couples you to a parent's implementation across time — the fragile base class problem — and spends your single inheritance slot on code reuse. Composition (hold a collaborator, delegate to it) keeps the contract explicit and swappable, combines independent axes of variation without subclass explosion (the Strategy and Decorator patterns are composition formalized), and tests cleanly. Modern language design agrees: Go and Rust omit implementation inheritance entirely. Inheritance retains a legitimate niche — genuine behavioral subtyping over a stable, shallow contract (template-method frameworks); the default, though, is compose.
"Talk only to your immediate collaborators": order.customer().address().city().zone() welds four object shapes into every caller — one schema change ripples everywhere. Fix by putting behavior where the data lives (order.shippingZone()) — Tell, Don't Ask. Honest boundary: chains on data (DTOs, builders, streams) are fine; the law targets reaching through objects with behavior.
The principles generalize past classes: SRP is module design everywhere; dependency inversion is "inject effects" (pass functions, capabilities); open/closed vs sum types is the expression problem — each paradigm opens one axis and closes the other. And every principle serves the same master as refactoring and clean code: the cost-of-change curve. When a principle and simplicity conflict on code that has never changed, simplicity wins — principles pay rent only where change actually happens.