Bridge Design Pattern
I was building a charting library. I had shapes (circles, rectangles) and rendering targets (SVG, Canvas). My first instinct was to create SvgCircle, Canv...
23 Mar 2024

I was building a charting library. I had shapes (circles, rectangles) and rendering targets (SVG, Canvas). My first instinct was to create SvgCircle, CanvasCircle, SvgRectangle, CanvasRectangle... and I immediately saw the explosion coming. Two shapes times two renderers equals four classes. Add a third renderer? Six classes. A fourth shape? Twelve.
The Bridge pattern stops that explosion. It splits the problem into two independent hierarchies — the what (shapes) and the how (renderers) — and connects them with composition instead of inheritance.
Think of it like a remote control and a TV. The remote (abstraction) doesn't care if the TV is Samsung or LG (implementation). You can swap either side independently.
class Renderer {
renderCircle(radius) {
throw new Error("renderCircle method must be implemented.");
}
}
class SvgRenderer extends Renderer {
renderCircle(radius) {
console.log(`Drawing circle with radius ${radius} using SVG.`);
}
}
class CanvasRenderer extends Renderer {
renderCircle(radius) {
console.log(`Drawing circle with radius ${radius} using Canvas.`);
}
}
class Shape {
constructor(renderer) {
this.renderer = renderer;
}
draw() {
throw new Error("draw method must be implemented.");
}
}
class Circle extends Shape {
constructor(renderer, radius) {
super(renderer);
this.radius = radius;
}
draw() {
this.renderer.renderCircle(this.radius);
}
}
const svgRenderer = new SvgRenderer();
const circle1 = new Circle(svgRenderer, 5);
circle1.draw(); // Drawing circle with radius 5 using SVG.
const canvasRenderer = new CanvasRenderer();
const circle2 = new Circle(canvasRenderer, 10);
circle2.draw(); // Drawing circle with radius 10 using Canvas.
Renderer is the implementation interface. SvgRenderer and CanvasRenderer are concrete implementations. Shape is the abstraction that holds a reference to a Renderer. Circle is a refined abstraction.
The key insight: Circle doesn't know how rendering works. It just calls this.renderer.renderCircle(). Add a new renderer? Write one class. Add a new shape? Write one class. No combinatorial explosion.
The benefit: Both hierarchies evolve independently. Adding a WebGLRenderer doesn't touch any shape code. Adding a Rectangle doesn't touch any renderer code.
The cost: More upfront abstraction. If you only have one shape and one renderer, this is over-engineering. The pattern shines when both dimensions are likely to grow.
I reach for Bridge when I see two orthogonal dimensions of variation. If only one dimension varies, a simpler Strategy or Adapter usually suffices.