Interface Segregation: split a fat interface
Give each caller an interface with only the methods it calls.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–10Payoff
Steps 11–12Recap
Steps 13–14The code: before and after
Before
interface Inventory {
int stockOf(String sku);
void reserve(String sku, int qty);
void restock(String sku, int qty);
}
class WarehouseInventory implements Inventory {
private final Map<String, Integer> stock =
new HashMap<>(Map.of("mug", 12));
public int stockOf(String sku) {
return stock.get(sku);
}
public void reserve(String sku, int qty) {
stock.put(sku, stock.get(sku) - qty);
System.out.println("reserved " + qty + " " + sku);
}
public void restock(String sku, int qty) {
stock.put(sku, stock.get(sku) + qty);
System.out.println("restocked " + qty + " " + sku);
}
}
class ProductPage {
void show(Inventory inventory, String sku) {
int count = inventory.stockOf(sku);
System.out.println(sku + ": " + count + " in stock");
}
}
class Checkout {
void buy(Inventory inventory, String sku, int qty) {
inventory.reserve(sku, qty);
}
}
class Receiving {
void receive(Inventory inventory, String sku, int qty) {
inventory.restock(sku, qty);
}
}
class Shop {
public static void main(String[] args) {
WarehouseInventory warehouse = new WarehouseInventory();
new ProductPage().show(warehouse, "mug");
new Checkout().buy(warehouse, "mug", 2);
new Receiving().receive(warehouse, "mug", 5);
}
}After
interface StockLevels {
int stockOf(String sku);
}
interface Reservations {
void reserve(String sku, int qty);
}
interface Restocking {
void restock(String sku, int qty);
}
interface Inventory extends StockLevels, Reservations, Restocking {}
class WarehouseInventory implements Inventory {
private final Map<String, Integer> stock =
new HashMap<>(Map.of("mug", 12));
public int stockOf(String sku) {
return stock.get(sku);
}
public void reserve(String sku, int qty) {
stock.put(sku, stock.get(sku) - qty);
System.out.println("reserved " + qty + " " + sku);
}
public void restock(String sku, int qty) {
stock.put(sku, stock.get(sku) + qty);
System.out.println("restocked " + qty + " " + sku);
}
}
record CachedStock(Map<String, Integer> counts) implements StockLevels {
public int stockOf(String sku) {
return counts.get(sku);
}
}
class ProductPage {
void show(StockLevels levels, String sku) {
int count = levels.stockOf(sku);
System.out.println(sku + ": " + count + " in stock");
}
}
class Checkout {
void buy(Reservations reservations, String sku, int qty) {
reservations.reserve(sku, qty);
}
}
class Receiving {
void receive(Restocking restocking, String sku, int qty) {
restocking.restock(sku, qty);
}
}
class Shop {
public static void main(String[] args) {
WarehouseInventory warehouse = new WarehouseInventory();
CachedStock cache = new CachedStock(Map.of("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
Inventory is an interface with three methods, stock of, reserve and restock. Warehouse Inventory implements all three over a map, 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 that a nightly job fills. The quick way adds Cached Stock, a record over a map, and makes it implement Inventory. Its stock of reads the map, which is all the page needs. But Inventory also demands reserve and restock, so Cached Stock throws Unsupported Operation 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
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 throw, because Product Page asks for an Inventory here. You might think the stubs are harmless, because the page never calls them. So what happens if Shop hands the cache to Checkout instead? It compiles, because Checkout asks for an Inventory, and it throws on the first sale. 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 an interface of its own called Stock Levels. Inventory extends Stock Levels and drops its own copy, so it still offers 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
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 a Warehouse Inventory is a Stock Levels through Inventory.
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 extends Stock Levels and Reservations, so it still offers the same three methods.
Step 8: Checkout asks for Reservations 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. Inventory now declares nothing of its own, and it only joins the three small interfaces together.
Step 10: Receiving asks for Restocking Change Function Declaration
Receiving only restocks, so its parameter becomes a Restocking. Warehouse Inventory still implements Inventory, and its code hasn't changed since we started. 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 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. Cached Stock implements one interface here, writes one method here, and 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. That's the Interface Segregation Principle, with each caller seeing only the methods it calls. But the split has a cost: 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 Inventory 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 both find and save. You'll spot it by stubs that just throw, like Cached Stock's reserve, or by test fakes stubbing methods the test never calls. That's why try with resources asks only for Auto Closeable, because a Scanner has many methods but the try only calls close.