2.2.11 · D3Design Principles

Worked examples — Design Patterns — Structural - Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

2,501 words11 min readBack to topic

The scenario matrix

Before any example, let us lay out every kind of situation these patterns can throw at you. Each row is a "case class"; the examples afterward are labelled with the cell they hit, so by the end every cell is covered.

# Case class What makes it tricky Pattern(s) hit
C1 Interface mismatch (normal) signatures differ, must translate Adapter
C2 Two-dimension explosion vs classes Bridge
C3 Stacking wrappers order of forwarding matters Decorator
C4 Degenerate tree — empty a folder with no children Composite
C5 Deep / recursive tree nested many levels Composite (Recursion)
C6 Zero / first-time creation flyweight pool empty at start Flyweight
C7 Repeated identical requests must return the same shared object Flyweight
C8 Access control / laziness do work only when needed Proxy
C9 Orchestration ordering many subsystem calls in fixed order Facade
C10 Word problem (real-world) translate a scenario into a pattern mixed
C11 Exam twist — "which pattern?" code shape identical, intent differs Adapter vs Decorator vs Proxy

Read the matrix once. Every example below names its cell in the callout title.


Example 1 — Adapter with a translated signature (C1)

Forecast: guess now — does the client ever see the name start_mp4?

  1. The client interface is fixed: it will only ever call .play(file). Why this step? The whole point of Adapter is the client stays ignorant of the adaptee. We anchor to the interface the client already speaks.
  2. The adapter holds the adaptee by composition (self._legacy = legacy) and forwards: play(file) calls self._legacy.start_mp4(file). Why this step? Forwarding-with-a-renamed-call is the translation. No inheritance from the legacy class is needed.
  3. Trace the call: play("clip.mp4")start_mp4("clip.mp4") → returns "playing clip.mp4". Why this step? This confirms the return value passes straight through, unchanged in content, changed only in which method delivered it.

Verify: The client learned exactly 1 method (play). The output string is "playing clip.mp4". Units sanity: input file name in, same file name echoed out — no data invented.


Example 2 — Bridge kills the class explosion (C2)

Forecast: guess both numbers before reading.

  1. Inheritance path: every combination is its own class: That is classes. Why this step? Inheritance bakes both dimensions into the type, so every pairing needs a fresh class — the "explosion".
  2. Bridge path: shapes form one hierarchy ( classes), colours another ( classes), joined by a reference. That is classes. Why this step? A Shape has-a Color; the two axes vary independently, so we add, not multiply.
  3. Growth check: add a 4th shape. Inheritance jumps by (to 16); Bridge jumps by (to 8). Why this step? This shows why Bridge scales — new work is per axis, not .
Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Verify: vs ; Bridge saves classes here, and the gap widens as either axis grows. This is Open-Closed Principle made concrete: extend by adding, not editing.


Example 3 — Decorator stacking, order matters (C3)

Forecast: guess whether both orders give the same number.

  1. Innermost first: Coffee().cost() . Why this step? A decorator always calls the wrapped object first, then adds its bit — so evaluation flows outward from the core.
  2. Stack A Sugar(Milk(Coffee())): Milk sees , returns ; Sugar sees , returns . Why this step? Each layer only knows "ask inner, add mine" — no layer knows the full stack.
  3. Stack B Milk(Sugar(Coffee())): Sugar sees , returns ; Milk returns . Why this step? Because addition is commutative here, order does not change the total — but it would if a decorator multiplied or logged, where order is visible.

Verify: Both give . Sanity: regardless of grouping — matches associativity of .


Example 4 — Composite on a degenerate empty folder (C4)

Forecast: guess the number before trusting the code.

  1. Folder.size() = sum(c.size() for c in self.children), and children is []. Why this step? We must check the base of the recursion. An empty composite is the degenerate case that breaks naive code if forgotten.
  2. sum([]) in Python is 0. Why this step? The uniform interface still returns a valid number — no special-casing, no crash. That robustness is the payoff of the pattern.

Verify: Folder().size() . Sanity: an empty folder occupies KB of file content — matches reality.


Example 5 — Composite deep recursion (C5)

Forecast: guess the total KB.

  1. Draw the tree (see figure): root → [File 10, docs], docs → [File 3, File 7]. Why this step? Composite is a tree; visualising it shows where recursion descends.
  2. Leaf sizes return themselves: File(3).size()=3, File(7).size()=7. Why this step? Leaves are the recursion's stopping points — they return their own value directly.
  3. docs.size() sums its children: . Why this step? A composite forwards size() to each child without checking the type — leaf and composite answered the same call.
  4. root.size() sums File(10) and docs: . Why this step? The call recursed one extra level automatically; no code knew how deep it went — that is Recursion doing the traversal.
Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Verify: KB. Total of all leaves — matches, confirming no node was double-counted or dropped.


Example 6 — Flyweight: empty pool, first creation (C6) and reuse (C7)

Forecast: guess the count of created objects (there are 6 letters).

  1. Start with an empty pool {}. Why this step? C6 — the zero case. The first request for any char must create, because nothing is cached yet.
  2. Walk the letters: b(new), a(new), n(new), a(cached), n(cached), a(cached). Why this step? Distinct characters are {b, a, n} = . Only first-sightings create; the rest hit the pool — C7, reuse.
  3. Identity, not equality: the two a requests return the same object (is true), because the pool returns the stored instance. Why this step? This is the memory win — 6 letters, only 3 glyph objects, shared by identity.

Verify: Created objects (unique letters in "banana"). Total letters . Memory saved objects. Positions/colours (extrinsic) are passed at draw time and are not stored in the pool.


Example 7 — Proxy: lazy loading does work only once (C8)

Forecast: guess: 0, 1, or 2?

  1. Construct the proxy — it stores the filename but creates no RealImage yet. Why this step? Proxy controls access: the expensive load is deferred (laziness). Nothing loads at construction.
  2. First draw(): proxy sees self._real is None, so it creates RealImage → prints "loading" once, then draws. Why this step? Access finally happened, so the guarded work runs — exactly once.
  3. Second draw(): self._real is now set, so the proxy skips creation and just draws. Why this step? Caching the subject means the costly step never repeats — the "control access" intent.

Verify: "loading" prints exactly 1 time across two draws. Contrast with Decorator (Example 3), which adds behaviour every call; Proxy guards and may do the work zero or one time.


Example 8 — Facade orchestrates in a fixed order (C9)

Forecast: guess the client's call count.

  1. Client makes one call: start(). Why this step? Facade's promise is one simple door — the client's cognitive load drops to a single method.
  2. Facade emits the ordered sequence: freeze → load → read → jump. Why this step? Boot order is not arbitrary: you freeze the CPU before loading memory, and jump only after disk is read. The facade encodes this ordering so the client can't get it wrong.

Verify: Client calls ; subsystem calls ; recorded order is exactly ["freeze","load","read","jump"]. Facade did not hide the subsystems (advanced clients can still reach computer.cpu), it only orchestrated them — this respects Single Responsibility Principle.


Example 9 — Word problem: pick the pattern (C10)

Forecast: name the pattern before reading.

  1. Nothing new is added, no access is controlled, no tree, no sharing — only the interface/units mismatch. Why this step? We eliminate by intent: not Decorator (no new behaviour), not Proxy (no guarding), not Composite/Flyweight/Bridge. What's left is translation.
  2. Answer: Adapter. Body: def charge(self, amount): self._gw.processPayment(int(amount * 100)). Why this step? It translates both the name (chargeprocessPayment) and the units (dollars→cents) — a signature+unit adapter.
  3. Unit trace: charge(2.30)processPayment(230). Why this step? — confirms the conversion factor.

Verify: cents. Pattern = Adapter (C1 in disguise, dressed as a word problem).


Example 10 — Exam twist: identical code shape, different intent (C11)

Forecast: guess all three before the reveal.

  1. A adds behaviour around the forward (logging) with the same interface → Decorator. Why this step? "Same interface + adds responsibility" is the Decorator signature.
  2. B controls whether the forward happens (permission) → Proxy. Why this step? Guarding access, possibly skipping the call, is Proxy's job.
  3. C exists to bridge a name mismatch to existing code → Adapter. Why this step? The reason-for-being is a mismatch fix, applied after the fact.

Verify: A=Decorator, B=Proxy, C=Adapter. All share the code shape "hold one object, forward request()" — the exam tests whether you read intent, not syntax. This is the parent note's 80/20 point made into a question.


Recall

Recall Empty folder size (C4)

What does Folder().size() return and why? ::: 0, because sum([]) is 0 — the uniform interface handles the degenerate base case with no special code.

Recall Flyweight object count for "banana" (C6+C7)

How many Glyph objects are created? ::: 3 — one per unique character {b,a,n}; repeated letters return the same shared instance by identity.

Recall Lazy proxy print count (C8)

Calling draw() twice on a lazy ProxyImage, how many times does "loading" print? ::: 1 — created on first access, cached after.

Recall Bridge vs inheritance class count (C2)

shapes colours: classes with inheritance vs Bridge? ::: () vs ().

Recall Decorator order (C3)

Does Sugar(Milk(c)) differ from Milk(Sugar(c)) for the coffee costs? ::: No, both are — but only because both add; non-commutative effects (tax/discount) would differ.