Low-Level Design · lesson 4 of 18
Beginner
4Intermediate
10- 5 Design a Parking Lot: Allocating and Freeing Spots
- 6 Design an Elevator System: Scheduling Requests
- 7 Design a Library Management System: Borrowing and Returning Books
- 8 Design an Expense-Splitting App: Balances and Minimal Settlements
- 9 Design a URL Shortener: Sequential Codes vs Real Hash Collisions
- 10 Wrapping Objects: Adapter, Facade, and Proxy in Python
- 11 The Composite Pattern in Python: Sizing a File Tree Without isinstance
- 12 The State Pattern in Python: Refactoring a Vending Machine Into State Classes
- 13 Undo and Redo in Python: the Memento and Command Patterns Compared
- 14 Chain of Responsibility in Python: Expense Approvals Without an if/elif Ladder
Advanced
4- 15 Concurrency in LLD: A Real Race Condition in the Parking Lot, Then the Fix
- 16 The Observer Pattern in Practice: A Stock Ticker Notifying Multiple Displays
- 17 Design Patterns That Are Just Language Features: Strategy, Command, Iterator
- 18 Abstract Factory and Prototype in Python: Matching Families and Cloning Objects
Also part of Low-Level Design
12- + SOLID Principles in Java from Java
- + Composition vs Inheritance in Java from Java
- + Design Patterns in Java from Java
- + Python Design Patterns from Python
- + Design Patterns in TypeScript from TypeScript
- + JavaScript Design Patterns from JavaScript
- + Design Patterns in C# from C#
- + Design Patterns in C++ from C++
- + Design Patterns in Go from Go
- + Design Patterns in Rust from Rust
- + Abstract Classes in Java from Java
- + Sealed Classes and Interfaces in Java from Java
SOLID in Practice: The Open/Closed Principle
An if/elif area calculator silently drops a new shape's area to zero; a polymorphic one sums it correctly with zero changes to the calculator itself.
By SkillByExample · Published
Adding a new shape should never require editing the class that adds up areas — but it’s an easy mistake to make.
The naive version: an if/elif chain
import math
class Circle:
def __init__(self, radius):
self.radius = radius
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
class AreaCalculator:
def total_area(self, shapes):
total = 0
for shape in shapes:
if isinstance(shape, Circle):
total += math.pi * shape.radius ** 2
elif isinstance(shape, Rectangle):
total += shape.width * shape.height
else:
print(f"Unknown shape: {type(shape).__name__}, treated as 0 area")
return total
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
calc = AreaCalculator()
shapes = [Circle(2), Rectangle(3, 4), Triangle(5, 6)]
print(f"Total area: {calc.total_area(shapes):.2f}")
$ python naive_shapes.py
Unknown shape: Triangle, treated as 0 area
Total area: 24.57
Triangle is a perfectly valid class — it just isn’t one of the two AreaCalculator knows
about. The real triangle area (0.5 * 5 * 6 = 15) silently vanishes from the total instead
of raising an error, because AreaCalculator.total_area has to be told about every shape
that will ever exist, and someone forgot to tell it about this one.
The fix: let each shape compute its own area
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class AreaCalculator:
def total_area(self, shapes):
return sum(shape.area() for shape in shapes)
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
calc = AreaCalculator()
shapes = [Circle(2), Rectangle(3, 4), Triangle(5, 6)]
print(f"Total area: {calc.total_area(shapes):.2f}")
$ python shapes.py
Total area: 39.57
39.57 (pi*4 + 12 + 15) is the correct total, and AreaCalculator didn’t change one
character to get it right — it never even imports math anymore. Every shape is responsible
for its own area, so AreaCalculator can sum shapes it will never be updated to know about
by name.
Practice
1. Add a Square shape without touching AreaCalculator.
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class AreaCalculator:
def total_area(self, shapes):
return sum(shape.area() for shape in shapes)
calc = AreaCalculator()
print(f"Total area: {calc.total_area([Circle(1), Square(4)]):.2f}")
$ python square.py
Total area: 19.14
3.14 + 16 = 19.14 — AreaCalculator.total_area is unchanged from the previous example,
proving the “closed for modification” half of the principle.
2. Use an abstract base class so forgetting area() fails immediately.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class BrokenShape(Shape):
def __init__(self):
pass
try:
b = BrokenShape()
except TypeError as e:
print(f"TypeError: {e}")
c = Circle(2)
print(f"Circle area: {c.area():.2f}")
$ python abstract_shape.py
TypeError: Can't instantiate abstract class BrokenShape without an implementation for abstract method 'area'
Circle area: 12.57
BrokenShape never even gets constructed — Python refuses at BrokenShape(), not later when
something eventually calls .area() on it and hits an AttributeError far from where the
real mistake was made.
3. Add total_perimeter following the same pattern.
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class ShapeCalculator:
def total_area(self, shapes):
return sum(s.area() for s in shapes)
def total_perimeter(self, shapes):
return sum(s.perimeter() for s in shapes)
calc = ShapeCalculator()
shapes = [Circle(2), Rectangle(3, 4)]
print(f"Total area: {calc.total_area(shapes):.2f}")
print(f"Total perimeter: {calc.total_perimeter(shapes):.2f}")
$ python perimeter.py
Total area: 24.57
Total perimeter: 26.57
Same shape, two independent capabilities (area, perimeter) — ShapeCalculator still
never branches on shape type for either one.
Next: a parking lot that has to allocate and free spots of different sizes without ever double-booking one.