2.2.7 · D3Design Principles

Worked examples — SOLID — Interface Segregation Principle

2,881 words13 min readBack to topic

Before we start, we reuse one tool from the parent — the forced-dependency count. Let me re-earn every symbol so a newcomer can read line one.

The bar chart below shows exactly what these two numbers look like. Each implementer is a column. Below the dashed yellow line at we colour the green portion (methods genuinely used, ) and stack the red portion on top (the forced gap ). The total red height across all columns is . The blue column beside each one is the segregated world: only the green used-portion remains, no red at all — that is . Look at how the two single-purpose machines carry tall red blocks while the all-capable AllInOne has none: fat interfaces punish the narrow clients.

Figure — SOLID — Interface Segregation Principle

The scenario matrix

Every problem this topic can throw at you falls into one of these cells. The worked examples below are tagged with the cell they cover. Two boundary cells (Z and Y) are the truly degenerate inputs — no implementers and no methods — and we discuss them right after the table.

Cell Case class Trigger to watch for Example
A Classic fat split one interface, mixed roles, a stub/throw Ex 1
B Multi-role class a class that legitimately needs many roles Ex 2
C Zero forced weight (already good) before you touch it — do nothing Ex 3
D Degenerate / single implementer , is splitting worth it? Ex 4
E Over-splitting trap (exam twist) methods always used together Ex 5
F Real-world word problem narrative, you extract Ex 6
G Limiting behaviour grows, or , or Ex 7
H Interaction with LSP a throw hides an ISP+LSP double fault Ex 8
Z No implementers () interface exists but nothing implements it below
Y Empty interface () a marker interface with no methods below

The diagram below lays out the eight main cells as a grid so you can see the space you're covering rather than read it as a list — colour marks whether each cell ends at already (green) or needs a fix (red).

Figure — SOLID — Interface Segregation Principle

We now walk every remaining cell.


Worked Examples

Example 1 — Cell A: the classic fat split

  1. Identify . (work, eat). implementers. Human uses both: . Robot only works: . Why this step? is defined purely from these three numbers, so we extract them first.
  2. Compute the fat cost. . Why this step? The single forced dependency is exactly the robot's fake eat() — the number pins the smell to one method.
  3. Segregate by role. Split into Workable { work() } and Feedable { eat() }. Human implements both; Robot implements only Workable. Why this step? Grouping by "who uses it together" removes the robot's obligation to eat().
  4. Compute the segregated cost. Now robot's interface has , ; human's roles each match its needs. . Why this step? Confirms the split actually removed the forced weight (didn't just move it).

Verify: , , so ISP removed forced dependency. The removed unit corresponds to the one UnsupportedOperationException we deleted — units match (1 forced method = 1 stub).


Example 2 — Cell B: a class that needs many roles

  1. Extract numbers. . . Uses: AllInOne , OldPrinter , Scanjet . Why this step? Same recipe — the formula only cares about these counts.
  2. Fat cost. . Why this step? Notice AllInOne contributes — a class using everything is never over-served. All forced weight comes from the single-purpose machines. This is the exact scenario drawn in figure s01.
  3. Segregate. Printer{print}, Scanner{scan}, Fax{fax}. AllInOne implements Printer, Scanner, Fax (multiple small interfaces — this is allowed and encouraged). Why this step? A class needing many roles simply composes them; ISP never forbids implementing several interfaces.
  4. Segregated cost. Each class now implements exactly the roles it uses: . Why this step? Verifies composition removed all forced units.

Verify: , ; win . The multi-role class added to the fat cost, confirming that fat interfaces punish the narrow implementers, not the broad one.


Example 3 — Cell C: already good, , do nothing

  1. Numbers. , , every . Why this step? A one-method interface cannot over-serve anyone.
  2. Cost. . Why this step? means there is nothing to fix — ISP is already satisfied.
  3. Decision. Leave it. Splitting a single cohesive method would create an empty win and hurt readability. Why this step? ISP is a diagnosis tool, not a mandate to shred every interface.

Verify: before and after; interfaces after . Sanity check: you cannot make negative, so is optimal — nothing to do.


Example 4 — Cell D: degenerate single implementer ()

  1. Compute now. . Why this step? With one all-using client, there is currently no forced weight.
  2. Ask the future question. ISP is also about anticipated clients. If a read-only ReportViewer will soon need only load, it would be forced into save/migrate/backup. Why this step? measures the present; ISP reasoning also weighs likely new clients — a degenerate present can hide a fat future.
  3. Judgment. If a narrow client is realistically coming, pre-split into Reader{load} and Writer{save;migrate;backup}. If not, leave it. Why this step? ISP balances real cohesion against speculative splitting — don't split for imaginary clients.

Verify: present ; hypothetical read-only client would raise to . The number quantifies exactly the future risk (3 forced methods), guiding the decision.


Example 5 — Cell E: the over-splitting trap (exam twist)

  1. Before. , say clients, both use both methods, so and . Then . Why this step? Cohesive methods used together give already — there was never any forced weight to remove.
  2. After naive split. Each client must now depend on both Reader and Writer anyway; nothing was over-served, so stays but you've added an interface. Why this step? Splitting cohesive methods creates "interface explosion" with no improvement — the classic ISP anti-pattern.
  3. Correct call. Keep Stream as one role, because cohesion, not minimal size, is the goal (see the parent mistake box). Relate to Cohesion and Coupling: high method cohesion ⇒ keep together. Why this step? ISP splits by client role, not by counting methods.

Verify: and — the split changed nothing measurable but added interface. Zero benefit, positive cost ⇒ over-splitting is objectively worse.


Example 6 — Cell F: real-world word problem

  1. Read off . Basic , Plus , Elite . , . Why this step? Translate the story into the three numbers the formula needs.
  2. Fat cost. . Why this step? The 3 tells us Basic is dragged into 3 unused abilities — the biggest sufferer, as the narrowest client always is.
  3. Segregate by tier need. Checkin{checkIn}, Pool{usePool}, Trainer{bookTrainer}, Sauna{useSauna}. Basic implements Checkin; Plus implements Checkin, Pool; Elite implements all four. Why this step? Each tier composes exactly the roles it sells.
  4. Segregated cost. . Why this step? Confirms all 5 forced units removed.

Verify: , , eliminated . Units: 5 forced methods removed across 3 classes; Basic accounted for 3 of them, matching "narrowest client suffers most."


Example 7 — Cell G: limiting behaviour

  1. Substitute. . Why this step? Every term is identical, so the sum is just copies of .
  2. Limit as . grows linearly in — add one method, add forced dependencies. Why this step? Shows why fat interfaces get catastrophically worse in big systems: cost scales with both size and client count.
  3. Opposite limit. If instead every (everyone uses everything), regardless of . Why this step? Confirms the degenerate "cohesive giant" boundary — big is fine iff every client uses all of it.

Verify: For : , matching Example 2's fat count. For : for any . Both boundary formulas check out.


Example 8 — Cell H: ISP meets the Liskov Substitution Principle

  1. ISP diagnosis. Penguin uses of methods; forced dependency (fly), flagged by the throwing stub. Why this step? The stub is the ISP fingerprint — a forced method the client can't honestly provide.
  2. LSP diagnosis. Any code holding a Bird may call fly() expecting flight; a Penguin crashes it. That breaks substitutability — the Penguin does not honour the Bird promise. Why this step? A throwing stub is simultaneously an ISP smell and a Liskov Substitution Principle violation — one bad line, two broken principles.
  3. Redesign. Split into Eater{eat} and Flyer{fly}. Penguin implements Eater; a Sparrow implements Eater, Flyer. Why this step? Removing fly from the penguin's contract fixes both faults at once, giving and restoring substitutability.

Verify: before (one stub); after . Both ISP (forced method gone) and LSP (no lying override) restored — one redesign, two checks pass.


Recall Self-test across the matrix

Which cell has and still might warrant splitting? ::: Cell D — single implementer, now, but a likely narrow future client justifies pre-splitting. In Example 7, what is when every client uses exactly one of methods? ::: . Why does over-splitting (Cell E) not improve ? ::: The methods are cohesive (always used together), so no client was ever over-served — was already . Which class contributes to in every example? ::: The one that uses all methods (). What is for an interface with no implementers () or no methods ()? ::: in both cases — an empty sum, or all-zero terms.


Connections