Pythonic Bridge Pattern: Decoupling with Protocols and ABCs

Overview of the Bridge Pattern

The Bridge pattern is a structural design pattern that separates a large class or a set of closely related classes into two separate hierarchies: abstraction and implementation. This matters because it allows both hierarchies to develop independently. Imagine a streaming application where you have services like

or
Twitch
and hardware like webcams or DSLR cameras. Without a bridge, you might end up with a combinatorial explosion of classes (e.g., YouTubeWebcam, YouTubeDSLR, TwitchWebcam). The Bridge pattern fixes this by making the service hold a reference to the device, decoupling the two.

Prerequisites

To follow this guide, you should understand

basics, specifically:

  • Class inheritance and composition.
  • The concept of interfaces (how one object interacts with another).
  • Basic familiarity with
    Type Hinting
    and decorators.

Key Libraries & Tools

  • abc: The Abstract Base Classes module for defining formal interfaces.
  • typing: Specifically Protocol for structural subtyping and Callable for functional approaches.
  • dataclasses: Used to reduce boilerplate when creating data-holding classes.
  • mermaid: A text-based diagramming tool used for visualizing UML relationships.
Pythonic Bridge Pattern: Decoupling with Protocols and ABCs
Let's Take The Bridge Pattern To The Next Level

Code Walkthrough: From Protocols to ABCs

Initially, we can define the relationship using a Protocol. This allows for duck-typing where the service doesn't care about the device's class hierarchy, only that it has a get_buffer_data method.

from typing import Protocol

class StreamingDevice(Protocol):
    def get_buffer_data(self) -> str: ...

However,

allows us to be even more functional. Since a device just "gets data," we can replace the entire device hierarchy with a Callable type alias. This removes the need for extra classes entirely.

from typing import Callable

BufferData = str
Buffer = Callable[[], BufferData]

def get_webcam_data() -> BufferData:
    return "Webcam data stream"

To add more power, we move back to an

(ABC). This allows us to implement shared logic, such as managing a list of multiple devices, directly in the parent class.

from abc import ABC, abstractmethod
from dataclasses import dataclass, field

@dataclass
class StreamingService(ABC):
    devices: list[Buffer] = field(default_factory=list)

    def add_device(self, device: Buffer):
        self.devices.append(device)

    @abstractmethod
    def start_stream(self):
        ...

Syntax Notes

  • First-Class Functions: We pass functions like get_webcam_data as arguments without calling them (omitting the parentheses). This treats logic as data.
  • Default Factory: In dataclasses, we use field(default_factory=list) to avoid the "mutable default argument" trap.

Practical Examples

  • Cross-Platform UI: Separating a UI component (Button) from the OS implementation (Windows vs. macOS).
  • Database Drivers: A generic Database abstraction using specific PostgreSQL or MongoDB implementations.

Tips & Gotchas

Avoid using Protocol when you need to share implementation logic; that's where ABC shines. If your implementation hierarchy is just a single method, skip the class and use a function to keep your codebase lean.

Pythonic Bridge Pattern: Decoupling with Protocols and ABCs

Fancy watching it?

Watch the full video and context

3 min read