I'll Be Backend
Principles · Python

Interface Segregation: split a fat interface

Give each caller an interface with only the methods it calls.

14 steps4 chapters~6 min at 1.25×#solid#interfaces#isp#decoupling
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the Python video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–10

Payoff

Steps 11–12

Recap

Steps 13–14

The code: before and after

Before

class Inventory(ABC):
    @abstractmethod
    def stock_of(self, sku: str) -> int: ...

    @abstractmethod
    def reserve(self, sku: str, qty: int) -> None: ...

    @abstractmethod
    def restock(self, sku: str, qty: int) -> None: ...


class WarehouseInventory(Inventory):
    def __init__(self) -> None:
        self._counts = {"mug": 12}

    def stock_of(self, sku: str) -> int:
        return self._counts[sku]

    def reserve(self, sku: str, qty: int) -> None:
        self._counts[sku] -= qty
        print(f"reserved {qty} {sku}")

    def restock(self, sku: str, qty: int) -> None:
        self._counts[sku] += qty
        print(f"restocked {qty} {sku}")


def product_page(stock: Inventory, sku: str) -> None:
    print(f"{sku}: {stock.stock_of(sku)} in stock")


def checkout(stock: Inventory, sku: str, qty: int) -> None:
    stock.reserve(sku, qty)


def receive(stock: Inventory, sku: str, qty: int) -> None:
    stock.restock(sku, qty)


if __name__ == "__main__":
    warehouse = WarehouseInventory()
    product_page(warehouse, "mug")
    checkout(warehouse, "mug", 2)
    receive(warehouse, "mug", 5)

After

class StockLevels(ABC):
    @abstractmethod
    def stock_of(self, sku: str) -> int: ...


class Reservations(ABC):
    @abstractmethod
    def reserve(self, sku: str, qty: int) -> None: ...


class Restocking(ABC):
    @abstractmethod
    def restock(self, sku: str, qty: int) -> None: ...


class Inventory(StockLevels, Reservations, Restocking):
    pass


class WarehouseInventory(Inventory):
    def __init__(self) -> None:
        self._counts = {"mug": 12}

    def stock_of(self, sku: str) -> int:
        return self._counts[sku]

    def reserve(self, sku: str, qty: int) -> None:
        self._counts[sku] -= qty
        print(f"reserved {qty} {sku}")

    def restock(self, sku: str, qty: int) -> None:
        self._counts[sku] += qty
        print(f"restocked {qty} {sku}")


@dataclass
class CachedStock(StockLevels):
    counts: dict[str, int]

    def stock_of(self, sku: str) -> int:
        return self.counts[sku]


def product_page(stock: StockLevels, sku: str) -> None:
    print(f"{sku}: {stock.stock_of(sku)} in stock")


def checkout(stock: Reservations, sku: str, qty: int) -> None:
    stock.reserve(sku, qty)


def receive(stock: Restocking, sku: str, qty: int) -> None:
    stock.restock(sku, qty)


if __name__ == "__main__":
    warehouse = WarehouseInventory()
    cache = CachedStock({"mug": 12})
    product_page(cache, "mug")
    checkout(warehouse, "mug", 2)
    receive(warehouse, "mug", 5)

Transcript, step by step

Problem

Step 1: A shop that tracks stock Before

Inventory is the interface here, an abstract base class with three methods, stock of, reserve and restock. Warehouse Inventory implements all three over a dictionary, and it starts with twelve mugs. Three functions call it, but each one calls only one method: the page stock of, checkout reserve, and receive restock. The main block hands the warehouse to all three, and the page prints mug, twelve in stock. Every caller so far talks to the warehouse, so one interface for all of them looked fine.

Step 2: The page reads a cache Code smell

The product page gets heavy traffic, so it must read stock from a cache instead of the warehouse. The quick way adds Cached Stock, a dataclass over a dictionary, and makes it subclass Inventory. Its stock of reads the dictionary, which is all the page needs. But Inventory marks reserve and restock abstract too, and Python won't create a Cached Stock until it overrides both. A read-only cache has nothing useful to put in them, so Cached Stock raises Type Error from both. The main block now hands the cache to the product page, and it still prints mug, twelve in stock.

Step 3: A fat interface Code smell

Inventory is a fat interface, which means it has more methods than any one caller uses. Cached Stock writes reserve here and restock here only to raise, because the product page asks for an Inventory here. You might think the stubs are harmless, because the page never calls them. So what happens if the main block hands the cache to checkout instead? Checkout's type hint also says Inventory, so a type checker accepts the cache, and the first sale raises Type Error. The rule that gives each caller an interface with only the methods it calls is the Interface Segregation Principle.

Refactor

Step 4: Refactor first Back it out

We take Cached Stock back out, so the shop runs on the warehouse alone again. Then we split Inventory one caller at a time, and the cache comes back once the page asks for less.

Step 5: Stock levels on their own Extract Interface

First, stock of moves into a small interface of its own called Stock Levels. Inventory subclasses Stock Levels and drops its own copy, so it still has all three methods. Warehouse Inventory doesn't change, and nothing asks for a Stock Levels yet.

Step 6: The page asks for StockLevels Change Function Declaration

The product page's parameter becomes a Stock Levels, because stock of is the only method it calls. The main block still passes the warehouse, and that type checks, because a Warehouse Inventory is a Stock Levels through Inventory. So now the page needs only stock of, and checkout and receive still ask for all three.

Step 7: Reservations on their own Extract Interface

But checkout still asks for all three methods, so reserve moves into a second small interface called Reservations. Inventory now subclasses Stock Levels and Reservations, so it still has the same three methods.

Step 8: Checkout asks for Reservations Change Function Declaration

Checkout only reserves, so its parameter becomes a Reservations. The main block passes the same warehouse to it, and the program still prints reserved two mug.

Step 9: Restocking on its own Extract Interface

But receive still asks for all three, so restock moves into an interface called Restocking. Each one is a role interface, an interface shaped by what one caller does. Inventory now declares nothing of its own, and it only joins the three small interfaces together.

Step 10: Receive asks for Restocking Change Function Declaration

Receive only restocks, so its parameter becomes a Restocking. Warehouse Inventory still subclasses Inventory, and its code hasn't changed since we started. We keep the empty Inventory, because that one name on the warehouse says it fills all three interfaces. Each caller now asks for the one interface it calls, and the warehouse fits all three.

Payoff

Step 11: The cache comes back Payoff

So the cache from the start of the video can come back in. Cached Stock subclasses Stock Levels alone, so it writes one method and has nothing to raise. The main block hands it to the page, and the page prints mug, twelve in stock, like before.

Step 12: Nothing else changed Payoff

Warehouse Inventory, checkout and receive didn't change to add the cache. Cached Stock subclasses one interface here, writes one method here, and the product page asks only for Stock Levels here. The cache has no stubs left, because no caller makes it carry a method it never uses. Hand the cache to checkout now, and the type checker rejects it, because Cached Stock is not a Reservations. That's the Interface Segregation Principle, with each caller seeing only the methods it calls. But the split has a cost: a new caller that reads and reserves needs the whole Inventory, or a fifth interface. And the shop now has four classes where it had one.

Recap

Step 13: Where we started, where we ended Before / after

On the left, one Inventory interface gave every caller all three methods. So anything handed to the product page had to write reserve and restock as well. On the right, each method has its own small interface, and Inventory only joins them. Cached Stock subclasses Stock Levels alone, which is all the page asks for.

Step 14: When to reach for it Takeaways

Reach for it when an implementation raises in methods its callers never call, or each caller uses a different slice. Skip it when every caller uses the whole interface, like a small repository whose one service calls both find and save. You'll spot it by stubs that only raise, like Cached Stock's reserve, or by test fakes stubbing methods the test never calls. Python's own collections dot abc does this, with one method in Sized, and Collection joining Sized, Iterable and Container.