본문으로 건너뛰기 C++ Bridge Pattern Complete Guide | Enhancing Scalability...

C++ Bridge Pattern Complete Guide | Enhancing Scalability by Separating Implementation and Abstraction

C++ Bridge Pattern Complete Guide | Enhancing Scalability by Separating Implementation and Abstraction

이 글의 핵심

C++ Bridge Pattern complete guide. A structural pattern that separates Implementation and Abstraction, enabling interchangeable platforms and drivers, with practical examples, renderer switching, and platform-independent design.

originalId: cpp-bridge-pattern

What is the Bridge Pattern? Why is it needed?

Problem Scenario: Combinatorial Explosion

Problem: Combining Shape (Circle, Rectangle) and Renderer (OpenGL, Vulkan) through inheritance leads to 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 classes...

Solution: The Bridge Pattern separates Abstraction (Shape) and Implementation (Renderer). Shape only references Renderer, allowing both to be extended independently.

// 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["Abstraction\n(Shape)"]
    implementor["Implementor\n(Renderer)"]
    refined["\"RefinedAbstraction\n(Circle, Rectangle)\""]
    concrete["\"ConcreteImplementor\n(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 Errors and Solutions
  5. Production Patterns
  6. Complete Example: Cross-Platform Window System

1. Basic Structure

#include <memory>
#include <iostream>
// Implementor interface
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

Changing Renderer at Runtime

#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 Point: You can switch implementations at runtime using 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 Point: Platform implementation is selected at compile time, while the abstraction layer provides a consistent API.

4. Common Errors and Solutions

Error 1: Circular Dependency

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

Solution: The Implementor should not know about the Abstraction. Maintain unidirectional dependency only.

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

Error 2: Implementation Leakage

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

Solution: Abstraction should only know the Implementor interface.

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

Error 3: Unnecessary Bridge

Bridge is overkill for simple cases.

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

Solution: Use Bridge only when you need 2+ implementations or expect platform/driver switching.

5. Production Patterns

Pattern 1: Combined with Factory

class RendererFactory {
public:
    static std::shared_ptr<Renderer> create(const std::string& type) {
        if (type == "opengl") return std::make_shared<OpenGLRenderer>();
        if (type == "vulkan") return std::make_shared<VulkanRenderer>();
        return nullptr;
    }
};
int main() {
    auto renderer = RendererFactory::create("opengl");
    Circle c(renderer, 0, 0, 10);
    c.draw();
}

Pattern 2: Dependency Injection

class Application {
    std::shared_ptr<Renderer> renderer_;
public:
    Application(std::shared_ptr<Renderer> r) : renderer_(std::move(r)) {}
    
    void run() {
        Circle c(renderer_, 0, 0, 10);
        c.draw();
    }
};
int main() {
    auto renderer = std::make_shared<OpenGLRenderer>();
    Application app(renderer);  // DI
    app.run();
}

6. Complete Example: Cross-Platform Window System

#include <memory>
#include <iostream>
#include <string>
// Implementation: Platform-specific window creation
class WindowImpl {
public:
    virtual void createWindow(const std::string& title, int w, int h) = 0;
    virtual void show() = 0;
    virtual void hide() = 0;
    virtual ~WindowImpl() = default;
};
class Win32Window : public WindowImpl {
    std::string title_;
public:
    void createWindow(const std::string& title, int w, int h) override {
        title_ = title;
        std::cout << "[Win32] CreateWindow: " << title << " " << w << "x" << h << '\n';
    }
    void show() override { std::cout << "[Win32] ShowWindow: " << title_ << '\n'; }
    void hide() override { std::cout << "[Win32] HideWindow: " << title_ << '\n'; }
};
class X11Window : public WindowImpl {
    std::string title_;
public:
    void createWindow(const std::string& title, int w, int h) override {
        title_ = title;
        std::cout << "[X11] XCreateWindow: " << title << " " << w << "x" << h << '\n';
    }
    void show() override { std::cout << "[X11] XMapWindow: " << title_ << '\n'; }
    void hide() override { std::cout << "[X11] XUnmapWindow: " << title_ << '\n'; }
};
// Abstraction: Platform-independent Window API
class Window {
protected:
    std::shared_ptr<WindowImpl> impl_;
    std::string title_;
    int width_, height_;
public:
    Window(std::shared_ptr<WindowImpl> impl, std::string title, int w, int h)
        : impl_(std::move(impl)), title_(std::move(title)), width_(w), height_(h) {
        impl_->createWindow(title_, width_, height_);
    }
    
    virtual void open() { impl_->show(); }
    virtual void close() { impl_->hide(); }
    virtual ~Window() = default;
};
class DialogWindow : public Window {
public:
    using Window::Window;
    
    void open() override {
        std::cout << "Opening dialog...\n";
        Window::open();
    }
};
class MainWindow : public Window {
public:
    using Window::Window;
    
    void open() override {
        std::cout << "Opening main window...\n";
        Window::open();
    }
};
int main() {
#ifdef _WIN32
    auto impl = std::make_shared<Win32Window>();
#else
    auto impl = std::make_shared<X11Window>();
#endif
    
    MainWindow mainWin(impl, "My App", 800, 600);
    mainWin.open();
    
    DialogWindow dialog(impl, "Settings", 400, 300);
    dialog.open();
    dialog.close();
    
    return 0;
}

Output (Windows):

[Win32] CreateWindow: My App 800x600
Opening main window...
[Win32] ShowWindow: My App
[Win32] CreateWindow: Settings 400x300
Opening dialog...
[Win32] ShowWindow: Settings
[Win32] HideWindow: Settings

Summary

ItemDescription
PurposeSeparate abstraction and implementation for independent extension
AdvantagesEasy platform/driver switching, prevents inheritance explosion, runtime implementation switching
DisadvantagesIncreased number of classes, design complexity, can be overkill for simple cases
When to Use2+ platforms/renderers/drivers, runtime switching needed, prevent combinatorial explosion
Related Posts: Adapter Pattern, Decorator Pattern, Proxy Pattern, Strategy Pattern, Facade Pattern.
One-line Summary: The Bridge Pattern enables independent extension of renderers, platforms, and drivers with runtime switching capability.

Other articles related to this topic.



Keywords Covered in This Article (Related Search Terms)

This article covers C++, Bridge, design pattern, structural, abstraction, implementation, renderer, platform.