I'll Be Backend
Principles · C#

Interface Segregation: split a fat interface

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

14 steps4 chapters~5 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 C# 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

interface IInventory
{
    int StockOf(string sku);
    void Reserve(string sku, int qty);
    void Restock(string sku, int qty);
}

class WarehouseInventory : IInventory
{
    private readonly Dictionary<string, int> stock =
        new() { ["mug"] = 12 };

    public int StockOf(string sku) => stock[sku];

    public void Reserve(string sku, int qty)
    {
        stock[sku] -= qty;
        Console.WriteLine($"reserved {qty} {sku}");
    }

    public void Restock(string sku, int qty)
    {
        stock[sku] += qty;
        Console.WriteLine($"restocked {qty} {sku}");
    }
}

class ProductPage
{
    public void Show(IInventory inventory, string sku) =>
        Console.WriteLine($"{sku}: {inventory.StockOf(sku)} in stock");
}

class Checkout
{
    public void Buy(IInventory inventory, string sku, int qty) =>
        inventory.Reserve(sku, qty);
}

class Receiving
{
    public void Receive(IInventory inventory, string sku, int qty) =>
        inventory.Restock(sku, qty);
}

class Shop
{
    public static void Main()
    {
        var warehouse = new WarehouseInventory();
        new ProductPage().Show(warehouse, "mug");
        new Checkout().Buy(warehouse, "mug", 2);
        new Receiving().Receive(warehouse, "mug", 5);
    }
}

After

interface IStockLevels
{
    int StockOf(string sku);
}

interface IReservations
{
    void Reserve(string sku, int qty);
}

interface IRestocking
{
    void Restock(string sku, int qty);
}

interface IInventory : IStockLevels, IReservations, IRestocking { }

class WarehouseInventory : IInventory
{
    private readonly Dictionary<string, int> stock =
        new() { ["mug"] = 12 };

    public int StockOf(string sku) => stock[sku];

    public void Reserve(string sku, int qty)
    {
        stock[sku] -= qty;
        Console.WriteLine($"reserved {qty} {sku}");
    }

    public void Restock(string sku, int qty)
    {
        stock[sku] += qty;
        Console.WriteLine($"restocked {qty} {sku}");
    }
}

class CachedStock(Dictionary<string, int> counts) : IStockLevels
{
    public int StockOf(string sku) => counts[sku];
}

class ProductPage
{
    public void Show(IStockLevels levels, string sku) =>
        Console.WriteLine($"{sku}: {levels.StockOf(sku)} in stock");
}

class Checkout
{
    public void Buy(IReservations reservations, string sku, int qty) =>
        reservations.Reserve(sku, qty);
}

class Receiving
{
    public void Receive(IRestocking restocking, string sku, int qty) =>
        restocking.Restock(sku, qty);
}

class Shop
{
    public static void Main()
    {
        var warehouse = new WarehouseInventory();
        var cache = new CachedStock(new() { ["mug"] = 12 });
        new ProductPage().Show(cache, "mug");
        new Checkout().Buy(warehouse, "mug", 2);
        new Receiving().Receive(warehouse, "mug", 5);
    }
}

Transcript, step by step

Problem

Step 1: A shop that tracks stock Before

The Inventory interface has three methods, stock of, reserve and restock. Warehouse Inventory implements them over a dictionary, and it starts with twelve mugs. Three small classes call it, and each one needs a different method. Product Page calls only stock of, Checkout calls only reserve, and Receiving calls only restock. Shop hands the warehouse to all three callers, 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 class over a dictionary, and makes it implement the Inventory interface. Its stock of reads the dictionary, which is all the page needs. But the interface also demands reserve and restock, so what can a read-only cache put in them? Nothing useful, so Cached Stock throws Not Supported Exception from both. Shop now hands the cache to the product page, and it still prints mug, twelve in stock.

Step 3: A fat interface Code smell

The Inventory interface 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 throw, because Product Page asks for the whole interface here. You might think the stubs are harmless, because the page never calls them. But Checkout also takes the Inventory interface, so handing it the cache compiles and throws at runtime. The rule that gives each caller an interface with only the methods it calls is the Interface Segregation Principle. So now the page asks for every method, and the cache pays for two it never uses.

Refactor

Step 4: Refactor first Back it out

So Cached Stock comes back out for now, and the shop runs on the warehouse alone again. Then we split the interface 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 an interface of its own called Stock Levels. The Inventory interface extends Stock Levels and drops its own copy, so it still offers all three methods. Warehouse Inventory doesn't change, and nothing asks for Stock Levels yet.

Step 6: The page asks for IStockLevels Change Function Declaration

Product Page's parameter becomes a Stock Levels, because stock of is the only method it calls. Shop still passes the warehouse, and that compiles, because Warehouse Inventory implements Stock Levels through the Inventory interface.

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. The Inventory interface now extends Stock Levels and Reservations, so it still offers the same three methods.

Step 8: Checkout asks for IReservations Change Function Declaration

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

Step 9: Restocking on its own Extract Interface

But Receiving still asks for all three, so restock moves into an interface called Restocking. The Inventory interface now declares nothing, but the warehouse keeps it as one name for all three, and old callers still compile.

Step 10: Receiving asks for IRestocking Change Function Declaration

Receiving only restocks, so its parameter becomes a Restocking. Warehouse Inventory still implements the Inventory interface, and its code hasn't changed since we started. Each caller now asks only for the interface it calls, and the warehouse still fits every one of them.

Payoff

Step 11: The cache comes back Payoff

So the cache from the start of the video can come back in. Cached Stock implements Stock Levels alone, so it writes one method and has nothing to throw. Shop 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 Receiving didn't change to add the cache. Product Page asks only for Stock Levels here. But the split has a cost, because anyone reading the code must now find the right one of four interfaces.

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 the Inventory interface only joins them. Cached Stock implements Stock Levels alone, which is all the page asks for.

Step 14: When to reach for it Takeaways

Reach for it when an implementation has to stub or throw in methods its callers never call. Skip it when every caller uses the whole interface, like a small repository whose one service calls find and save. You'll spot it by methods that only throw, like Cached Stock's reserve, or by fakes that stub unused methods. The using statement in C sharp asks only for I Disposable, an interface with one method, Dispose.