<< Home

Chapter 4: Structural Patterns

Structural patterns focus on how parts of a system connect. They help you arrange components so that your design stays flexible even as it grows. Each pattern introduces a different way to link objects, wrap behaviour or combine pieces into meaningful structures that stay easy to reason about.

Adapter

Adapter lets incompatible interfaces work together by placing a small translation layer between them. It changes the shape of one object so that it matches what another part of the system expects.

Intent

Convert one interface into another so that existing code can use a new class without modification.

Structure

An adapter holds a reference to an adaptee. When clients call methods on the adapter, it forwards those calls to the adaptee using names or formats the adaptee understands. The client sees a clean and expected interface.

Pseudocode

class LegacyPrinter {
  method old_print(text) { … }
}

interface Printer { method print(text) }

class Adapter implements Printer {
  constructor(adaptee)
  method print(text) {
    adaptee.old_print(text)
  }
}

This keeps the legacy code untouched while giving new clients a compatible entry point.

Python

class LegacyPrinter:
  def old_print(self, text):
    return f"Legacy: {text}"

class PrinterAdapter:
  def __init__(self, adaptee):
    self.adaptee = adaptee
  def print(self, text):
    return self.adaptee.old_print(text)
💡 Adapter is ideal when integrating external libraries that cannot be changed.

When to Use

Use Adapter when you must reuse existing components that have incompatible interfaces or when you want to smooth over library inconsistencies.

Tradeoffs

You gain compatibility with minimal intrusion; you pay with an extra layer of indirection.

Bridge

Bridge separates an abstraction from its implementation so that both can vary without forcing constant rewrites. It keeps your code flexible when you anticipate multiple combinations of concepts and underlying mechanisms.

Intent

Split abstraction and implementation into separate class hierarchies that connect through composition.

Structure

The abstraction holds a reference to an implementor. The implementor defines low-level operations. Together they allow you to mix and match behaviours at runtime without creating a large number of subclasses.

Pseudocode

interface Device { method turn_on(); method turn_off() }

class TV implements Device { … }
class Radio implements Device { … }

class RemoteControl {
  constructor(device)
  method on() { device.turn_on() }
  method off() { device.turn_off() }
}

The remote is the abstraction; the devices are implementations. Either side can grow independently.

Python

class Device:
  def turn_on(self):
    raise NotImplementedError
  def turn_off(self):
    raise NotImplementedError

class TV(Device):
  def turn_on(self): return "TV on"
  def turn_off(self): return "TV off"

class RemoteControl:
  def __init__(self, device):
    self.device = device
  def on(self): return self.device.turn_on()
  def off(self): return self.device.turn_off()

When to Use

Use Bridge when a hierarchy has two changing dimensions such as abstraction and platform or shape and rendering engine.

Tradeoffs

Bridge adds structural clarity at scale; for small hierarchies it may feel heavier than needed.

Composite

Composite lets you treat single objects and groups of objects in the same way. This makes tree structures easier to navigate and manipulate because leaves and containers share a common interface.

Intent

Represent part-whole hierarchies so clients can handle individual items and groups uniformly.

Structure

A component interface offers operations shared by both leaves and composites. The composite stores children and forwards operations to them, building large recursive structures with simple rules.

Pseudocode

interface Node { method show() }

class Leaf implements Node {
  method show() { … }
}

class Group implements Node {
  children = []
  method add(n) { children.append(n) }
  method show() {
    for each child in children:
      child.show()
  }
}

This creates unified behaviour across simple and nested elements.

Python

class Leaf:
  def __init__(self, name):
    self.name = name
  def show(self):
    return self.name

class Group:
  def __init__(self):
    self.children = []
  def add(self, node):
    self.children.append(node)
  def show(self):
    return [child.show() for child in self.children]
⚠️ Composite works best when all children share meaningful operations; avoid forcing unrelated components into the structure.

When to Use

Use Composite when working with nested or hierarchical data such as file trees or UI elements.

Tradeoffs

Uniformity simplifies client code; enforcing a common interface may require compromise in component responsibilities.

Decorator

Decorator adds behaviour to objects by wrapping them in layers. Each decorator keeps the original interface while injecting new features around the existing operations.

Intent

Attach optional behaviour dynamically instead of using large inheritance chains.

Structure

Both component and decorator share the same interface. A decorator stores a component and calls it while adding its own pre or post logic.

Pseudocode

interface Writer { method write(x) }

class BasicWriter implements Writer { … }

class LoggingDecorator implements Writer {
  constructor(inner)
  method write(x) {
    log("write called")
    return inner.write(x)
  }
}

Multiple decorators can wrap one another to build a chain of small enhancements.

Python

class Writer:
  def write(self, x):
    raise NotImplementedError

class BasicWriter(Writer):
  def write(self, x):
    return x

class LoggingDecorator(Writer):
  def __init__(self, inner):
    self.inner = inner
  def write(self, x):
    return f"log: {self.inner.write(x)}"

When to Use

Use Decorator when behaviour must be extended step by step without modifying original classes.

Tradeoffs

Decorators compose cleanly; however long chains can obscure control flow.

Facade

Facade provides a simplified gateway to a complex subsystem. It hides the inner maze behind a clean and unified interface.

Intent

Wrap many components behind one straightforward interface that clients can use without learning the entire subsystem.

Structure

The facade holds and coordinates subsystem objects. Clients send high-level requests to the facade; the facade delegates work to the appropriate parts.

Pseudocode

class Engine { method start() }
class Lights { method on() }

class CarFacade {
  constructor(engine, lights)
  method begin_trip() {
    engine.start()
    lights.on()
  }
}

The client sees a simple operation while the facade handles the detailed sequence.

Python

class Engine:
  def start(self): return "engine"

class Lights:
  def on(self): return "lights"

class CarFacade:
  def __init__(self, engine, lights):
    self.engine = engine
    self.lights = lights
  def begin_trip(self):
    return [self.engine.start(), self.lights.on()]
💡 Facade is handy when exposing a module to new users who only need common operations.

When to Use

Use Facade when a subsystem becomes too detailed for direct use or when you want clearer boundaries between layers.

Tradeoffs

Simplification can hide useful features; avoid making the facade the only entry unless that is the explicit goal.

Flyweight

Flyweight reduces memory usage by sharing common internal state across many similar objects. Only the small pieces of state that vary remain unique.

Intent

Share intrinsic data to support large numbers of fine-grained objects efficiently.

Structure

The factory returns shared flyweights for repeated intrinsic values. Extrinsic values are passed to operations at runtime, keeping each flyweight light.

Pseudocode

class Flyweight {
  constructor(shared)
  method render(extrinsic) { … }
}

class FlyweightFactory {
  pool = {}
  method get(shared):
    if shared not in pool:
      pool[shared] = new Flyweight(shared)
    return pool[shared]
}

This keeps the number of created objects small even when the system uses many of them.

Python

class Flyweight:
  def __init__(self, shared):
    self.shared = shared
  def render(self, extrinsic):
    return f"{self.shared}:{extrinsic}"

class FlyweightFactory:
  def __init__(self):
    self.pool = {}
  def get(self, shared):
    if shared not in self.pool:
      self.pool[shared] = Flyweight(shared)
    return self.pool[shared]

When to Use

Use Flyweight when an application stores many small similar objects such as characters in a document or map tiles.

Tradeoffs

State must be split carefully; incorrect separation of intrinsic and extrinsic values creates bugs.

Proxy

Proxy controls access to another object. It behaves like a stand-in that forwards calls while adding checks or extra behaviour.

Intent

Provide a substitute that manages or protects access to a real object.

Structure

The proxy stores a reference to a subject. Each operation may add permission checks, caching or lazy creation before delegating to the real subject.

Pseudocode

interface Service { method request() }

class RealService implements Service { … }

class Proxy implements Service {
  constructor(real)
  method request() {
    if allowed:
      return real.request()
    else:
      return "blocked"
  }
}

This allows the proxy to protect or manage use of the real service.

Python

class RealService:
  def request(self):
    return "real"

class Proxy:
  def __init__(self, real):
    self.real = real
  def request(self):
    return f"proxy:{self.real.request()}"
⚠️ If a proxy becomes too feature-heavy, it may grow into a separate subsystem; keep responsibilities focused.

When to Use

Use Proxy for lazy loading, permission control, caching layers or network stubs that stand in for remote services.

Tradeoffs

Extra indirection can affect performance; thoughtful design keeps overhead small while preserving clarity.