Low-Level Design · lesson 18 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
Abstract Factory and Prototype in Python: Matching Families and Cloning Objects
Fix a dark dialog paired with a light button with one Abstract Factory per theme, then clone costly objects with Prototype and avoid shallow copies.
By SkillByExample · Published
Two creational patterns solve two different problems: making sure objects that belong together are created together, and creating many similar objects without paying the full setup cost each time.
Widgets from two themes, mixed by accident
Each screen creates its widget classes directly. One was copied from an older screen.
# themes_mixed.py: screens create widget classes directly, and one picks the wrong theme
class LightButton:
def render(self): return "[ OK ] (light)"
class DarkButton:
def render(self): return "[ OK ] (dark)"
class LightDialog:
def render(self): return "| Save changes? | (light)"
class DarkDialog:
def render(self): return "| Save changes? | (dark)"
def settings_screen(): # written for the dark theme
return [DarkDialog(), DarkButton()]
def save_prompt(): # also meant to be dark, copied from an old screen
return [DarkDialog(), LightButton()]
for name, screen in [("settings", settings_screen()), ("save prompt", save_prompt())]:
print(f"{name}:", " ".join(w.render() for w in screen))
$ python themes_mixed.py
settings: | Save changes? | (dark) [ OK ] (dark)
save prompt: | Save changes? | (dark) [ OK ] (light)
The save prompt shows a dark dialog with a light button. Nothing failed, because both classes have
a render() method; the mistake is only visible on screen. Every function that names widget
classes is another place to pick the wrong one.
Abstract Factory: one factory per family
A factory has a creation method for each widget, and each theme is its own factory. Screens ask the factory for widgets and never name a class.
# abstract_factory.py: one factory per theme creates every widget, so a screen can't mix them
from abc import ABC, abstractmethod
class LightButton:
def render(self): return "[ OK ] (light)"
class DarkButton:
def render(self): return "[ OK ] (dark)"
class LightDialog:
def render(self): return "| Save changes? | (light)"
class DarkDialog:
def render(self): return "| Save changes? | (dark)"
class ThemeFactory(ABC):
@abstractmethod
def button(self): ...
@abstractmethod
def dialog(self): ...
class LightTheme(ThemeFactory):
def button(self): return LightButton()
def dialog(self): return LightDialog()
class DarkTheme(ThemeFactory):
def button(self): return DarkButton()
def dialog(self): return DarkDialog()
def save_prompt(theme): # asks the factory; never names a widget class
return [theme.dialog(), theme.button()]
for theme in (LightTheme(), DarkTheme()):
print(f"{type(theme).__name__}:", " ".join(w.render() for w in save_prompt(theme)))
$ python abstract_factory.py
LightTheme: | Save changes? | (light) [ OK ] (light)
DarkTheme: | Save changes? | (dark) [ OK ] (dark)
save_prompt now takes a theme and calls theme.dialog() and theme.button(). Whichever factory
it gets, the two widgets come from the same family, so the mix-up can’t happen. Switching the whole
app to dark mode is passing a different factory. The abstract base class also makes Python refuse
a new theme that forgets one of the widgets.
The cost is a factory class per family and a method per product. If an app has one theme, plain constructors are fine. If it only needs to vary one product, a factory method is enough.
Prototype: build once, then clone
Some objects are expensive to construct. Prototype builds one fully configured instance, then copies it.
# prototype_costly.py: building a report template is slow; cloning a configured one is not
import copy, time
class ReportTemplate:
def __init__(self):
time.sleep(0.2) # stands in for loading fonts and a style sheet
self.styles = {f"style{i}": {"size": 10 + i % 4} for i in range(50)}
self.sections = ["Summary", "Details"]
self.title = "Untitled"
start = time.perf_counter()
built = [ReportTemplate() for month in ["Jan", "Feb", "Mar"]]
print(f"3 built from scratch: {time.perf_counter() - start:.1f}s")
start = time.perf_counter()
prototype = ReportTemplate() # pay the setup once
reports = []
for month in ["Jan", "Feb", "Mar"]:
r = copy.deepcopy(prototype) # a clone skips __init__ entirely
r.title = f"Sales {month}"
reports.append(r)
print(f"1 built + 3 clones: {time.perf_counter() - start:.1f}s")
print("titles:", [r.title for r in reports])
$ python prototype_costly.py
3 built from scratch: 0.6s
1 built + 3 clones: 0.2s
titles: ['Sales Jan', 'Sales Feb', 'Sales Mar']
Building three templates paid the 0.2-second setup three times. Building one and cloning it
paid it once, because copy.deepcopy doesn’t call __init__; each clone only needed its title
changed. Cloning has a cost too: deepcopy still copies the 50 style entries per report, which is
why it only wins when the setup does slow work the copy can skip, such as reading files or
querying a database. For a constructor that only builds a dict, just call it.
The shallow-copy trap
The clone has to be independent of the prototype. copy.copy isn’t.
# prototype_shallow.py: a shallow copy shares the list inside, so the clone edits the original
import copy
class ReportTemplate:
def __init__(self):
self.sections = ["Summary", "Details"]
original = ReportTemplate()
shallow = copy.copy(original)
shallow.sections.append("Appendix") # meant for this one report only
print("shallow clone:", shallow.sections)
print("original: ", original.sections, "<- changed too")
print("same list object?", shallow.sections is original.sections)
original = ReportTemplate()
deep = copy.deepcopy(original)
deep.sections.append("Appendix")
print("deep clone: ", deep.sections)
print("original: ", original.sections)
print("same list object?", deep.sections is original.sections)
$ python prototype_shallow.py
shallow clone: ['Summary', 'Details', 'Appendix']
original: ['Summary', 'Details', 'Appendix'] <- changed too
same list object? True
deep clone: ['Summary', 'Details', 'Appendix']
original: ['Summary', 'Details']
same list object? False
copy.copy created a new ReportTemplate, but its sections attribute is the same list object as
the original’s, so appending through the clone changed both. copy.deepcopy copies the list too.
Any prototype holding lists, dicts or other mutable objects needs deepcopy, or a custom
__deepcopy__ (Practice 3).
Practice
1. Add a high-contrast theme. How much of save_prompt has to change?
# p23_1.py: a high-contrast theme is one new factory; save_prompt doesn't change
class HighContrastButton:
def render(self): return "[[ OK ]] (high contrast)"
class HighContrastDialog:
def render(self): return "|| Save changes? || (high contrast)"
class HighContrastTheme:
def button(self): return HighContrastButton()
def dialog(self): return HighContrastDialog()
def save_prompt(theme):
return [theme.dialog(), theme.button()]
print(" ".join(w.render() for w in save_prompt(HighContrastTheme())))
$ python p23_1.py
|| Save changes? || (high contrast) [[ OK ]] (high contrast)
None of it. A new family is one factory class plus its widgets; every screen that takes a theme gets the new look without any edits. (Duck typing means this short version doesn’t need the ABC, but keeping it would catch a missing method.)
2. Keep a registry of named email templates, and write clone(name, **changes). Check that changing a clone leaves the registry alone.
# p23_2.py: a registry of named email templates; clone() hands out independent copies
import copy
class EmailTemplate:
def __init__(self, subject, paragraphs):
self.subject, self.paragraphs = subject, paragraphs
registry = {
"welcome": EmailTemplate("Welcome aboard", ["Thanks for signing up.", "Here's how to start."]),
"receipt": EmailTemplate("Your receipt", ["Payment received."]),
}
def clone(name, **changes):
email = copy.deepcopy(registry[name])
for field, value in changes.items():
setattr(email, field, value)
return email
a = clone("welcome")
b = clone("welcome", subject="Welcome back")
b.paragraphs.append("We missed you.")
for label, e in [("a", a), ("b", b), ("registry", registry["welcome"])]:
print(f"{label:>8}: {e.subject!r}, {len(e.paragraphs)} paragraphs")
$ python p23_2.py
a: 'Welcome aboard', 2 paragraphs
b: 'Welcome back', 3 paragraphs
registry: 'Welcome aboard', 2 paragraphs
Clone b got a new subject and a third paragraph; a and the registry’s template still have the
original subject and two paragraphs. deepcopy made the paragraphs list independent for every
clone.
3. Give each cloned ticket its own id by defining __deepcopy__.
# p23_3.py: __deepcopy__ lets a clone get a fresh id instead of copying the prototype's
import copy, itertools
ids = itertools.count(1)
class Ticket:
def __init__(self, queue):
self.id, self.queue, self.tags = next(ids), queue, ["new"]
def __deepcopy__(self, memo):
clone = Ticket.__new__(Ticket)
memo[id(self)] = clone # register first, so cycles back to self work
clone.id = next(ids) # every ticket needs its own id
clone.queue = self.queue
clone.tags = copy.deepcopy(self.tags, memo)
return clone
template = Ticket("support")
t1, t2 = copy.deepcopy(template), copy.deepcopy(template)
print([(t.id, t.queue, t.tags) for t in (template, t1, t2)])
$ python p23_3.py
[(1, 'support', ['new']), (2, 'support', ['new']), (3, 'support', ['new'])]
Without the custom method, deepcopy would copy id too and every clone would be ticket 1.
__deepcopy__ runs instead of the default, assigns a new id, and still deep-copies tags with
memo. Registering the clone in memo before copying the children is what keeps shared and
recursive references correct: if tags ever pointed back at the ticket, the copy would reuse the
clone instead of recursing forever.