Bridge Design Pattern: Decoupling Abstraction from Implementation

Motivation and Purpose

Some types inherently possess multiple dimensions of variation—sometimes two, sometimes more. Traditional inheritance hierarchies struggle to accommodate this complexity without creating an unwieldy class structure. The Bridge pattern exists precisely to address this challenge by separating concerns along different axes of change, allowing each dimension to evolve independently without affecting the other.

Pattern Definition

Bridge pattern is a structural design pattern that separates an abstraction from its implementation, enabling both to vary independently. By introducing an abstract interface that bridges the abstraction and its implementation, this pattern reduces coupling and enhances flexibility.

Pattern Structure

The Bridge pattern comprises four key participants:

  1. Abstraction: Defines the abstraction's interface and maintains a reference to an Implementor.
  2. RefinedAbstraction: Extends the Abstraction interface.
  3. Implementor: Defines the interface for implementation classes.
  4. ConcreteImplementor: Provides concrete implementations of the Implementor interface.

Structure Breakdown

  • Abstraction holds a reference to an Implementor and defines high-level operations that depend on the Implementor's basic operations.
  • RefinedAbstraction extends the Abstraction, implementing specific business logic.
  • Implementor declares the interface for basic operations that the Abstraction relies on.
  • ConcreteImplementor provides specific implementations of these operations.

Code Demonstration

The Problem: Class Explosion

Consider a notification system that needs to work across multiple platforms with different feature sets:

class Notification {
public:
    virtual void login(std::string username, std::string password) = 0;
    virtual void sendText(std::string message) = 0;
    virtual void sendImage(Image img) = 0;

    virtual void playAnimation() = 0;
    virtual void renderBadge() = 0;
    virtual void displayText() = 0;
    virtual void establishConnection() = 0;
    
    virtual ~Notification() {}
};

// Platform implementations
class DesktopNotificationBase : public Notification {
public:
    void playAnimation() override { /* desktop animation */ }
    void renderBadge() override { /* desktop rendering */ }
    void displayText() override { /* desktop text display */ }
    void establishConnection() override { /* desktop connection logic */ }
};

class MobileNotificationBase : public Notification {
public:
    void playAnimation() override { /* mobile animation */ }
    void renderBadge() override { /* mobile rendering */ }
    void displayText() override { /* mobile text display */ }
    void establishConnection() override { /* mobile connection logic */ }
};

// Business abstractions - each combination requires a new class
class DesktopBasicNotification : public DesktopNotificationBase {
public:
    void login(std::string user, std::string pwd) override {
        DesktopNotificationBase::establishConnection();
        // login sequence
    }
    // ... other methods
};

class DesktopPremiumNotification : public DesktopNotificationBase {
public:
    void login(std::string user, std::string pwd) override {
        DesktopNotificationBase::playAnimation();
        DesktopNotificationBase::establishConnection();
        // premium login sequence
    }
    // ... other methods
};

// Mobile variants follow the same pattern
class MobileBasicNotification : public MobileNotificationBase { /* ... */ };
class MobilePremiumNotification : public MobileNotificationBase { /* ... */ };

This approach creates a combinatorial explosion: adding a new platform or feature set requires creating multiple new classes. If you have N platforms and M feature levels, you need 1 + N + (N × M) classes.

The Solution: Bridge Pattern

Separate the abstraction hierarchy from the implementation hierarchy:

// Implementation interface
class NotificationPlatform {
public:
    virtual void playAnimation() = 0;
    virtual void renderBadge() = 0;
    virtual void displayText() = 0;
    virtual void establishConnection() = 0;
    virtual ~NotificationPlatform() {}
};

class DesktopPlatform : public NotificationPlatform {
public:
    void playAnimation() override { /* desktop animation */ }
    void renderBadge() override { /* desktop rendering */ }
    void displayText() override { /* desktop text display */ }
    void establishConnection() override { /* desktop connection logic */ }
};

class MobilePlatform : public NotificationPlatform {
public:
    void playAnimation() override { /* mobile animation */ }
    void renderBadge() override { /* mobile rendering */ }
    void displayText() override { /* mobile text display */ }
    void establishConnection() override { /* mobile connection logic */ }
};

// Abstraction
class Notification {
protected:
    NotificationPlatform* platform;
public:
    Notification(NotificationPlatform* p) : platform(p) {}
    virtual void login(std::string username, std::string password) = 0;
    virtual void sendText(std::string message) = 0;
    virtual void sendImage(Image img) = 0;
    virtual ~Notification() {}
};

class BasicNotification : public Notification {
public:
    BasicNotification(NotificationPlatform* p) : Notification(p) {}
    
    void login(std::string user, std::string pwd) override {
        platform->establishConnection();
        // basic login flow
    }
    
    void sendText(std::string msg) override {
        platform->displayText();
        // text sending logic
    }
    
    void sendImage(Image img) override {
        platform->renderBadge();
        // image sending logic
    }
};

class PremiumNotification : public Notification {
public:
    PremiumNotification(NotificationPlatform* p) : Notification(p) {}
    
    void login(std::string user, std::string pwd) override {
        platform->playAnimation();
        platform->establishConnection();
        // premium login with animation
    }
    
    void sendText(std::string msg) override {
        platform->playAnimation();
        platform->displayText();
        // premium text sending
    }
    
    void sendImage(Image img) override {
        platform->playAnimation();
        platform->renderBadge();
        // premium image sending
    }
};

Now you can freely combine platforms and feature levels without class explosion. The class count becomes 1 + N + M instead of 1 + N + (N × M).

Adding Features with Decorator Pattern

class NotificationDecorator : public Notification {
protected:
    Notification* wrapped;
public:
    NotificationDecorator(NotificationPlatform* p, Notification* n) 
        : Notification(p), wrapped(n) {}
    
    void login(std::string u, std::string p) override {
        wrapped->login(u, p);
    }
    
    void sendText(std::string m) override {
        wrapped->sendText(m);
    }
    
    void sendImage(Image i) override {
        wrapped->sendImage(i);
    }
};

class EncryptedNotification : public NotificationDecorator {
public:
    EncryptedNotification(Notification* n) : NotificationDecorator(n->platform, n) {}
    
    void login(std::string u, std::string p) override {
        // encrypt credentials
        NotificationDecorator::login(u, p);
    }
    
    void sendText(std::string m) override {
        // encrypt message
        NotificationDecorator::sendText(m);
    }
    
    void sendImage(Image i) override {
        // encrypt image
        NotificationDecorator::sendImage(i);
    }
};

Why Protected for the Implementation Reference?

In the Abstraction class, the platform member is declared as protected rather than private or public. This design choice serves a specific purpose.

Design Rationale

  • Protected vs Private: If the reference were private, subclasses would need getter methods to access it, adding unnecessary complexity and method call overhead. Making it protected allows direct access in derived classes while hiding it from external code.
  • Protected vs Public: Making it public would expose implementation details to all clients, breaking encapsulation and creating tight coupling throughout the codebase.

Implementation

class Notification {
protected:
    NotificationPlatform* platform;  // accessible to subclasses
public:
    Notification(NotificationPlatform* p) : platform(p) {}
    // public interface
};

class BasicNotification : public Notification {
public:
    void login(std::string user, std::string pwd) override {
        platform->establishConnection();  // direct access from subclass
        // login logic
    }
};

The protected access modifier strikes the right balance: it keeps the implementation reference hidden from external users while allowing subclasses to interact with it directly, enabling flexible and efficient code.

Key Takeaways

  • Bridge pattern uses composition to decouple abstraction from implementation, allowing each to vary independently along its own dimension
  • This pattern resembles multiple inheritance but avoids its drawbacks—multiple inheritance often violates the Single Responsibility Principle, whereas Bridge maintains cleaner separation of concerns
  • The pattern works best when dealing with multiple independent dimensions of change
  • For more than two dimensions, the Bridge pattern can be extended by adding additional abstraction layers

Tags: design-pattern bridge-pattern structural-design-pattern OOP software-architecture

Posted on Mon, 22 Jun 2026 18:39:39 +0000 by waynerooney