Structural Design Patterns Cheat Sheet
Adapter, Decorator, Facade, and other Gang of Four structural patterns for composing classes and objects into larger, flexible structures.
Adapter Pattern
Making an incompatible interface work with the client's expected interface.
class EuropeanSocket: def voltage(self): return 230class USPlug: def plug_in(self, socket): raise NotImplementedErrorclass SocketAdapter(USPlug): """Adapts a EuropeanSocket to the interface USPlug expects.""" def __init__(self, socket: EuropeanSocket): self.socket = socket def plug_in(self, _=None): return self.socket.voltage() / 2 # step down to ~115Vadapter = SocketAdapter(EuropeanSocket())adapter.plug_in() # 115.0
Decorator Pattern
Adding responsibilities to an object dynamically without subclassing.
class Coffee: def cost(self): return 2.0 def description(self): return "Coffee"class MilkDecorator: def __init__(self, coffee): self._coffee = coffee def cost(self): return self._coffee.cost() + 0.5 def description(self): return self._coffee.description() + " + Milk"class SyrupDecorator: def __init__(self, coffee): self._coffee = coffee def cost(self): return self._coffee.cost() + 0.3 def description(self): return self._coffee.description() + " + Syrup"order = SyrupDecorator(MilkDecorator(Coffee()))order.description() # "Coffee + Milk + Syrup"order.cost() # 2.8
Facade Pattern
Hiding subsystem complexity behind one simple interface.
class CPU: def start(self): print("CPU starting")class Memory: def load(self): print("Memory loading")class HardDrive: def read(self): print("Disk reading")class ComputerFacade: """Simple interface hiding the complexity of the subsystems.""" def __init__(self): self.cpu, self.memory, self.disk = CPU(), Memory(), HardDrive() def start(self): self.cpu.start() self.memory.load() self.disk.read()ComputerFacade().start() # one call instead of three
Structural Pattern Catalog
One-line definitions of the classic Gang of Four structural patterns.
- Adapter- Convert the interface of a class into another interface clients expect, letting incompatible classes work together
- Decorator- Attach additional responsibilities to an object dynamically, without altering its class or affecting other instances
- Facade- Provide a simplified, unified interface to a complex subsystem of classes
- Composite- Compose objects into tree structures and let clients treat individual objects and compositions uniformly
- Proxy- Provide a placeholder/surrogate for another object to control access to it, e.g. lazy loading or caching
- Bridge- Decouple an abstraction from its implementation so the two can vary independently
- Flyweight- Share common state across many fine-grained objects to reduce memory usage
Composite Pattern
Treating individual objects and groups of objects uniformly through a shared interface, ideal for tree structures.
from abc import ABC, abstractmethodclass FileSystemNode(ABC): @abstractmethod def size(self) -> int: ...class File(FileSystemNode): def __init__(self, name, bytes_): self.name, self.bytes_ = name, bytes_ def size(self): return self.bytes_class Folder(FileSystemNode): def __init__(self, name): self.name = name self.children: list[FileSystemNode] = [] def add(self, node: FileSystemNode): self.children.append(node) return self def size(self): # a Folder's size() delegates to children uniformly — no type checks return sum(child.size() for child in self.children)root = Folder("root")root.add(File("a.txt", 100)).add(Folder("src").add(File("b.py", 250)))root.size() # 350 — client code never distinguishes File from Folder
Proxy Pattern
Controlling access to a real object with a stand-in that adds lazy loading, caching, or access checks.
class RemoteImage: """The real subject — expensive to construct.""" def __init__(self, url): self.url = url print(f"downloading {url}...") # expensive I/O happens here def render(self): return f"<rendered {self.url}>"class LazyImageProxy: """Virtual proxy: defers construction until render() is actually called.""" def __init__(self, url): self.url = url self._real: RemoteImage | None = None def render(self): if self._real is None: # only pay the cost on first use self._real = RemoteImage(self.url) return self._real.render()gallery = [LazyImageProxy(f"img{i}.png") for i in range(100)]# no downloads happened yetgallery[0].render() # downloads img0.png now, lazily
Bridge Pattern
Decoupling an abstraction from its implementation so both can vary and be extended independently.
from abc import ABC, abstractmethod# Implementation hierarchy — how the message actually gets sentclass MessageSender(ABC): @abstractmethod def send(self, text: str) -> None: ...class EmailSender(MessageSender): def send(self, text): print(f"Email: {text}")class SmsSender(MessageSender): def send(self, text): print(f"SMS: {text}")# Abstraction hierarchy — what kind of message it isclass Notification: def __init__(self, sender: MessageSender): self.sender = sender # the bridge: composed, not inherited def notify(self, text): self.sender.send(text)class UrgentNotification(Notification): def notify(self, text): self.sender.send(f"URGENT: {text}")# Any Notification subtype can use any MessageSender — no class explosionUrgentNotification(SmsSender()).notify("server down") # SMS: URGENT: server down
Flyweight Pattern
Sharing common immutable state across many fine-grained objects to cut memory usage.
class GlyphStyle: """The shared, immutable 'intrinsic' state — one instance per (font, size, color).""" def __init__(self, font, size, color): self.font, self.size, self.color = font, size, colorclass GlyphStyleFactory: _cache: dict[tuple, GlyphStyle] = {} @classmethod def get(cls, font, size, color) -> GlyphStyle: key = (font, size, color) if key not in cls._cache: cls._cache[key] = GlyphStyle(font, size, color) # created once, reused return cls._cache[key]class Character: """The 'extrinsic' state (position, the letter) stays per-instance.""" def __init__(self, char, x, y, style: GlyphStyle): self.char, self.x, self.y, self.style = char, x, y, styledocument = [ Character(ch, i, 0, GlyphStyleFactory.get("Arial", 12, "black")) for i, ch in enumerate("a million character document")]len(GlyphStyleFactory._cache) # 1 — every character shares the same style object
Structural Pattern Trade-offs
Cost and applicability notes for choosing between structural patterns that look similar on the surface.
- Adapter vs. Bridge- Adapter is retrofitted after the fact to reconcile two existing incompatible interfaces; Bridge is designed upfront to let abstraction and implementation evolve separately
- Decorator vs. Proxy- Both wrap an object behind the same interface, but Decorator stacks to ADD behavior while Proxy controls or restricts ACCESS to the wrapped object
- Facade vs. Adapter- Facade simplifies a whole subsystem's many interfaces into one; Adapter translates a single interface into another expected shape
- Composite traversal cost- Operations like size() walk the whole tree on every call; cache results or use a visitor with memoization for large trees
- Proxy transparency- A well-built proxy is indistinguishable from the real subject to the caller — breaking that (e.g. leaking proxy-only methods) defeats the pattern
- Flyweight mutability rule- Shared flyweight state must stay immutable; any per-instance mutable state has to live outside the flyweight (in the extrinsic context)
Decorator and inheritance both extend behavior, but Decorator does it at runtime and composably — prefer it over creating a new subclass for every feature combination (avoid a MilkAndSyrupCoffee / SyrupOnlyCoffee explosion).