Loading
Loading
SOLID
The three that describe how types relate: what a subtype owes, how wide an interface should be, and which way dependencies should point.
By the end of this chapter
Concept
Anywhere the supertype is expected, a subtype must work without the caller knowing the difference. A subtype may accept more and promise more; it may never accept less or promise less.
A Rectangle with setWidth and setHeight, and a Square that inherits from it. Setting the width of a square must also set its height, or it is not a square. Now any code that sets both and expects width × height breaks — silently, only for squares.
The lesson is not about geometry. It is that "is a" in English is not "is substitutable for" in code. A square is a rectangle mathematically; a mutable Square is not substitutable for a mutable Rectangle. Mutability is what breaks it — immutable versions substitute perfectly.
| A subtype may not | Because the caller |
|---|---|
| Throw where the supertype did not | Has no reason to catch it |
| Require more of its inputs | Passes what the supertype allowed |
| Return less than promised | Relies on the contract |
| Do nothing where an effect was promised | Depends on the effect |
Concept
No client should be forced to depend on methods it does not use. Split interfaces by how they are consumed, not by what happens to implement them.
A wide Repository interface with save, delete, findById, findAll, search and count forces a read-only cache implementation to provide save and delete, and the honest implementation of those is to throw. A method that exists only to throw is a contract that lies.
// Split along how callers use it, not along storage technology.interface ReadOnly<T> { Optional<T> findById(Id id); }interface Writable<T> { void save(T entity); }interface Searchable<T> { List<T> search(Query query); } // A cache implements what it can honour, and nothing else.final class InMemoryCache<T> implements ReadOnly<T> { ... }Concept
High-level policy should not depend on low-level detail; both should depend on an abstraction. And critically: that abstraction should be defined by and owned by the high-level module.
The second sentence is the one usually dropped, and it is the one that does the work. If your OrderService depends on an interface that lives in the Stripe adapter package and is shaped like Stripe's API, the dependency arrow still points at Stripe. Nothing was inverted — an interface was added.
// in the ordering module — it owns this interface and its vocabularypackage ordering;public interface PaymentGateway { PaymentResult charge(Money amount, CustomerId customer);} // in the infrastructure module — it depends on ordering, not the reversepackage infrastructure;final class StripeGateway implements ordering.PaymentGateway { ... }The payoff is that the ordering module compiles and is testable with no knowledge that Stripe exists, and swapping providers touches one package. The cost is a small amount of indirection and one wiring point at the composition root — which is where the concrete choice now lives, visibly, in one line.
Sign in to keep a note here. It is free, and nobody else can see it.
Sign inWhat this chapter was for: