The idea: SOLID is about the cost of a change
The five SOLID principles, collected by Robert C. Martin, are rules for arranging classes and the dependencies between them. They don't change what a program does. They change what it costs to change it: how many classes a request touches, how many must be rebuilt and retested, and whether something nobody meant to touch breaks.
So each tab shows the same small program designed two ways: ✗ violates on the left, ✓ follows on the right. A Call runs a request through both; it gives the same answer. A Change sends both the same change request, and the scoreboards under them show the difference.
How to read the page
- A box is a class: its name on top, then its fields and methods. A blue box is an
«interface». An arrowA → Bmeans "the source code of A mentions B": A uses it, creates it with new, or implements / extends it (blue). - A change request flies in from the person who asked for it. The classes a developer must edit turn orange (the edited line gets a ✎), new classes turn teal.
- Then the ripple: whatever depends on a changed class must be recompiled, retested and redeployed (yellow). It spreads backwards along the arrows, one step at a time. An interface that did not change stops it: code that only knows the interface is not affected by a new or edited implementation.
- Every test that exercises a rebuilt class runs again. Its result is computed (the pay formula, the discount table, …), so a regression shows up as a red ✗ with what it got and what it wanted.
S: Single Responsibility
"A class should have one reason to change." Martin's sharper version: a module should be responsible to
one, and only one, actor, the group of people who ask for changes to it. The Employee class
has three: the CFO's accounting (calculatePay), the COO's HR (reportHours) and the CTO's
DBAs (save).
Both pay and the hours report call one private helper, regularHours(). When the CFO asks for overtime
to start after 42 hours, the developer edits that helper. Pay becomes $930, as asked. But the COO's legal hours
report also says 42 regular hours now. Nobody asked for that, and only HoursReportTest catches it.
On the right each actor has its own class with its own copy of regularHours(). The copies look like
duplication, but they are not: they answer to different people and must be free to change apart.
(Demo: overtime change)
Even a harmless change costs more on the left: the CTO's new table edits Employee, so pay and hours
are rebuilt, retested and redeployed too (Demo: new DB table).
O: Open/Closed
"Software entities should be open for extension, but closed for modification" (Bertrand Meyer, 1988). Adding a feature should mean adding code, not editing code that already works and is already tested.
The left side picks the discount with a switch (type) in CheckoutService, and the invoice
label with another switch in InvoicePrinter. Adding VIP customers means editing the
enum and every switch on it. The developer finds one and misses the other: the price is right, the invoice is not.
On the right each type is a DiscountPolicy class that knows its rate and its label. VIP is a
new class, VipPolicy, plus one wiring line in Main, where types are mapped to policies.
Checkout and invoice code is not opened. (Demo: add VIP)
"Closed" is never total: something has to choose which class to create. The trick is to push that choice into one place, a factory or the program's start-up code, instead of every place that needs the behaviour.
L: Liskov Substitution
Barbara Liskov (1987), later with Jeannette Wing: if S is a subtype of T, then
code written for a T must keep working when given an S. A subtype may
accept more and promise more, never less. In contract terms: preconditions no stronger, postconditions no weaker,
and the parent's invariants kept.
Square extends Rectangle looks right ("a square is a rectangle") and compiles. But a mutable
Rectangle promises that setHeight leaves the width alone. A square can't keep that promise
and stay square, so its setters change both sides. stretch() sets width 5 and height 4 and gets an area
of 16, not 20, at run time, in code that never heard of squares. On the right both are immutable Shapes.
Code that needs an area takes a Shape. Code that needs independent sides takes a Rectangle,
and the compiler rejects a square there. (Demo: Square as Rectangle)
"Is-a" in code is about behaviour, not about how things are named in the real world.
I: Interface Segregation
"No client should be forced to depend on methods it does not use." One fat Machine
interface with print, scan and fax ties every job and every machine to all
three methods. When fax() gets a phone-number parameter, SimplePrinter has to be edited
too, though all it can do with a fax is throw. And all six classes are rebuilt. With Printer,
Scanner and Fax interfaces, only the three classes that fax are touched.
(Demo: change fax())
A method whose only body is throw new UnsupportedOperationException() is a sign: the interface promises
something the class can't do. Users find it at run time. With small interfaces the compiler finds it first
(Demo: scan on a printer). This is also a Liskov problem: that printer is not a substitutable
Machine.
D: Dependency Inversion
"High-level modules should not depend on low-level modules; both should depend on abstractions." The high-level
policy (OrderService: what placing an order means) should not name the low-level
detail (MySqlOrderRepo: how rows reach MySQL).
On the left the service does new MySqlOrderRepo(), so its source arrow points down into the details.
On the right the service depends on an OrderRepository interface. The interface sits in the policy band,
because the service owns it and defines it in its own terms. MySqlOrderRepo implements it, so its
arrow points up. That is the inversion: at run time control still flows down (controller → service →
repository → database), but the source dependency points against it (Call: placeOrder).
What it buys: OrderServiceTest injects an in-memory fake and runs with no database
(Demo: unit test). Moving to PostgreSQL is a new class plus one line in Main, the
composition root where concrete classes are chosen; the business logic is not opened
(Demo: move to PostgreSQL). Note that dependency injection (passing the repository in) is
the usual mechanism, but the principle is about which way the arrow points and who owns the interface.
Applied to a whole application, this gives ports and adapters, also called hexagonal architecture: see Hexagonal Architecture.
The five in one table
| Principle | Smell | Fix | What the scoreboard shows |
|---|---|---|---|
| S: single responsibility | one class, several actors; shared helpers between their features | split by actor | overtime: rebuilt 4 vs 2, failed 1 vs 0 |
| O: open/closed | switch / if on a type code, repeated | polymorphism, one class per case | add VIP: 2 classes edited + 1 missed vs 1 wiring line |
| L: Liskov substitution | an override that weakens a promise, or throws | don't inherit; share a smaller interface | run-time failure vs compile error |
| I: interface segregation | fat interface; stub methods that throw | one small interface per client role | change fax(): rebuilt 6 vs 3 |
| D: dependency inversion | new ConcreteDetail() inside business logic | an interface owned by the policy; inject the detail | unit test fails vs passes; PostgreSQL: rebuilt 3 vs 2 |
When not to apply them
Each principle adds a seam: another class, another interface, another indirection to read through. A seam pays off
where change actually happens, and costs where it doesn't. An interface for every class, a strategy for a
switch that will never grow, or a plugin point "in case" makes code harder to follow, not easier.
A practical rule: write the simple version, and add the seam the second time that kind of change comes in, or as
soon as a test needs it.
What the page leaves out
- Real builds are smarter: Java tools recompile only on API (signature) changes, and binary compatibility lets some changes skip dependents. The page uses the simple rule "a change ripples to every dependent", which is what redeploy and retest usually look like.
- Martin's component principles (REP, CCP, CRP, ADP, SDP, SAP), the same ideas at the level of packages and services.
- Functional-style equivalents: passing functions instead of strategy objects, modules instead of classes.
- Mocks, test doubles and DI containers beyond a hand-written fake and a
Main.
See also Java compilation for what "rebuild" means for a class, and Hexagonal Architecture for dependency inversion across a whole application.