Structural

Bridge

Decouple an abstraction from its implementation so that the two can vary independently.

Advanced Pattern #07 of 23

What is the Bridge Pattern?

Bridge splits a large class (or set of closely related classes) into two separate hierarchies — abstraction and implementation — that can be developed and extended independently. The abstraction holds a reference to the implementation and delegates work to it.

Without Bridge, combining M abstractions with N implementations would require M×N subclasses. Bridge reduces this to M+N classes.

Real-world analogy: A TV remote (abstraction) and a TV (implementation). You can have different remotes (basic, advanced) and different TVs (Samsung, Sony). Any remote works with any TV — the remote doesn't know the TV's internals, it just sends signals through a standard interface.

The Problem It Solves

You have a Shape class with subtypes Circle and Square. You want to add colours — Red and Blue. Using inheritance: RedCircle, BlueCircle, RedSquare, BlueSquare — 4 classes for 2 shapes × 2 colours. Add one more shape and one more colour and you have 9 classes. This explodes exponentially.

Bridge solves this by separating the Shape hierarchy from the Color hierarchy and connecting them via composition.

Participants

Abstraction
High-level control layer. Holds a reference to an Implementor and delegates the actual work to it. Defines the interface clients use.
RefinedAbstraction
Extends Abstraction with optional variations. Adds higher-level operations on top of the base abstraction.
Implementor (interface)
Defines the interface for implementation classes. Does not have to match the Abstraction interface — typically lower-level primitive operations.
ConcreteImplementor
Provides platform-specific or variant-specific implementations of the Implementor interface.

Visual Flow Diagram

Abstraction - impl: Implementor + operation() RefinedAbstraction Circle / Square + draw() extends «bridge» delegates to impl «interface» Implementor + drawImpl() RedColor drawImpl() BlueColor drawImpl() Abstraction dimension → Implementation dimension → vary independently — no class explosion

Java Code Example

Java
// Implementor interface — lower-level rendering operations
public interface Color {
    void applyColor();
}

// Concrete Implementors
public class RedColor implements Color {
    public void applyColor() { System.out.println("Applying RED color"); }
}
public class BlueColor implements Color {
    public void applyColor() { System.out.println("Applying BLUE color"); }
}
public class GreenColor implements Color {
    public void applyColor() { System.out.println("Applying GREEN color"); }
}

// Abstraction — holds reference to Implementor (the bridge)
public abstract class Shape {
    protected Color color; // THE BRIDGE — composition over inheritance

    public Shape(Color color) { this.color = color; }

    public abstract void draw();
}

// Refined Abstractions — vary independently of Color
public class Circle extends Shape {
    private int radius;

    public Circle(int radius, Color color) {
        super(color);
        this.radius = radius;
    }

    public void draw() {
        System.out.print("Circle radius=" + radius + " — ");
        color.applyColor(); // delegates to implementor
    }
}

public class Square extends Shape {
    private int side;

    public Square(int side, Color color) {
        super(color);
        this.side = side;
    }

    public void draw() {
        System.out.print("Square side=" + side + " — ");
        color.applyColor();
    }
}

// Client — combines any shape with any color freely
public class Main {
    public static void main(String[] args) {
        Shape redCircle  = new Circle(10, new RedColor());
        Shape blueSquare = new Square(5,  new BlueColor());
        Shape greenCircle = new Circle(7, new GreenColor());

        redCircle.draw();   // Circle radius=10 — Applying RED color
        blueSquare.draw();  // Square side=5 — Applying BLUE color
        greenCircle.draw(); // Circle radius=7 — Applying GREEN color

        // Switch implementor at runtime — the bridge flexes!
        redCircle.color = new BlueColor();
        redCircle.draw(); // same circle, now blue
    }
}

When to Use / Avoid

✓ Use When

  • You want to avoid a permanent binding between abstraction and implementation
  • Both abstractions and implementations should be extensible via subclassing
  • You have a class explosion from combining two independent dimensions
  • You want to switch implementations at runtime
  • Implementation changes should have no impact on client code

✕ Avoid When

  • Only one implementation exists — unnecessary abstraction layer
  • The abstraction and implementation are tightly coupled by nature
  • Simple inheritance clearly solves the problem without class explosion

Real-World Examples

Pros & Cons

Pros

  • Eliminates M×N class explosion — reduces to M+N classes
  • Open/Closed — add new abstractions and implementations independently
  • Runtime binding — swap implementor without changing abstraction
  • Single Responsibility — abstraction and implementation each focused

Cons

  • Increased complexity for simple scenarios — overkill if only one or two combinations needed
  • Harder to understand than plain inheritance — requires upfront design insight
  • Indirection overhead — every call goes through the bridge

How Bridge Can Be Broken

⚠ Attack Vectors

  • Downcasting the implementor: Abstraction casts the Implementor to a specific ConcreteImplementor to call methods not in the interface — shatters the decoupling
  • Null implementor: Abstraction's impl field is null when draw() is called — NullPointerException with no clear error message
  • Public implementor field: If the impl reference is public, clients replace it with an incompatible object at runtime causing unexpected behaviour
  • Fat Implementor interface: Adding too many methods to the Implementor interface means every ConcreteImplementor must implement them all — partial implementations with empty bodies silently break the contract
  • Cross-dimension dependencies: A RefinedAbstraction references a specific ConcreteImplementor by type — re-introduces the tight coupling Bridge was designed to eliminate

✓ Prevention

  • Never downcast the implementor: The Abstraction must only call methods defined on the Implementor interface — zero knowledge of concrete type
  • Guard against null: Validate the implementor in the constructor or provide a NullObject default — never allow a null bridge reference
  • Keep implementor field protected/private: Expose it only through a setter with validation so external code cannot corrupt the bridge
  • Keep Implementor interface lean: Only primitive operations that all implementations genuinely support. Use multiple Implementor interfaces if needed rather than one fat one
  • Inject the implementor: Use constructor injection so the bridge is set at creation time — prevents partial or invalid states
Java — Break & Fix
// ❌ BREAKING — downcasting implementor
public void draw() {
    ((RedColor) color).applySpecialEffect(); // ❌ tied to concrete impl
}

// ✅ FIX — only call interface methods
public void draw() {
    color.applyColor(); // ✅ only Implementor interface methods
}

// ❌ BREAKING — null implementor
Shape broken = new Circle(5, null); // NPE on draw()

// ✅ FIX — validate in constructor
public Shape(Color color) {
    if (color == null)
        throw new IllegalArgumentException("Color implementor required");
    this.color = color;
}

// ✅ OR — use NullObject pattern as safe default
public class NoColor implements Color {
    public void applyColor() { /* intentional no-op safe default */ }
}

// ❌ BREAKING — public bridge field, client corrupts it
public Color color; // client sets incompatible object

// ✅ FIX — protected with validated setter
protected Color color;
public void setColor(Color c) {
    if (c == null) throw new IllegalArgumentException("null not allowed");
    this.color = c;
}

How Other Patterns Relate

Interview Cheat Sheet

  1. What: Separates two independent dimensions (abstraction + implementation) into separate hierarchies connected via composition — eliminates M×N subclass explosion.
  2. How: Abstraction holds a reference to an Implementor interface. RefinedAbstractions extend Abstraction. ConcreteImplementors implement the Implementor interface. Client sets the implementor at construction or runtime.
  3. Key interview distinction: Adapter = retrofit (fixes existing incompatibility). Bridge = upfront design (prevents future class explosion). Both use composition but serve opposite purposes.