Skip to content

Structural Design Patterns in Python

Introduction

What Are Structural Patterns?

Structural patterns explain how to assemble objects and classes into larger structures while maintaining flexibility and efficiency. They help ensure that when one part of a system changes, the entire structure doesn't need to change.

The Seven Structural Patterns

1. Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

class EuropeanSocketInterface:
    def voltage(self): pass
    def live(self): pass
    def neutral(self): pass
    def earth(self): pass

class Socket(EuropeanSocketInterface):
    def voltage(self):
        return 230

    def live(self):
        return 1

    def neutral(self):
        return -1

    def earth(self):
        return 0

class USASocketInterface:
    def voltage(self): pass
    def live(self): pass
    def neutral(self): pass

class Adapter(USASocketInterface):
    def __init__(self, socket):
        self.socket = socket

    def voltage(self):
        return 110

    def live(self):
        return self.socket.live()

    def neutral(self):
        return self.socket.neutral()

# Usage
socket = Socket()
adapter = Adapter(socket)
print(f"Adapted voltage: {adapter.voltage()}V")

2. Bridge Pattern

The Bridge pattern separates an object's abstraction from its implementation, allowing both to vary independently.

from abc import ABC, abstractmethod

class DrawingAPI(ABC):
    @abstractmethod
    def draw_circle(self, x, y, radius):
        pass

class DrawingAPI1(DrawingAPI):
    def draw_circle(self, x, y, radius):
        return f"API1.circle at ({x}, {y}) with radius {radius}"

class DrawingAPI2(DrawingAPI):
    def draw_circle(self, x, y, radius):
        return f"API2.circle at ({x}, {y}) with radius {radius}"

class Shape(ABC):
    def __init__(self, drawing_api):
        self.drawing_api = drawing_api

    @abstractmethod
    def draw(self):
        pass

class Circle(Shape):
    def __init__(self, x, y, radius, drawing_api):
        super().__init__(drawing_api)
        self.x = x
        self.y = y
        self.radius = radius

    def draw(self):
        return self.drawing_api.draw_circle(self.x, self.y, self.radius)

# Usage
circle1 = Circle(1, 2, 3, DrawingAPI1())
circle2 = Circle(5, 7, 11, DrawingAPI2())
print(circle1.draw())
print(circle2.draw())

3. Composite Pattern

The Composite pattern lets you compose objects into tree structures to represent part-whole hierarchies.

from abc import ABC, abstractmethod

class Component(ABC):
    @abstractmethod
    def operation(self):
        pass

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

    def operation(self):
        return f"Leaf {self.name}"

class Composite(Component):
    def __init__(self, name):
        self.name = name
        self.children = []

    def add(self, component):
        self.children.append(component)

    def remove(self, component):
        self.children.remove(component)

    def operation(self):
        results = [f"Branch {self.name}"]
        for child in self.children:
            results.append(child.operation())
        return "\\n".join(results)

# Usage
tree = Composite("root")
branch1 = Composite("branch1")
branch1.add(Leaf("leaf1"))
branch1.add(Leaf("leaf2"))

branch2 = Composite("branch2")
branch2.add(Leaf("leaf3"))

tree.add(branch1)
tree.add(branch2)
print(tree.operation())

4. Decorator Pattern

The Decorator pattern attaches new behaviors to objects dynamically by placing them inside wrapper objects.

class Coffee:
    def cost(self):
        return 5

    def description(self):
        return "Simple coffee"

class CoffeeDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost()

    def description(self):
        return self._coffee.description()

class Milk(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 2

    def description(self):
        return self._coffee.description() + ", milk"

class Sugar(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 1

    def description(self):
        return self._coffee.description() + ", sugar"

# Usage
coffee = Coffee()
coffee_with_milk = Milk(coffee)
coffee_with_milk_and_sugar = Sugar(coffee_with_milk)

print(f"{coffee_with_milk_and_sugar.description()}: ${coffee_with_milk_and_sugar.cost()}")

5. Facade Pattern

The Facade pattern provides a simplified interface to a complex subsystem.

class CPU:
    def freeze(self):
        print("CPU: Freezing...")

    def jump(self, position):
        print(f"CPU: Jumping to {position}")

    def execute(self):
        print("CPU: Executing...")

class Memory:
    def load(self, position, data):
        print(f"Memory: Loading {data} at {position}")

class HardDrive:
    def read(self, lba, size):
        return f"Data from sector {lba} with size {size}"

class ComputerFacade:
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hard_drive = HardDrive()

    def start(self):
        print("Starting computer...")
        self.cpu.freeze()
        boot_data = self.hard_drive.read(0, 1024)
        self.memory.load(0, boot_data)
        self.cpu.jump(0)
        self.cpu.execute()
        print("Computer started!")

# Usage
computer = ComputerFacade()
computer.start()

6. Flyweight Pattern

The Flyweight pattern minimizes memory usage by sharing data with similar objects.

class TreeType:
    def __init__(self, name, color, texture):
        self.name = name
        self.color = color
        self.texture = texture

    def draw(self, x, y):
        print(f"Drawing {self.name} tree at ({x}, {y}) with {self.color} color")

class TreeFactory:
    _tree_types = {}

    @classmethod
    def get_tree_type(cls, name, color, texture):
        key = (name, color, texture)
        if key not in cls._tree_types:
            cls._tree_types[key] = TreeType(name, color, texture)
            print(f"Created new tree type: {name}")
        return cls._tree_types[key]

class Tree:
    def __init__(self, x, y, tree_type):
        self.x = x
        self.y = y
        self.tree_type = tree_type

    def draw(self):
        self.tree_type.draw(self.x, self.y)

# Usage
factory = TreeFactory()
trees = []

trees.append(Tree(1, 2, factory.get_tree_type("Oak", "Green", "Rough")))
trees.append(Tree(3, 4, factory.get_tree_type("Oak", "Green", "Rough")))
trees.append(Tree(5, 6, factory.get_tree_type("Pine", "Dark Green", "Smooth")))

for tree in trees:
    tree.draw()

7. Proxy Pattern

The Proxy pattern provides a placeholder for another object to control access to it.

from abc import ABC, abstractmethod

class Image(ABC):
    @abstractmethod
    def display(self):
        pass

class RealImage(Image):
    def __init__(self, filename):
        self.filename = filename
        self._load_from_disk()

    def _load_from_disk(self):
        print(f"Loading image: {self.filename}")

    def display(self):
        print(f"Displaying image: {self.filename}")

class ProxyImage(Image):
    def __init__(self, filename):
        self.filename = filename
        self._real_image = None

    def display(self):
        if self._real_image is None:
            self._real_image = RealImage(self.filename)
        self._real_image.display()

# Usage
image1 = ProxyImage("photo1.jpg")
image2 = ProxyImage("photo2.jpg")

print("Images created, but not loaded yet")
image1.display()  # Loads and displays
image1.display()  # Just displays
image2.display()  # Loads and displays

When to Use Structural Patterns

  • Adapter: When you need to use an existing class with an incompatible interface
  • Bridge: When you want to avoid permanent binding between abstraction and implementation
  • Composite: When you need to represent part-whole hierarchies of objects
  • Decorator: When you need to add responsibilities to objects dynamically
  • Facade: When you want to provide a simple interface to a complex subsystem
  • Flyweight: When you need to support large numbers of similar objects efficiently
  • Proxy: When you need to control access to an object

Best Practices

  1. Choose the right pattern: Not every problem needs a design pattern. Use them when they genuinely simplify your code.
  2. Keep it Pythonic: Python has built-in features (decorators, descriptors, etc.) that can sometimes replace pattern implementations.
  3. Favor composition over inheritance: Structural patterns often emphasize object composition.
  4. Document your patterns: Make it clear which patterns you're using and why.
  5. Don't over-engineer: Start simple and refactor to patterns when complexity demands it.

Conclusion

Structural design patterns are powerful tools for creating flexible, maintainable Python code. By understanding and applying these patterns appropriately, you can build systems that are easier to understand, modify, and extend. Remember that patterns are guidelines, not rules—adapt them to fit Python's idioms and your specific needs.