Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 4 – Design principles: SOLID and beyond

Interface Segregation Principle (ISP)

The Interface Segregation Principle in practice: split a fat interface into role interfaces sized by their callers, in 13 languages, and measure the gain.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Recognise a fat interface by its stubs, its test fakes and the edits that one caller's change spreads
  • Split a fat interface into role interfaces cut from what each kind of caller uses
  • Measure what a fat interface costs in stub methods, fake methods and classes a change touches
  • Explain what isinstance() checks on a runtime-checkable protocol, and what it misses
  • Compose two roles where one caller needs both, and stop splitting where methods change together

Before you start

What you’ll learn · Before you start

What you will learn

  • Recognise a fat interface by its stubs, its test fakes and the edits that one caller's change spreads
  • Split a fat interface into role interfaces cut from what each kind of caller uses
  • Measure what a fat interface costs in stub methods, fake methods and classes a change touches
  • Explain what isinstance() checks on a runtime-checkable protocol, and what it misses
  • Compose two roles where one caller needs both, and stop splitting where methods change together

Before you start

On this page

A housing society’s gate software describes its three devices with one interface of four operations, although each device can carry out only two or three of them: six of the twelve methods the devices define are stubs that fail. The Interface Segregation Principle (ISP) says that no caller should have to depend on methods it does not use, so an interface is sized by what one kind of caller needs, and a class implements as many of those roles as it really plays.

You will watch the stubs of that interface fail in two different ways, split it into roles from the callers’ side, read the role design in 13 languages, measure what the split saves and find where splitting should stop. The lesson on the Liskov Substitution Principle fixed a hierarchy whose parent promised too much; this one looks at the same problem from the callers’ side.

No caller should depend on a method it does not use

Robert C. Martin described the principle in 1996, in a column for The C++ Report, in one sentence:

Clients should not be forced to depend upon interfaces that they do not use.

His list of the principles of class design puts it as “Make fine grained interfaces that are client specific.” The argument is about the direction of change. We usually worry about an interface changing under its callers, but callers push changes into interfaces too: one caller needs a new parameter, a new method or a different return value. When one interface serves several kinds of callers, that push reaches every class that implements it and every caller tied to it, including the callers that never use the method that changed. Step through one such change at the gate, where the visitor desk wants each pass to show how long it is valid:

Interactive · stage by stage One caller's change, spreading through a fat interface Step through one change the visitor desk asks for, first behind GateDevice, then behind role interfaces.
One caller's change, spreading through a fat interface, step 6 of 6. Showing The desk asks a change, GateDevice changes, VisitorKiosk changes, Two stubs change, Every fake changes. In focus: The desk asks a change, VisitorKiosk changes.with rolesThe desk asks a changea validity time on passesGateDevice changesprint_pass gains valid_untilVisitorKiosk changesits working print_passTwo stubs changebarrier and pedestrian gateEvery fake changesfakes of entry and lockdown

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A caller asks. The visitor desk wants every pass to show how long it is valid, so print_pass() needs a new parameter, valid_until.
  2. The one interface changes. print_pass() is declared on GateDevice, the interface that every device and every caller depends on, so the change starts there.
  3. The kiosk changes. VisitorKiosk is the device that prints passes, so its print_pass() takes the validity time and prints it. This is the edit the desk asked for.
  4. Two stubs change. The boom barrier and the pedestrian gate have no printer, but their stub print_pass() methods must still match the interface, so both are edited for a parameter they will never use.
  5. The fakes change. Every test fake of a GateDevice gains the parameter too, including the fakes in the tests of resident entry and of the lockdown, although neither of them prints anything.
  6. The same change, behind roles. Behind role interfaces print_pass() lives only in VisitorDesk, which only the kiosk plays. The change goes from the desk to the kiosk and stops there, with no stub and no other caller's fake to edit.

Behind the one interface, a change that one caller asked for edited the kiosk, two stubs in devices that have no printer, and the test fakes of callers that never print anything. Behind roles, it edited the desk’s role and the kiosk, and stopped there.

In C++ in 1996 the visible cost was recompiling and redeploying code that had not changed. In a 2020 post Martin calls the problem especially acute in statically typed languages, and says dynamic languages are affected much less but are not immune. In Python the cost shows up in three places, and this lesson measures each of them: stub methods that cannot do their job, test fakes that must imitate methods the test never calls, and edits to classes that a change was never meant for.

A fat interface fills the devices with stubs that fail

A housing society’s gate has three devices. A boom barrier lifts for residents’ cars and reads the tag on each windscreen. A pedestrian gate lets people walk in and out. At the guard’s desk, a kiosk prints a pass for each visitor a resident has invited. The first version of the software describes all three with one abstract base class, GateDevice, a fat interface: An interface with more methods than any one of its callers uses, so implementers fill the gaps with stubs and every caller depends on methods it never calls. that lists every operation any of them can perform:

One interface for every device at the gate

fat/main.py

from gate import BoomBarrier, GateDevice, PedestrianGate, VisitorKiosk


def resident_entry(device: GateDevice) -> str:
    tag = device.read_tag()
    if tag is None:
        return "no resident tag in range"
    return f"resident tag {tag}: {device.open()}"


def night_lockdown(devices: list[GateDevice]) -> None:
    for device in devices:
        try:
            print(f"night lockdown: {device.close()}")
        except NotImplementedError as error:
            print(f"night lockdown stopped at {type(device).__name__}: {error}")
            return


car_gate = BoomBarrier(tag_in_range="TAG-1042")
foot_gate = PedestrianGate()
kiosk = VisitorKiosk()
every_device = [car_gate, kiosk, foot_gate]  # each one is a GateDevice, so each one "can" close

print(resident_entry(car_gate))
print(kiosk.print_pass("Meera", "B-204"))
print(resident_entry(foot_gate))
night_lockdown(every_device)
print(f"pedestrian gate at the end: {'open' if foot_gate.is_open else 'closed'}")

Output

resident tag TAG-1042: barrier opened
pass printed for Meera, visiting B-204
no resident tag in range
night lockdown: barrier closed
night lockdown stopped at VisitorKiosk: a kiosk has no gate to close
pedestrian gate at the end: open

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py

fat/gate.py

"""The devices at a housing society's gate, behind one interface that lists everything any of them can do."""

from abc import ABC, abstractmethod


class GateDevice(ABC):
    """Every gate device: a barrier, a tag reader and a visitor desk in one interface."""

    @abstractmethod
    def open(self) -> str: ...

    @abstractmethod
    def close(self) -> str: ...

    @abstractmethod
    def read_tag(self) -> str | None: ...

    @abstractmethod
    def print_pass(self, visitor: str, flat: str) -> str: ...


class BoomBarrier(GateDevice):
    """The car gate: an arm that lifts, with a reader for the tags on residents' windscreens."""

    def __init__(self, tag_in_range=None):
        self.is_open = False
        self._tag = tag_in_range

    def open(self):
        self.is_open = True
        return "barrier opened"

    def close(self):
        self.is_open = False
        return "barrier closed"

    def read_tag(self):
        return self._tag

    def print_pass(self, visitor, flat):
        raise NotImplementedError("a boom barrier has no printer")


class PedestrianGate(GateDevice):
    """The gate people walk through: it only opens and closes."""

    def __init__(self):
        self.is_open = True

    def open(self):
        self.is_open = True
        return "pedestrian gate opened"

    def close(self):
        self.is_open = False
        return "pedestrian gate closed"

    def read_tag(self):
        return None  # it has no reader, so it never sees a tag

    def print_pass(self, visitor, flat):
        raise NotImplementedError("a pedestrian gate has no printer")


class VisitorKiosk(GateDevice):
    """The guard's desk: it prints a pass for each visitor a resident has invited."""

    def open(self):
        raise NotImplementedError("a kiosk has no gate to open")

    def close(self):
        raise NotImplementedError("a kiosk has no gate to close")

    def read_tag(self):
        return None

    def print_pass(self, visitor, flat):
        return f"pass printed for {visitor}, visiting {flat}"

Each device can carry out two or three of the four operations, so it fills the rest with stubs, and two kinds of stub cause two kinds of failure:

  • Stubs that raise. The night lockdown closes every GateDevice in its list, because the interface says every device can close. It closes the barrier, reaches the kiosk, gets NotImplementedError and stops.
  • Stubs that quietly return. The pedestrian gate’s read_tag() returns None, which is also what a real reader returns when no tag is in range. resident_entry() cannot tell “nobody is here” from “this device cannot read tags”, so it reports the first when the truth is the second.

Step through the lockdown that stopped at the kiosk, and watch which device it never reached:

Interactive · step by step The night lockdown, stopped by a stub Step through the lockdown as it asks each GateDevice in its list to close.
The night lockdown, stopped by a stub, step 5 of 5. Showing night_lockdown() (stopped at the kiosk), BoomBarrier (closed), VisitorKiosk (raises NotImplementedError), PedestrianGate (still open). In focus: PedestrianGate.night_lockdown()stopped at the kioskBoomBarrierclosedVisitorKioskraises NotImplementedErrorPedestrianGatestill open

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The list. The lockdown takes every GateDevice in its list, in order, the boom barrier, the kiosk and the pedestrian gate, because the interface says that each of them can close.
  2. The barrier closes. close() on the boom barrier works, and the run prints barrier closed.
  3. The kiosk raises. The kiosk has no gate, so its close() is a stub that raises NotImplementedError.
  4. The loop stops. The lockdown catches the error and stops, so the device after the kiosk in the list is never asked to close.
  5. Open all night. The pedestrian gate is still open at the end of the run, and it would stay open all night. A stub in one device broke a caller that only ever wanted to close gates.

The pedestrian gate is still open at the end of the run, and it would stay open all night. Java’s standard library has a fat interface of the same kind: Collection marks seven of its methods, among them add and clear, as optional operations, which an implementation that does not support them defines to throw UnsupportedOperationException. The lists that List.of() returns throw it from every method that would change them, so code that receives a Collection cannot tell from its type which methods will work.

Making every caller careful

The tempting fix is to make the callers careful: skip any device that raises NotImplementedError, or check hasattr() before each call. Then every caller has to know which device can do what, which is the knowledge the interface was supposed to provide, and a stub that returns None still slips through. Fix the interface instead.

Checkpoint

Check: The direction of change behind the principle · Stubs that raise and stubs that quietly return

  1. Question 1 of 3 The visitor desk asks for a validity time on every pass, so print_pass() of the fat GateDevice gains a parameter. Which classes have to change?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: The kiosk, the two devices whose print_pass() is a stub, and every test fake of a GateDevice

    A change that one caller asks for reaches every implementer of the interface the method lives in: the kiosk, the barrier's and the pedestrian gate's stubs, and the fakes in the tests of resident entry and the lockdown. Behind roles, print_pass() lives in VisitorDesk, and only the kiosk changes.

  2. Question 2 of 3 In the fat design, the lockdown's list is the boom barrier, then the kiosk, then the pedestrian gate. What state does the run leave the pedestrian gate in?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: Open, because the lockdown stopped at the kiosk's stub before it reached the pedestrian gate

    The run printed barrier closed, then night lockdown stopped at VisitorKiosk: a kiosk has no gate to close, and its last line was pedestrian gate at the end: open. A stub in one device broke a caller that only wanted to close gates.

  3. Question 3 of 3 resident_entry() is called with the pedestrian gate, whose read_tag() is a stub that returns None. What does it report, and why is that wrong?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: No resident tag in range, although the truth is that this device cannot read tags at all

    A stub that quietly returns None looks exactly like a real answer. The run printed no resident tag in range for the pedestrian gate, and nothing in the result says that the device has no reader.

Role interfaces are cut from what each caller uses

Start from the callers, not from the devices, and write down what each one uses:

  • resident_entry() reads a tag and opens the barrier: read_tag() and open().
  • The visitor desk prints passes: print_pass().
  • night_lockdown() closes things: close(), which always comes with open().

Each group becomes a role interface: An interface cut from one interaction between a supplier and its callers, holding only the methods those callers use together; one class often plays several roles.. Barrier has open() and close(), TagReader has read_tag(), and VisitorDesk has print_pass(). Resident entry needs two roles at once, so TagGate combines TagReader and Barrier into a third role without adding a method. In Python the roles are typing.Protocol classes, matched by structural typing: Matching a type to an interface by the methods it has, not by a declaration: a class with the right methods plays the role. Python's protocols and Go's and TypeScript's interfaces work this way., which PEP 544 calls static duck typing: a class that has the methods plays the role without inheriting from it. The diagram puts the two designs one above the other:

One GateDevice interface used by three callers and three devices, then role interfaces, each used by one kind of caller.1. One fat interface2. Role interfaces, one per kind of callerresident_entrythe visitor desknight_lockdownGateDevice4 methodsBoomBarrier, PedestrianGate,VisitorKiosk: 6 of their12 methods are stubsResident entryThe visitor deskNight lockdownresident_entryBoomBarrierTagGate= TagReader+ Barrierprinting a passVisitorKioskVisitorDeskprint_passnight_lockdownBoomBarrier,PedestrianGateBarrieropen, closedepend on all 4provide all 4providesprovidesprovide

The gate devices behind one fat interface, and behind role interfaces

Text description of the diagram

The diagram shows two designs of the same gate software, one above the other.

  1. One fat interface: the three callers, resident_entry, the visitor desk and night_lockdown, all depend on GateDevice, which has 4 methods. The three devices, BoomBarrier, PedestrianGate and VisitorKiosk, must each provide all 4, so 6 of their 12 methods are stubs that raise NotImplementedError or return None.
  2. Role interfaces: resident_entry depends on TagGate, which combines TagReader and Barrier; printing a pass depends on VisitorDesk, with print_pass; night_lockdown depends on Barrier, with open and close. BoomBarrier provides TagGate, PedestrianGate provides Barrier and VisitorKiosk provides VisitorDesk. No device has a method it cannot carry out.

The program below is the role design in 13 languages, and every one of them printed the same five lines. C, which has no interfaces, is left out:

One program · 13 languages Role interfaces at a housing society's gate

Python 3.14 · main.py

from typing import Protocol


class Barrier(Protocol):
    def open(self) -> str: ...

    def close(self) -> str: ...


class TagReader(Protocol):
    def read_tag(self) -> str | None: ...  # a resident's tag in range, or None


class TagGate(TagReader, Barrier, Protocol): ...  # two roles, composed


class VisitorDesk(Protocol):
    def print_pass(self, visitor: str, flat: str) -> str: ...


class Gate:
    def __init__(self, name: str, is_open: bool) -> None:
        self.name = name
        self.is_open = is_open

    def open(self) -> str:
        self.is_open = True
        return f"{self.name} opened"

    def close(self) -> str:
        self.is_open = False
        return f"{self.name} closed"


class BoomBarrier(Gate):
    def __init__(self, tag_in_range: str | None) -> None:
        super().__init__("barrier", False)
        self.tag_in_range = tag_in_range

    def read_tag(self) -> str | None:
        return self.tag_in_range


class VisitorKiosk:
    def print_pass(self, visitor: str, flat: str) -> str:
        return f"pass printed for {visitor}, visiting {flat}"


def resident_entry(gate: TagGate) -> str:
    tag = gate.read_tag()
    if tag is None:
        return "no resident tag in range"
    return f"resident tag {tag}: {gate.open()}"


def night_lockdown(barriers: list[Barrier]) -> None:
    for barrier in barriers:
        print(f"night lockdown: {barrier.close()}")


car_gate = BoomBarrier("TAG-1042")
foot_gate = Gate("pedestrian gate", True)
desk: VisitorDesk = VisitorKiosk()
print(resident_entry(car_gate))
print(desk.print_pass("Meera", "B-204"))
night_lockdown([car_gate, foot_gate])  # only barriers: the kiosk has no close()
print(f"pedestrian gate at the end: {'open' if foot_gate.is_open else 'closed'}")

In Python: The roles are typing.Protocol classes, matched by structure. Gate and BoomBarrier never name a role, yet a type checker refuses the kiosk in the lockdown's list, because it has no close().

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

/** @typedef {{ open(): string, close(): string }} Barrier */
/** @typedef {{ readTag(): string | null }} TagReader a resident's tag in range, or null */
/** @typedef {TagReader & Barrier} TagGate two roles, composed */
/** @typedef {{ printPass(visitor: string, flat: string): string }} VisitorDesk */

class Gate {
  constructor(name, isOpen) {
    this.name = name;
    this.isOpen = isOpen;
  }

  open() {
    this.isOpen = true;
    return `${this.name} opened`;
  }

  close() {
    this.isOpen = false;
    return `${this.name} closed`;
  }
}

class BoomBarrier extends Gate {
  #tagInRange;

  constructor(tagInRange) {
    super("barrier", false);
    this.#tagInRange = tagInRange;
  }

  readTag() {
    return this.#tagInRange;
  }
}

class VisitorKiosk {
  printPass(visitor, flat) {
    return `pass printed for ${visitor}, visiting ${flat}`;
  }
}

/** @param {TagGate} gate */
function residentEntry(gate) {
  const tag = gate.readTag();
  return tag === null ? "no resident tag in range" : `resident tag ${tag}: ${gate.open()}`;
}

/** @param {Barrier[]} barriers */
function nightLockdown(barriers) {
  for (const barrier of barriers) {
    console.log(`night lockdown: ${barrier.close()}`);
  }
}

const carGate = new BoomBarrier("TAG-1042");
const footGate = new Gate("pedestrian gate", true);
/** @type {VisitorDesk} */
const desk = new VisitorKiosk();
console.log(residentEntry(carGate));
console.log(desk.printPass("Meera", "B-204"));
nightLockdown([carGate, footGate]); // only barriers: the kiosk has no close()
console.log(`pedestrian gate at the end: ${footGate.isOpen ? "open" : "closed"}`);

In JavaScript: JavaScript has no interfaces, so the roles are JSDoc types that editors and tsc --checkJs read. Nothing checks them while the program runs, so a kiosk in the lockdown's list would fail only at its close() call.

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.List;

public class Main {
  interface Barrier {
    String open();

    String close();
  }

  interface TagReader {
    String readTag(); // a resident's tag in range, or null
  }

  interface TagGate extends TagReader, Barrier {} // two roles, composed

  interface VisitorDesk {
    String printPass(String visitor, String flat);
  }

  static class Gate implements Barrier {
    final String name;
    boolean isOpen;

    Gate(String name, boolean isOpen) {
      this.name = name;
      this.isOpen = isOpen;
    }

    @Override
    public String open() {
      isOpen = true;
      return name + " opened";
    }

    @Override
    public String close() {
      isOpen = false;
      return name + " closed";
    }
  }

  static class BoomBarrier extends Gate implements TagGate {
    private final String tagInRange;

    BoomBarrier(String tagInRange) {
      super("barrier", false);
      this.tagInRange = tagInRange;
    }

    @Override
    public String readTag() {
      return tagInRange;
    }
  }

  static class VisitorKiosk implements VisitorDesk {
    @Override
    public String printPass(String visitor, String flat) {
      return "pass printed for " + visitor + ", visiting " + flat;
    }
  }

  static String residentEntry(TagGate gate) {
    String tag = gate.readTag();
    return tag == null ? "no resident tag in range" : "resident tag " + tag + ": " + gate.open();
  }

  static void nightLockdown(List<Barrier> barriers) {
    barriers.forEach(barrier -> System.out.println("night lockdown: " + barrier.close()));
  }

  public static void main(String[] args) {
    BoomBarrier carGate = new BoomBarrier("TAG-1042");
    Gate footGate = new Gate("pedestrian gate", true);
    System.out.println(residentEntry(carGate));
    System.out.println(new VisitorKiosk().printPass("Meera", "B-204"));
    nightLockdown(List.of(carGate, footGate)); // only barriers: the kiosk has no close()
    System.out.println("pedestrian gate at the end: " + (footGate.isOpen ? "open" : "closed"));
  }
}

In Java: A class plays a role only when it says so. BoomBarrier inherits open and close from Gate and declares implements TagGate, and List<Barrier> refuses a VisitorKiosk when the program is compiled.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <optional>
#include <print>
#include <string>
#include <utility>
#include <vector>

struct Barrier {
  virtual ~Barrier() = default;
  virtual std::string open() = 0;
  virtual std::string close() = 0;
};

struct TagReader {
  virtual ~TagReader() = default;
  // A resident's tag in range, or none.
  virtual std::optional<std::string> readTag() const = 0;
};

struct TagGate : TagReader, virtual Barrier {}; // two roles, composed

struct VisitorDesk {
  virtual ~VisitorDesk() = default;
  virtual std::string printPass(const std::string &visitor,
                                const std::string &flat) const = 0;
};

struct Gate : virtual Barrier {
  Gate(std::string name, bool isOpen) : name(std::move(name)), isOpen(isOpen) {}
  std::string open() override {
    isOpen = true;
    return name + " opened";
  }
  std::string close() override {
    isOpen = false;
    return name + " closed";
  }
  std::string name;
  bool isOpen;
};

class BoomBarrier : public Gate, public TagGate {
public:
  explicit BoomBarrier(std::optional<std::string> tagInRange)
      : Gate("barrier", false), tagInRange_(std::move(tagInRange)) {}
  std::optional<std::string> readTag() const override { return tagInRange_; }

private:
  std::optional<std::string> tagInRange_;
};

struct VisitorKiosk : VisitorDesk {
  std::string printPass(const std::string &visitor,
                        const std::string &flat) const override {
    return "pass printed for " + visitor + ", visiting " + flat;
  }
};

std::string residentEntry(TagGate &gate) {
  const auto tag = gate.readTag();
  return tag ? "resident tag " + *tag + ": " + gate.open()
             : "no resident tag in range";
}

void nightLockdown(const std::vector<Barrier *> &barriers) {
  for (Barrier *barrier : barriers) {
    std::println("night lockdown: {}", barrier->close());
  }
}

int main() {
  BoomBarrier carGate("TAG-1042");
  Gate footGate("pedestrian gate", true);
  std::println("{}", residentEntry(carGate));
  std::println("{}", VisitorKiosk{}.printPass("Meera", "B-204"));
  // only barriers: the kiosk has no close()
  nightLockdown({&carGate, &footGate});
  std::println("pedestrian gate at the end: {}",
               footGate.isOpen ? "open" : "closed");
}

In C++: Barrier is a virtual base, so a BoomBarrier, which is both a Gate and a TagGate, holds one Barrier, whose open and close come from Gate.

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

var carGate = new BoomBarrier("TAG-1042");
var footGate = new Gate("pedestrian gate", true);
IVisitorDesk desk = new VisitorKiosk();
Console.WriteLine(ResidentEntry(carGate));
Console.WriteLine(desk.PrintPass("Meera", "B-204"));
NightLockdown([carGate, footGate]); // only barriers: the kiosk has no Close()
Console.WriteLine($"pedestrian gate at the end: {(footGate.IsOpen ? "open" : "closed")}");

static string ResidentEntry(ITagGate gate)
{
    string? tag = gate.ReadTag();
    return tag is null ? "no resident tag in range" : $"resident tag {tag}: {gate.Open()}";
}

static void NightLockdown(List<IBarrier> barriers)
{
    foreach (IBarrier barrier in barriers)
    {
        Console.WriteLine($"night lockdown: {barrier.Close()}");
    }
}

interface IBarrier
{
    string Open();
    string Close();
}

interface ITagReader
{
    string? ReadTag(); // a resident's tag in range, or null
}

interface ITagGate : ITagReader, IBarrier { } // two roles, composed

interface IVisitorDesk
{
    string PrintPass(string visitor, string flat);
}

class Gate(string name, bool isOpen) : IBarrier
{
    public bool IsOpen { get; private set; } = isOpen;

    public string Open()
    {
        IsOpen = true;
        return $"{name} opened";
    }

    public string Close()
    {
        IsOpen = false;
        return $"{name} closed";
    }
}

class BoomBarrier(string? tagInRange) : Gate("barrier", false), ITagGate
{
    public string? ReadTag() => tagInRange;
}

class VisitorKiosk : IVisitorDesk
{
    public string PrintPass(string visitor, string flat) =>
        $"pass printed for {visitor}, visiting {flat}";
}

In C#: Interface names start with I, as .NET's naming guidelines ask. The primary constructors of Gate and BoomBarrier keep their parameters as the objects' private state.

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

C is left out of this example: C has no interfaces or classes; a struct of function pointers for each role would be another design.

PHP 8.5 · main.php

<?php

declare(strict_types=1);

interface Barrier
{
    public function open(): string;

    public function close(): string;
}

interface TagReader
{
    public function readTag(): ?string; // a resident's tag in range, or null
}

interface TagGate extends TagReader, Barrier {} // two roles, composed

interface VisitorDesk
{
    public function printPass(string $visitor, string $flat): string;
}

class Gate implements Barrier
{
    public function __construct(private string $name, public bool $isOpen) {}

    public function open(): string
    {
        $this->isOpen = true;
        return "{$this->name} opened";
    }

    public function close(): string
    {
        $this->isOpen = false;
        return "{$this->name} closed";
    }
}

final class BoomBarrier extends Gate implements TagGate
{
    public function __construct(private ?string $tagInRange)
    {
        parent::__construct('barrier', false);
    }

    public function readTag(): ?string
    {
        return $this->tagInRange;
    }
}

final class VisitorKiosk implements VisitorDesk
{
    public function printPass(string $visitor, string $flat): string
    {
        return "pass printed for {$visitor}, visiting {$flat}";
    }
}

function residentEntry(TagGate $gate): string
{
    $tag = $gate->readTag();
    return $tag === null ? 'no resident tag in range' : "resident tag {$tag}: {$gate->open()}";
}

function nightLockdown(Barrier ...$barriers): void
{
    foreach ($barriers as $barrier) {
        echo "night lockdown: {$barrier->close()}\n";
    }
}

$carGate = new BoomBarrier('TAG-1042');
$footGate = new Gate('pedestrian gate', true);
echo residentEntry($carGate), "\n";
echo (new VisitorKiosk())->printPass('Meera', 'B-204'), "\n";
nightLockdown($carGate, $footGate); // only barriers: the kiosk has no close()
echo 'pedestrian gate at the end: ', $footGate->isOpen ? 'open' : 'closed', "\n";

In PHP: TagGate extends two interfaces, which PHP allows for interfaces only. nightLockdown takes Barrier ...$barriers, so each argument's type is checked when the call is made.

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

trait Barrier {
    fn open(&mut self) -> String;
    fn close(&mut self) -> String;
}

trait TagReader {
    fn read_tag(&self) -> Option<String>; // a resident's tag in range, or None
}

trait TagGate: TagReader + Barrier {} // two roles, composed
impl<T: TagReader + Barrier> TagGate for T {}

trait VisitorDesk {
    fn print_pass(&self, visitor: &str, flat: &str) -> String;
}

struct Gate<R> {
    name: &'static str,
    is_open: bool,
    reader: R,
}

impl<R> Barrier for Gate<R> {
    fn open(&mut self) -> String {
        self.is_open = true;
        format!("{} opened", self.name)
    }

    fn close(&mut self) -> String {
        self.is_open = false;
        format!("{} closed", self.name)
    }
}

struct TagInRange(Option<&'static str>);

impl TagReader for Gate<TagInRange> {
    fn read_tag(&self) -> Option<String> {
        self.reader.0.map(String::from)
    }
}

struct VisitorKiosk;

impl VisitorDesk for VisitorKiosk {
    fn print_pass(&self, visitor: &str, flat: &str) -> String {
        format!("pass printed for {visitor}, visiting {flat}")
    }
}

fn resident_entry(gate: &mut impl TagGate) -> String {
    match gate.read_tag() {
        Some(tag) => format!("resident tag {tag}: {}", gate.open()),
        None => "no resident tag in range".to_string(),
    }
}

fn night_lockdown(barriers: &mut [&mut dyn Barrier]) {
    for barrier in barriers.iter_mut() {
        println!("night lockdown: {}", barrier.close());
    }
}

fn main() {
    let mut car_gate = Gate {
        name: "barrier",
        is_open: false,
        reader: TagInRange(Some("TAG-1042")),
    };
    let mut foot_gate = Gate {
        name: "pedestrian gate",
        is_open: true,
        reader: (),
    };
    println!("{}", resident_entry(&mut car_gate));
    println!("{}", VisitorKiosk.print_pass("Meera", "B-204"));
    night_lockdown(&mut [&mut car_gate, &mut foot_gate]); // only barriers: the kiosk has no close()
    let state = if foot_gate.is_open { "open" } else { "closed" };
    println!("pedestrian gate at the end: {state}");
}

In Rust: Rust has no inheritance. The barrier is a Gate whose reader is a TagInRange, and only Gate<TagInRange> implements TagReader, so the pedestrian gate, a Gate<()>, can never reach resident_entry.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

interface Barrier {
  open(): string;
  close(): string;
}

interface TagReader {
  readTag(): string | null; // a resident's tag in range, or null
}

interface TagGate extends TagReader, Barrier {} // two roles, composed

interface VisitorDesk {
  printPass(visitor: string, flat: string): string;
}

class Gate implements Barrier {
  readonly name: string;
  isOpen: boolean;

  constructor(name: string, isOpen: boolean) {
    this.name = name;
    this.isOpen = isOpen;
  }

  open(): string {
    this.isOpen = true;
    return `${this.name} opened`;
  }

  close(): string {
    this.isOpen = false;
    return `${this.name} closed`;
  }
}

class BoomBarrier extends Gate implements TagGate {
  readonly #tagInRange: string | null;

  constructor(tagInRange: string | null) {
    super("barrier", false);
    this.#tagInRange = tagInRange;
  }

  readTag(): string | null {
    return this.#tagInRange;
  }
}

class VisitorKiosk implements VisitorDesk {
  printPass(visitor: string, flat: string): string {
    return `pass printed for ${visitor}, visiting ${flat}`;
  }
}

function residentEntry(gate: TagGate): string {
  const tag = gate.readTag();
  return tag === null ? "no resident tag in range" : `resident tag ${tag}: ${gate.open()}`;
}

function nightLockdown(barriers: Barrier[]): void {
  for (const barrier of barriers) {
    console.log(`night lockdown: ${barrier.close()}`);
  }
}

const carGate = new BoomBarrier("TAG-1042");
const footGate = new Gate("pedestrian gate", true);
const desk: VisitorDesk = new VisitorKiosk();
console.log(residentEntry(carGate));
console.log(desk.printPass("Meera", "B-204"));
nightLockdown([carGate, footGate]); // only barriers: the kiosk has no close()
console.log(`pedestrian gate at the end: ${footGate.isOpen ? "open" : "closed"}`);

In TypeScript: Interfaces are structural, like Python's protocols, so BoomBarrier would be a TagGate even without implements; implements asks the compiler to check the class against the role where it is declared.

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import "fmt"

type Barrier interface {
	Open() string
	Close() string
}

type TagReader interface {
	ReadTag() (tag string, ok bool) // a resident's tag in range, or ok false
}

// TagGate is two roles, composed.
type TagGate interface {
	TagReader
	Barrier
}

type VisitorDesk interface {
	PrintPass(visitor, flat string) string
}

type Gate struct {
	name   string
	isOpen bool
}

func (g *Gate) Open() string {
	g.isOpen = true
	return g.name + " opened"
}

func (g *Gate) Close() string {
	g.isOpen = false
	return g.name + " closed"
}

type BoomBarrier struct {
	Gate
	tagInRange string
}

func (b *BoomBarrier) ReadTag() (string, bool) {
	return b.tagInRange, b.tagInRange != ""
}

type VisitorKiosk struct{}

func (VisitorKiosk) PrintPass(visitor, flat string) string {
	return fmt.Sprintf("pass printed for %s, visiting %s", visitor, flat)
}

func residentEntry(gate TagGate) string {
	tag, ok := gate.ReadTag()
	if !ok {
		return "no resident tag in range"
	}
	return fmt.Sprintf("resident tag %s: %s", tag, gate.Open())
}

func nightLockdown(barriers []Barrier) {
	for _, barrier := range barriers {
		fmt.Println("night lockdown: " + barrier.Close())
	}
}

func main() {
	carGate := &BoomBarrier{Gate{"barrier", false}, "TAG-1042"}
	footGate := &Gate{"pedestrian gate", true}
	var desk VisitorDesk = VisitorKiosk{}
	fmt.Println(residentEntry(carGate))
	fmt.Println(desk.PrintPass("Meera", "B-204"))
	nightLockdown([]Barrier{carGate, footGate}) // only barriers: the kiosk has no Close()
	state := "closed"
	if footGate.isOpen {
		state = "open"
	}
	fmt.Println("pedestrian gate at the end: " + state)
}

In Go: Interfaces are satisfied without being named, and TagGate embeds TagReader and Barrier as io.ReadWriter embeds io.Reader and io.Writer. ReadTag reports no tag with a second result, false, instead of a null.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

protocol Barrier {
  func open() -> String
  func close() -> String
}

protocol TagReader {
  func readTag() -> String?  // a resident's tag in range, or nil
}

typealias TagGate = TagReader & Barrier  // two roles, composed

protocol VisitorDesk {
  func printPass(for visitor: String, visiting flat: String) -> String
}

class Gate: Barrier {
  let name: String
  private(set) var isOpen: Bool

  init(name: String, isOpen: Bool) {
    self.name = name
    self.isOpen = isOpen
  }

  func open() -> String {
    isOpen = true
    return "\(name) opened"
  }

  func close() -> String {
    isOpen = false
    return "\(name) closed"
  }
}

final class BoomBarrier: Gate, TagReader {
  private let tagInRange: String?

  init(tagInRange: String?) {
    self.tagInRange = tagInRange
    super.init(name: "barrier", isOpen: false)
  }

  func readTag() -> String? { tagInRange }
}

struct VisitorKiosk: VisitorDesk {
  func printPass(for visitor: String, visiting flat: String) -> String {
    "pass printed for \(visitor), visiting \(flat)"
  }
}

func residentEntry(_ gate: any TagGate) -> String {
  guard let tag = gate.readTag() else { return "no resident tag in range" }
  return "resident tag \(tag): \(gate.open())"
}

func nightLockdown(_ barriers: [any Barrier]) {
  for barrier in barriers {
    print("night lockdown: \(barrier.close())")
  }
}

let carGate = BoomBarrier(tagInRange: "TAG-1042")
let footGate = Gate(name: "pedestrian gate", isOpen: true)
let desk: any VisitorDesk = VisitorKiosk()
print(residentEntry(carGate))
print(desk.printPass(for: "Meera", visiting: "B-204"))
nightLockdown([carGate, footGate])  // only barriers: the kiosk has no close()
print("pedestrian gate at the end: \(footGate.isOpen ? "open" : "closed")")

In Swift: TagReader & Barrier is a protocol composition, so TagGate needs no protocol of its own. Gate is a class, because closing a struct held in an [any Barrier] array would close only the array's copy.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

# Roles, by what each caller uses; Ruby checks none of them before the program runs:
# Barrier: open and close. TagReader: read_tag, a resident's tag in range, or nil.
# TagGate: two roles, composed. VisitorDesk: print_pass.

# A barrier that people or cars pass: it plays Barrier.
class Gate
  def initialize(name, open)
    @name = name
    @open = open
  end

  def open?
    @open
  end

  def open
    @open = true
    "#{@name} opened"
  end

  def close
    @open = false
    "#{@name} closed"
  end
end

# The car gate: a Gate with its own reader, so it plays TagGate.
class BoomBarrier < Gate
  def initialize(tag_in_range)
    super('barrier', false)
    @tag_in_range = tag_in_range
  end

  def read_tag
    @tag_in_range
  end
end

# The guard's desk: it plays VisitorDesk.
class VisitorKiosk
  def print_pass(visitor, flat)
    "pass printed for #{visitor}, visiting #{flat}"
  end
end

def resident_entry(gate)
  tag = gate.read_tag
  return 'no resident tag in range' if tag.nil?

  "resident tag #{tag}: #{gate.open}"
end

def night_lockdown(barriers)
  barriers.each { |barrier| puts "night lockdown: #{barrier.close}" }
end

car_gate = BoomBarrier.new('TAG-1042')
foot_gate = Gate.new('pedestrian gate', true)
desk = VisitorKiosk.new
puts resident_entry(car_gate)
puts desk.print_pass('Meera', 'B-204')
night_lockdown([car_gate, foot_gate]) # only barriers: the kiosk has no close
puts "pedestrian gate at the end: #{foot_gate.open? ? 'open' : 'closed'}"

In Ruby: Ruby has no interface declarations, so the roles are written down as comments and a method call is the only check. open? says whether the gate is open; open opens it.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

interface Barrier {
    fun open(): String

    fun close(): String
}

interface TagReader {
    fun readTag(): String? // a resident's tag in range, or null
}

interface TagGate :
    TagReader,
    Barrier // two roles, composed

interface VisitorDesk {
    fun printPass(
        visitor: String,
        flat: String,
    ): String
}

open class Gate(
    private val name: String,
    isOpen: Boolean,
) : Barrier {
    var isOpen = isOpen
        private set

    override fun open(): String {
        isOpen = true
        return "$name opened"
    }

    override fun close(): String {
        isOpen = false
        return "$name closed"
    }
}

class BoomBarrier(
    private val tagInRange: String?,
) : Gate("barrier", false),
    TagGate {
    override fun readTag(): String? = tagInRange
}

class VisitorKiosk : VisitorDesk {
    override fun printPass(
        visitor: String,
        flat: String,
    ): String = "pass printed for $visitor, visiting $flat"
}

fun residentEntry(gate: TagGate): String {
    val tag = gate.readTag() ?: return "no resident tag in range"
    return "resident tag $tag: ${gate.open()}"
}

fun nightLockdown(barriers: List<Barrier>) {
    for (barrier in barriers) {
        println("night lockdown: ${barrier.close()}")
    }
}

fun main() {
    val carGate = BoomBarrier("TAG-1042")
    val footGate = Gate("pedestrian gate", true)
    val desk: VisitorDesk = VisitorKiosk()
    println(residentEntry(carGate))
    println(desk.printPass("Meera", "B-204"))
    nightLockdown(listOf(carGate, footGate)) // only barriers: the kiosk has no close()
    println("pedestrian gate at the end: ${if (footGate.isOpen) "open" else "closed"}")
}

In Kotlin: TagGate composes the two roles in its supertype list. Gate is open, so BoomBarrier may extend it, and isOpen has a private setter, so only open() and close() change it.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

abstract interface class Barrier {
  String open();
  String close();
}

abstract interface class TagReader {
  String? readTag(); // a resident's tag in range, or null
}

abstract interface class TagGate
    implements TagReader, Barrier {} // two roles, composed

abstract interface class VisitorDesk {
  String printPass(String visitor, String flat);
}

class Gate implements Barrier {
  Gate(this.name, this._isOpen);

  final String name;
  bool _isOpen;

  bool get isOpen => _isOpen;

  @override
  String open() {
    _isOpen = true;
    return '$name opened';
  }

  @override
  String close() {
    _isOpen = false;
    return '$name closed';
  }
}

final class BoomBarrier extends Gate implements TagGate {
  BoomBarrier(this._tagInRange) : super('barrier', false);

  final String? _tagInRange;

  @override
  String? readTag() => _tagInRange;
}

final class VisitorKiosk implements VisitorDesk {
  @override
  String printPass(String visitor, String flat) =>
      'pass printed for $visitor, visiting $flat';
}

String residentEntry(TagGate gate) {
  final tag = gate.readTag();
  if (tag == null) return 'no resident tag in range';
  return 'resident tag $tag: ${gate.open()}';
}

void nightLockdown(List<Barrier> barriers) {
  for (final barrier in barriers) {
    print('night lockdown: ${barrier.close()}');
  }
}

void main() {
  final carGate = BoomBarrier('TAG-1042');
  final footGate = Gate('pedestrian gate', true);
  final VisitorDesk desk = VisitorKiosk();
  print(residentEntry(carGate));
  print(desk.printPass('Meera', 'B-204'));
  nightLockdown([carGate, footGate]); // only barriers: the kiosk has no close()
  print('pedestrian gate at the end: ${footGate.isOpen ? 'open' : 'closed'}');
}

In Dart: An abstract interface class can be implemented but not extended outside its library. BoomBarrier extends Gate and implements TagGate, so its open and close are Gate's.

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

resident tag TAG-1042: barrier opened
pass printed for Meera, visiting B-204
night lockdown: barrier closed
night lockdown: pedestrian gate closed
pedestrian gate at the end: closed

The lockdown now asks for a list of Barrier objects, and both barriers close, so the pedestrian gate ends the night closed. The kiosk cannot be put in that list by mistake: Python’s type checker reports that it has no close(), and so do the compilers of the statically typed languages. The languages part ways on how a class comes to play a role:

  • It says so. In Java, C#, Kotlin, PHP, Dart and C++ a class names its roles, as BoomBarrier names TagGate.
  • It has the methods. In Python, TypeScript and Go a class plays every role whose methods it has, and Swift composes TagReader & Barrier without a new protocol.
  • Nothing checks. In JavaScript and Ruby a role is checked only when a method is called, so the roles live in JSDoc types or comments.

Rust, which has no inheritance, makes the barrier a Gate whose reader is a TagInRange, and only that kind of gate implements TagReader. Wherever the language leaves the choice to you, keep the roles next to the code that uses them, because their shape comes from the callers.

What the split saves, measured from the source

This program reads the source of both designs with Python’s ast module and counts what the fat interface cost:

The two gate designs, measured from their source Python · measure.py
"""Measures the two gate designs from their source code: interfaces, stub methods, what a test fake for resident
entry must define, and how many device classes a change to print_pass() touches."""

import ast
from pathlib import Path


def classes(*files):
    found = {}
    for file in files:
        tree = ast.parse(Path(file).read_text(encoding="utf-8"))
        found.update({node.name: node for node in tree.body if isinstance(node, ast.ClassDef)})
    return found


def methods(cls):
    return [node for node in cls.body if isinstance(node, ast.FunctionDef)]


def is_interface(cls):
    bases = {getattr(base, "id", "") for base in cls.bases}
    return bool(bases & {"ABC", "Protocol"})


def is_stub(method):
    """A method that cannot do its job: it only raises NotImplementedError, or only returns None."""
    body = [node for node in method.body if not (isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant))]
    if len(body) != 1:
        return False
    statement = body[0]
    if isinstance(statement, ast.Raise) and isinstance(statement.exc, ast.Call):
        return getattr(statement.exc.func, "id", "") == "NotImplementedError"
    if isinstance(statement, ast.Return):
        return statement.value is None or (isinstance(statement.value, ast.Constant) and statement.value.value is None)
    return False


def all_methods(name, found):
    """The methods of an interface, including those of the interfaces it extends."""
    cls = found[name]
    names = [m.name for m in methods(cls)]
    for base in cls.bases:
        if getattr(base, "id", "") in found:
            names += all_methods(base.id, found)
    return sorted(set(names))


def measure(found, fake_for):
    interfaces = [c for c in found.values() if is_interface(c) and methods(c)]
    devices = [c for c in found.values() if not is_interface(c)]
    return {
        "interfaces": str(len(interfaces)),
        "methods in each interface": ", ".join(str(len(methods(c))) for c in interfaces),
        "stub methods in the devices": str(sum(is_stub(m) for c in devices for m in methods(c))),
        "methods a fake for resident_entry needs": str(len(all_methods(fake_for, found))),
        "device classes that define print_pass": str(sum(any(m.name == "print_pass" for m in methods(c)) for c in devices)),
    }


fat = measure(classes("fat/gate.py"), fake_for="GateDevice")
roles = measure(classes("roles/main.py"), fake_for="TagGate")

print(f"{'':42}{'fat':>10}{'roles':>12}")
for row in fat:
    print(f"{row:42}{fat[row]:>10}{roles[row]:>12}")

Output

                                                 fat       roles
interfaces                                         1           3
methods in each interface                          4     2, 1, 1
stub methods in the devices                        6           0
methods a fake for resident_entry needs            4           3
device classes that define print_pass              3           1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 measure.py

In the fat design the three devices define twelve operations, and six of them are stubs; the role design has none. A test of resident entry needs a fake gate: to count as a GateDevice it must define all four abstract methods, while a TagGate needs three. Then suppose the society asks for a validity time on every printed pass, so print_pass() gets a new parameter. In the fat design three device classes define print_pass(), two of them as stubs that must be edited to keep matching the interface; in the role design one class does.

The role design has more interfaces, three instead of one, but each is small, and the devices now define only the six operations they can really carry out. ISP does not promise fewer types.

Key idea

An interface sized by its callers lets a change reach only the classes and callers that use what changed: the split adds a few small types and removes the stubs, the fake methods and the edits.

Checkpoint

Check: Role interfaces cut from each caller's use · What a fat interface costs, measured

  1. Question 1 of 3 In the role design, which interface does night_lockdown() take, and what happens if someone puts the kiosk in its list?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: A list of Barrier; a type checker or compiler refuses the kiosk, because it has no close()

    Each caller asks for the role it uses. night_lockdown() closes gates, so it takes Barrier, which BoomBarrier and the pedestrian gate play. The kiosk cannot be listed by mistake in the statically checked languages, and in JavaScript and Ruby it would fail at its close() call.

  2. Question 2 of 3 A test of resident_entry() needs a fake gate. How many methods must the fake define in the fat design, where it must count as a GateDevice?

    Type a number.

    A hint

    GateDevice declares every operation of every device.

    Show the answer: Question 2 of 3

    Answer: 4 methods

    GateDevice has four abstract methods, open, close, read_tag and print_pass, so a fake must define all four, although resident entry calls two of them. A fake of TagGate needs three: read_tag, open and close.

  3. Question 3 of 3 The society now wants read_tag() to report the reader's signal strength as well. How many device classes define read_tag() in each design?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: Three in the fat design, two of them stubs, and one in the role design

    measure.py counted the same thing for print_pass(): three classes in the fat design and one in the role design. read_tag() follows the same pattern, so a change to it edits two stubs in the fat design and nothing but the barrier in the role design.

isinstance() on a protocol checks names, not parameters

A protocol is mainly for a static type checker, which compares the methods of a class with the protocol’s, signatures included. Decorated with @runtime_checkable, a protocol can also be used with isinstance(), but the typing documentation calls such protocols simple-minded: the check looks only for the presence of the attributes, ignoring their type signatures. Run the check on a pedestrian gate and on a kiosk written for an older desk:

A structural check that sees names, not parameters Python · protocol/protocol_check.py
"""What isinstance() checks when the interface is a runtime-checkable protocol, and what it does not."""

from typing import Protocol, runtime_checkable


@runtime_checkable
class VisitorDesk(Protocol):
    def print_pass(self, visitor: str, flat: str) -> str: ...


class PedestrianGate:
    def open(self):
        return "pedestrian gate opened"


class OldKiosk:
    """Written for an earlier version of the desk: the method's name matches, its parameters do not."""

    def print_pass(self):
        return "pass printed"


print("PedestrianGate is a VisitorDesk:", isinstance(PedestrianGate(), VisitorDesk))
print("OldKiosk is a VisitorDesk:", isinstance(OldKiosk(), VisitorDesk))
try:
    OldKiosk().print_pass("Meera", "B-204")
except TypeError as error:
    print("calling it as a VisitorDesk:", error)

Output

PedestrianGate is a VisitorDesk: False
OldKiosk is a VisitorDesk: True
calling it as a VisitorDesk: OldKiosk.print_pass() takes 1 positional argument but 3 were given

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 protocol_check.py

The pedestrian gate has no print_pass(), so it is correctly not a VisitorDesk. The old kiosk has the method’s name with the wrong parameters, passes the check and fails when it is used as a desk. Use isinstance() with a protocol as a coarse check, for example when a plugin is loaded, and rely on the type checker and on tests for the rest.

Small roles compose, and splitting stops where callers stop

Go’s standard library is built in this style. Effective Go observes that Go code is full of interfaces with only one or two methods, each named after its method: io.Writer is the interface of anything with a Write method, and io.Reader of anything with Read. When code needs both, io.ReadWriter embeds the two interfaces instead of listing their methods again, the same move as TagGate. The Go project’s guidance on interfaces adds two rules that suit any language: an interface belongs in the package that uses it, not in the package that implements it, and it should not be defined before a real use shows what it needs. Python’s collections.abc works the same way: Sized asks only for __len__(), Iterable for __iter__() and Container for __contains__(), and Collection combines the three.

Splitting can also go too far. Martin Fowler draws the line with two names: a role interface comes from one interaction between a supplier and its callers, while a header interface: An interface that copies every public method of one class, so that its size follows the class instead of what any caller needs. copies all of a class’s public methods. The table compares three ways to cut the gate’s interfaces:

Cut Interfaces at the gate What it costs
One header interface GateDevice, 4 methods 6 stubs; a fake defines 4 methods; a pass change edits 3 classes
By caller Barrier, TagReader, VisitorDesk, and TagGate composed No stubs; a fake of TagGate defines 3 methods; the change edits 1 class
One method each Opener, Closer, TagReader, PassPrinter No stubs, but two callers each ask for two roles that always travel together

If Barrier became Opener and Closer, every caller that opens and closes a gate would ask for both, and no device would ever implement one without the other. Methods that one kind of caller uses together, and that change together, belong in one role: that is cohesion, measured from the callers’ side.

In a design interview

When you draw an interface, name its callers next to it: “the lockdown uses Barrier; the desk uses VisitorDesk”. If an implementation would need a method that throws “not supported”, say that the interface is too wide and split it on the spot.

Checkpoint

Check: What isinstance() checks on a protocol · Composing small roles, and where splitting stops

  1. Question 1 of 3 A plugin's class defines print_pass(self) with no other parameters. VisitorDesk is a runtime-checkable protocol whose print_pass takes a visitor and a flat. What does isinstance(plugin, VisitorDesk) return?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: True, because the check only looks for a method with the right name

    This is the old kiosk of protocol_check.py: it passed isinstance() and then failed with OldKiosk.print_pass() takes 1 positional argument but 3 were given. Use the check as a coarse filter and let a type checker and tests check the signatures.

  2. Question 2 of 3 A teammate splits Barrier into Opener, with open(), and Closer, with close(). What does the split cost at the gate?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: Every caller that opens and closes now asks for two roles that always travel together, and no device plays only one

    One method per interface is right only when callers really use one method, as with Go's io.Reader. open() and close() are used together by resident entry and change together, which is cohesion measured from the callers' side.

  3. Question 3 of 3 The Go project's guidance on interfaces says where an interface should be declared. Where?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: In the package that uses the values, not in the package that implements them

    An interface belongs to the code that uses it, and it should not be defined before a real use shows what it needs. In the gate software that means Barrier lives beside the lockdown and TagGate beside resident entry.

Practise: split a school’s staff interface

A school’s software puts every member of staff behind one interface of nine methods, and each class carries six stubs. Split it into three roles, remove every stub and type each caller with the role it needs. The sample tests run in your browser:

Exercise · Medium · Python

Split a school's staff interface into roles

A school's software puts every member of staff behind one interface, StaffMember, with nine methods. Each of the three classes can really do three of them, so each one carries six stubs that raise NotImplementedError, and every caller depends on all nine methods although it uses one.

Split StaffMember into three role interfaces, written as typing.Protocol classes (or as abstract base classes):

  • Teaching: teach, mark_attendance and grade;
  • Transport: drive, check_bus_pass and log_fuel;
  • FeeCounter: collect_fee, issue_receipt and day_total.

Then remove every stub, so that Teacher, BusDriver and Accountant keep only the methods they can carry out, and change the type hints of the three caller functions to the role each one needs. Everything the classes and the callers did before must still work. The sample tests check the old behaviour, the three roles, that no stub is left and that a fake fee counter with only its role's three methods works with close_fee_counter().

Starter code · school_staff.py

"""A school's staff behind one interface. Split StaffMember into three role interfaces (see the prompt)."""

from abc import ABC, abstractmethod


class StaffMember(ABC):
    @abstractmethod
    def teach(self, period: int) -> str: ...

    @abstractmethod
    def mark_attendance(self, roll: list[str], present: set[str]) -> dict[str, bool]: ...

    @abstractmethod
    def grade(self, answers: list[str], key: list[str]) -> int: ...

    @abstractmethod
    def drive(self, stops: list[str]) -> list[str]: ...

    @abstractmethod
    def check_bus_pass(self, pass_id: str) -> bool: ...

    @abstractmethod
    def log_fuel(self, litres: int) -> int: ...

    @abstractmethod
    def collect_fee(self, student: str, amount: int) -> int: ...

    @abstractmethod
    def issue_receipt(self, receipt_no: int) -> str: ...

    @abstractmethod
    def day_total(self) -> int: ...


class Teacher(StaffMember):
    def __init__(self, name, subject):
        self.name = name
        self.subject = subject

    def teach(self, period):
        return f"{self.name} teaches {self.subject} in period {period}"

    def mark_attendance(self, roll, present):
        return {student: student in present for student in roll}

    def grade(self, answers, key):
        return sum(1 for given, right in zip(answers, key) if given == right)

    def drive(self, stops):
        raise NotImplementedError("teachers do not drive the bus")

    def check_bus_pass(self, pass_id):
        raise NotImplementedError("teachers do not check bus passes")

    def log_fuel(self, litres):
        raise NotImplementedError("teachers do not fill the tank")

    def collect_fee(self, student, amount):
        raise NotImplementedError("teachers do not collect fees")

    def issue_receipt(self, receipt_no):
        raise NotImplementedError("teachers do not issue receipts")

    def day_total(self):
        raise NotImplementedError("teachers keep no cash book")


class BusDriver(StaffMember):
    def __init__(self, name, valid_passes):
        self.name = name
        self.valid_passes = set(valid_passes)
        self.fuel_litres = 0

    def drive(self, stops):
        return [f"stop {number}: {stop}" for number, stop in enumerate(stops, start=1)]

    def check_bus_pass(self, pass_id):
        return pass_id in self.valid_passes

    def log_fuel(self, litres):
        self.fuel_litres = self.fuel_litres + litres
        return self.fuel_litres

    def teach(self, period):
        raise NotImplementedError("drivers do not teach")

    def mark_attendance(self, roll, present):
        raise NotImplementedError("drivers do not take the register")

    def grade(self, answers, key):
        raise NotImplementedError("drivers do not grade papers")

    def collect_fee(self, student, amount):
        raise NotImplementedError("drivers do not collect fees")

    def issue_receipt(self, receipt_no):
        raise NotImplementedError("drivers do not issue receipts")

    def day_total(self):
        raise NotImplementedError("drivers keep no cash book")


class Accountant(StaffMember):
    def __init__(self, name):
        self.name = name
        self._payments = []

    def collect_fee(self, student, amount):
        if amount <= 0:
            raise ValueError("a fee is a positive amount")
        self._payments.append((student, amount))
        return len(self._payments)

    def issue_receipt(self, receipt_no):
        student, amount = self._payments[receipt_no - 1]
        return f"receipt {receipt_no}: {amount} from {student}"

    def day_total(self):
        return sum(amount for _, amount in self._payments)

    def teach(self, period):
        raise NotImplementedError("accountants do not teach")

    def mark_attendance(self, roll, present):
        raise NotImplementedError("accountants do not take the register")

    def grade(self, answers, key):
        raise NotImplementedError("accountants do not grade papers")

    def drive(self, stops):
        raise NotImplementedError("accountants do not drive the bus")

    def check_bus_pass(self, pass_id):
        raise NotImplementedError("accountants do not check bus passes")

    def log_fuel(self, litres):
        raise NotImplementedError("accountants do not fill the tank")


def first_period(teachers: list[StaffMember]) -> list[str]:
    return [teacher.teach(1) for teacher in teachers]


def morning_route(driver: StaffMember, stops: list[str]) -> list[str]:
    return driver.drive(stops)


def close_fee_counter(counter: StaffMember) -> str:
    return f"collected today: {counter.day_total()}"
The sample tests · test_school_staff.py
import inspect

import school_staff
from school_staff import Accountant, BusDriver, Teacher, close_fee_counter, first_period, morning_route


def role_methods(role):
    return {name for name, value in vars(role).items() if callable(value) and not name.startswith("_")}


def test_teacher_still_works():
    """a teacher teaches, takes the register and grades as before"""
    teacher = Teacher("Ms Iyer", "maths")
    assert teacher.teach(3) == "Ms Iyer teaches maths in period 3"
    assert teacher.mark_attendance(["Asha", "Ravi"], {"Ravi"}) == {"Asha": False, "Ravi": True}
    assert teacher.grade(["b", "c", "a"], ["b", "a", "a"]) == 2
    assert first_period([teacher]) == ["Ms Iyer teaches maths in period 1"]


def test_driver_still_works():
    """a driver drives the route, checks passes and logs fuel as before"""
    driver = BusDriver("Mr Khan", valid_passes=["P-17"])
    assert morning_route(driver, ["Gate 2", "Market"]) == ["stop 1: Gate 2", "stop 2: Market"]
    assert driver.check_bus_pass("P-17") is True
    assert driver.check_bus_pass("P-99") is False
    assert driver.log_fuel(20) == 20
    assert driver.log_fuel(15) == 35


def test_accountant_still_works():
    """an accountant collects fees, issues receipts and totals the day as before"""
    accountant = Accountant("Mrs Das")
    assert accountant.collect_fee("Asha", 1200) == 1
    assert accountant.collect_fee("Ravi", 800) == 2
    assert accountant.issue_receipt(2) == "receipt 2: 800 from Ravi"
    assert close_fee_counter(accountant) == "collected today: 2000"
    try:
        accountant.collect_fee("Asha", 0)
    except ValueError:
        pass
    else:
        raise AssertionError("a fee of 0 must be refused")


def test_three_role_interfaces():
    """Teaching, Transport and FeeCounter each list the methods of one role"""
    assert role_methods(school_staff.Teaching) == {"teach", "mark_attendance", "grade"}
    assert role_methods(school_staff.Transport) == {"drive", "check_bus_pass", "log_fuel"}
    assert role_methods(school_staff.FeeCounter) == {"collect_fee", "issue_receipt", "day_total"}


def test_no_stub_methods():
    """no class raises NotImplementedError, and no class has a method of another role"""
    assert "NotImplementedError" not in inspect.getsource(school_staff)
    assert not hasattr(Teacher("Ms Iyer", "maths"), "drive")
    assert not hasattr(BusDriver("Mr Khan", []), "collect_fee")
    assert not hasattr(Accountant("Mrs Das"), "teach")


def test_a_fake_needs_only_its_role():
    """a fake with only the fee counter's three methods works with close_fee_counter()"""

    class FakeCounter:
        def collect_fee(self, student, amount):
            return 1

        def issue_receipt(self, receipt_no):
            return "receipt 1"

        def day_total(self):
            return 500

    assert close_fee_counter(FakeCounter()) == "collected today: 500"
A hint

Start from the callers, not from the classes: first_period() calls only teach, morning_route() only drive and close_fee_counter() only day_total, and each class's real methods fall into exactly one of the three groups. With typing.Protocol, the classes do not need to inherit from anything: a Teacher with the three teaching methods already is a Teaching, so StaffMember and its abc import can go.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Add a fourth device, such as a camera at the car gate, to the role design and see which roles it plays.

Then check yourself on the whole lesson, the idea you know least first:

Practice that adapts to you

Practise this lesson

13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 Which statement is the Interface Segregation Principle?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: Clients should not be forced to depend on methods they do not use

    Robert C. Martin's wording is that clients should not be forced to depend upon interfaces that they do not use. An interface should be the size of what its callers use, and a class may implement as many small interfaces as it has roles.

  2. Question 2 of 13 Martin's argument for the principle is about the direction of change. Which change is it mainly about?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: A change that one caller asks of an interface, which then reaches every implementer and every other caller

    The desk's request for a validity time edited the kiosk, two stubs and other callers' fakes behind GateDevice, and only the kiosk behind roles. The principle protects callers from changes that other callers ask for.

  3. Question 3 of 13 Which of these are signs that an interface is too wide for its callers?

    Choose every answer that is right.

    Show the answer: Question 3 of 13

    Answer:

    • A change requested by one caller makes classes that never use that method change their signatures
    • A test fake must define methods that the code under test never calls
    • Implementations carry methods that only raise NotImplementedError

    Stubs, fakes that must fake unused methods and changes that ripple into unrelated classes all come from callers and implementations depending on more than they use.

  4. Question 4 of 13 Wallet, Card and Cash all implement a PaymentMethod interface. What does this method of Cash tell you?

    Read the code, then choose one answer.

    class Cash(PaymentMethod):
        def refund_to_source(self, amount):
            raise NotImplementedError("cash cannot be refunded to a card")
    Show the answer: Question 4 of 13

    Answer: The interface promises more than every implementation can do, so code holding a PaymentMethod cannot know which calls will work

    Every caller written for PaymentMethod can call refund_to_source(), and one kind of payment fails at run time. Move refunds to a Refundable role that only wallets and cards play, and let the refund code ask for that role.

  5. Question 5 of 13 How should you decide which methods belong together in one role interface?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: Group the methods that one kind of caller uses together

    The night lockdown needs open() and close(), so they form Barrier; resident entry needs read_tag() as well, so it asks for TagGate, which composes TagReader and Barrier.

  6. Question 6 of 13 resident_entry() needs read_tag() and open(), which live in the TagReader and Barrier protocols. What is the cleanest way to say so?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: A TagGate protocol that extends TagReader and Barrier, used as the parameter's type

    TagGate composes two roles the way Go builds io.ReadWriter from io.Reader and io.Writer, and Swift writes TagReader & Barrier. The boom barrier plays it; the pedestrian gate cannot be passed in.

  7. Question 7 of 13 The society adds a camera at the car gate that reads number plates as tags but cannot open or close anything. Which role does it play, and can resident_entry() use it on its own?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: TagReader only, so resident_entry() cannot take it alone; it would need a TagGate made of the camera and a barrier

    A new device plays only the roles whose methods it can carry out. A small class that pairs the camera with the boom barrier's arm could play TagGate, and nothing else in the design changes.

  8. Question 8 of 13 In the fat design, three devices each define the four operations of GateDevice. How many of those twelve methods are stubs?

    Type a number.

    A hint

    Count, for each device, the operations it cannot carry out.

    Show the answer: Question 8 of 13

    Answer: 6 stubs

    The boom barrier cannot print, the pedestrian gate cannot read or print, and the kiosk cannot open, close or read: 1 + 2 + 3 = 6, the number measure.py counted. The role design has none.

  9. Question 9 of 13 The role design has three interfaces with methods instead of one. Why is that not a cost the principle worries about?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: Each interface is small and used by one kind of caller, so a change reaches only the classes that use what changed

    The principle does not promise fewer types. Measured on the gate, the split went from 6 stubs to 0, a fake of four methods to three, and a pass change from three classes to one.

  10. Question 10 of 13 VisitorDesk is a @runtime_checkable protocol with one method, print_pass(visitor, flat). What does protocol_check.py print?

    What does this program print? Choose one answer.

    """What isinstance() checks when the interface is a runtime-checkable protocol, and what it does not."""
    
    from typing import Protocol, runtime_checkable
    
    
    @runtime_checkable
    class VisitorDesk(Protocol):
        def print_pass(self, visitor: str, flat: str) -> str: ...
    
    
    class PedestrianGate:
        def open(self):
            return "pedestrian gate opened"
    
    
    class OldKiosk:
        """Written for an earlier version of the desk: the method's name matches, its parameters do not."""
    
        def print_pass(self):
            return "pass printed"
    
    
    print("PedestrianGate is a VisitorDesk:", isinstance(PedestrianGate(), VisitorDesk))
    print("OldKiosk is a VisitorDesk:", isinstance(OldKiosk(), VisitorDesk))
    try:
        OldKiosk().print_pass("Meera", "B-204")
    except TypeError as error:
        print("calling it as a VisitorDesk:", error)
    Show the answer: Question 10 of 13

    Answer: it prints

    PedestrianGate is a VisitorDesk: False
    OldKiosk is a VisitorDesk: True
    calling it as a VisitorDesk: OldKiosk.print_pass() takes 1 positional argument but 3 were given

    isinstance() with a runtime-checkable protocol only checks that the methods exist. The pedestrian gate has no print_pass(), so it is not a VisitorDesk; the old kiosk has one with the wrong parameters, so it passes, and calling it the way the protocol describes then fails.

  11. Question 11 of 13 The old kiosk passed isinstance(kiosk, VisitorDesk) although its print_pass() takes the wrong parameters. What would have caught the mistake before the program ran?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: A static type checker, which compares the method's signature with the protocol's

    Use isinstance() with a protocol as a coarse check, for example when a plugin is loaded, and rely on a type checker and on tests that call the methods for the rest.

  12. Question 12 of 13 Martin Fowler names two kinds of interface. What is a header interface?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: An interface that copies every public method of one class

    A role interface comes from one interaction between a supplier and its callers, so one class often plays several; a header interface mirrors a class and helps no particular caller.

  13. Question 13 of 13 A function only loops over its argument once. Which collections.abc type should its parameter ask for?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Iterable, because it asks only for __iter__()

    Ask for the smallest role the code uses. Collection combines Sized, Iterable and Container for code that needs all three, as TagGate combines two of the gate's roles.

Interview questions

Fresher · What does the Interface Segregation Principle say, and how do you spot a violation?

No caller should be forced to depend on methods it does not use, so an interface should be the size of what one kind of caller needs. The signs of a violation are on both sides of the interface. Implementations carry stubs that raise NotImplementedError or quietly do nothing, because the interface lists operations they cannot perform. Callers and their test fakes depend on methods they never call, so a change that one caller asks for makes unrelated classes change too. The fix is role interfaces: group the methods by the callers that use them together, let a class implement several roles, and type each caller’s parameters with the role it needs. Splitting further, down to one method each, is only right when callers really use one method.

Interviewers follow up on that answer. Choose a reply to each question below, and read why each reply holds up or falls short:

Checkpoint

Interview round: three follow-up questions

  1. Question 1 of 3 Interview follow-up: one implementation of your interface leaves a method as a stub that returns None, and another raises. Which is worse, and why?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: The quiet one, because its result looks like a real answer and no caller can tell the difference

    The pedestrian gate's read_tag() returned None, and resident_entry() reported no resident tag in range when the device simply had no reader. Both kinds of stub say the interface is too wide; the quiet kind also hides it.

  2. Question 2 of 3 Interview follow-up: a Python team says the Interface Segregation Principle does not apply to them, because Python has duck typing and nothing is recompiled. What is the strongest answer?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: The costs move rather than vanish, to stubs, to fakes that imitate unused methods and to edits a change was never meant for

    measure.py found 6 stubs, a fake of four methods where three are used and a pass change that edits three classes, all in Python. Writing the roles as protocols lets a type checker catch the kiosk in the wrong list before the program runs.

  3. Question 3 of 3 Interview follow-up: two teams use a shared client with ten methods. Team A calls three of them, team B four. Where should the narrower role interfaces be declared?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: In each team's own code, one role per kind of caller, which the shared client satisfies

    The Go project's guidance puts an interface in the package that uses it. With structural typing, as in Python's protocols, TypeScript and Go, the shared client satisfies each team's role without knowing about it; in languages where a class names its roles, an adapter in each team's code plays the role.

Two harder questions build on the same split, one on a wide interface in a real codebase and one on splitting too far:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Your FileStore interface has 12 methods, from upload() to make_thumbnail() and bill_usage(). Every new storage backend takes a week to write, and half of each backend's methods raise NotImplementedError. How would you redesign the interface without breaking the 40 places in the code that use it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A teammate split a six-method Cart interface into six one-method interfaces, one per method, to follow the Interface Segregation Principle. Is the new design better? Argue for or against it, and name the cost that each design creates.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The principle says that callers should not depend on methods they do not use, so each interface is sized by what one kind of caller needs.
  • A fat interface shows itself in stubs that raise or quietly return, in test fakes that imitate unused methods, and in changes that reach classes they were never meant for.
  • Role interfaces are built from the callers: list what each caller uses, group those methods, and compose roles when one caller needs several, as TagGate and Go’s io.ReadWriter do.
  • Measured on the gate, the split removed all six stubs, cut a resident-entry fake from four methods to three and a pass change from three classes to one.
  • isinstance() with a runtime-checkable protocol checks method names only; signatures need a type checker or tests.
  • Methods that one kind of caller uses and changes together stay in one role; one method per interface is a special case, not the goal.

Terms in this lesson

fat interface
An interface with more methods than any one of its callers uses, so implementers fill the gaps with stubs and every caller depends on methods it never calls.
header interface
An interface that copies every public method of one class, so that its size follows the class instead of what any caller needs.
role interface
An interface cut from one interaction between a supplier and its callers, holding only the methods those callers use together; one class often plays several roles.
structural typing
Matching a type to an interface by the methods it has, not by a declaration: a class with the right methods plays the role. Python's protocols and Go's and TypeScript's interfaces work this way.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.