Design Patterns, Explained Simply · Foundations: Thinking in Patterns
SOLID, Part 2: Liskov, Interface Segregation, Dependency Inversion
The L, I and D of SOLID: subclasses must keep their parent's promises, interfaces should stay small, and high-level code should depend on abstractions.
The problem
Three bugs from one codebase:
SquareextendsRectangle. Code that sets width 5 and height 4 expects area 20, but on a square it gets 16, because setting the height also changed the width.- A
Printerinterface hasprint(),scan()andfax(). A cheap printer must implementfax()by throwing an exception, and a report job crashes when it calls the wrong method. OrderServicecreates aMySqlDatabaseinside itself. To unit test it you need a real MySQL, and switching to Postgres means editing business logic.
Real-world analogy
- Liskov: if a rental company promises "any car in this class", a car where the brake pedal accelerates breaks the promise, even if it is technically a car.
- Interface segregation: a TV remote with 60 buttons, most of which do nothing for your TV, is worse than a small remote with the buttons you need.
- Dependency inversion: a lamp plugs into a standard socket. Neither the lamp nor the power station knows about the other, only about the socket standard.
The idea
- L: Liskov Substitution Principle (LSP). Anywhere code uses a parent type, a subtype must work without surprises: same promises, no stricter inputs, no weaker results, no new exceptions. "Is-a" in real life (a square is a rectangle) does not guarantee a correct subtype in code.
- I: Interface Segregation Principle (ISP). Clients shouldn't depend on methods they don't use. Prefer several small, focused interfaces (
Printer,Scanner) to one large one. - D: Dependency Inversion Principle (DIP). High-level policy (
OrderService) and low-level details (MySqlDatabase) both depend on an abstraction (OrderStore), and the abstraction belongs to the high-level side. Details are passed in, not created inside.
The code
The demo shows the Liskov violation, then the fix: shapes that don't pretend to share mutable width and height. It finishes with an order service that depends on a small storage abstraction, so an in-memory store can stand in for the database.
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h
def set_height(self, h):
self.h = h
def area(self):
return self.w * self.h
class Square(Rectangle): # violates LSP: changes width when height is set
def set_height(self, h):
self.w = self.h = h
def stretch(rect): # written against Rectangle's promise
rect.set_height(4)
return rect.area()
print("rectangle 5 wide ->", stretch(Rectangle(5, 2)))
print("square 5 ->", stretch(Square(5, 5)), "(caller expected 20)")
# Fix: immutable shapes that only share what is really common
class Rect:
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
class Sq:
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
print("areas:", [s.area() for s in [Rect(5, 4), Sq(4)]])
# Dependency inversion: the service depends on a tiny "store" abstraction
class InMemoryOrders:
def __init__(self):
self.rows = []
def save(self, order):
self.rows.append(order)
class OrderService:
def __init__(self, store): # any object with save() works
self.store = store
def place(self, item):
self.store.save(item)
return f"placed {item}"
store = InMemoryOrders()
print(OrderService(store).place("book"), "| saved:", len(store.rows))
Output
rectangle 5 wide -> 20 square 5 -> 16 (caller expected 20) areas: [20, 16] placed book | saved: 1
class Rectangle {
constructor(public w: number, public h: number) {}
setHeight(h: number) { this.h = h; }
area() { return this.w * this.h; }
}
class Square extends Rectangle { // violates LSP: changes width when height is set
setHeight(h: number) { this.w = this.h = h; }
}
function stretch(rect: Rectangle): number { // written against Rectangle's promise
rect.setHeight(4);
return rect.area();
}
console.log("rectangle 5 wide ->", stretch(new Rectangle(5, 2)));
console.log("square 5 ->", stretch(new Square(5, 5)), "(caller expected 20)");
// Fix: immutable shapes that only share what is really common
interface Shape { area(): number; }
class Rect implements Shape {
constructor(readonly w: number, readonly h: number) {}
area() { return this.w * this.h; }
}
class Sq implements Shape {
constructor(readonly side: number) {}
area() { return this.side * this.side; }
}
const shapes: Shape[] = [new Rect(5, 4), new Sq(4)];
console.log("areas:", `[${shapes.map((s) => s.area()).join(", ")}]`);
// Dependency inversion: the service depends on a tiny "store" abstraction
interface OrderStore { save(order: string): void; }
class InMemoryOrders implements OrderStore {
rows: string[] = [];
save(order: string) { this.rows.push(order); }
}
class OrderService {
constructor(private store: OrderStore) {}
place(item: string) {
this.store.save(item);
return `placed ${item}`;
}
}
const store = new InMemoryOrders();
console.log(new OrderService(store).place("book"), "| saved:", store.rows.length);
Output
rectangle 5 wide -> 20 square 5 -> 16 (caller expected 20) areas: [20, 16] placed book | saved: 1
import java.util.ArrayList;
import java.util.List;
public class Main {
static int stretch(Rectangle rect) { // written against Rectangle's promise
rect.setHeight(4);
return rect.area();
}
public static void main(String[] args) {
System.out.println("rectangle 5 wide -> " + stretch(new Rectangle(5, 2)));
System.out.println("square 5 -> " + stretch(new Square(5)) + " (caller expected 20)");
List<Shape> shapes = List.of(new Rect(5, 4), new Sq(4));
System.out.println("areas: " + shapes.stream().map(Shape::area).toList());
var store = new InMemoryOrders();
System.out.println(new OrderService(store).place("book") + " | saved: " + store.rows.size());
}
}
class Rectangle {
protected int w, h;
Rectangle(int w, int h) { this.w = w; this.h = h; }
void setHeight(int h) { this.h = h; }
int area() { return w * h; }
}
class Square extends Rectangle { // violates LSP: changes width when height is set
Square(int side) { super(side, side); }
@Override void setHeight(int h) { this.w = h; this.h = h; }
}
// Fix: immutable shapes that only share what is really common
interface Shape { int area(); }
record Rect(int w, int h) implements Shape { public int area() { return w * h; } }
record Sq(int side) implements Shape { public int area() { return side * side; } }
// Dependency inversion: the service depends on a tiny "store" abstraction
interface OrderStore { void save(String order); }
class InMemoryOrders implements OrderStore {
final List<String> rows = new ArrayList<>();
public void save(String order) { rows.add(order); }
}
class OrderService {
private final OrderStore store;
OrderService(OrderStore store) { this.store = store; }
String place(String item) {
store.save(item);
return "placed " + item;
}
}
Output
rectangle 5 wide -> 20 square 5 -> 16 (caller expected 20) areas: [20, 16] placed book | saved: 1
Run this on your computer — see “Get Started” for setup.
<?php
class Rectangle {
public function __construct(protected int $w, protected int $h) {}
public function setHeight(int $h): void { $this->h = $h; }
public function area(): int { return $this->w * $this->h; }
}
class Square extends Rectangle { // violates LSP: changes width when height is set
public function __construct(int $side) { parent::__construct($side, $side); }
public function setHeight(int $h): void { $this->w = $this->h = $h; }
}
function stretch(Rectangle $rect): int { // written against Rectangle's promise
$rect->setHeight(4);
return $rect->area();
}
echo "rectangle 5 wide -> ", stretch(new Rectangle(5, 2)), "\n";
echo "square 5 -> ", stretch(new Square(5)), " (caller expected 20)\n";
// Fix: immutable shapes that only share what is really common
interface Shape { public function area(): int; }
final class Rect implements Shape {
public function __construct(public readonly int $w, public readonly int $h) {}
public function area(): int { return $this->w * $this->h; }
}
final class Sq implements Shape {
public function __construct(public readonly int $side) {}
public function area(): int { return $this->side * $this->side; }
}
$shapes = [new Rect(5, 4), new Sq(4)];
echo "areas: [", implode(", ", array_map(fn(Shape $s) => $s->area(), $shapes)), "]\n";
// Dependency inversion: the service depends on a tiny "store" abstraction
interface OrderStore { public function save(string $order): void; }
class InMemoryOrders implements OrderStore {
public array $rows = [];
public function save(string $order): void { $this->rows[] = $order; }
}
class OrderService {
public function __construct(private OrderStore $store) {}
public function place(string $item): string {
$this->store->save($item);
return "placed $item";
}
}
$store = new InMemoryOrders();
echo (new OrderService($store))->place("book"), " | saved: ", count($store->rows), "\n";
Output
rectangle 5 wide -> 20 square 5 -> 16 (caller expected 20) areas: [20, 16] placed book | saved: 1
stretch() was written for rectangles and returns 16 for a square instead of 20. Nothing crashed: the bug is a silently wrong answer, which is why LSP violations are hard to spot.
Common mistakes
- Modelling real-world taxonomy instead of behaviour. "A square is a rectangle" is true in geometry but false for a mutable
Rectangleclass. Subtype by behaviour (promises), not by vocabulary. - Throwing "not supported" from an overridden method. That is an LSP and ISP smell: the interface is too wide for this class.
- Confusing dependency inversion with dependency injection. DIP is the design rule (depend on abstractions you own). DI is one technique for supplying them (the next lesson).
Key points
- LSP: a subtype must keep every promise of its parent. If callers need
instanceofchecks, the hierarchy is wrong. - ISP: small, client-focused interfaces beat one large interface.
- DIP: business logic depends on abstractions; details are plugged in from outside.
- Together these make code easy to test (swap in fakes) and easy to change (swap implementations).
- Further reading: Liskov substitution principle and SOLID.
Practice questions
Why is `Square extends Rectangle` the classic Liskov violation?
Answer
A mutable Rectangle promises that setting the height leaves the width unchanged. A Square must keep both sides equal, so it breaks that promise. Code written for rectangles (set width 5, height 4, expect area 20) gets 16. The fix is to model shapes by what they really share, such as an immutable Shape with area().
What is a sign that an interface violates the Interface Segregation Principle?
Answer
Implementations that leave methods empty or throw "not supported", and clients that use only a small part of the interface. Split it into smaller interfaces focused on what each client needs.
What is the difference between dependency inversion and dependency injection?
Answer
Dependency inversion is a design principle: high-level code depends on abstractions it owns, not on low-level details. Dependency injection is a technique: dependencies are passed in (constructor, setter or framework container) instead of created inside. DI is the usual way to apply DIP.
How do the SOLID principles make unit testing easier?
Answer
Because classes depend on small abstractions that are passed in, a test can supply fakes (an in-memory store, a fixed clock) instead of real databases or network calls, and each class has one responsibility to test.
Open in the app to mark lessons complete, take notes and track your progress.