C++ Bridge Pattern Complete Guide | Separate Implementation and Abstraction for Extensibility

C++ Bridge Pattern Complete Guide | Separate Implementation and Abstraction for Extensibility

이 글의 핵심

C++ Bridge pattern separates implementation (Implementor) and abstraction (Abstraction) to enable swappable platforms and drivers, with practical examples, renderer switching, and platform-independent design.

What is Bridge Pattern? Why Needed?

In the Structural Patterns Series, viewing Bridge alongside Adapter and Composite makes it easy to distinguish where “separating abstraction and implementation” applies.

Problem Scenario: Combinatorial Explosion

Problem: Combining Shape (circle, rectangle) and Renderer (OpenGL, Vulkan) with inheritance causes class explosion.

// Bad design: combinatorial explosion
class OpenGLCircle : public Shape { };
class VulkanCircle : public Shape { };
class OpenGLRectangle : public Shape { };
class VulkanRectangle : public Shape { };
// 3 Shapes × 2 Renderers = 6 classes
// Adding Color: 3 × 2 × 3 = 18...

Solution: Bridge pattern separates abstraction (Shape) and implementation (Renderer). Shape only references Renderer, allowing independent extension of both.

// Good design: Bridge
class Shape {
protected:
    std::shared_ptr<Renderer> renderer_;
public:
    explicit Shape(std::shared_ptr<Renderer> r) : renderer_(std::move(r)) {}
    virtual void draw() = 0;
};
class Circle : public Shape {
    void draw() override { renderer_->drawCircle(...); }
};
// Adding Shape: 1 class only, Adding Renderer: 1 class only
flowchart LR
    client[Client]
    abstraction[Abstractionbr/(Shape)]
    implementor[Implementorbr/(Renderer)]
    refined["RefinedAbstractionbr/(Circle, Rectangle)"]
    concrete["ConcreteImplementorbr/(OpenGLRenderer, VulkanRenderer)"]
    
    client --> abstraction
    abstraction --> implementor
    refined -.extends.-> abstraction
    concrete -.implements.-> implementor

Table of Contents

  1. Basic Structure
  2. Renderer Switching Example
  3. Platform-Independent Design
  4. Common Problems and Solutions
  5. Production Patterns
  6. Complete Example: Cross-Platform Window System

1. Basic Structure

#include <memory>
#include <iostream>
// Implementation interface (Implementor)
class Renderer {
public:
    virtual void drawCircle(float x, float y, float r) = 0;
    virtual void drawRect(float x, float y, float w, float h) = 0;
    virtual ~Renderer() = default;
};
// Concrete implementation 1
class OpenGLRenderer : public Renderer {
public:
    void drawCircle(float x, float y, float r) override {
        std::cout << "[OpenGL] Circle at (" << x << "," << y << ") r=" << r << '\n';
    }
    void drawRect(float x, float y, float w, float h) override {
        std::cout << "[OpenGL] Rect at (" << x << "," << y << ") " << w << "x" << h << '\n';
    }
};
// Concrete implementation 2
class VulkanRenderer : public Renderer {
public:
    void drawCircle(float x, float y, float r) override {
        std::cout << "[Vulkan] Circle at (" << x << "," << y << ") r=" << r << '\n';
    }
    void drawRect(float x, float y, float w, float h) override {
        std::cout << "[Vulkan] Rect at (" << x << "," << y << ") " << w << "x" << h << '\n';
    }
};
// Abstraction - references implementation
class Shape {
protected:
    std::shared_ptr<Renderer> renderer_;
public:
    explicit Shape(std::shared_ptr<Renderer> r) : renderer_(std::move(r)) {}
    virtual void draw() = 0;
    virtual ~Shape() = default;
};
class Circle : public Shape {
    float x_, y_, r_;
public:
    Circle(std::shared_ptr<Renderer> r, float x, float y, float radius)
        : Shape(std::move(r)), x_(x), y_(y), r_(radius) {}
    void draw() override { renderer_->drawCircle(x_, y_, r_); }
};
class Rectangle : public Shape {
    float x_, y_, w_, h_;
public:
    Rectangle(std::shared_ptr<Renderer> r, float x, float y, float w, float h)
        : Shape(std::move(r)), x_(x), y_(y), w_(w), h_(h) {}
    void draw() override { renderer_->drawRect(x_, y_, w_, h_); }
};
int main() {
    auto gl = std::make_shared<OpenGLRenderer>();
    auto vk = std::make_shared<VulkanRenderer>();
    
    Circle c1(gl, 0, 0, 10);
    Circle c2(vk, 5, 5, 3);
    Rectangle r1(gl, 10, 10, 50, 30);
    
    c1.draw();  // [OpenGL] Circle
    c2.draw();  // [Vulkan] Circle
    r1.draw();  // [OpenGL] Rect
    return 0;
}

2. Renderer Switching Example

Runtime Renderer Change

#include <memory>
#include <iostream>
class Renderer {
public:
    virtual void render(const std::string& content) = 0;
    virtual ~Renderer() = default;
};
class HTMLRenderer : public Renderer {
public:
    void render(const std::string& content) override {
        std::cout << "<html><body>" << content << "</body></html>\n";
    }
};
class MarkdownRenderer : public Renderer {
public:
    void render(const std::string& content) override {
        std::cout << "# " << content << "\n";
    }
};
class Document {
protected:
    std::shared_ptr<Renderer> renderer_;
    std::string content_;
public:
    Document(std::shared_ptr<Renderer> r, std::string content)
        : renderer_(std::move(r)), content_(std::move(content)) {}
    
    void setRenderer(std::shared_ptr<Renderer> r) {
        renderer_ = std::move(r);
    }
    
    virtual void display() = 0;
    virtual ~Document() = default;
};
class Article : public Document {
public:
    using Document::Document;
    
    void display() override {
        std::cout << "=== Article ===\n";
        renderer_->render(content_);
    }
};
int main() {
    auto html = std::make_shared<HTMLRenderer>();
    auto md = std::make_shared<MarkdownRenderer>();
    
    Article article(html, "Hello World");
    article.display();  // HTML rendering
    
    article.setRenderer(md);
    article.display();  // Markdown rendering
    return 0;
}

Key: Can switch implementation at runtime with setRenderer().

3. Platform-Independent Design

Cross-Platform File System

#include <memory>
#include <iostream>
#include <string>
// Implementation: platform-specific file operations
class FileSystemImpl {
public:
    virtual bool exists(const std::string& path) = 0;
    virtual std::string read(const std::string& path) = 0;
    virtual void write(const std::string& path, const std::string& data) = 0;
    virtual ~FileSystemImpl() = default;
};
class WindowsFileSystem : public FileSystemImpl {
public:
    bool exists(const std::string& path) override {
        std::cout << "[Windows] Checking: " << path << '\n';
        return true;
    }
    std::string read(const std::string& path) override {
        return "[Windows] File content";
    }
    void write(const std::string& path, const std::string& data) override {
        std::cout << "[Windows] Writing to " << path << '\n';
    }
};
class LinuxFileSystem : public FileSystemImpl {
public:
    bool exists(const std::string& path) override {
        std::cout << "[Linux] Checking: " << path << '\n';
        return true;
    }
    std::string read(const std::string& path) override {
        return "[Linux] File content";
    }
    void write(const std::string& path, const std::string& data) override {
        std::cout << "[Linux] Writing to " << path << '\n';
    }
};
// Abstraction: platform-independent API
class File {
protected:
    std::shared_ptr<FileSystemImpl> fs_;
    std::string path_;
public:
    File(std::shared_ptr<FileSystemImpl> fs, std::string path)
        : fs_(std::move(fs)), path_(std::move(path)) {}
    
    bool exists() { return fs_->exists(path_); }
    std::string read() { return fs_->read(path_); }
    void write(const std::string& data) { fs_->write(path_, data); }
};
class ConfigFile : public File {
public:
    using File::File;
    
    void load() {
        if (exists()) {
            std::cout << "Config loaded: " << read() << '\n';
        }
    }
};
int main() {
#ifdef _WIN32
    auto fs = std::make_shared<WindowsFileSystem>();
#else
    auto fs = std::make_shared<LinuxFileSystem>();
#endif
    
    ConfigFile config(fs, "/etc/app.conf");
    config.load();
    return 0;
}

Key: Select platform implementation at compile time, abstraction layer provides same API.

4. Common Problems and Solutions

Problem 1: Circular Dependency

// ❌ Bad: Renderer references Shape
class Renderer {
    std::vector<Shape*> shapes_;  // Circular dependency!
};

Solution: Implementation (Implementor) should not know about abstraction (Abstraction). Maintain unidirectional dependency only.

// ✅ Good: Only Shape references Renderer
class Shape {
    std::shared_ptr<Renderer> renderer_;  // Unidirectional
};

Problem 2: Implementation Leak

// ❌ Bad: Abstraction depends on concrete type
class Circle : public Shape {
    OpenGLRenderer* gl_;  // Concrete type!
};

Solution: Abstraction should only know Implementor interface.

// ✅ Good
class Circle : public Shape {
    std::shared_ptr<Renderer> renderer_;  // Interface only
};

Problem 3: Unnecessary Bridge

Bridge is excessive for simple cases.

// ❌ Over-engineering: Only 1 implementation
class Logger {
    std::shared_ptr<LoggerImpl> impl_;  // Unnecessary
};

Solution: Use Bridge only when 2+ implementations needed or platform/driver switching expected.

Summary

ItemDescription
PurposeSeparate abstraction and implementation for independent extension
AdvantagesEasy platform/driver switching, prevent inheritance explosion, runtime implementation change
DisadvantagesIncreased class count, design complexity, can be excessive for simple cases
When to Use2+ platforms/renderers/drivers, runtime switching needed, prevent combinatorial explosion
Related: Adapter Pattern, Decorator Pattern, Proxy Pattern, Strategy Pattern, Facade Pattern.
One-line summary: Bridge pattern enables independent extension and runtime switching of renderers, platforms, and drivers.