CMD Guide
HomeOO & Low-Level DesignSOLID Principles

Restructuring the code to follow ISP

ISP works by giving each client a separate type to depend on, so that recompilation and the obligation to implement a method propagate only to the classes that actually use that method — a fat Printer interface couples every implementer to every capability, so adding one fax tweak forces BasicPrinter to recompile and re-stub a method it can never honestly run. The previous lesson left us with that fat interface; here we split it along roles and watch the dependency graph collapse.

The starting point: one fat interface, dishonest implementers

The pre-refactor design has one interface with four methods. A copier supports everything; a cheap office printer supports only printing. With a single interface the cheap printer is forced to declare all four — and the only way to satisfy the compiler for capabilities it lacks is to throw:

// BEFORE — the ISP violation
interface Printer {
    void printDocument(String d);
    void scanDocument(String d);
    void faxDocument(String d);
    void stapleDocument(String d);
}

class BasicPrinter implements Printer {
    public void printDocument(String d) { System.out.println("Print: " + d); }
    public void scanDocument(String d)  { throw new UnsupportedOperationException(); }
    public void faxDocument(String d)   { throw new UnsupportedOperationException(); }
    public void stapleDocument(String d){ throw new UnsupportedOperationException(); }
}

Why the naive version is wrong: the throw stubs are not a style nit — they break the Liskov Substitution Principle. Any code holding a Printer reference is told, by the type, that scanDocument is callable; calling it on a BasicPrinter blows up at runtime. The type system promised something the object cannot keep. ISP is the fix that keeps that promise honest.

The refactor: split by role (one interface per client need)

Cut the fat interface into the smallest pieces a real caller would ever depend on. Each becomes a role — a contract a class opts into only when it can truly fulfill it. BasicPrinter implements just Printable; the office multifunction copier implements all four. No stubs, no throws.

// AFTER — four role interfaces
interface Printable { void printDocument(String d); }
interface Scannable { void scanDocument(String d); }
interface Faxable   { void faxDocument(String d); }
interface Stapler   { void stapleDocument(String d); }

class BasicPrinter implements Printable {
    public void printDocument(String d) { System.out.println("Print: " + d); }
}

class OfficeCopier implements Printable, Scannable, Faxable, Stapler {
    public void printDocument(String d)  { System.out.println("Print: "  + d); }
    public void scanDocument(String d)   { System.out.println("Scan: "   + d); }
    public void faxDocument(String d)    { System.out.println("Fax: "    + d); }
    public void stapleDocument(String d) { System.out.println("Staple: " + d); }
}

public class Main {
    // A client that only needs to print depends ONLY on Printable
    static void runPrintJob(Printable p, String doc) { p.printDocument(doc); }

    public static void main(String[] args) {
        Printable basic   = new BasicPrinter();
        OfficeCopier copier = new OfficeCopier();

        runPrintJob(basic,  "Invoice #4471");
        runPrintJob(copier, "Contract.pdf");   // copier IS-A Printable too
        copier.scanDocument("Contract.pdf");
        copier.faxDocument("Contract.pdf");
        // basic.scanDocument(...)  // WON'T COMPILE — and that is the win
    }
}
diagram
diagram

Traced run — what the compiler and the runtime each enforce

Running main with the inputs above:

Call siteStatic type seenResolves toOutput
runPrintJob(basic, "Invoice #4471")PrintableBasicPrinter.printDocumentPrint: Invoice #4471
runPrintJob(copier, "Contract.pdf")PrintableOfficeCopier.printDocumentPrint: Contract.pdf
copier.scanDocument("Contract.pdf")OfficeCopierOfficeCopier.scanDocumentScan: Contract.pdf
copier.faxDocument("Contract.pdf")OfficeCopierOfficeCopier.faxDocumentFax: Contract.pdf
basic.scanDocument(...)Printable— no such method —compile error

The last row is the payoff. In the fat-interface version that line compiled and threw at runtime; after the split it fails at compile time. ISP moves the error from production back to the build.

Pitfalls

When to split — and when NOT to

Decision signals that point to ISP: implementers contain empty bodies or UnsupportedOperationException stubs; a change to one method's signature recompiles classes that never call it; different callers clearly use disjoint subsets of the methods; mocking the interface in a test forces you to stub five methods to exercise one.

Trade-offs vs the obvious alternatives:

ApproachYou gainIt costs
Split into role interfaces (ISP)honest types, compile-time safety, narrow mocks, independent evolutionmore type declarations; a class may list several implements; risk of over-fragmentation
Fat interface + UnsupportedOperationExceptionone type, fewer files, trivial to add an implementerruntime failures, LSP violation, callers can't trust the contract
Composition / capability objects (copier has-a Scanner field rather than is-a)swap capabilities at runtime, avoids deep implements listsextra indirection and delegation boilerplate; capability discovery becomes a runtime query, not a compile-time type

Choose role interfaces when capabilities are stable per-class and you want the compiler to enforce who-can-do-what; prefer composition when a device's capabilities vary at runtime (a printer that gains a stapler add-on module) or when the implements list would grow unboundedly. Keep methods together when every client uses them as a unit — splitting cohesive operations is the opposite mistake.

Concrete call: for the office device above, the capability set is fixed at manufacture, callers like runPrintJob want one narrow contract, and we want basic.scanDocument(...) to fail to compile — so role interfaces win over both alternatives here.

Takeaways


Re-authored and deepened for this guide. Draws on Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (the original ISP statement) and his Clean Architecture; Martin Fowler's "RoleInterface" vs "HeaderInterface" note (martinfowler.com); and the Java Language Specification on interface method resolution and default methods. The original lesson's mechanical split is preserved; added the before/after dependency graph, a compile-vs-runtime trace, the role-vs-header and interface-explosion pitfalls, and the composition trade-off.

Interview drills

Q1. What is the smell that says "apply ISP here"?
Implementers with empty bodies or UnsupportedOperationException stubs; a signature change recompiling classes that never call that method; different callers using disjoint subsets of the interface; a test that must stub five methods to exercise one.

Q2. When do you prefer composition over role interfaces?
When capabilities vary at runtime (a printer that gains a stapler add-on module) or when the implements list would grow unboundedly — attach capability objects (copier has-a Scanner) instead of an is-a type. The cost is delegation boilerplate and capability discovery becoming a runtime query rather than a compile-time type.

Q3. Why aren't Java default methods an ISP fix?
Adding default void faxDocument(...) { throw ...; } to the fat interface silences the compiler but reintroduces the exact LSP lie — the throw is now hidden in the interface instead of the class. The type still promises a capability the object cannot keep.

🤖 Don't fully get this? Learn it with Claude

Stuck on Restructuring the code to follow ISP? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Restructuring the code to follow ISP** (OO & Low-Level Design) and want to truly understand it. Explain Restructuring the code to follow ISP from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Restructuring the code to follow ISP** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Restructuring the code to follow ISP** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Restructuring the code to follow ISP** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes