PushBackLog

Interface Segregation Principle (ISP)

Advisory enforcement Complete by PushBackLog team
Topic: quality Topic: architecture Methodology: SOLID Skillset: any Technology: generic Stage: execution Stage: review

Interface Segregation Principle (ISP)

Status: Complete
Category: SOLID Principles
Default enforcement: Advisory
Author: PushBackLog team


Tags

  • Topic: quality, architecture
  • Methodology: SOLID
  • Skillset: any
  • Technology: generic
  • Stage: execution, review

Summary

No client should be forced to depend on interfaces it does not use. Large interfaces should be split into smaller, more specific ones so that implementing classes only need to know about the methods that are relevant to them.


Rationale

Fat interfaces spread coupling

When an interface has 15 methods and a class only needs 3 of them, that class still depends on all 15. Any change to any of those 15 methods forces recompilation (or re-evaluation) of every class that implements or imports the interface, even if the change is irrelevant to that class. Fat interfaces create unnecessary coupling between unrelated clients.

Implementing unused methods leads to LSP violations

Classes forced to implement methods they don’t need must do something with them — usually throw NotImplementedException or return a useless default. Either outcome violates the Liskov Substitution Principle: callers using the interface cannot trust that every method is safe to call.

Role interfaces vs header interfaces

Martin Fowler distinguishes a “header interface” (a mirror of all methods a given class happens to have) from a “role interface” (a narrow interface expressing a specific capability a client needs). ISP is an argument for role interfaces. Readable, Writable, Printable are better than a monolithic Document interface. The client depends on only the role it cares about.


Guidance

Signs an interface is too fat

  • Implementations throw NotImplementedException for some methods
  • Different clients use mutually exclusive subsets of the interface
  • New methods added to the interface break implementations that had nothing to do with the new feature
  • The interface name ends in Manager, Handler, or Service with no further qualification

How to segregate

  1. Identify which methods are actually used by each client
  2. Group methods by the role they express
  3. Extract a focused interface for each role
  4. Where a class still needs to play multiple roles, it can implement multiple interfaces
// Before: fat interface
interface UserRepository {
  findById(id: string): User;
  save(user: User): void;
  delete(id: string): void;
  findAll(): User[];
  search(query: string): User[];
  export(format: string): Buffer;
}

// After: segregated by role
interface UserReader { findById(id: string): User; findAll(): User[]; }
interface UserWriter { save(user: User): void; delete(id: string): void; }
interface UserSearcher { search(query: string): User[]; }
interface UserExporter { export(format: string): Buffer; }

A UserService that only reads can depend solely on UserReader. The caching layer, search service, and export service each depend only on the interface they need.


Examples

Worker interface violation

// Violates ISP — every implementor must deal with every method
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

class Robot implements Worker {
  work() { /* ... */ }
  eat() { throw new Error('Robots do not eat'); }  // Forced fiction
  sleep() { throw new Error('Robots do not sleep'); }
}

Corrected with role interfaces

interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Restable { sleep(): void; }

class Human implements Workable, Feedable, Restable { ... }
class Robot implements Workable { ... } // Only depends on what it can do

Practical test: can you write this implementation without stubs?

If you find yourself writing fake or stub implementations for interface methods just to satisfy the compiler, the interface is too broad. Each method on an interface should be something every implementor can meaningfully provide.


Anti-patterns

1. The god interface

One interface for everything a subsystem can do. Any change to any feature forces recompilation of all clients. Often named IService, IManager, or IHandler.

2. Stub implementations to satisfy fat interfaces

export() { throw new Error('Not supported'); }

If an implementation must throw for methods it “doesn’t use”, the interface is wrong, not the implementation.

3. Adding methods to an existing interface as a shortcut

Adding a new method to a widely-used interface is tempting when you only need one implementation to have that method. The correct move is a new, narrower interface. The shortcut forces every other implementation to handle a method it didn’t need.

4. Confusing ISP with SRP

ISP is about clients and the interfaces they depend on. SRP is about classes and why they change. An interface can be well-segregated from the client’s perspective while the implementing class still has too many responsibilities.



Part of the PushBackLog Best Practices Library. Suggest improvements →