Bridge Pattern — Decouple Abstraction from Implementation
In this tutorial, you'll learn about Bridge Pattern. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
What You'll Learn
You will understand how the Bridge pattern prevents an explosion of class combinations by separating an object's high-level operations from their platform-specific implementations. You will distinguish Bridge from Adapter, which is similar in structure but different in intent.
Why It Matters
Without Bridge, adding a new platform or a new abstraction forces you to subclass across both dimensions. A classic example: shapes and renderers — adding Circle and SVGRenderer shouldn't require creating CircleSVGRenderer, SquareSVGRenderer, CircleCanvasRenderer, and so on. With 5 shapes and 4 renderers, you'd need 20 classes. Bridge reduces this to 9 classes (5 shapes + 4 renderers) by letting them vary independently.
Real-World Use
Cross-platform graphics libraries (Skia, Cairo, Direct2D) expose a uniform drawing API backed by platform-specific implementations. DodaTech's document renderer separates document models (PDF, HTML, Markdown) from output backends (file, S3, HTTP response). Adding a new output format like JSON doesn't require touching any document model, and adding a new document template doesn't require touching any output backend.
The Pattern
Abstraction defines the high-level control logic. Implementor declares the platform-specific interface. ConcreteImplementor provides the platform binding. RefinedAbstraction extends the abstraction.
from abc import ABC, abstractmethod
class Renderer(ABC):
@abstractmethod
def render_circle(self, x: float, y: float, r: float) -> str:
pass
@abstractmethod
def render_square(self, x: float, y: float, side: float) -> str:
pass
class SVGRenderer(Renderer):
def render_circle(self, x: float, y: float, r: float) -> str:
return f'<circle cx="{x}" cy="{y}" r="{r}" />'
def render_square(self, x: float, y: float, side: float) -> str:
return f'<rect x="{x}" y="{y}" width="{side}" height="{side}" />'
class CanvasRenderer(Renderer):
def render_circle(self, x: float, y: float, r: float) -> str:
return f"Canvas: circle at ({x},{y}) radius {r}"
def render_square(self, x: float, y: float, side: float) -> str:
return f"Canvas: square at ({x},{y}) size {side}"
class Shape(ABC):
def __init__(self, renderer: Renderer):
self._renderer = renderer
@abstractmethod
def draw(self) -> str:
pass
class Circle(Shape):
def __init__(self, renderer: Renderer, x: float, y: float, r: float):
super().__init__(renderer)
self._x = x
self._y = y
self._r = r
def draw(self) -> str:
return self._renderer.render_circle(self._x, self._y, self._r)
class Square(Shape):
def __init__(self, renderer: Renderer, x: float, y: float, side: float):
super().__init__(renderer)
self._x = x
self._y = y
self._side = side
def draw(self) -> str:
return self._renderer.render_square(self._x, self._y, self._side)
svg = SVGRenderer()
canvas = CanvasRenderer()
shapes = [Circle(svg, 10, 10, 5), Square(canvas, 0, 0, 20)]
for s in shapes:
print(s.draw())
<circle cx="10" cy="10" r="5" />
Canvas: square at (0,0) size 20
Structure
classDiagram
class Abstraction {
#implementor: Implementor
+operation()
}
class RefinedAbstraction {
+operation()
}
class Implementor {
<>
+operationImpl()
}
class ConcreteImplementorA {
+operationImpl()
}
class ConcreteImplementorB {
+operationImpl()
}
RefinedAbstraction --|> Abstraction
ConcreteImplementorA ..|> Implementor
ConcreteImplementorB ..|> Implementor
Abstraction --> Implementor : delegates
Real-World Usage
- Java JDBC — the
DriverManagerabstraction works with any database driver implementation (MySQL, PostgreSQL, H2). - Python
logginghandlers — loggers (abstraction) send output to handlers (implementation) likeFileHandler,StreamHandler,SysLogHandler. - React Native — JavaScript components (abstraction) render via platform-specific threads (implementation).
- Qt QPainter — painting abstraction runs on raster, OpenGL, or PDF backends.
Related Patterns
- Adapter makes unrelated classes work together; Bridge is designed up-front.
- Abstract Factory can create and configure a particular Bridge implementation.
- Strategy is similar but focuses on algorithms rather than platform-binding.
- Composite can be combined with Bridge when tree structures need platform-specific rendering.
Pros and Cons
| Pros | Cons |
|---|---|
| Eliminates combinatorial class explosion | Increases complexity if the abstraction is stable |
| Implementation details stay isolated | Requires upfront design — hard to retrofit |
| Both hierarchies can be extended independently | Indirect call adds a small runtime cost |
| Follows the Open/Closed Principle perfectly | Can feel like over-engineering for single-platform apps |
The code demonstrates the combinatorial savings. With 2 shapes (Circle, Square) and 2 renderers (SVG, Canvas), the Bridge approach requires only 4 shape classes and 2 renderer classes = 6 total. The non-Bridge approach (one class per shape-renderer combination) would require 4 classes already, and the gap widens as dimensions grow.
Practice Questions
- How does Bridge differ from Adapter in terms of when each pattern is applied?
- Implement a messaging bridge where the abstraction is a
Notificationand implementations areEmailSender,SMSSender, andPushSender. - What happens to the Bridge when both sides have more than one dimension of variation? How does the class count scale?
- How would you configure a Bridge using Dependency Injection to select implementations at runtime?
Challenge
Add a third implementor — AsciiRenderer that renders shapes using ASCII art characters. Verify it works with both Circle and Square without modifying either class.
Real-World Task
Identify two orthogonal dimensions of variation in your architecture (e.g., data format × storage backend, or notification type × delivery channel). Use DodaTech's variant explorer to map them and refactor with Bridge.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro