Bridge

The Bridge Pattern is a structural design pattern that separates an abstraction from its implementation, allowing both to vary independently. This pattern is handy when you want to avoid permanent binding between an abstraction and its implementation, or when you need to share an implementation among multiple objects.

Problem Statement

Imagine you’re developing a graphics application that needs to draw shapes on different platforms (Windows, Linux, macOS). Without the Bridge Pattern, you might create a hierarchy like this:

Shape
├── WindowsCircle
├── LinuxCircle
├── MacCircle
├── WindowsRectangle
├── LinuxRectangle
└── MacRectangle

This approach leads to an exponential growth of classes as you add new shapes or platforms. The Bridge Pattern solves this by separating the shape abstraction from the platform implementation.

Structure

    Abstraction                    Implementation
        │                              │
        │                              │
   ┌────▼────┐                   ┌─────▼─────┐
   │ Shape   │◇──────────────────│ Renderer  │
   │         │                   │           │
   └─────────┘                   └───────────┘
        │                              │
        │                              │
   ┌────▼────┐                   ┌─────▼─────┐
   │ Circle  │                   │ Windows   │
   │         │                   │ Renderer  │
   └─────────┘                   └───────────┘
        │                              │
   ┌────▼────┐                   ┌─────▼─────┐
   │Rectangle│                   │ Linux     │
   │         │                   │ Renderer  │
   └─────────┘                   └───────────┘
                                        │
                                  ┌─────▼─────┐
                                  │ Mac       │
                                  │ Renderer  │
                                  └───────────┘

Key Components

Abstraction: Defines the high-level interface and maintains a reference to the implementation object.

Refined Abstraction: Extends the abstraction with additional functionality.

Implementation: Defines the interface for implementation classes. This interface doesn’t need to correspond exactly to the abstraction’s interface.

Concrete Implementation: Contains platform-specific code.

When to Use

  • You want to avoid permanent binding between abstraction and implementation
  • Both abstractions and implementations should be extensible through inheritance
  • Changes in implementation should not impact client code
  • You want to share the implementation among multiple objects
  • You need to switch implementations at runtime

Benefits

  • Decoupling: Abstraction and implementation can vary independently
  • Extensibility: Easy to add new abstractions or implementations
  • Runtime Switching: Can change implementation dynamically
  • Code Reusability: Implementations can be shared across different abstractions

Drawbacks

  • Increases complexity by introducing additional layers
  • May impact performance due to indirection
  • Can be overkill for simple scenarios

Basic Implementation

from abc import ABC, abstractmethod
from typing import Protocol

# Implementation interface
class Renderer(Protocol):
    """Protocol defining the interface for rendering implementations."""
    
    def render_circle(self, x: int, y: int, radius: int) -> None:
        """Render a circle at given position with specified radius."""
        ...
    
    def render_rectangle(self, x: int, y: int, width: int, height: int) -> None:
        """Render a rectangle at given position with specified dimensions."""
        ...

# Concrete implementations for different platforms
class WindowsRenderer:
    """Windows-specific rendering implementation."""
    
    def render_circle(self, x: int, y: int, radius: int) -> None:
        print(f"Windows: Drawing circle at ({x}, {y}) with radius {radius}")
    
    def render_rectangle(self, x: int, y: int, width: int, height: int) -> None:
        print(f"Windows: Drawing rectangle at ({x}, {y}) with size {width}x{height}")

class LinuxRenderer:
    """Linux-specific rendering implementation."""
    
    def render_circle(self, x: int, y: int, radius: int) -> None:
        print(f"Linux: Rendering circle at ({x}, {y}) with radius {radius}")
    
    def render_rectangle(self, x: int, y: int, width: int, height: int) -> None:
        print(f"Linux: Rendering rectangle at ({x}, {y}) with size {width}x{height}")

# Abstraction
class Shape(ABC):
    """Abstract base class for all shapes."""
    
    def __init__(self, renderer: Renderer) -> None:
        """Initialize shape with a specific renderer."""
        self._renderer = renderer
        self._x = 0
        self._y = 0
    
    def set_position(self, x: int, y: int) -> None:
        """Set the position of the shape."""
        self._x = x
        self._y = y
    
    @abstractmethod
    def draw(self) -> None:
        """Draw the shape using the assigned renderer."""
        pass

# Refined abstractions
class Circle(Shape):
    """Circle shape implementation."""
    
    def __init__(self, renderer: Renderer, radius: int) -> None:
        """Initialize circle with renderer and radius."""
        super().__init__(renderer)
        self._radius = radius
    
    def draw(self) -> None:
        """Draw circle using the renderer."""
        self._renderer.render_circle(self._x, self._y, self._radius)
    
    def set_radius(self, radius: int) -> None:
        """Set the radius of the circle."""
        self._radius = radius

class Rectangle(Shape):
    """Rectangle shape implementation."""
    
    def __init__(self, renderer: Renderer, width: int, height: int) -> None:
        """Initialize rectangle with renderer and dimensions."""
        super().__init__(renderer)
        self._width = width
        self._height = height
    
    def draw(self) -> None:
        """Draw rectangle using the renderer."""
        self._renderer.render_rectangle(self._x, self._y, self._width, self._height)
    
    def set_dimensions(self, width: int, height: int) -> None:
        """Set the dimensions of the rectangle."""
        self._width = width
        self._height = height

# Example usage
def main() -> None:
    """Demonstrate the Bridge Pattern."""
    # Create different renderers
    windows_renderer = WindowsRenderer()
    linux_renderer = LinuxRenderer()
    
    # Create shapes with different renderers
    circle_windows = Circle(windows_renderer, 10)
    circle_linux = Circle(linux_renderer, 15)
    
    rectangle_windows = Rectangle(windows_renderer, 20, 30)
    rectangle_linux = Rectangle(linux_renderer, 25, 35)
    
    # Set positions and draw shapes
    circle_windows.set_position(100, 100)
    circle_windows.draw()
    
    circle_linux.set_position(200, 200)
    circle_linux.draw()
    
    rectangle_windows.set_position(50, 50)
    rectangle_windows.draw()
    
    rectangle_linux.set_position(150, 150)
    rectangle_linux.draw()
    
    # Demonstrate that we can change properties
    circle_windows.set_radius(20)
    circle_windows.draw()

if __name__ == "__main__":
    main()

The basic implementation demonstrates the core concepts of the Bridge Pattern:

  • Renderer Protocol: Defines the interface that all rendering implementations must follow. Using Python’s Protocol provides type safety while maintaining flexibility.
  • Concrete Renderers: WindowsRenderer and LinuxRenderer implement platform-specific rendering logic. Each handles rendering operations differently.
  • Shape Abstraction: The abstract Shape class holds a reference to a Renderer and provides common functionality like position management.
  • Refined Abstractions: Circle and Rectangle extend the base Shape class with specific drawing logic, delegating the actual rendering to their assigned renderer.

The key insight is that shapes don’t know how they’re being rendered - they just know they have a renderer that can handle the drawing operations. This separation allows you to add new shapes without modifying existing renderers, and add new renderers without modifying existing shapes.

Advanced Implementation

The advanced implementation showcases more sophisticated uses of the Bridge Pattern:

from abc import ABC, abstractmethod
from typing import Protocol

# Implementation interface
class Renderer(Protocol):
    """Protocol defining the interface for rendering implementations."""
    
    def render_circle(self, x: int, y: int, radius: int) -> None:
        """Render a circle at given position with specified radius."""
        ...
    
    def render_rectangle(self, x: int, y: int, width: int, height: int) -> None:
        """Render a rectangle at given position with specified dimensions."""
        ...

# Concrete implementations for different platforms
class WindowsRenderer:
    """Windows-specific rendering implementation."""
    
    def render_circle(self, x: int, y: int, radius: int) -> None:
        print(f"Windows: Drawing circle at ({x}, {y}) with radius {radius}")
    
    def render_rectangle(self, x: int, y: int, width: int, height: int) -> None:
        print(f"Windows: Drawing rectangle at ({x}, {y}) with size {width}x{height}")

class LinuxRenderer:
    """Linux-specific rendering implementation."""
    
    def render_circle(self, x: int, y: int, radius: int) -> None:
        print(f"Linux: Rendering circle at ({x}, {y}) with radius {radius}")
    
    def render_rectangle(self, x: int, y: int, width: int, height: int) -> None:
        print(f"Linux: Rendering rectangle at ({x}, {y}) with size {width}x{height}")

# Abstraction
class Shape(ABC):
    """Abstract base class for all shapes."""
    
    def __init__(self, renderer: Renderer) -> None:
        """Initialize shape with a specific renderer."""
        self._renderer = renderer
        self._x = 0
        self._y = 0
    
    def set_position(self, x: int, y: int) -> None:
        """Set the position of the shape."""
        self._x = x
        self._y = y
    
    @abstractmethod
    def draw(self) -> None:
        """Draw the shape using the assigned renderer."""
        pass

# Refined abstractions
class Circle(Shape):
    """Circle shape implementation."""
    
    def __init__(self, renderer: Renderer, radius: int) -> None:
        """Initialize circle with renderer and radius."""
        super().__init__(renderer)
        self._radius = radius
    
    def draw(self) -> None:
        """Draw circle using the renderer."""
        self._renderer.render_circle(self._x, self._y, self._radius)
    
    def set_radius(self, radius: int) -> None:
        """Set the radius of the circle."""
        self._radius = radius

class Rectangle(Shape):
    """Rectangle shape implementation."""
    
    def __init__(self, renderer: Renderer, width: int, height: int) -> None:
        """Initialize rectangle with renderer and dimensions."""
        super().__init__(renderer)
        self._width = width
        self._height = height
    
    def draw(self) -> None:
        """Draw rectangle using the renderer."""
        self._renderer.render_rectangle(self._x, self._y, self._width, self._height)
    
    def set_dimensions(self, width: int, height: int) -> None:
        """Set the dimensions of the rectangle."""
        self._width = width
        self._height = height

# Example usage
def main() -> None:
    """Demonstrate the Bridge Pattern."""
    # Create different renderers
    windows_renderer = WindowsRenderer()
    linux_renderer = LinuxRenderer()
    
    # Create shapes with different renderers
    circle_windows = Circle(windows_renderer, 10)
    circle_linux = Circle(linux_renderer, 15)
    
    rectangle_windows = Rectangle(windows_renderer, 20, 30)
    rectangle_linux = Rectangle(linux_renderer, 25, 35)
    
    # Set positions and draw shapes
    circle_windows.set_position(100, 100)
    circle_windows.draw()
    
    circle_linux.set_position(200, 200)
    circle_linux.draw()
    
    rectangle_windows.set_position(50, 50)
    rectangle_windows.draw()
    
    rectangle_linux.set_position(150, 150)
    rectangle_linux.draw()
    
    # Demonstrate that we can change properties
    circle_windows.set_radius(20)
    circle_windows.draw()

if __name__ == "__main__":
    main()

Enhanced Protocol: The GraphicsRenderer protocol includes initialization, color management, and cleanup methods, demonstrating how the implementation interface can be rich and comprehensive.

Platform-Specific Implementations: Each renderer (WindowsGDIRenderer, LinuxX11Renderer, WebCanvasRenderer) implements platform-specific graphics calls, showing how the same operations can have vastly different implementations.

Runtime Renderer Switching: The switch_renderer method demonstrates one of the Bridge Pattern’s key strengths, the ability to change implementations at runtime without affecting client code.

Factory Pattern Integration: The RendererFactory example shows how the Bridge Pattern often works well with other patterns to provide clean object creation.

  • State Management: Each renderer maintains its own state (color, initialization status) while providing a consistent interface.
  • Error Handling: The implementations include proper error handling for uninitialized renderers, showing robust real-world usage.

Track your progress

Mark this subtopic as completed when you finish reading.