Bridge

Structural Design Patterns

Imagine you are developing a graphics application. You have a set of Shape classes, like Circle and Square. You also need to support rendering these shapes on different operating systems (e.g., Windows, macOS), which have different drawing APIs.

If you try to model this with inheritance, you'll face a combinatorial explosion. You'll need a class for every possible combination:

  • WindowsCircle, MacCircle
  • WindowsSquare, MacSquare

This hierarchy is already messy with just two shapes and two operating systems. Now, what happens if you add a new shape, like a Triangle? You have to add a WindowsTriangle and a MacTriangle. What if you add a new rendering target, like Linux? You have to add a LinuxCircle and a LinuxSquare.

Your class hierarchy is growing exponentially in two independent dimensions: the shape's abstraction (what it is) and its rendering implementation (how it's drawn). This is a maintenance nightmare.

Separating Abstraction from Implementation

The Bridge is a structural design pattern that solves this problem by decoupling an abstraction from its implementation so that the two can vary independently.

Instead of one giant class hierarchy, the Bridge pattern splits it into two separate, independent hierarchies:

  1. The Abstraction Hierarchy: The high-level shapes (Circle, Square).
  2. The Implementation Hierarchy: The low-level drawing APIs (WindowsRenderer, MacRenderer).

The "Bridge" is created by having the Abstraction object hold a reference to an Implementation object. The client interacts with the Abstraction, which in turn delegates the low-level "implementation" work to the implementation object it holds.

The Participants

  1. Abstraction: The high-level control logic that defines the interface for the client (e.g., an abstract Shape class). It holds a reference to an Implementor.
  2. Refined Abstraction: Concrete subclasses of the Abstraction (e.g., Circle, Square).
  3. Implementor: The interface for the implementation classes. It defines the contract for the low-level work (e.g., an IRenderer interface).
  4. Concrete Implementor: The concrete classes that do the actual low-level work (e.g., WindowsRenderer, MacRenderer).

Example: Shapes and Renderers

Let's build our Shape application using the Bridge pattern.

Step 1: The Implementation Hierarchy This hierarchy deals only with the low-level details of drawing.

// The Implementor interface
public interface IRenderer {
    void renderCircle(float radius);
    void renderSquare(float side);
}

// Concrete Implementors
public class WindowsRenderer implements IRenderer {
    @Override public void renderCircle(float radius) { /* Use Windows GDI API... */ }
    @Override public void renderSquare(float side) { /* Use Windows GDI API... */ }
}

public class MacRenderer implements IRenderer {
    @Override public void renderCircle(float radius) { /* Use macOS Core Graphics API... */ }
    @Override public void renderSquare(float side) { /* Use macOS Core Graphics API... */ }
}

Step 2: The Abstraction Hierarchy This hierarchy deals only with the high-level logic of what a shape is.

// The Abstraction
public abstract class Shape {
    // The "Bridge" is this reference to the implementor.
    protected IRenderer renderer;

    public Shape(IRenderer renderer) {
        this.renderer = renderer;
    }

    public abstract void draw();
}

// Refined Abstractions
public class Circle extends Shape {
    private float radius;
    public Circle(IRenderer renderer, float radius) {
        super(renderer);
        this.radius = radius;
    }

    @Override public void draw() {
        // High-level logic delegates to the low-level implementor
        renderer.renderCircle(radius);
    }
}

public class Square extends Shape {
    private float side;
    public Square(IRenderer renderer, float side) {
        super(renderer);
        this.side = side;
    }
    
    @Override public void draw() {
        renderer.renderSquare(side);
    }
}

Step 3: The Client Code The client can now create any combination of shape and renderer independently.

IRenderer windows = new WindowsRenderer();
IRenderer mac = new MacRenderer();

// Create any combination at runtime
Shape macCircle = new Circle(mac, 5);
Shape windowsSquare = new Square(windows, 10);

macCircle.draw();
windowsSquare.draw();

Now you can add a Triangle shape without touching any renderer code, or add a LinuxRenderer without touching any shape code.

Interview Focus: Analysis and Trade-offs

Benefits:

  • Decoupling and Independence: This is the primary goal. You can extend the abstraction and implementation hierarchies independently of each other. This is a massive win for OCP and maintainability.
  • Hides Implementation Details: The client code only interacts with the high-level Shape abstraction and is completely shielded from the platform-specific rendering details.

Drawbacks:

  • Increased Complexity: This is one of the more advanced structural patterns. It introduces a number of new interfaces and classes, which can make a simple system harder to understand if the problem doesn't truly have two independent dimensions of variation.

How Bridge Relates to Other Concepts

The Bridge pattern is a powerful, strategic tool for managing complex dependencies in a system.

  • Bridge vs. Adapter:
    • An Adapter is used reactively, after the fact, to make existing, incompatible interfaces work together.
    • A Bridge is an up-front design decision. You use it proactively when you recognize that a module has two independent dimensions of change (like Abstraction and Implementation) and you want to keep them separate from the start.
  • Bridge vs. Strategy: They look very similar structurally (an object holding a reference to an interface). The intent is the key difference.
    • Strategy is about providing different ways to perform a complete algorithm. The client is often aware of the different strategies and can swap them.
    • Bridge is about separating a high-level abstraction from its low-level implementation. The client usually only knows about the abstraction and is unaware of the different implementations.
  • Real-World Examples:
    • Cross-platform Frameworks: The most common use case, separating application logic from native OS APIs.
    • JDBC Drivers: The standard Java JDBC API is the "Abstraction," and the vendor-specific drivers (for MySQL, PostgreSQL, Oracle) are the "Concrete Implementations." The JDBC API acts as a bridge, allowing your application code to work with any database.