الدرس 2 من 3
Substitution and Interface Size: LSP and ISP
Two principles about promises: Liskov is about honouring the promise a type makes, and interface segregation is about not forcing implementers to promise what they cannot deliver.
The middle two letters of SOLID are the ones most often reduced to textbook shapes — rectangles, squares and birds that cannot fly. Those examples are memorable and they teach the wrong lesson, because they suggest the problem is about taxonomy. It is not. Both principles are about promises: what a caller is entitled to assume, and what an implementer is forced to provide.
In everyday code the violations rarely look like biology. They look like an implementation that throws where the interface implied a value, one that quietly ignores a parameter, one that is dramatically slower than every sibling, or one that requires callers to check its concrete type first. The symptom is always the same: `instanceof` checks or capability flags appearing in code that was supposed to be polymorphic — that is, code written against the interface and expected to work with any implementation of it.
export interface PayoutProvider {
/** Sends money, optionally on a future date. */
send(request: PayoutRequest): Promise<PayoutReceipt>;
}
class BankTransferProvider implements PayoutProvider {
async send(request: PayoutRequest): Promise<PayoutReceipt> {
return this.api.transfer(request); // honours request.scheduledFor
}
}
class WalletProvider implements PayoutProvider {
async send(request: PayoutRequest): Promise<PayoutReceipt> {
// Violation: strengthens the precondition. Callers holding a
// PayoutProvider have no way to know this restriction exists.
if (request.scheduledFor) {
throw new Error('Wallet payouts are immediate only');
}
return this.api.credit(request);
}
}
// The symptom leaks into the caller, and polymorphism becomes decorative.
if (provider instanceof WalletProvider) {
request = { ...request, scheduledFor: undefined };
}- Fix one: narrow the shared interface to what every provider can honestly do, and expose scheduling as a separate optional capability callers can ask for.
- Fix two: let the wallet provider satisfy the promise by scheduling internally — storing the request and releasing it at the due time — so the caller's assumption stays true.
- Fix three: accept that these are different concepts and stop making them share a type, which is often the right answer when only one of two implementations fits.
- What is not a fix: documenting the exception in a comment. A promise that holds only if you read the docs is not a promise a caller can rely on.
The two principles are linked more tightly than their separate letters suggest. Fat interfaces cause substitution violations: when an interface has eleven methods and a new implementation can honour six, the other five become throwing stubs and the interface now lies. Segregating the interface removes the pressure to lie.
One capability interface versus several
One broad `PaymentService` interface
A single interface with send, schedule, refund, batch, reconcile and cancel. Every provider implements all six.
- One type to learn and one to inject
- Simple registry and simple configuration
- Consumers can move between providers without changing type
- Providers lacking a capability must throw or silently no-op
- Every consumer depends on methods it never calls
- A method added for one consumer breaks every implementer
Choose when: All plausible implementations genuinely support the whole surface — for example several adapters over the same protocol, where the interface simply is the protocol.
Capability interfaces the caller asks for
A small `PayoutSender` that every provider implements, plus optional `SupportsScheduling` and `SupportsRefunds` that only some do. Callers request the capability they need.
- No implementation is forced to lie about what it does
- A capability gap is a compile-time fact, not a runtime exception
- Adding a capability touches only the providers that offer it
- Callers must handle "this provider cannot do that" explicitly
- More types, and a slightly more complex resolution step
- Easy to over-split into interfaces with one method each
Choose when: Implementations differ in what they can honestly support, or consumers use clearly different subsets — the normal case for third-party integrations.
Where a fat interface concentrates change
Every consumer need is transmitted to every implementer through the shared interface, so unrelated parties become coupled.
Three consumers — a checkout flow, an admin refund tool and a nightly reconciliation job — all depend on one large payment interface. Three providers implement that interface. A method added for the reconciliation job forces all three providers to change, even though two of them are only ever used by checkout.
Consumers
Checkout flow
needs send only
Admin refunds
needs refund, cancel
Reconciliation job
needs batch, reconcile
Shared contract
PaymentService (6 methods)
the union of everyone's needs
Implementers
Bank adapter
supports all six
Wallet adapter
throws on three
Test double
stubs all six
A caching implementation of your `UserRepository` interface silently returns data up to sixty seconds stale. The interface says nothing about freshness. Is this a Liskov violation?RevealHide
Yes, in the way that matters. Callers were entitled to assume a read after a write returns the written value, because nothing told them otherwise, and code written against the non-caching implementation is now subtly wrong. The fix is to make the promise explicit rather than argue about it: either the interface states that reads may be up to N seconds stale — so every caller must handle it — or the caching implementation invalidates on write so the original promise still holds. Violations of this kind are dangerous precisely because they type-check, pass unit tests against a fake, and fail only under real timing in production.
Key takeaways
- Liskov is about honouring promises: a subtype may not demand more, deliver less, or fail in new ways.
- The visible symptom of a substitution violation is `instanceof` or capability flags appearing in supposedly polymorphic code.
- Behavioural promises — freshness, ordering, latency, error modes — are part of an interface even when the type system cannot express them.
- Interface segregation removes the pressure that causes substitution violations: no fat interface, no lying stubs.
- Name interfaces for the need they serve, not for the implementation behind them.