Введение
Завершаем блок структурных паттернов темой Bridge — паттерном, решающим задачу избежания «взрыва» количества классов при попытке комбинировать две независимо изменяющиеся «оси» вариативности (например, разные виды UI-элементов и разные платформы их отрисовки) через простое наследование, разделяя абстракцию (что делает класс) от реализации (как именно это реализовано) через два отдельных, независимых класса, связанных композицией, а не наследованием.
Концепция
Без Bridge, попытка обеспечить, например, несколько разных типов фигур (Circle, Square) для нескольких разных способов отрисовки (VectorRenderer, RasterRenderer) через прямое наследование привела бы к комбинаторному взрыву классов (VectorCircle, RasterCircle, VectorSquare, RasterSquare, и так далее, растущему мультипликативно с каждой новой фигурой или новым способом отрисовки) — Bridge разделяет эти две оси вариативности на два отдельные, независимые класса (абстракция Shape, реализация Renderer), связанных через композицию (абстракция хранит указатель/ссылку на объект реализации), что позволяет добавлять новые фигуры или новые способы отрисовки НЕЗАВИСИМО друг от друга, без мультипликативного роста числа классов.
Пример кода
#include <iostream>
#include <memory>
// Реализация (Implementation) — КАК именно рисовать примитивы
class Renderer
{
public:
virtual void renderCircle(double x, double y, double radius) = 0;
virtual ~Renderer() = default;
};
class VectorRenderer : public Renderer
{
public:
void renderCircle(double x, double y, double radius) override
{
std::cout << "Векторная отрисовка круга в (" << x << "," << y << ") радиусом " << radius << std::endl;
}
};
class RasterRenderer : public Renderer
{
public:
void renderCircle(double x, double y, double radius) override
{
std::cout << "Растровая отрисовка круга через пиксели в (" << x << "," << y << ")" << std::endl;
}
};
// Абстракция (Abstraction) — ЧТО представляет фигура, ссылается на Renderer через КОМПОЗИЦИЮ, не наследование
class Shape
{
public:
explicit Shape(std::shared_ptr<Renderer> renderer) : m_renderer(renderer) {}
virtual void draw() = 0;
virtual ~Shape() = default;
protected:
std::shared_ptr<Renderer> m_renderer; // общий renderer для нескольких фигур
};
class Circle : public Shape
{
public:
Circle(double x, double y, double radius, std::shared_ptr<Renderer> renderer)
: Shape(renderer), m_x(x), m_y(y), m_radius(radius) {}
void draw() override { m_renderer->renderCircle(m_x, m_y, m_radius); } // ДЕЛЕГИРОВАНИЕ реализации
private:
double m_x, m_y, m_radius;
};
int main()
{
auto vectorRenderer = std::make_shared<VectorRenderer>();
auto rasterRenderer = std::make_shared<RasterRenderer>();
Circle vectorCircle(0, 0, 5, vectorRenderer);
Circle rasterCircle(10, 10, 3, rasterRenderer);
vectorCircle.draw(); // "Векторная отрисовка..."
rasterCircle.draw(); // "Растровая отрисовка..." — ТА ЖЕ Circle, но ДРУГАЯ реализация отрисовки!
return 0;
}
Пояснения к коду
Shape, хранящий std::shared_ptr<Renderer> m_renderer как поле (композиция, а не наследование), показывает суть моста между абстракцией и реализацией — Circle::draw() просто делегирует реальную отрисовку объекту m_renderer, не зная и не заботясь о том, векторный это рендерер или растровый, что позволяет создавать Circle с ЛЮБЫМ конкретным рендерером без необходимости в отдельном подклассе Circle для каждой комбинации. Пример с vectorCircle/rasterCircle, оба экземпляры одного и того же класса Circle, но с разными объектами Renderer, явно демонстрирует развязку двух осей вариативности — добавление нового типа фигуры (например, Square) или нового способа отрисовки (SvgRenderer) требует создания лишь ОДНОГО нового класса, а не множества комбинаций с уже существующими классами противоположной оси вариативности.
Подводные камни
- Применение Bridge для случая с единственной осью вариативности, где простое наследование (без введения отдельного класса реализации, связанного через композицию) было бы достаточным и более простым решением — Bridge оправдан именно при наличии ДВУХ (или более) независимо изменяющихся аспектов, потенциальный комбинаторный рост которых через прямое наследование был бы проблематичным, и для случая с единственной осью вариативности (только разные типы фигур, без разных способов их отрисовки) дополнительная сложность Bridge не оправдана.
- Путаница между Bridge и Adapter — оба паттерна структурно похожи (используют композицию для делегирования через интерфейс), но решают принципиально разные архитектурные задачи: Adapter согласует уже СУЩЕСТВУЮЩИЕ, изначально несовместимые интерфейсы постфактум, тогда как Bridge — это ОСОЗНАННОЕ, заранее спроектированное разделение архитектуры на абстракцию и реализацию с самого начала проектирования системы, что отличает мотивацию и момент применения этих двух структурно похожих, но концептуально различных паттернов.
- Игнорирование владения объектом реализации (Renderer), особенно при совместном использовании одного экземпляра реализации несколькими объектами абстракции (как
vectorRenderer, потенциально используемый несколькими разными объектамиCircle/Squareодновременно) — пример явно используетstd::shared_ptrименно для корректного, безопасного совместного владения единственным экземпляром реализации, разделяемым между несколькими объектами абстракции, и использование «сырого» указателя или эксклюзивногоunique_ptrбез понимания этой потребности в совместном владении может привести к проблемам времени жизни объекта реализации. - Чрезмерно глубокая, многоуровневая иерархия как для абстракции, так и для реализации одновременно, существенно усложняющая понимание общей архитектуры — хотя Bridge поддерживает независимое развитие иерархий наследования и абстракции, и реализации (например, дополнительные подклассы
ShapeилиRenderer), чрезмерное усложнение ОБЕИХ иерархий одновременно может сделать общую архитектуру системы значительно сложнее для понимания, чем была бы более простая, не полностью использующая весь потенциал гибкости Bridge структура, соразмерная реальной сложности и реальным потребностям конкретного проекта.