67%

الدرس 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.

قراءة 10 دقيقة

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.

A substitution violation no type checker will catch: the interface promises a scheduled payout, one implementation cannot schedule.typescript
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?Reveal

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.