Design Patterns

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

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, 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.

Javascript
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.