C++ Design Patterns: Bridge Pattern

The Bridge Pattern is a structural design pattern that separates the abstraction from its implementation, allowing both to vary independently. This pattern introduces a bridge interface that decouples the abstraction layer from the implementation layer, enabling independent extension of both.

Core Roles of the Bridge Pattern

  1. Abstraction: Defines the abstract interface and contains a reference to the implementor interface.
  2. Refined Abstraction: Inherits from the abstraction class and extends its functionality.
  3. Implementor: Defines the interface for implementor classes, which the abstraction class calls.
  4. Concrete Implementor: Implements the implementor interface and provides specific functionality.

Example Implementation of the Bridge Pattern

Below is an example of the Bridge Pattern implemented in a “Graphics Rendering System”. The graphics (abstraction) can have different shapes, while the rendering methods (implementation) can utilize different technologies. The Bridge Pattern allows them to vary and combine independently:

#include <iostream>
#include <string>

// Implementor interface: Renderer (defines rendering methods)
class Renderer {
public:
    virtual void renderCircle(float x, float y, float radius) = 0;
    virtual void renderSquare(float x, float y, float side) = 0;
    virtual ~Renderer() = default;
};

// Concrete Implementor: Software Renderer
class SoftwareRenderer : public Renderer {
public:
    void renderCircle(float x, float y, float radius) override {
        std::cout << "Software rendering circle - Center(" << x << ", " << y << "), Radius " << radius << std::endl;
    }

    void renderSquare(float x, float y, float side) override {
        std::cout << "Software rendering square - Top-left corner(" << x << ", " << y << "), Side length " << side << std::endl;
    }
};

// Concrete Implementor: Hardware Renderer
class HardwareRenderer : public Renderer {
public:
    void renderCircle(float x, float y, float radius) override {
        std::cout << "Hardware accelerated rendering circle - Center(" << x << ", " << y << "), Radius " << radius << std::endl;
    }

    void renderSquare(float x, float y, float side) override {
        std::cout << "Hardware accelerated rendering square - Top-left corner(" << x << ", " << y << "), Side length " << side << std::endl;
    }
};

// Abstraction: Shape
class Shape {
protected:
    Renderer* renderer;  // Bridge reference: points to the implementor interface

public:
    Shape(Renderer* r) : renderer(r) {}
    virtual void draw() = 0;
    virtual void resize(float factor) = 0;
    virtual ~Shape() = default;
};

// Refined Abstraction: Circle
class Circle : public Shape {
private:
    float x, y, radius;

public:
    Circle(Renderer* r, float x, float y, float radius)
        : Shape(r), x(x), y(y), radius(radius) {}

    void draw() override {
        renderer->renderCircle(x, y, radius);
    }

    void resize(float factor) override {
        radius *= factor;
    }
};

// Refined Abstraction: Square
class Square : public Shape {
private:
    float x, y, side;

public:
    Square(Renderer* r, float x, float y, float side)
        : Shape(r), x(x), y(y), side(side) {}

    void draw() override {
        renderer->renderSquare(x, y, side);
    }

    void resize(float factor) override {
        side *= factor;
    }
};

// Client usage
int main() {
    // Create different renderers (implementation)
    Renderer* software = new SoftwareRenderer();
    Renderer* hardware = new HardwareRenderer();

    // Create different shapes (abstraction) and bridge with renderers
    Shape* circle = new Circle(software, 5, 5, 10);
    Shape* square = new Square(hardware, 2, 3, 8);

    // Draw shapes
    std::cout << "=== Initial Drawing ===" << std::endl;
    circle->draw();
    square->draw();

    // Resize and draw again
    std::cout << "\n=== Drawing After Resizing ===" << std::endl;
    circle->resize(1.5f);
    square->resize(0.8f);
    circle->draw();
    square->draw();

    // Change renderer (demonstrating the flexibility of the bridge)
    std::cout << "\n=== Drawing After Changing Renderer ===" << std::endl;
    delete circle;
    circle = new Circle(hardware, 5, 5, 10);
    circle->draw();

    // Clean up resources
    delete circle;
    delete square;
    delete hardware;
    delete software;

    return 0;
}

How the Bridge Pattern Works

  1. The abstraction layer (e.g., shapes) defines the interface for the objects and contains a reference to the implementation layer.
  2. The implementation layer (e.g., renderers) defines the interface for concrete implementations, decoupling it from the abstraction layer.
  3. The abstraction layer completes specific functionalities by calling the interfaces of the implementation layer without knowing the details of the implementation.
  4. Both the abstraction and implementation layers can be extended independently, allowing for easy addition of new abstraction or implementation classes.

Differences Between Bridge Pattern and Adapter Pattern

  • Bridge Pattern: Considers the separation of abstraction and implementation from the design phase, allowing both to vary independently.
  • Adapter Pattern: Used when existing interfaces are incompatible, connecting two incompatible interfaces.

Application Scenarios of the Bridge Pattern

  1. When a class has two or more independent dimensions that need to vary and extend.
  2. When it is necessary to avoid a permanent binding relationship between the abstraction and implementation.
  3. When functionality needs to be extended through composition rather than inheritance.
  4. When a system needs to run on multiple platforms and requires different implementations based on the platform.

Advantages and Disadvantages of the Bridge Pattern

Advantages:

  • Separates abstraction from implementation, allowing both to vary and extend independently.
  • Reduces the number of subclasses, avoiding class explosion issues.
  • Increases the flexibility and scalability of the system.
  • Follows the Open-Closed Principle, allowing new abstractions or implementations without modifying existing code.

Disadvantages:

  • Increases the complexity of the system, requiring an understanding of the relationship between the abstraction and implementation layers.
  • Requires a higher level of understanding from developers to correctly identify the two independent dimensions of variation in the system.

The Bridge Pattern is widely used in actual C++ development, for example, in GUI libraries where window controls (abstraction) are often separated from specific drawing systems (implementation) using the Bridge Pattern; in database access, database operation interfaces (abstraction) and the driver implementations for different databases (implementation) are also commonly designed using the Bridge Pattern.

Leave a Comment