Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use

Definition

The Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use. Essentially, it's better to have many small, specific interfaces than one large, general-purpose interface (often called a "fat" interface).

When interfaces are too large, implementing classes are forced to implement methods they don't need, often leading to empty implementations or throwing exceptions. This violates the principle and can make the system more rigid and harder to maintain.

Why ISP Matters

Bad Example: Violating ISP

Scenario: Multi-function Printer

Imagine an interface IMultiFunctionDevice that includes methods for printing, scanning, and faxing.

// "Fat" Interface
interface IMultiFunctionDevice {
    print(document);
    scan(document);
    fax(document);
}

// A simple printer that only prints
class SimplePrinter implements IMultiFunctionDevice {
    print(document) {
        console.log("Printing document...");
    }

    // Forced to implement scan and fax, even though it can't perform these actions.
    scan(document) {
        throw new Error("SimplePrinter cannot scan."); // Or empty implementation
    }

    fax(document) {
        throw new Error("SimplePrinter cannot fax."); // Or empty implementation
    }
}

// A dedicated scanner
class DedicatedScanner implements IMultiFunctionDevice {
    print(document) {
        throw new Error("DedicatedScanner cannot print.");
    }

    scan(document) {
        console.log("Scanning document...");
    }

    fax(document) {
        throw new Error("DedicatedScanner cannot fax.");
    }
}

In this example, SimplePrinter is forced to implement scan and fax methods it doesn't use. Similarly, DedicatedScanner is forced to implement print and fax. This violates ISP because clients (the implementing classes) are forced to depend on methods they don't need.

Good Example: Applying ISP

Scenario: Segregated Printer Interfaces

We can fix the previous example by segregating the IMultiFunctionDevice interface into smaller, more specific interfaces.

// Segregated Interfaces
interface IPrinter {
    print(document);
}

interface IScanner {
    scan(document);
}

interface IFax {
    fax(document);
}

// Simple printer implements only IPrinter
class SimplePrinter implements IPrinter {
    print(document) {
        console.log("Printing document...");
    }
}

// Photocopier implements IPrinter and IScanner
class Photocopier implements IPrinter, IScanner {
    print(document) {
        console.log("Photocopier printing document...");
    }

    scan(document) {
        console.log("Photocopier scanning document...");
    }
}

// Multi-function device implements all interfaces
class MultiFunctionMachine implements IPrinter, IScanner, IFax {
    print(document) {
        console.log("MultiFunctionMachine printing...");
    }
    scan(document) {
        console.log("MultiFunctionMachine scanning...");
    }
    fax(document) {
        console.log("MultiFunctionMachine faxing...");
    }
}

By breaking down the large interface into smaller ones (IPrinter, IScanner, IFax), each class now only implements the interfaces relevant to its capabilities. SimplePrinter only needs IPrinter. Photocopier needs IPrinter and IScanner. No class is forced to implement methods it doesn't use.

Interactive Demo: Worker Tasks

Select a worker type to see which tasks they can perform based on segregated interfaces (IWork, IEat, IManage).

Select a worker and click the button.