Table of Contents
- What Are Structural Design Patterns?
- Adapter Pattern
- Bridge Pattern
- Composite Pattern
- Decorator Pattern
- Facade Pattern
- Flyweight Pattern
- Proxy Pattern
- Comparing Structural Patterns
- Conclusion
- References
What Are Structural Design Patterns?
Structural design patterns are concerned with how objects and classes interact to form larger structures while keeping these structures flexible and efficient. They focus on:
- Simplifying object relationships: Reducing dependencies between classes.
- Reusability: Enabling components to be reused across different contexts.
- Flexibility: Allowing the system to evolve without major overhauls.
Common structural patterns include Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy. Each solves a specific problem related to object composition, and we’ll explore them one by one.
Adapter Pattern
Intent
Convert the interface of a class into another interface that clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
Problem
You have a legacy component (e.g., an old library) with a useful feature, but its interface doesn’t match the one your new code expects. Rewriting the legacy code is costly, and you need to integrate it without breaking existing functionality.
Solution
The Adapter pattern acts as a wrapper around the legacy component, exposing the interface your new code expects. It translates calls from the new interface to the legacy interface.
Python Example
Suppose you’re building a payment processing system that expects a process_payment(amount) method. You need to integrate a legacy LegacyPaymentGateway that uses make_payment(amount) instead.
# Legacy component with incompatible interface
class LegacyPaymentGateway:
def make_payment(self, amount: float) -> str:
return f"Legacy gateway processed ${amount}"
# Target interface (what the new system expects)
class PaymentProcessor:
def process_payment(self, amount: float) -> str:
raise NotImplementedError
# Adapter: Wraps LegacyPaymentGateway to match PaymentProcessor interface
class PaymentAdapter(PaymentProcessor):
def __init__(self, legacy_gateway: LegacyPaymentGateway):
self.legacy_gateway = legacy_gateway
def process_payment(self, amount: float) -> str:
# Translate new interface to legacy interface
return self.legacy_gateway.make_payment(amount)
# Client code
def main():
legacy_gateway = LegacyPaymentGateway()
adapter = PaymentAdapter(legacy_gateway)
print(adapter.process_payment(100.0)) # Output: "Legacy gateway processed $100.0"
if __name__ == "__main__":
main()
Use Cases
- Integrating third-party libraries with incompatible interfaces.
- Reusing legacy code in modern systems.
- Supporting multiple data formats (e.g., JSON to XML adapters).
Pros & Cons
| Pros | Cons |
|---|---|
| Enables reuse of existing components. | Adds a layer of indirection, which may slightly slow down calls. |
| Decouples client code from legacy implementations. | Can complicate the codebase if overused. |
Bridge Pattern
Intent
Separate an abstraction from its implementation so that the two can vary independently.
Problem
You have two orthogonal dimensions of variation in your code (e.g., abstractions like Shape and implementations like Color). If you combine them directly (e.g., RedCircle, BlueSquare), you end up with a “class explosion” (n abstractions × m implementations = n×m subclasses).
Solution
Split the code into two hierarchies:
- Abstraction: Defines the high-level interface (e.g.,
Shape). - Implementation: Defines the low-level interface (e.g.,
Color).
The abstraction holds a reference to an implementation object, delegating concrete work to it. This lets you vary abstractions and implementations independently.
Python Example
Let’s model shapes and colors. Instead of creating RedCircle, BlueSquare, etc., we’ll separate Shape (abstraction) and Color (implementation).
# Implementation hierarchy: Color
class Color:
def fill(self) -> str:
raise NotImplementedError
class Red(Color):
def fill(self) -> str:
return "red"
class Blue(Color):
def fill(self) -> str:
return "blue"
# Abstraction hierarchy: Shape (depends on Color)
class Shape:
def __init__(self, color: Color):
self.color = color # Reference to implementation
def draw(self) -> str:
raise NotImplementedError
class Circle(Shape):
def draw(self) -> str:
return f"Drawing a {self.color.fill()} circle"
class Square(Shape):
def draw(self) -> str:
return f"Drawing a {self.color.fill()} square"
# Client code
def main():
red = Red()
blue = Blue()
red_circle = Circle(red)
blue_square = Square(blue)
print(red_circle.draw()) # Output: "Drawing a red circle"
print(blue_square.draw()) # Output: "Drawing a blue square"
if __name__ == "__main__":
main()
Here, adding a new color (e.g., Green) or shape (e.g., Triangle) only requires a new class in one hierarchy, avoiding subclass explosion.
Use Cases
- When both abstraction and implementation need to evolve independently (e.g., UI frameworks with themes and widgets).
- When you want to hide implementation details from clients.
Pros & Cons
| Pros | Cons |
|---|---|
| Reduces subclass proliferation. | Increases complexity by introducing extra layers. |
| Abstraction and implementation can be developed independently. | Requires careful design to avoid over-engineering. |
Composite Pattern
Intent
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Problem
You need to work with tree-like structures (e.g., file systems, organization charts) where elements can be either “leaves” (individual items) or “composites” (collections of items). Clients should interact with leaves and composites the same way.
Solution
Define a common Component interface for all elements (leaves and composites). Leaves implement the interface directly, while composites implement it by delegating to their child components.
Python Example
Model a file system with File (leaf) and Directory (composite) components:
from abc import ABC, abstractmethod
from typing import List
# Component interface
class FileSystemComponent(ABC):
@abstractmethod
def display(self, indent: int = 0) -> None:
pass
# Leaf: File
class File(FileSystemComponent):
def __init__(self, name: str):
self.name = name
def display(self, indent: int = 0) -> None:
print(" " * indent + f"File: {self.name}")
# Composite: Directory (contains other components)
class Directory(FileSystemComponent):
def __init__(self, name: str):
self.name = name
self.children: List[FileSystemComponent] = []
def add(self, component: FileSystemComponent) -> None:
self.children.append(component)
def remove(self, component: FileSystemComponent) -> None:
self.children.remove(component)
def display(self, indent: int = 0) -> None:
print(" " * indent + f"Directory: {self.name}")
for child in self.children:
child.display(indent + 1) # Delegate to children
# Client code
def main():
root = Directory("root")
docs = Directory("docs")
root.add(docs)
docs.add(File("resume.pdf"))
docs.add(File("notes.txt"))
root.add(File("README.md"))
root.display()
if __name__ == "__main__":
main()
Output:
Directory: root
Directory: docs
File: resume.pdf
File: notes.txt
File: README.md
Clients can call display() on any FileSystemComponent (file or directory) without needing to distinguish between them.
Use Cases
- Tree structures (file systems, organization charts, XML/JSON parsers).
- UI components (e.g., buttons, panels containing other components).
Pros & Cons
| Pros | Cons |
|---|---|
| Clients treat leaves and composites uniformly. | Can make the design overly general (e.g., adding add/remove to leaves is unnecessary). |
| Easy to add new component types. | Requires careful handling of composite-specific operations (e.g., remove on a leaf). |
Decorator Pattern
Intent
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Problem
You need to add features to an object (e.g., logging, caching) without modifying its code (Open/Closed Principle). Subclassing would create a proliferation of classes (e.g., LoggedFile, CachedLoggedFile).
Solution
Wrap the original object in a series of “decorator” objects, each adding a specific behavior. Decorators implement the same interface as the original object, so clients can interact with them transparently.
Python Example
Let’s model pizza customization, where toppings (decorators) can be added to a base pizza (component):
from abc import ABC, abstractmethod
# Component interface
class Pizza(ABC):
@abstractmethod
def get_description(self) -> str:
pass
@abstractmethod
def get_cost(self) -> float:
pass
# Concrete Component: Base pizza
class Margherita(Pizza):
def get_description(self) -> str:
return "Margherita"
def get_cost(self) -> float:
return 8.99
# Decorator base class (implements Pizza interface)
class ToppingDecorator(Pizza):
def __init__(self, pizza: Pizza):
self.pizza = pizza # Wrap the pizza
@abstractmethod
def get_description(self) -> str:
pass
@abstractmethod
def get_cost(self) -> float:
pass
# Concrete Decorators
class Cheese(ToppingDecorator):
def get_description(self) -> str:
return f"{self.pizza.get_description()}, Extra Cheese"
def get_cost(self) -> float:
return self.pizza.get_cost() + 1.50
class Mushrooms(ToppingDecorator):
def get_description(self) -> str:
return f"{self.pizza.get_description()}, Mushrooms"
def get_cost(self) -> float:
return self.pizza.get_cost() + 1.00
# Client code
def main():
pizza = Margherita()
print(f"Basic: {pizza.get_description()} - ${pizza.get_cost()}")
pizza = Cheese(pizza) # Add cheese
pizza = Mushrooms(pizza) # Add mushrooms
print(f"Custom: {pizza.get_description()} - ${pizza.get_cost()}")
if __name__ == "__main__":
main()
Output:
Basic: Margherita - $8.99
Custom: Margherita, Extra Cheese, Mushrooms - $11.49
Use Cases
- Adding features dynamically (e.g., logging, validation, caching).
- Implementing optional features (e.g., pizza toppings, UI theme extensions).
Pros & Cons
| Pros | Cons |
|---|---|
| Add/remove responsibilities at runtime. | Can lead to many small decorator classes. |
| Follows Open/Closed Principle. | Decorators can obscure the original object’s behavior. |
Facade Pattern
Intent
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
Problem
A complex subsystem (e.g., a home theater system with an amplifier, DVD player, projector, and speakers) has many components with intricate interactions. Clients must understand and coordinate these components, leading to tight coupling and error-prone code.
Solution
Create a Facade class that encapsulates the subsystem’s complexity, exposing simple methods (e.g., watch_movie()) that handle all component interactions.
Python Example
Model a home theater system with multiple components and a facade to simplify movie watching:
# Subsystem components
class Amplifier:
def on(self) -> None: print("Amplifier on")
def set_volume(self, level: int) -> None: print(f"Volume set to {level}")
class DVDPlayer:
def on(self) -> None: print("DVD Player on")
def play(self, movie: str) -> None: print(f"Playing '{movie}'")
class Projector:
def on(self) -> None: print("Projector on")
def set_input(self, source: str) -> None: print(f"Input set to {source}")
# Facade
class HomeTheaterFacade:
def __init__(self, amp: Amplifier, dvd: DVDPlayer, projector: Projector):
self.amp = amp
self.dvd = dvd
self.projector = projector
def watch_movie(self, movie: str) -> None:
print("\n=== Preparing to watch a movie ===")
self.projector.on()
self.projector.set_input("DVD")
self.amp.on()
self.amp.set_volume(10)
self.dvd.on()
self.dvd.play(movie)
# Client code
def main():
amp = Amplifier()
dvd = DVDPlayer()
projector = Projector()
theater = HomeTheaterFacade(amp, dvd, projector)
theater.watch_movie("Inception")
if __name__ == "__main__":
main()
Output:
=== Preparing to watch a movie ===
Projector on
Input set to DVD
Amplifier on
Volume set to 10
DVD Player on
Playing 'Inception'
Use Cases
- Simplifying access to complex subsystems (e.g., databases, APIs with many endpoints).
- Reducing coupling between clients and subsystems.
Pros & Cons
| Pros | Cons |
|---|---|
| Reduces subsystem complexity for clients. | Can become a “god object” if it grows to handle too many responsibilities. |
| Decouples clients from subsystem components. | May hide useful subsystem features from advanced clients. |
Flyweight Pattern
Intent
Use sharing to support large numbers of fine-grained objects efficiently. Flyweight minimizes memory usage by sharing common state between objects.
Problem
You need to create thousands of similar objects (e.g., characters in a text editor, trees in a game), each with mostly identical “intrinsic” state (e.g., font, color) and some unique “extrinsic” state (e.g., position). Storing duplicate intrinsic state wastes memory.
Solution
Separate intrinsic state (shared) from extrinsic state (unique). Create a Flyweight factory that caches objects with the same intrinsic state, reusing them instead of creating new ones.
Python Example
Model characters in a text editor where most share the same font and size (intrinsic state), but have unique positions (extrinsic state):
from typing import Dict
# Flyweight: Shared intrinsic state
class CharacterFlyweight:
def __init__(self, font: str, size: int):
self.font = font # Intrinsic state (shared)
self.size = size # Intrinsic state (shared)
def render(self, char: str, x: int, y: int) -> None:
# Extrinsic state (position) passed as arguments
print(f"Rendering '{char}' at ({x},{y}) with {self.font} {self.size}pt")
# Flyweight Factory: Caches flyweights
class CharacterFactory:
_flyweights: Dict[tuple, CharacterFlyweight] = {}
@classmethod
def get_flyweight(cls, font: str, size: int) -> CharacterFlyweight:
key = (font, size)
if key not in cls._flyweights:
cls._flyweights[key] = CharacterFlyweight(font, size)
return cls._flyweights[key]
# Client code: Manages extrinsic state
def main():
factory = CharacterFactory()
# Render 5 'A's with the same font/size (shared flyweight)
flyweight = factory.get_flyweight("Arial", 12)
for i in range(5):
flyweight.render("A", x=i*20, y=100) # Extrinsic state: x varies
# Render a 'B' with different font/size (new flyweight)
flyweight = factory.get_flyweight("Times New Roman", 14)
flyweight.render("B", x=0, y=120)
print(f"Total flyweights created: {len(factory._flyweights)}") # Output: 2
if __name__ == "__main__":
main()
Use Cases
- Large object collections with shared state (e.g., game sprites, text rendering, map tiles).
- Memory-constrained systems (e.g., mobile apps).
Pros & Cons
| Pros | Cons |
|---|---|
| Reduces memory usage significantly. | Adds complexity (separating intrinsic/extrinsic state). |
| Centralizes shared state management. | Extrinsic state must be passed around, which can be cumbersome. |
Proxy Pattern
Intent
Provide a surrogate or placeholder for another object to control access to it.
Problem
You need to restrict access to an object (e.g., for security), delay its creation (lazy initialization), or log interactions. Directly accessing the object would expose it to unintended use or inefficiency.
Solution
Create a Proxy object that implements the same interface as the target object. The Proxy intercepts calls to the target, adding behavior (e.g., lazy loading, access control) before delegating to the target.
Python Example
A ProxyImage that delays loading an image from disk until it’s needed (lazy initialization):
from abc import ABC, abstractmethod
# Subject interface
class Image(ABC):
@abstractmethod
def display(self) -> None:
pass
# Real Subject: Loads image from disk (expensive)
class RealImage(Image):
def __init__(self, filename: str):
self.filename = filename
self._load_from_disk() # Heavy operation
def _load_from_disk(self) -> None:
print(f"Loading image: {self.filename}")
def display(self) -> None:
print(f"Displaying image: {self.filename}")
# Proxy: Controls access to RealImage (lazy loading)
class ProxyImage(Image):
def __init__(self, filename: str):
self.filename = filename
self._real_image: RealImage | None = None # Defer creation
def display(self) -> None:
if not self._real_image:
self._real_image = RealImage(self.filename) # Create only when needed
self._real_image.display()
# Client code
def main():
image = ProxyImage("vacation.jpg")
print("Image created (but not loaded yet)")
print("\nFirst display:")
image.display() # Loads and displays
print("\nSecond display:")
image.display() # Uses cached RealImage
if __name__ == "__main__":
main()
Output:
Image created (but not loaded yet)
First display:
Loading image: vacation.jpg
Displaying image: vacation.jpg
Second display:
Displaying image: vacation.jpg
Use Cases
- Remote Proxy: Represent an object in a different address space (e.g., API clients).
- Virtual Proxy: Lazy initialization (e.g., loading large images).
- Protection Proxy: Control access (e.g., restricting sensitive operations).
Pros & Cons
| Pros | Cons |
|---|---|
| Adds functionality without modifying the target object. | Introduces indirection and potential latency. |
| Enables lazy initialization and access control. | Proxies must mimic the target’s interface, which can be error-prone. |
Comparing Structural Patterns
To choose the right structural pattern, consider their core goals:
| Pattern | Core Goal | Key Differentiator |
|---|---|---|
| Adapter | Make incompatible interfaces compatible | Translates between interfaces. |
| Bridge | Separate abstraction from implementation | Decouples hierarchies to enable independent variation. |
| Composite | Treat individual objects and composites uniformly | Tree structure with part-whole relationships. |
| Decorator | Add responsibilities dynamically | Wraps objects to extend behavior. |
| Facade | Simplify a complex subsystem | Provides a unified, high-level interface. |
| Flyweight | Reduce memory usage for large object sets | Shares intrinsic state between objects. |
| Proxy | Control access to an object | Acts as a surrogate for the target object. |
Conclusion
Structural design patterns are powerful tools for building flexible, maintainable, and efficient systems. By focusing on object composition, they help simplify relationships between components, promote reuse, and enable systems to evolve gracefully.
Whether you need to integrate legacy code (Adapter), reduce subclass proliferation (Bridge), or add features dynamically (Decorator), there’s a structural pattern to solve the problem. The key is to understand the tradeoffs and apply patterns judiciously—over-engineering with patterns can complicate code just as much as under-engineering.
By mastering these patterns, you’ll write code that is not only functional but also elegant and adaptable to change.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Simionato, M. (2006). Python Design Patterns. O’Reilly Media.
- Real Python: Design Patterns in Python
- Python.org: Design Patterns