(Credits: Blog von Vishal Chovatiya)
„Das Bridge Pattern trennt eine Abstraktion von ihrer Implementierung, sodass beide unabhängig voneinander variiert und weiterentwickelt werden können.”
Das Bridge Pattern ist ein strukturelles Entwurfsmuster, das eine Klassenhierarchie in zwei getrennte, aber über eine Referenz verbundene Hierarchien aufspaltet: eine für die Abstraktion und eine für die Implementierung.
Ohne dieses Pattern führt die Kombination mehrerer unabhängiger Variationsdimensionen – etwa verschiedene Formen und verschiedene Rendering-Verfahren – schnell zu einer explosionsartig wachsenden Anzahl von Unterklassen.
Bridge löst dieses Problem, indem die Abstraktion nicht selbst die Implementierung enthält, sondern lediglich einen Zeiger oder eine Referenz auf ein Implementierungs-Interface hält. Neue Abstraktionen und neue Implementierungen lassen sich dadurch jeweils unabhängig voneinander hinzufügen, ohne bestehenden Code anzupassen.
In C++ wird dies typischerweise über eine abstrakte Basisklasse für die Implementierung sowie eine (ggf. abstrakte) Klasse für die Abstraktion realisiert, die einen Pointer auf die Implementierungsklasse hält. Zur Laufzeit kann die konkrete Implementierung sogar ausgetauscht werden, was zusätzliche Flexibilität bietet.
Das Pattern ähnelt oberflächlich dem Strategy-Pattern, unterscheidet sich aber in der Absicht: Bridge zielt auf die strukturelle Entkopplung zweier Hierarchien ab, nicht primär auf austauschbares Verhalten. Besonders nützlich ist Bridge, wenn man plattformübergreifenden Code schreibt oder mehrere Implementierungsvarianten (z. B. verschiedene Grafik-APIs) unterstützen möchte.
Das folgende UML-Diagramm beschreibt eine Implementierung des Bridge Patterns. Es besteht im Wesentlichen aus vier Teilen:
- Abstraction: Definiert eine Abstraktionsschnittstelle. Sie fungiert als Basisklasse für andere verfeinerte Abstraktionsklassen. Sie bezieht sich auch auf eine bestimmte Implementierung, die für plattformspezifische Funktionen verwendet wird.
- RefinedAbstraction: Stellt eine verfeinerte Variation einer Abstraktionsschnittstelle dar, enthält jedoch keine Implementierungsdetails. De facto erweitert sie nur die Abstraktion.
- Implementor: Definiert die Schnittstelle für Implementierungsklassen.
- ConcreteImplementor: Diese Klasse erbt von der Klasse
Implementor. Es kann mehr als eine Instanz vonImplementor-Klassen geben, die dieselbe Schnittstelle unterstützen, aber plattformspezifische Funktionen bereitstellen.
Abbildung 1: Schematische Darstellung des Bridge Patterns.
In beiden konzeptionellen Beispielen tritt eine Zeile
using Abstraction::Abstraction;in Erscheinung. Das macht exakt dasselbe wie:
RefinedAbstractionbekommt einen eigenen Konstruktor mit demselben Parameter wieAbstraction.- Im Initialisierungsteil –
Abstraction(std::move(implementor))– wird der Basisklassen-Konstruktor explizit aufgerufen und derstd::unique_ptrweitergereicht.
Also ausgeschrieben:
class RefinedAbstraction final : public Abstraction
{
public:
explicit RefinedAbstraction(std::unique_ptr<Implementor> implementor)
: Abstraction(std::move(implementor))
{
}
void operation() override
{
m_implementor->concreteOperation();
}
};Dieses Feature lautet (seit C++ 11) „Inheriting Constructors”:
Es sagt dem Compiler: „Übernimm alle Konstruktoren der Basisklasse – hier: Abstraction – auch als Konstruktoren
von RefinedAbstraction, ohne dass man sie einzeln neu schreiben muss.”
Bei nur einem Konstruktor wie hier ist das Ersparte gering, aber bei mehreren überladenen Basiskonstruktoren spart es viel Schreibarbeit. Beide Varianten sind funktional identisch.
- Das Adapter Pattern implementiert als Objektadapter ähnelt dem Bridge Pattern, hat jedoch eine andere Absicht:
- Der Zweck des Bridge Patterns besteht darin, die Schnittstelle von der Implementierung zu trennen.
- Der Zweck des Adapter Patterns besteht darin, eine vorhandene Schnittstelle zu modifizieren.
Quellcode 1 – Sehr einfache Version
Quellcode 2 – Ein etwas ausführlicheres Beispiel
Abbildung 2: Ein Notification System
Das ist für mich der Kern des Bridge Patterns:
Nicht „ich möchte ein Verhalten austauschen” (Strategy Pattern), sondern „ich habe zwei Achsen, die unabhängig voneinander wachsen”.
Abbildung 3: Architektur des Notification Systems
Bei diesem Muster steht eine Schnittstelle im Mittelpunkt, die als Brücke fungiert, die die Funktionalität konkreter Klassen unabhängig von den Schnittstellenimplementierungsklassen macht. Beide Klassentypen können strukturell verändert werden, ohne sich gegenseitig zu beeinflussen.
Wir demonstrieren die Verwendung des Bridge-Entwurfsmusters anhand des folgenden Beispiels, in dem ein Kreis in verschiedenen Farben mit derselben abstrakten Basisklassenmethode, aber unterschiedlichen Bridge-Implementiererungsklassen gezeichnet werden kann.
In Abbildung 4 finden Sie eine IDrawAPI-Schnittstelle vor, die als Bridge-Schnittstellenklasse fungiert,
und zwei konkrete Klassen RedCircleDrawer und GreenCircleDrawer,
die die IDrawAPI-Schnittstelle implementieren.
Shape ist eine abstrakte Klasse und besitzt einen IDrawAPI-Schnittstellenzeiger
(Raw-Pointer oder Smart-Pointer). In der Anwendung finden Sie ein Beispiel, in dem ein Kreis mit zwei verschiedenen Farben
gezeichnet wird:
Abbildung 4: Ein Anwendungsbeispiel des Bridge Patterns.
Das Pimpl Pattern kann als ein Anwendungsfall des Bridge Patterns angesehen werden: Bei diesem Pattern geht es darum, die Implementierungsdetails einer bestimmten Klasse zu verbergen, indem diese in eine separate Implementierungsklasse ausgelagert werden, auf die ein Zeiger zeigt. Weitere Details hierzu finden sich hier.
Die Anregungen zum konzeptionellen Beispiel finden Sie unter
https://refactoring.guru/design-patterns
und
vor.
