Bridge
Intent
Section titled “Intent”Bridge decouples an abstraction from its implementation by putting each in its own class hierarchy, so the two can change and grow independently instead of multiplying together.
Problem
Section titled “Problem”Suppose you have shapes — circle, square — and you want to draw each one with different rendering backends, say a vector renderer and a raster renderer. The naive route is inheritance: VectorCircle, RasterCircle, VectorSquare, RasterSquare. Add a third shape or a third backend and the class count explodes as the product of the two dimensions. Worse, every shape now knows about every backend.
Bridge breaks that product apart. One hierarchy models the abstraction (the shapes and their high-level operations); a separate hierarchy models the implementation (the rendering primitives). The abstraction holds a reference to an implementation object and delegates the low-level work to it. Now shapes and renderers vary on independent axes: three shapes plus two renderers means five classes, not six, and adding either side does not touch the other.
Structure
Section titled “Structure”classDiagram
class Shape {
<<abstract>>
#renderer: Renderer
+draw()
}
class Circle {
+draw()
}
class Square {
+draw()
}
class Renderer {
<<interface>>
+drawCircle(r)
+drawRectangle(w, h)
}
class VectorRenderer
class RasterRenderer
Shape <|-- Circle
Shape <|-- Square
Shape o--> Renderer : delegates to
Renderer <|.. VectorRenderer
Renderer <|.. RasterRenderer - Abstraction (
Shape) — the high-level interface clients use. It holds a reference to an implementor and delegates primitive operations to it. - Refined Abstraction (
Circle,Square) — concrete variants of the abstraction that may add their own operations. - Implementor (
Renderer) — the interface for the low-level operations the abstraction relies on. - Concrete Implementor (
VectorRenderer,RasterRenderer) — the actual implementations the abstraction can be paired with at runtime.
Example
Section titled “Example”Shapes delegate their drawing to a pluggable renderer. Any shape works with any renderer, chosen when the shape is created.
// Implementor: the low-level drawing operations.interface Renderer { drawCircle(radius: number): string; drawRectangle(width: number, height: number): string;}
class VectorRenderer implements Renderer { drawCircle = (r: number) => `vector circle r=${r}`; drawRectangle = (w: number, h: number) => `vector rect ${w}x${h}`;}
class RasterRenderer implements Renderer { drawCircle = (r: number) => `raster pixels for circle r=${r}`; drawRectangle = (w: number, h: number) => `raster pixels for rect ${w}x${h}`;}
// Abstraction: holds a renderer and delegates to it.abstract class Shape { constructor(protected readonly renderer: Renderer) {} abstract draw(): string;}
class Circle extends Shape { constructor(renderer: Renderer, private readonly radius: number) { super(renderer); } draw = () => this.renderer.drawCircle(this.radius);}
console.log(new Circle(new VectorRenderer(), 5).draw()); // vector circle r=5console.log(new Circle(new RasterRenderer(), 5).draw()); // raster pixels for circle r=5from abc import ABC, abstractmethodfrom typing import Protocol
class Renderer(Protocol): def draw_circle(self, radius: float) -> str: ... def draw_rectangle(self, width: float, height: float) -> str: ...
class VectorRenderer: def draw_circle(self, radius: float) -> str: return f"vector circle r={radius}"
def draw_rectangle(self, width: float, height: float) -> str: return f"vector rect {width}x{height}"
class RasterRenderer: def draw_circle(self, radius: float) -> str: return f"raster pixels for circle r={radius}"
def draw_rectangle(self, width: float, height: float) -> str: return f"raster pixels for rect {width}x{height}"
class Shape(ABC): def __init__(self, renderer: Renderer) -> None: self._renderer = renderer
@abstractmethod def draw(self) -> str: ...
class Circle(Shape): def __init__(self, renderer: Renderer, radius: float) -> None: super().__init__(renderer) self._radius = radius
def draw(self) -> str: return self._renderer.draw_circle(self._radius)
print(Circle(VectorRenderer(), 5).draw()) # vector circle r=5print(Circle(RasterRenderer(), 5).draw()) # raster pixels for circle r=5package main
import "fmt"
// Implementor: the low-level drawing operations.type Renderer interface { DrawCircle(radius float64) string DrawRectangle(width, height float64) string}
type VectorRenderer struct{}
func (VectorRenderer) DrawCircle(r float64) string { return fmt.Sprintf("vector circle r=%g", r)}func (VectorRenderer) DrawRectangle(w, h float64) string { return fmt.Sprintf("vector rect %gx%g", w, h)}
type RasterRenderer struct{}
func (RasterRenderer) DrawCircle(r float64) string { return fmt.Sprintf("raster pixels for circle r=%g", r)}func (RasterRenderer) DrawRectangle(w, h float64) string { return fmt.Sprintf("raster pixels for rect %gx%g", w, h)}
// Abstraction: composes a renderer.type Circle struct { renderer Renderer radius float64}
func (c Circle) Draw() string { return c.renderer.DrawCircle(c.radius) }
func main() { fmt.Println(Circle{VectorRenderer{}, 5}.Draw()) // vector circle r=5 fmt.Println(Circle{RasterRenderer{}, 5}.Draw()) // raster pixels for circle r=5}// Implementor: the low-level drawing operations.trait Renderer { fn draw_circle(&self, radius: f64) -> String; fn draw_rectangle(&self, width: f64, height: f64) -> String;}
struct VectorRenderer;impl Renderer for VectorRenderer { fn draw_circle(&self, r: f64) -> String { format!("vector circle r={r}") } fn draw_rectangle(&self, w: f64, h: f64) -> String { format!("vector rect {w}x{h}") }}
struct RasterRenderer;impl Renderer for RasterRenderer { fn draw_circle(&self, r: f64) -> String { format!("raster pixels for circle r={r}") } fn draw_rectangle(&self, w: f64, h: f64) -> String { format!("raster pixels for rect {w}x{h}") }}
// Abstraction: holds a renderer and delegates to it.struct Circle<'a> { renderer: &'a dyn Renderer, radius: f64,}
impl Circle<'_> { fn draw(&self) -> String { self.renderer.draw_circle(self.radius) }}
fn main() { let circle = Circle { renderer: &VectorRenderer, radius: 5.0 }; println!("{}", circle.draw()); // vector circle r=5 let circle = Circle { renderer: &RasterRenderer, radius: 5.0 }; println!("{}", circle.draw()); // raster pixels for circle r=5}When to use / trade-offs
Section titled “When to use / trade-offs”- Pro: abstraction and implementation grow on independent axes, avoiding a combinatorial class explosion.
- Pro: you can swap implementations at runtime, since the abstraction only holds a reference.
- Pro: each hierarchy can be developed, tested, and extended without touching the other.
- Con: it adds indirection and more moving parts up front, overkill when you only ever have one implementation.
- Con: designing the two interfaces well requires foresight about which dimensions truly vary.
Related patterns
Section titled “Related patterns”- Adapter makes two existing interfaces work together after the fact; Bridge is planned from the start to let two hierarchies evolve separately.
- Abstract Factory can build and pair the right implementor with an abstraction, often used alongside Bridge.