Card 03/ 08
Refactoring to Interface Segregation, Step by Step
Split Worker by what each caller uses, then combine pieces where a caller needs several.
An interface is split by looking at its clients, not at its methods. This card takes the Worker interface from the violation and splits it into Workable, Feedable and Restable, with a combined ShiftWorker for the one caller that needs two of them. The robot then implements only work, and nothing raises.
Splitting Worker by its clients
List each client and what it calls
The assembly line calls work. The lunch break calls eat. The shift planner calls work and then eat. Nothing in this program calls sleep yet, but the human can sleep.
Give each group of calls its own interface
Create Workable with work, Feedable with eat and Restable with sleep. Each one is the exact view one client needs.
Combine where a client needs several
The shift planner needs both work and eat, so declare ShiftWorker as an interface that extends Workable and Feedable. Combining small interfaces is cheap; splitting a wide one later is not.
Re-declare the implementers and narrow the callers
Human implements ShiftWorker and Restable. Robot implements Workable only, and its stubs are deleted. Each function now takes the narrowest interface it uses.
In the final code below, each function names the smallest interface it needs. In Python the interfaces are Protocol classes, so Human and Robot match them by shape without inheriting from them.
Worker split into Workable, Feedable and Restable
from typing import Protocol
class Workable(Protocol):
def work(self) -> str: ...
class Feedable(Protocol):
def eat(self) -> str: ...
class Restable(Protocol):
def sleep(self) -> str: ...
class ShiftWorker(Workable, Feedable, Protocol):
pass
class Human:
def work(self) -> str:
return "Human assembles a chair"
def eat(self) -> str:
return "Human has lunch"
def sleep(self) -> str:
return "Human sleeps"
class Robot:
def work(self) -> str:
return "Robot assembles a chair"
def run_line(workers: list[Workable]) -> None:
for worker in workers:
print(worker.work())
def lunch_break(workers: list[Feedable]) -> None:
for worker in workers:
print(worker.eat())
def plan_shift(worker: ShiftWorker) -> None:
print(f"{worker.work()}, then {worker.eat().lower()}")
run_line([Human(), Robot()])
lunch_break([Human()])
plan_shift(Human())Human assembles a chair
Robot assembles a chair
Human has lunch
Human assembles a chair, then human has lunchimport java.util.List;
public class IspWorkerSplit {
static void runLine(List<? extends Workable> workers) {
for (Workable worker : workers) System.out.println(worker.work());
}
static void lunchBreak(List<? extends Feedable> workers) {
for (Feedable worker : workers) System.out.println(worker.eat());
}
static void planShift(ShiftWorker worker) {
System.out.println(worker.work() + ", then " + worker.eat().toLowerCase());
}
public static void main(String[] args) {
runLine(List.of(new Human(), new Robot()));
lunchBreak(List.of(new Human()));
planShift(new Human());
}
}
interface Workable {
String work();
}
interface Feedable {
String eat();
}
interface Restable {
String sleep();
}
interface ShiftWorker extends Workable, Feedable {}
class Human implements ShiftWorker, Restable {
public String work() { return "Human assembles a chair"; }
public String eat() { return "Human has lunch"; }
public String sleep() { return "Human sleeps"; }
}
class Robot implements Workable {
public String work() { return "Robot assembles a chair"; }
}Human assembles a chair
Robot assembles a chair
Human has lunch
Human assembles a chair, then human has lunchinterface Workable {
work(): string;
}
interface Feedable {
eat(): string;
}
interface Restable {
sleep(): string;
}
interface ShiftWorker extends Workable, Feedable {}
class Human implements ShiftWorker, Restable {
work(): string { return "Human assembles a chair"; }
eat(): string { return "Human has lunch"; }
sleep(): string { return "Human sleeps"; }
}
class Robot implements Workable {
work(): string { return "Robot assembles a chair"; }
}
function runLine(workers: Workable[]): void {
for (const worker of workers) console.log(worker.work());
}
function lunchBreak(workers: Feedable[]): void {
for (const worker of workers) console.log(worker.eat());
}
function planShift(worker: ShiftWorker): void {
console.log(`${worker.work()}, then ${worker.eat().toLowerCase()}`);
}
runLine([new Human(), new Robot()]);
lunchBreak([new Human()]);
planShift(new Human());Human assembles a chair
Robot assembles a chair
Human has lunch
Human assembles a chair, then human has lunchThe robot works on the line beside the human, and only the human goes to lunch. Passing a robot to lunch_break is now a compile error in Java and TypeScript, and a type checker error in Python, rather than an exception at run time.
Notice what the split did not require: no method changed and no behaviour moved. Interface segregation is often the cheapest SOLID refactoring, because it changes declarations, not logic. The work is in finding the clients, which is why the first step is a list of callers rather than a list of methods.
← → move through sections and cards