Skip to content

Bridge Pattern

Intent

The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It separates the interface from the implementation and establishes a relationship between them through composition.

Also known as: Handle/Body

Problem

When an abstraction can have multiple implementations, inheritance is typically used to bind the implementation to the abstraction. However, this approach has several drawbacks:

  • It permanently binds the implementation to the abstraction
  • It makes it difficult to modify, extend, or reuse abstractions and implementations independently
  • Changes to the implementation require recompiling the abstraction
  • The class hierarchy can explode with combinations of abstractions and implementations

For example, if you have a Shape class that can be drawn on different platforms (Windows, Linux, macOS), and you want to support different types of shapes (Circle, Rectangle), using inheritance would result in classes like WindowsCircle, LinuxCircle, WindowsRectangle, LinuxRectangle, etc. This leads to a combinatorial explosion of classes.

Solution

The Bridge pattern suggests splitting a large class or a set of closely related classes into two separate hierarchies:

  1. Abstraction - The high-level control layer (interface/API)
  2. Implementation - The platform-specific code (low-level operations)

The abstraction contains a reference to an implementation object and delegates the actual work to it. This allows both hierarchies to evolve independently.

Structure

Bridge Pattern Diagram 0

Participants

  • Abstraction - Defines the abstraction's interface - Maintains a reference to an Implementation object - In our example: Abstraction class with operation() method
  • RefinedAbstraction - Extends the interface defined by Abstraction - Can provide variants of the abstraction's operations - In our example: RefinedAbstraction class
  • Implementation - Defines the interface for implementation classes - Doesn't have to match Abstraction's interface exactly - In our example: Implementation interface with operationImplementation()
  • ConcreteImplementation - Implements the Implementation interface - Provides platform-specific implementations - In our example: ConcreteImplementationA and ConcreteImplementationB
  • Client - Works with Abstraction objects - Doesn't know about implementation details

Implementation

Implementation Interface

class Implementation {
public:
    virtual ~Implementation() = default;
    virtual std::string operationImplementation() const = 0;
};

class ConcreteImplementationA : public Implementation {
public:
    std::string operationImplementation() const override {
        return "ConcreteImplementationA: Platform A specific operation.";
    }
};

Abstraction Class

class Abstraction {
protected:
    std::unique_ptr<Implementation> implementation_;

public:
    explicit Abstraction(std::unique_ptr<Implementation> implementation)
        : implementation_(std::move(implementation)) {}

    virtual std::string operation() const {
        return "Abstraction: Base operation with:\n" +
               this->implementation_->operationImplementation();
    }
};

class RefinedAbstraction : public Abstraction {
public:
    explicit RefinedAbstraction(std::unique_ptr<Implementation> implementation)
        : Abstraction(std::move(implementation)) {}

    std::string operation() const override {
        return "RefinedAbstraction: Extended operation with:\n" +
               this->implementation_->operationImplementation();
    }
};

Usage Example

// Client code works with Abstraction
void clientCode(const Abstraction& abstraction) {
    std::cout << abstraction.operation() << std::endl;
}

int main() {
    // Different abstraction-implementation combinations

    // Base Abstraction + Implementation A
    Abstraction abs1(std::make_unique<ConcreteImplementationA>());
    clientCode(abs1);

    // Refined Abstraction + Implementation B
    RefinedAbstraction refAbs(std::make_unique<ConcreteImplementationB>());
    clientCode(refAbs);

    return 0;
}

Applicability

Use the Bridge pattern when:

  • You want to avoid a permanent binding between abstraction and implementation - The implementation can be selected or switched at runtime - Example: Selecting database backend at runtime
  • Both abstractions and implementations should be extensible by subclassing - Bridge lets you combine different abstractions with different implementations - Each can be extended independently
  • Changes in the implementation should not impact clients - Implementation details are hidden from clients - You can change implementation without recompiling abstraction
  • You want to share an implementation among multiple objects - Implementation can be shared (using reference counting) - Hidden from the client
  • You have a proliferation of classes from a coupled interface and implementation - Class explosion from combinations (e.g., Windows/Linux × Circle/Rectangle) - Bridge reduces the number of classes

Consequences

Benefits

  1. Decoupling interface and implementation - Implementation is not bound permanently to an interface - Can vary independently

  2. Improved extensibility - Extend Abstraction and Implementation hierarchies independently - No impact on existing code

  3. Hiding implementation details from clients - Client code depends only on Abstraction - Can change implementation without affecting clients

  4. Reducing the number of classes - Avoids class explosion from combining multiple dimensions - Uses composition instead of inheritance

  5. Runtime binding of implementation - Can change implementation at runtime - Swap implementations dynamically

Drawbacks

  1. Increased complexity - Adds extra classes and indirection - May be overkill for simple scenarios

  2. Performance overhead - Extra level of indirection through pointer - Usually negligible in practice

Bridge vs Adapter

While Bridge and Adapter have similar structures, they have different intents:

Aspect Bridge Adapter
Intent Separate abstraction from implementation to allow both to vary Make incompatible interfaces work together
When Designed up-front for flexibility Applied to make existing classes work
Structure Both hierarchies designed together Wraps existing class
Purpose Prevent class explosion, allow independent variation Integrate legacy/third-party code
Participants Abstraction and Implementation hierarchies Target, Adapter, Adaptee

Real-world Examples

  1. GUI Frameworks - Abstraction: Window, Button, Dialog - Implementation: Windows API, X11, macOS Cocoa - Example: Qt, wxWidgets use Bridge for cross-platform support

  2. Device Drivers - Abstraction: Printer, Scanner, Display - Implementation: HP Driver, Canon Driver, Epson Driver - Allows adding new devices and drivers independently

  3. Database Access - Abstraction: Connection, Statement, ResultSet - Implementation: MySQL, PostgreSQL, Oracle drivers - Example: JDBC, ODBC use Bridge pattern

  4. Graphics Rendering - Abstraction: Shape, Text, Image - Implementation: OpenGL, DirectX, Vulkan - Different rendering backends without changing shape code

  5. Remote Communication - Abstraction: RemoteService, RemoteObject - Implementation: HTTP, TCP, WebSocket protocols - Switch protocols without changing service interface

  • Abstract Factory: Can be used to create and configure a particular Bridge
  • Adapter: Adapter makes things work after they're designed; Bridge makes them work before they are designed
  • State/Strategy: Similar structure but different intent. State/Strategy focus on behavior variation, Bridge focuses on structural variation
  • Decorator: Has similar structure but different intent. Decorator adds responsibilities, Bridge separates interface from implementation

Implementation Considerations

  1. One Implementation - Can start with one implementation and add more later - Bridge provides flexibility for future extensions

  2. Creating the Implementation object - Abstraction can select the implementation - Can be passed in from outside (dependency injection) - Abstract Factory can help create the correct implementation

  3. Sharing implementations - Multiple Abstractions can share the same Implementation - Use reference counting if needed

  4. Multiple dimensions - Bridge works well when there are multiple dimensions of variation - Each dimension becomes a separate hierarchy

  5. Implementation interface - Keep Implementation interface minimal - Define only primitive operations - Let Abstraction build higher-level operations from primitives

Sample Output

=== Bridge Pattern Demo ===

Client: Testing Abstraction with Implementation A:
Abstraction: Base operation with:
ConcreteImplementationA: Platform A specific operation.

Client: Testing RefinedAbstraction with Implementation B:
RefinedAbstraction: Extended operation with:
ConcreteImplementationB: Platform B specific operation.

Client: Different abstraction-implementation combinations:

1. Base Abstraction + Implementation A:
Abstraction: Base operation with:
ConcreteImplementationA: Platform A specific operation.

2. Base Abstraction + Implementation B:
Abstraction: Base operation with:
ConcreteImplementationB: Platform B specific operation.

3. Refined Abstraction + Implementation A:
RefinedAbstraction: Extended operation with:
ConcreteImplementationA: Platform A specific operation.

4. Refined Abstraction + Implementation B:
RefinedAbstraction: Extended operation with:
ConcreteImplementationB: Platform B specific operation.

=== Real-world Scenario ===

Scenario: Cross-platform GUI framework
- Abstraction: Window, Dialog (UI components)
- Implementation: Windows, Linux, macOS (platform APIs)
- Bridge allows UI components to work on any platform
- New platforms can be added without changing UI code
- New UI components can be added without changing platform code

Example combinations:
- Window (Abstraction) + Windows API (Implementation)
- Dialog (RefinedAbstraction) + Linux API (Implementation)
- Both can vary independently!

Key Takeaways

  1. Bridge decouples abstraction from implementation for independent variation
  2. Prevents class explosion from combining multiple dimensions
  3. Uses composition over inheritance
  4. Allows runtime selection of implementation
  5. Designed up-front, unlike Adapter which is applied later
  6. Ideal for cross-platform development and multiple implementation variants
  7. Both abstraction and implementation can be extended without affecting each other