2.2.11 · D2Design Principles

Visual walkthrough — Design Patterns — Structural - Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

3,172 words14 min readBack to topic

Before any code, three plain words we will keep reusing:

Everything below is the wrapper, redrawn with a different job written on it. See Composition over Inheritance for why "holding a reference" beats "inheriting from".


Step 1 — The bare call (no pattern yet)

WHAT: A client calls a method on a subject directly.

WHY: We need the before picture. Every pattern is a modification of this one arrow, so we must see the arrow with nothing on it first.

PICTURE: One box (Client) points at another box (Subject). The label on the arrow is the method name.

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
  • — the message. The client sends it; the subject runs it.
  • The arrow is a direct dependency: if the subject's method name changes, the client breaks. That fragility is the problem every following step fixes in a different way.

Step 2 — Insert the wrapper (the universal shape)

WHAT: Put a third box between client and subject. The client now calls the wrapper; the wrapper calls the subject.

WHY: A middle box gives us a place to do something on the way through — translate, add, block, or fan out. Nothing here yet does anything but pass the call, but the empty seat is the birthplace of all seven patterns.

PICTURE: Two arrows in a row. The wrapper holds the subject (composition), shown by a small "has-a" line.

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
class Wrapper:
    def __init__(self, subject):
        self._subject = subject      # HOLD it (composition, not inheritance)
    def do(self):
        return self._subject.do()    # FORWARD: pure pass-through, adds nothing yet
  • self._subject — the held reference. This single line is the "has-a" that replaces "is-a".
  • return self._subject.do() — the forward. Read it as: "whatever I was asked, ask the same of the thing I hold."

Now we differ only by what we insert around that forward line. Each following step edits one method body.


Step 3 — Twist the name: Adapter

WHAT: The subject's method is named differently (start_mp4, say). The wrapper forwards, but renames the call.

WHY: The client speaks one vocabulary (play); a legacy/third-party subject speaks another (start_mp4). We can't edit the subject. So the wrapper translates the interface. This obeys Open-Closed Principle — new class, zero edits to old code.

PICTURE: Same two arrows, but the second arrow carries a different label than the first — a little translator sitting on the wire.

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
# Target: the interface the CLIENT speaks.
class MediaPlayer:
    def play(self, file):
        raise NotImplementedError
 
# Adaptee: a legacy class we cannot edit; different method name.
class LegacyMp4:
    def start_mp4(self, path):
        print("playing", path)
 
# Adapter: IS a MediaPlayer, HOLDS a LegacyMp4, translates the call.
class Mp4Adapter(MediaPlayer):        # speaks the CLIENT's language: play()
    def __init__(self, legacy):
        self._legacy = legacy         # composition: hold the adaptee
    def play(self, file):
        self._legacy.start_mp4(file)  # SAME job, DIFFERENT name -> translation
  • class MediaPlayer — the target interface: what the client already calls (play).
  • class LegacyMp4 — the adaptee: works, but named start_mp4; we may not change it.
  • class Mp4Adapter(MediaPlayer) — it is what the client expects, so the client never learns a legacy class exists.
  • play(...) in, start_mp4(...) out — the two labels differ. That difference is the whole pattern.
Recall Quick self-test (reveal the answer)

Reveal lines below use the vault convention Question ::: Answer — read up to the :::, answer in your head, then check what follows it.

Adapter changes the ______ of the forwarded call ::: the name / interface (the method signature), not the behaviour.


Step 4 — Twist the behaviour: Decorator

WHAT: Keep the same method name in and out, but do the subject's work and add a little of your own.

WHY: We want to add features (milk, sugar, logging, caching) at runtime without a class per combination. Because the name matches, a decorated object is interchangeable with a plain one — so wrappers stack.

PICTURE: The forward arrow keeps its name, but a little "+extra" tag hangs off the wrapper. Two wrappers chain: Sugar → Milk → Coffee, each adding its bit.

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
# Component: the base object and the shared interface, cost().
class Coffee:
    def cost(self):
        return 2.0
 
# Decorator base: IS a Coffee, HOLDS a Coffee. Adds nothing by itself.
class AddOn(Coffee):
    def __init__(self, drink):
        self.drink = drink            # composition: hold the wrapped drink
 
# Concrete decorators: forward FIRST, then add their own bit.
class Milk(AddOn):
    def cost(self):
        return self.drink.cost() + 0.5
class Sugar(AddOn):
    def cost(self):
        return self.drink.cost() + 0.2
 
order = Sugar(Milk(Coffee()))         # 2.0 + 0.5 + 0.2 = 2.7
  • class Coffee — the component, and the interface everyone shares: cost().
  • class AddOn(Coffee) — the decorator base; being a Coffee and holding one is what lets wrappers stack.
  • self.drink.cost() — forward, same name (costcost). This is what lets layers stack.
  • + 0.5 — the added responsibility. Adapter renamed; Decorator augments.
  • Sugar(Milk(Coffee())) — three layers, evaluated inside-out: 2.0 then +0.5 then +0.2.

Step 5 — Twist the access: Proxy

WHAT: Same name in and out, but the wrapper decides whether, when, or how often to forward — lazy-loading, caching, or permission checks.

WHY: Sometimes the real work is expensive (load a huge image) or restricted (needs a login). The proxy controls access to a single fixed subject instead of always calling straight through.

PICTURE: The forward arrow now passes through a gate. On the first call the gate builds the subject; on later calls it may short-circuit (cache) or block.

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
# The real, expensive subject. Building it loads a big file from disk.
class RealImage:
    def __init__(self, path):
        self._path = path
        print("loading", path)        # the costly work we want to defer
    def display(self):
        print("showing", self._path)
 
# Proxy: same interface (display), but GATES when RealImage is built.
class ImageProxy:
    def __init__(self, path):
        self._path, self._real = path, None
    def display(self):
        if self._real is None:            # GATE: only build on real need
            self._real = RealImage(self._path)   # lazy: created on first use
        return self._real.display()       # then forward
  • class RealImage — the real subject: heavy to build, cheap to reuse once built.
  • if self._real is None: — the gate. Direct call has no gate; proxy's whole job is that if.
  • Proxy wraps one fixed subject and does not aim to stack — that separates it from Decorator, whose gate would instead be a + extra.

Step 6 — Twist the count: Composite

WHAT: The wrapper holds many subjects (a list of children) and forwards the call to all of them, treating one and many identically.

WHY: Files/folders, panels/widgets, org charts — recursive part-whole trees. We want size() to work whether we call it on a single leaf or a whole subtree, without the client checking types. This is Recursion made structural.

PICTURE: One box fans out into a tree. A leaf answers directly; a folder asks each child and sums. The same size() arrow points at both.

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

Recall the (sum) sign introduced up top — "add one value per child":

# Component: the ONE shared interface every node must provide.
# Both leaves and composites are Nodes, so the client can call size()
# on any of them without ever asking "which kind are you?".
class Node:
    def size(self):
        raise NotImplementedError     # every Node promises a size()
 
class File(Node):                     # leaf: no children, answers directly
    def __init__(self, kb):
        self.kb = kb
    def size(self):
        return self.kb
 
class Folder(Node):                   # composite: holds children, sums them
    def __init__(self):
        self.children = []
    def add(self, node):
        self.children.append(node)
    def size(self):
        return sum(c.size() for c in self.children)  # forward to MANY, uniformly
  • class Node — the component: the common interface that promises a size(). Because both File and Folder are Nodes, the client treats one and many alike.
  • — forward to every child. Count went from one to many.
  • c.size() never asks "are you a File or Folder?" — the uniform interface is the point, and the recursion bottoms out at leaves.
  • Empty folderchildren = [] makes the sum ; the recursion terminates cleanly with no special case. An empty tree is just a node with a bottom already reached.
Recall

What makes Composite recursion terminate? ::: leaves return a value directly (no children to recurse into), so the tree has a bottom.


Step 7 — Twist into two dimensions: Bridge

WHAT: Split one concept into two hierarchies — an abstraction and an implementation — each holding a reference to the other's side, so they vary independently.

WHY: To dodge a class explosion. Shapes () × colours () by inheritance needs classes; a bridge (has-a) needs only . This is Dependency Inversion in miniature: the high-level Shape depends on a Color abstraction, not a concrete colour.

PICTURE: Two parallel ladders (Shape side, Color side) joined by one horizontal reference — the "bridge".

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
# Implementation side. This is the interface every colour must provide:
# a fill() that returns the colour name. Concrete colours implement it.
class Color:
    def fill(self):
        raise NotImplementedError     # every Color promises fill()
class Red(Color):
    def fill(self):
        return "red"
 
# Abstraction side. Shape declares its own interface: a draw().
# It HOLDS a Color and forwards across the bridge to color.fill().
class Shape:
    def __init__(self, color):
        self.color = color            # the reference across the bridge
    def draw(self):
        raise NotImplementedError     # every Shape promises draw()
class Circle(Shape):
    def draw(self):
        return "Circle in " + self.color.fill()   # forward across the bridge
  • class Color with fill() — the implementation interface: what every colour must provide.
  • class Shape with draw() — the abstraction interface: what every shape must provide. It holds a Color (has-a) instead of being a coloured shape (is-a).
  • Add a shape → one class; add a colour → one class. That is the saving.

Step 8 — Twist the door: Facade

WHAT: The wrapper forwards to many unrelated subsystems behind one simple call, orchestrating their order.

WHY: Starting a computer means CPU, Memory, and Disk cooperating in a fixed sequence. The client just wants start() — it should not know the four ordered steps. Facade simplifies by owning that orchestration. See Single Responsibility Principle: the client's one responsibility is "start", not "sequence the subsystems".

PICTURE: One door (the facade) fans out to several different subsystem boxes; the client only ever touches the door.

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

The arrow below simply means "then do the next thing":

class ComputerFacade:
    def __init__(self):
        self.cpu, self.mem, self.disk = CPU(), Mem(), Disk()
    def start(self):
        self.cpu.freeze()             # one call fans out to
        self.mem.load()               # several DIFFERENT subsystems,
        self.disk.read()              # in a fixed ORDER the client
        self.cpu.jump()               # never has to know
  • Unlike Composite (many children of the same interface, summed), Facade drives different subsystems in sequence.
  • The facade does not hide the subsystem — an advanced client can still reach cpu, mem, disk directly.

Step 9 — Twist toward sharing: Flyweight

WHAT: Instead of every object owning its heavy data, many objects share one copy of the intrinsic part; the extrinsic part is handed in per call.

WHY: A text editor with a million characters would waste memory storing the glyph shape of 'a' a million times. Since that shape is identical everywhere, keep one shared copy. You end up with 26 glyph objects for the lowercase alphabet, not a million.

PICTURE: A small shared pool of glyph objects; many draw-calls point at the same glyph but carry their own .

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
# The flyweight itself: holds ONLY intrinsic state (the shared shape).
class Glyph:
    def __init__(self, char):
        self.char = char              # intrinsic: identical for every 'a'
    def draw(self, x, y):             # extrinsic (x, y) PASSED IN, not stored
        print(self.char, "at", x, y)
 
# Factory hands out shared flyweights, creating each at most once.
class GlyphFactory:
    _pool = {}                        # the share table: char -> one Glyph
    def get(self, char):
        if char not in self._pool:
            self._pool[char] = Glyph(char)   # create once,
        return self._pool[char]              # reuse the SHARED instance
 
factory = GlyphFactory()
a1 = factory.get("a")
a2 = factory.get("a")                 # SAME object as a1 (shared)
a1.draw(1, 2)                         # extrinsic position passed at draw time
a2.draw(9, 3)                         # same glyph, different position
  • class Glyph — the flyweight: it stores only the intrinsic char, never a position.
  • _pool — the share table: one Glyph per distinct character, reused forever. a1 and a2 are the same object.
  • draw(self, x, y) — the extrinsic arrives as arguments, so a million positions cost no extra glyph objects.
Recall

Which half of an object's state does Flyweight share, and which does it pass in? ::: it shares the intrinsic state (identical across objects), and passes in the extrinsic state (differs per object).


The one-picture summary

WHAT: All seven patterns are the Step-2 wrapper with one thing twisted. This final figure lays them side by side, each labelled by the twist.

Figure — Design Patterns — Structural -  Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
Twist on the forward Pattern One-word job
Rename the call Adapter translate
Same name + extra, stackable Decorator augment
Gate the call Proxy control
Forward to many, uniformly Composite tree
Two hierarchies referencing Bridge de-explode
Fan to subsystems, one door Facade simplify
Share one, pass the rest in Flyweight save memory
Recall Feynman retelling — say it to a 12-year-old

Imagine you want to ask your friend a question, but you go through a helper standing in the doorway. That helper is a wrapper: they take your question and pass it on. Now change the helper's rule: — If they translate your question into your friend's language, that's Adapter. — If they answer, then add a joke of their own, that's Decorator — and you can line up several helpers, each adding a joke. — If they check whether you're allowed in first, or remember last time's answer, that's Proxy. — If they pass your question to a whole room of friends and add up the replies, that's Composite. — If your friends come in two kinds — who they are and what colour shirt they wear — and you mix-and-match freely instead of naming every combination, that's Bridge. — If the helper does four ordered chores for you when you just say "start", that's Facade. — If a million people all reuse one shared drawing of the letter A and only carry their own position, that's Flyweight. One doorway, seven different rules pinned above it. Learn the doorway once; the rest are just the sign on the wall.

Back to the parent: the Hinglish overview. Related principles: Single Responsibility Principle, Creational Patterns, Behavioral Patterns.