What problem does the Bridge design pattern solve, and how does its structure solve it?
answer
- M×N classes → M+N classes
- Two hierarchies, one reference
- "What" vs "how"; Handle/Body
- Composition replaces the second inheritance axis
- Implementor = primitive ops, not a mirror
basics
~20 sBridge splits a design into two separate hierarchies — an abstraction (what the thing is) and an implementation (how it does its work) — connected by a reference instead of inheritance, so each side can grow independently.
solid answer
~50 sBridge attacks the combinatorial class explosion you get when one type varies along two independent dimensions and you express both with inheritance. If you have 3 shape kinds and 3 rendering backends, subclassing gives you 9 leaf classes (VectorCircle, RasterCircle, ...), and adding a fourth backend costs 3 more. Bridge keeps the two dimensions in two hierarchies: an Abstraction (Shape, with refinements Circle/Square) holding a reference to an Implementor interface (Renderer, with implementations Vector/Raster). Shape delegates primitive operations to the Renderer it was given. Now 3 + 3 = 6 classes, and adding a backend costs exactly one class. The composition link — the 'bridge' — is set at construction or configuration time, which also means the pairing can be chosen at runtime rather than baked in at compile time. Cost: one extra level of indirection and a designed-up-front interface between the halves.
code
pseudocode · 25 lines// --- Implementor: the "how" (primitive operations) ---
interface Renderer {
drawCircle(x, y, radius)
drawLine(x1, y1, x2, y2)
}
class VectorRenderer implements Renderer { ... }
class RasterRenderer implements Renderer { ... }
// --- Abstraction: the "what" ---
abstract class Shape {
protected renderer: Renderer // <-- the bridge
constructor(r: Renderer) { this.renderer = r }
abstract draw()
}
class Circle extends Shape {
draw() { renderer.drawCircle(x, y, radius) } // delegates; never touches pixels
}
class Square extends Shape {
draw() { renderer.drawLine(...) x4 }
}
// Client picks the pairing at runtime:
shape = new Circle(new RasterRenderer())
// 2 shapes + 2 renderers = 4 classes, not 4 combination classes;
// adding SvgRenderer costs exactly one class.go deeper
State the intent — separate what from how so each varies independently — and give the M×N → M+N counting argument with a concrete pair of axes.
Name the four participants (Abstraction, RefinedAbstraction, Implementor, ConcreteImplementor), show who holds the reference, and note that the pairing can be chosen at runtime.
Discuss designing the primitive Implementor interface (granularity, why it must not mirror the Abstraction), who instantiates the implementor, and when Bridge is over-engineering.
Frame it as a versioning and deployment boundary — the bridge interface is a contract two independently evolving halves depend on (pimpl/ABI stability, plugin SPIs, ports in hexagonal architecture) — and reason about the cost of ever changing it.
## The problem: two dimensions of variation Software types often vary along more than one **independent axis**. A drawing object varies by *shape* (circle, square, polygon) and by *how it is rendered* (vector output, raster output, SVG, a GPU API). A notification varies by *kind* (alert, reminder, digest) and by *delivery channel* (email, SMS, push). If you model both axes with **inheritance** (subclassing), you must create one concrete class per combination: ``` Shape ├─ VectorCircle RasterCircle SvgCircle ├─ VectorSquare RasterSquare SvgSquare └─ VectorPolygon RasterPolygon SvgPolygon ``` That is `M × N` classes. This is called the **class explosion** (or combinatorial explosion). It has three practical costs: 1. **Growth is multiplicative.** Adding one new renderer adds M new classes; adding one new shape adds N. 2. **Duplication.** The shape-specific geometry logic is copied across every renderer variant, and the renderer-specific drawing logic is copied across every shape variant. Fixing a bug means fixing it in many places. 3. **Compile-time binding.** The pairing (`VectorCircle`) is fixed when you write the class; you cannot pick a renderer at runtime without a factory that knows every combination. ## The Bridge solution Bridge (from the *Design Patterns* / "Gang of Four" catalogue, in its **structural** category) says: **stop expressing the second axis with inheritance; express it with composition.** The canonical participants: - **Abstraction** — the high-level type clients talk to (`Shape`). It holds a reference to an Implementor and defines operations in terms of it. - **RefinedAbstraction** — subclasses that extend or specialise the abstraction (`Circle`, `Square`). This hierarchy captures axis 1. - **Implementor** — an interface of *primitive* operations (`Renderer` with `drawCircle`, `drawLine`, `fill`). Note: it is **not** a mirror of the Abstraction's interface; it is a lower-level vocabulary the Abstraction is written on top of. - **ConcreteImplementor** — the actual implementations (`VectorRenderer`, `RasterRenderer`). This hierarchy captures axis 2. The reference from Abstraction to Implementor is *the bridge*. Client code creates a concrete implementor and passes it into the abstraction (constructor injection is typical). Result: `M + N` classes instead of `M × N`. Adding a renderer = **one** new class, zero changes to any shape. Adding a shape = one new class, zero changes to any renderer. ## Why the name "abstraction" is confusing In Bridge, "Abstraction" does **not** mean "abstract class" and "Implementation" does **not** mean "the concrete subclass of that abstract class." Both sides are their own abstract/concrete hierarchies. Read them as **"the what" (domain-facing, higher level) vs "the how" (mechanism-facing, lower level)**. GoF's own alternate name for the pattern, **Handle/Body**, captures this better: a thin handle object clients hold, and a swappable body object doing the work. C++'s **pimpl** (pointer-to-implementation) idiom is exactly this, used to hide implementation details from a header file and keep the binary interface stable. ## Mechanics and edge cases - **Who creates the Implementor?** Usually the client, or a factory/DI container. Sometimes the Abstraction picks a default lazily ("if no renderer was supplied, use the platform default"). Letting the Abstraction hard-code a concrete implementor re-couples the halves and undoes the pattern. - **Can the implementor be swapped at runtime?** Yes, if you expose a setter or rebuild the abstraction. This is a real benefit over inheritance: the same `Circle` instance can be re-pointed at a different renderer. - **Interface granularity.** If the Implementor interface is too *narrow*, abstractions must synthesise missing behaviour awkwardly; too *wide*, and every new concrete implementor must implement operations it doesn't care about. Choosing the primitive set is the hard design work in Bridge. - **Degenerate case.** With exactly one implementor, Bridge is over-engineering — you have paid indirection for no variation. It earns its keep when the second axis genuinely has (or will have) multiple values, or when the two sides must be *compiled/deployed/versioned* separately. ## Trade-offs summary **Gains:** linear rather than multiplicative growth; independent evolution and independent deployment of the two sides; runtime binding; implementation details hidden from clients; better testability (inject a fake implementor). **Costs:** one more indirection per call; two hierarchies to navigate, so the code is less obvious to a newcomer; you must design the primitive interface *up front* and it is expensive to change later because both sides depend on it.
- What happens to a Bridge design when a third independent dimension appears (say, shape × renderer × coordinate system)?Bridge does not automatically generalise to three axes: chaining a second bridge (the renderer itself holding a coordinate-system reference) works if the axes are genuinely layered, but if all three are peers you often move to composition-by-parts or a strategy-per-axis object, or accept a small explosion on the least-volatile axis. Some designs push the third axis into an Abstract Factory that assembles a consistent triple.
- Does the Implementor interface have to mirror the Abstraction's interface?No — and it usually shouldn't. If it mirrors it one-to-one you have a delegating wrapper, not a real bridge. The Implementor should expose lower-level primitives that the Abstraction composes into higher-level operations.
A TV remote and a TV. Remotes vary (basic, advanced, voice); devices vary (TV, radio, projector). You do not build a BasicTvRemote, AdvancedTvRemote, BasicRadioRemote... — the remote holds a reference to a device and sends generic commands like power/volume. Any remote works any device; new devices need no new remotes.
saying these in an interview costs you the question
- Saying "Abstraction means the abstract class and Implementor means its subclass" — they are two separate hierarchies
- Claiming Bridge is about hiding implementation details only; hiding is a side effect, the intent is independent variation of two axes
- Making the Implementor interface an exact mirror of the Abstraction's interface (that is a wrapper, not a bridge)
- Having the Abstraction construct a hard-coded concrete Implementor, which re-couples the halves
- Applying Bridge when there is exactly one implementation and no realistic second one — pure indirection tax