Exercises — Design Patterns — Structural - Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
Level 1 — Recognition
The goal here is only to name the pattern from its fingerprint. No code to write yet — just match intent to pattern.
Exercise 1.1
You are handed a class XmlDataSource with a method parse_xml(text). Your program everywhere calls read(source) on a DataSource interface. You cannot edit XmlDataSource. You write a small class implementing read() that internally calls parse_xml(). Which pattern?
Recall Solution
Adapter. Fingerprint: an existing, unchangeable class whose method names don't match what the client speaks, so you build a translator. The client wants read(); the adaptee offers parse_xml(); your wrapper implements read() and forwards to parse_xml(). The clue words are "cannot edit" + "signatures differ" + "translate".
Exercise 1.2
An online store wraps a PaymentService so that the wrapper first checks a cache, and if the value is missing, then calls the real service and stores the result. The wrapper has the same interface as PaymentService. Which pattern, and how do you know it is not a Decorator?
Recall Solution
Proxy (a caching proxy). Same interface + forwards calls — that alone matches both Proxy and Decorator. The deciding word is control access: the wrapper decides whether to call the real service at all (cache hit ⇒ skip it). A Decorator would always call the inner object and add behaviour around the result, never suppress the call. Here the point is access control, so it is a Proxy.
Exercise 1.3
A drawing app has classes Circle, Square (the "what to draw") and separately VectorRenderer, RasterRenderer (the "how to draw"). A Shape holds a reference to a renderer. Which pattern?
Recall Solution
Bridge. Two independent hierarchies — abstraction (shapes) and implementation (renderers) — joined by a reference so each can grow without the other. Fingerprint: designed up front to stop an class explosion, giving classes instead.
Level 2 — Application
Now you write small code. Each answer is short but must compile in your head.
Exercise 2.1
Given the Composite skeleton from the parent note, build a tree of one Folder containing a File(10), a File(25), and a sub-Folder holding File(5). Compute root.size().
class Node:
def size(self): raise NotImplementedError
class File(Node):
def __init__(self, kb): self.kb = kb
def size(self): return self.kb
class Folder(Node):
def __init__(self): self.children = []
def add(self, n): self.children.append(n)
def size(self): return sum(c.size() for c in self.children)Recall Solution
Build it:
root = Folder()
root.add(File(10))
root.add(File(25))
sub = Folder()
sub.add(File(5))
root.add(sub)
print(root.size()) # -> 40Why 40? root.size() sums its children uniformly: File(10).size()=10, File(25).size()=25, and sub.size() recurses into its one child File(5).size()=5. Total . The key: Folder.size() never checks whether a child is a leaf or another folder — Recursion plus a shared interface does all the work. See the tree in the figure below.

Exercise 2.2
Using the coffee Decorator from the parent note, compute the cost of Milk(Sugar(Milk(Coffee()))).
class Coffee:
def cost(self): return 2.0
class AddOn(Coffee):
def __init__(self, drink): self.drink = drink
class Milk(AddOn):
def cost(self): return self.drink.cost() + 0.5
class Sugar(AddOn):
def cost(self): return self.drink.cost() + 0.2Recall Solution
Peel the wrappers from the inside out — each layer calls the inner one first, then adds its bit:
Answer: 3.2. Note that Milk appears twice and each application adds independently — that stackability is exactly the Decorator payoff. The figure shows the nesting as onion layers.

Exercise 2.3
A Flyweight GlyphFactory shares glyph objects. You render the string "banana". How many Glyph objects get created (intrinsic state), and how many (x, y) positions are passed (extrinsic state)?
Recall Solution
"banana" has 6 characters but only 3 distinct letters: b, a, n.
- Intrinsic (shared) objects created: 3 — one
Glyphper distinct character. Theaobject is reused for all threeas,nfor bothns. - Extrinsic positions passed: 6 — one
(x, y)per character drawn, because every letter sits somewhere different.
The whole win of Flyweight: memory scales with distinct intrinsic values (3), not with total usages (6).
Level 3 — Analysis
Here you compare, debug, and justify. The answer is an argument, not just a name.
Exercise 3.1
A teammate replaced a Facade start() (which calls cpu.freeze(), mem.load(), disk.read(), cpu.jump() in that order) with four separate public methods so callers "have more control." Bugs appear. Explain what design guarantee was lost and which principle it violated.
Recall Solution
The Facade's job was to enforce a correct ordering and hide it. Splitting it exposes four steps whose order matters, and now every caller must remember freeze → load → read → jump. Any caller who reorders them gets a broken boot. The lost guarantee is orchestration: the subsystem's usage protocol is no longer encapsulated in one place.
This violates the Single Responsibility Principle indirectly — the responsibility "know the correct boot sequence" got scattered across every client — and it undermines the whole reason for a Facade, which is to give one simple, correct-by-construction door to a complex subsystem.
Exercise 3.2
You have Shape × Color handled by Bridge, giving classes for shapes and colors. Your manager instead wants deep inheritance: RedCircle, BlueCircle, RedSquare, ... For shapes and colors, compute both class counts and state the growth rule when a new color is added.
Recall Solution
- Inheritance (class explosion): classes.
- Bridge (composition): classes.
- Adding one new color: inheritance needs one new subclass per shape → classes (total ). Bridge needs one new
Colorclass → (total ).
The gap vs widens fast; this is precisely the combinatorial explosion Bridge prevents by letting the two dimensions vary independently (a Composition over Inheritance win, and directly the Open-Closed Principle: extend by adding, not editing).
Exercise 3.3
Given Logging(Cache(Service())) where each layer shares the Service interface, Cache is a Proxy and Logging is a Decorator, trace what happens on the second identical call and explain why calling Logging a Decorator but Cache a Proxy is correct.
Recall Solution
Second identical call: Logging.handle(req) logs "incoming", then forwards to Cache.handle(req). Cache finds the request in its store from the first call and returns the cached value without ever touching Service. Logging logs "outgoing" and returns.
Cacheis a Proxy because it controls access: it decides whether the realServiceruns at all (skipped on a cache hit).Loggingis a Decorator because it adds a responsibility (recording) but always forwards; it never suppresses the inner call.
Same interface, same wrap-and-forward shape — separated purely by intent: access control vs added behaviour.
Level 4 — Synthesis
Now you design: combine patterns to meet requirements.
Exercise 4.1
Design (in code sketch) a media library that must: (a) work with a legacy Flac player exposing open_flac(path), and (b) let users add optional volume-boost and fade-in effects in any combination without a class per combination. Name each pattern you use and where.
Recall Solution
Two patterns, cleanly separated:
- Adapter for the legacy player (translate
play→open_flac). - Decorator for stackable effects (each shares the
playinterface, adds behaviour, forwards).
class Player: # Target interface
def play(self, file): ...
class Flac: # Adaptee (legacy, unchangeable)
def open_flac(self, path): print("flac:", path)
class FlacAdapter(Player): # ADAPTER
def __init__(self, legacy): self._legacy = legacy
def play(self, file): self._legacy.open_flac(file)
class Effect(Player): # DECORATOR base: wraps a Player, IS a Player
def __init__(self, inner): self.inner = inner
class VolumeBoost(Effect):
def play(self, file):
print("+volume"); self.inner.play(file)
class FadeIn(Effect):
def play(self, file):
print("+fade"); self.inner.play(file)
# Compose freely — no combination classes:
p = FadeIn(VolumeBoost(FlacAdapter(Flac())))
p.play("song.flac")Why this split works: the Adapter makes the legacy class speak the target interface, so the Decorators — which require every layer to share that interface — can wrap it exactly like any native player. Adapter solves the compatibility problem; Decorator solves the combinatorial-effects problem. Neither knows about the other. This is Composition over Inheritance doing double duty and honouring the Open-Closed Principle.
Exercise 4.2
You must render a forest of 1,000,000 trees. Each tree shares one of 3 mesh models (intrinsic) but has its own position and scale (extrinsic). Sketch a Flyweight factory, and compute how many heavy TreeModel objects live in memory.
Recall Solution
class TreeFactory:
_pool = {}
def get(self, kind):
if kind not in self._pool:
self._pool[kind] = TreeModel(kind) # heavy, created once
return self._pool[kind] # shared instance
def draw_tree(model, x, y, scale): # extrinsic passed in
...Heavy TreeModel objects in memory: 3 — one per distinct kind, regardless of the million placements. The per-tree (x, y, scale) is lightweight extrinsic state stored in a plain list, so memory scales with distinct models (3), not with instances (). That is the entire point of Flyweight: share the heavy invariant part.
Level 5 — Mastery
Open-ended judgement calls with defensible answers.
Exercise 5.1
A logging Proxy caches results but a bug means it also adds "[logged] " to every returned string. Is it still "just a Proxy"? Argue precisely using the intent definitions.
Recall Solution
No — it has become a Proxy + Decorator hybrid, and that mixing is the smell.
- The caching / access-control behaviour (deciding whether to hit the real service) is genuine Proxy intent.
- Modifying the returned value by prepending text is adding a responsibility to the output — that is Decorator intent.
A pure Proxy returns the subject's result unchanged; the moment it alters the payload it is decorating. The clean fix is to split them: a CachingProxy for access control wrapped by a separate LoggingDecorator for the string change — restoring Single Responsibility Principle and making each layer independently testable and removable.
Exercise 5.2
Explain why every structural pattern in this chapter can be summarised as "a wrapper that forwards calls, differing by intent," yet why that summary is dangerous if used alone to pick a pattern.
Recall Solution
Why the summary is true: each pattern holds a reference to another object and forwards work to it (composition) — Adapter forwards+translates, Decorator forwards+adds, Proxy forwards+gates, Facade forwards to many, Composite forwards to a collection, Bridge forwards across a dimension, Flyweight forwards to a shared instance. Structurally, they are one idea. This is why Composition over Inheritance and Dependency Inversion underpin the whole family. Why it is dangerous alone: because the code shape is identical, shape can never tell you which pattern you need. Picking a pattern requires reading the problem's intent — mismatch? extension? access control? explosion prevention? memory sharing? simplification? A student who reasons only from shape will name whichever pattern they saw last, and get it wrong exactly when it matters (as in 5.1). The summary compresses knowledge; it does not replace intent analysis.