SOLIDIntermediate 12 min Lesson 4 of 5

Interface Segregation — Small Contracts

No class should be forced to implement methods it does not need. See a fat interface, the empty methods it produces, and the split that fixes it.

SOLID · Lesson 4 of 5
0/5 done(0%)

What is it? #

The Interface Segregation Principle says no class should be forced to depend on methods it does not use.

A large interface forces every implementation to provide everything, even the parts that make no sense for it. The result is empty methods, methods that raise, and callers who cannot tell what is really supported.

The fix is several small interfaces, each describing one capability. A class then implements exactly the ones it can honour.

Small interfaces also make substitution safe, which is why this principle and Liskov Substitution usually get violated together and fixed together.

Think of it like this #

A job description listing forty duties across four departments. Nobody can do all of them, so every hire quietly ignores half the list, and the list stops meaning anything.

Four focused job descriptions let each person commit to what they actually do — and let you tell at a glance who can do what.

Simple example #

A Worker interface used by a job system, covering everything any worker might do. A report-generating worker is forced to implement file upload and email sending, neither of which it does.

Code #

PYTHON
# ---------- BAD: one fat interface ----------
from abc import ABC, abstractmethod


class Job(ABC):
    @abstractmethod
    def run(self): ...
    @abstractmethod
    def upload_result(self, storage): ...
    @abstractmethod
    def send_email(self, mailer): ...
    @abstractmethod
    def schedule(self) -> str: ...
    @abstractmethod
    def rollback(self): ...


class CleanupJob(Job):
    def run(self): print("deleting old files")
    def upload_result(self, storage): pass          # nothing to upload
    def send_email(self, mailer): pass              # nobody to email
    def schedule(self): return "03:00"
    def rollback(self): raise NotImplementedError   # cannot undo a delete
TEXT
What is wrong

- Three of the five methods are meaningless for this job.
- Empty methods hide intent: does an empty body mean "not applicable" or "not finished"?
- rollback() raises, so any caller relying on the interface can crash.
- Adding a sixth method to Job forces edits in every job class ever written.
- Callers cannot tell which jobs actually support which capability.
PYTHON
# ---------- BETTER: small capability interfaces ----------
class Runnable(ABC):
    @abstractmethod
    def run(self): ...


class Schedulable(ABC):
    @abstractmethod
    def schedule(self) -> str: ...


class Reversible(ABC):
    @abstractmethod
    def rollback(self): ...


class Reporting(ABC):
    @abstractmethod
    def report(self) -> str: ...


class CleanupJob(Runnable, Schedulable):
    def run(self): print("deleting old files")
    def schedule(self): return "03:00"


class InvoiceBatchJob(Runnable, Schedulable, Reversible, Reporting):
    def run(self): print("generating invoices")
    def schedule(self): return "23:30"
    def rollback(self): print("marking generated invoices as void")
    def report(self): return "480 invoices generated"


# Callers ask for exactly the capability they need
def run_all(jobs: list[Runnable]):
    for job in jobs:
        job.run()


def run_with_undo(job: Reversible):
    try:
        job.run()          # type checkers want Runnable too; see note below
    except Exception:
        job.rollback()
        raise


run_all([CleanupJob(), InvoiceBatchJob()])

How it works #

Each interface now describes one capability in one or two methods. Runnable means it can be executed. Reversible means it can be undone. Nothing more is implied.

CleanupJob implements two interfaces and has zero empty methods. Every method it defines is meaningful, and a reader can see immediately that cleanup cannot be rolled back — because it does not claim to be Reversible.

InvoiceBatchJob implements four. Combining small interfaces is how a richer capability set is expressed, and it stays explicit.

run_all accepts anything runnable. run_with_undo requires reversibility, so a job that cannot be undone can never be passed to it. The type system, or a reviewer, catches the mistake rather than a runtime crash.

The note on run_with_undo is worth making: a function that needs two capabilities should declare both, either by accepting an intersection type or by defining a small combined interface. Being explicit about what you require is the whole point.

The payoff appears when a new capability is needed. Adding a Retryable interface affects only the jobs that can actually retry. In the fat-interface version, adding a method forces an edit to every job class in the codebase.

Real-world use #

Standard libraries are built this way. Python's collections.abc offers Iterable, Sized, Container and Mapping as separate protocols, so a class can be iterable without pretending to be a full collection.

In application code, the common fat interface is a repository with twenty methods — find, filter, count, bulk insert, soft delete, restore, export — where most callers need one or two. Narrower interfaces per use case make dependencies honest and tests trivial.

File-like objects are another example: code that only reads should ask for something with a read method, not for a full file object, so an in-memory buffer works just as well in tests.

The caution is the usual one: splitting into a dozen one-method interfaces can become noise. Group by capability that is genuinely used together, and let real usage drive the boundaries.

Common mistakes #

  • Writing empty methods to satisfy an interface. That is the smell itself.
  • Adding methods to a shared interface for the benefit of one implementation.
  • Creating one interface per method regardless of how they are used together.
  • Depending on a large interface when your function uses one of its methods.
  • Keeping a capability in the interface "for future use" that nothing implements.

Practice #

Design interfaces for a document system where some documents can be printed, some can be signed, some can be archived, and only a few can do all three. Then write two functions that each require a different capability and confirm that an unsuitable document cannot be passed.

Quick quiz

  1. 1. What does Interface Segregation say?

  2. 2. What is the clearest symptom of a fat interface?

  3. 3. Why do ISP and LSP violations often appear together?

  4. 4. What should a function depend on?

  5. 5. What is the risk of over-applying this principle?

Summary

  • No class should have to implement methods it cannot honour.
  • Empty or raising methods are the symptom of a fat interface.
  • Split by capability, then combine interfaces where needed.
  • Functions should depend on the smallest interface they actually use.
  • Let real usage, not speculation, define the boundaries.