Session 11 · Bridge pattern#

The big idea#

When a class can vary in two independent directions, inheritance explodes. A Report can be Sales or Inventory, and can be exported as PDF or Excel. With subclasses you need SalesPDFReport, SalesExcelReport, InventoryPDFReport, InventoryExcelReport. Add CSV and you write two more. Add a third report type and you write three more.

The Bridge pattern splits the two directions into two small hierarchies connected by composition: the report (abstraction) holds an exporter (implementation). Mix and match freely.

Everyday example: a TV and a remote. Any remote brand works with any TV that speaks the standard signal. You don’t build a “Samsung-TV-with-Sony-remote” product for every pair.

In plain words

When a thing can change in two separate ways (what it is, and how it’s output), keep those two apart and connect them, instead of a subclass for every combination.

Reach for it when:

  • class names start combining two ideas: SalesPDFReport, SalesCSVReport…

  • both directions keep growing independently

  • you want to mix and match at run time

Without Bridge#

0.class_explosion.py:

class SalesPDFReport: ...
class SalesExcelReport: ...
class InventoryPDFReport: ...
class InventoryExcelReport: ...
# + CSV → 2 more, + Finance report → 3 more ...   2 × 2 = 4,  3 × 3 = 9

With Bridge#

1.bridge.py:

from abc import ABC, abstractmethod

# Implementation side: HOW to output
class Exporter(ABC):
    @abstractmethod
    def export(self, title: str, rows: list[str]) -> str: ...

class PDFExporter(Exporter):
    def export(self, title, rows):
        return f"[PDF] {title}: " + " | ".join(rows)

class CSVExporter(Exporter):
    def export(self, title, rows):
        return f"[CSV] {title}\n" + "\n".join(rows)

# Abstraction side: WHAT the report is
class Report(ABC):
    def __init__(self, exporter: Exporter):
        self.exporter = exporter          # ← the bridge

    @abstractmethod
    def data(self) -> list[str]: ...

    def generate(self) -> str:
        return self.exporter.export(self.__class__.__name__, self.data())

class SalesReport(Report):
    def data(self): return ["Jan: 100", "Feb: 150"]

class InventoryReport(Report):
    def data(self): return ["Laptops: 12", "Mice: 80"]

print(SalesReport(PDFExporter()).generate())
print(InventoryReport(CSVExporter()).generate())
[PDF] SalesReport: Jan: 100 | Feb: 150
[CSV] InventoryReport
Laptops: 12
Mice: 80

2 reports + 2 exporters = 4 classes instead of 4 combos. With 3 + 3 it is 6 instead of 9, and the gap grows fast. Adding ExcelExporter doesn’t touch any report.

Bridge: two hierarchies joined by one link WHAT the report is and HOW it is exported vary on their own. Any report works with any exporter: 2 + 2 classes instead of 2 × 2. ABSTRACTION: what IMPLEMENTATION: how Report Exporter has an exporter (the bridge) SalesReport InventoryReport PDFExporter CSVExporter SalesReport(PDFExporter()) · InventoryReport(CSVExporter())
WHAT the report is and HOW it is exported vary on their own. Any report works with any exporter: 2 + 2 classes instead of 2 × 2.

Switch at run time#

Because it is composition, you can change the implementation while the program runs:

report = SalesReport(PDFExporter())
report.exporter = CSVExporter()      # now it exports CSV

Bridge + AI: agent × model#

An agent type (Support, Researcher) is one direction. The model backend (Groq, OpenAI, Local) is the other (2.agent_bridge.py):

class Backend(ABC):
    @abstractmethod
    def complete(self, prompt: str) -> str: ...

class Agent:
    system_prompt = ""
    def __init__(self, backend: Backend):
        self.backend = backend
    def ask(self, question: str) -> str:
        return self.backend.complete(f"{self.system_prompt}\n\n{question}")

class SupportAgent(Agent):
    system_prompt = "You are a polite customer support agent."

class ResearchAgent(Agent):
    system_prompt = "You are a careful researcher. Cite sources."

Any agent works with any backend. Swap the model for cost or privacy without rewriting agents.

Bridge vs Adapter vs Strategy#

  • Adapter fixes a mismatch after the fact. Bridge is planned up front.

  • Strategy (session 13) swaps one algorithm. Bridge splits a whole class into two hierarchies that evolve separately. The code looks similar; the scale and intent differ.

Common mistakes#

  • Using Bridge when only one dimension varies. A plain interface is enough.

  • Putting export logic back in the report. Then the two sides aren’t independent.

  • Wide implementation interface. Keep Exporter small, or every new exporter is painful.

Hands-on exercises#

Exercise 1 · Add Excel. Add an ExcelExporter and use it with both reports.

Solution
class ExcelExporter(Exporter):
    def export(self, title, rows):
        return f"[XLSX] {title} ({len(rows)} rows)"

print(SalesReport(ExcelExporter()).generate())
print(InventoryReport(ExcelExporter()).generate())

Zero changes to the reports.

Exercise 2 · Shapes and colours. Circle and Square can be Red or Blue. Build it with Bridge.

Solution
class Color(ABC):
    @abstractmethod
    def fill(self) -> str: ...
class Red(Color):
    def fill(self): return "red"
class Blue(Color):
    def fill(self): return "blue"

class Shape:
    def __init__(self, color: Color): self.color = color
class Circle(Shape):
    def draw(self): return f"{self.color.fill()} circle"
class Square(Shape):
    def draw(self): return f"{self.color.fill()} square"

print(Square(Blue()).draw())    # blue square

Exercise 3 · Notification × channel. Alert/Reminder messages, sent through Email/SMS. Write it and print Reminder via SMS.

Solution
class Channel(ABC):
    @abstractmethod
    def send(self, text: str): ...
class Email(Channel):
    def send(self, text): print(f"EMAIL: {text}")
class SMS(Channel):
    def send(self, text): print(f"SMS: {text}")

class Message:
    def __init__(self, channel: Channel): self.channel = channel
class Alert(Message):
    def notify(self, what): self.channel.send(f"🚨 ALERT: {what}")
class Reminder(Message):
    def notify(self, what): self.channel.send(f"⏰ Reminder: {what}")

Reminder(SMS()).notify("Class at 8 PM")

Full source#

1.bridge.py
"""Bridge: WHAT the report is and HOW it is exported vary independently."""
from abc import ABC, abstractmethod


# Implementation side: HOW to output
class Exporter(ABC):
    @abstractmethod
    def export(self, title: str, rows: list[str]) -> str: ...


class PDFExporter(Exporter):
    def export(self, title, rows):
        return f"[PDF] {title}: " + " | ".join(rows)


class CSVExporter(Exporter):
    def export(self, title, rows):
        return f"[CSV] {title}\n" + "\n".join(rows)


# Abstraction side: WHAT the report is
class Report(ABC):
    def __init__(self, exporter: Exporter):
        self.exporter = exporter          # <- the bridge

    @abstractmethod
    def data(self) -> list[str]: ...

    def generate(self) -> str:
        return self.exporter.export(self.__class__.__name__, self.data())


class SalesReport(Report):
    def data(self): return ["Jan: 100", "Feb: 150"]


class InventoryReport(Report):
    def data(self): return ["Laptops: 12", "Mice: 80"]


print(SalesReport(PDFExporter()).generate())
print(InventoryReport(CSVExporter()).generate())

report = SalesReport(PDFExporter())
report.exporter = CSVExporter()      # switch the implementation at run time
print(report.generate())
2.agent_bridge.py
"""Bridge + AI: agent type x model backend, mixed and matched freely.

The backends are offline stand-ins; a real one would call Groq, OpenAI or a local model.
"""
from abc import ABC, abstractmethod


class Backend(ABC):
    @abstractmethod
    def complete(self, prompt: str) -> str: ...


class FakeGroq(Backend):
    def complete(self, prompt): return f"[groq] {prompt.splitlines()[0]}"


class FakeLocal(Backend):
    def complete(self, prompt): return f"[local] {prompt.splitlines()[0]}"


class Agent:
    system_prompt = ""

    def __init__(self, backend: Backend):
        self.backend = backend

    def ask(self, question: str) -> str:
        return self.backend.complete(f"{self.system_prompt}\n\n{question}")


class SupportAgent(Agent):
    system_prompt = "You are a polite customer support agent."


class ResearchAgent(Agent):
    system_prompt = "You are a careful researcher. Cite sources."


for agent in (SupportAgent(FakeGroq()), SupportAgent(FakeLocal()), ResearchAgent(FakeLocal())):
    print(type(agent).__name__, type(agent.backend).__name__, "->", agent.ask("Where is my order?"))

Downloads: 0.class_explosion.py · 1.bridge.py · 2.agent_bridge.py