Patterns in der Softwareentwicklung: Das Brückenmuster
Das Brückenmuster ist ein strukturelles Muster. Es entkoppelt die Schnittstelle von seiner Implementierung.
![](https://heise.cloudimg.io/width/610/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/2/6/8/9/6/Fluss_Bruecke-3017215d25eb6aa4.jpeg)
(Bild: Asvolas / Shutterstock.com)
- Rainer Grimm
Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das klassische Buch "Design Patterns: Elements of Reusable Object-Oriented Software" (kurz Design Patterns) enthält 23 Muster, darunter das Brückenmuster, die zu den Strukturmustern gehört. Es entkoppelt die Schnittstelle von seiner Implementierung. In C++ wird oft eine vereinfachte Version verwendet: das Pimpl Idiom.
Bevor ich über das Pimpl Idiom schreibe, sind hier die Fakten zu dem Brückenmuster.
Das Brückenmuster
Zweck
- Entkoppelt die Schnittstelle von der Implementierung
Auch bekannt als
- Handle/Body
- Pimpl (Pointer to Implementation) Idiom
Anwendbarkeit
- Die Schnittstelle und die Implementierung können erweitert werden
- Eine Änderung der Schnittstelle der Implementierung hat keine Auswirkungen auf den Client
- Die Implementierung ist versteckt
Struktur
Abstraktion
- Definiert die Schnittstelle der Abstraktion
- Besitzt ein Objekt vom Typ
Implementor
RedefinedAbstraction
- Implementiert oder verfeinert die Schnittstelle der
Abstraction
Implementor
- Definiert die Schnittstelle der Implementierung
ConcreteImplementor
- Implementiert die Schnittstelle des
Implementor
Das Brückenmuster hat zwei Hierarchien: Eine für die Abstraktion (Schnittstelle) und eine für die Implementierung. Der Client programmiert gegen die Abstraktion, und die Abstraktion nutzt die Implementierung. Folglich können verschiedene Implementierungen der Abstraktionsschnittstelle und verschiedene Implementierungen der Implementierungsschnittstelle transparent verwendet werden. Das Brückenmuster bietet große Flexibilität, da die Abstraktion und die Implementierung variiert und während der Laufzeit des Programms ausgetauscht werden können.
Das Brückenmuster ist ein gutes Beispiel für die Kombination von Vererbung und Komposition. Einerseits hat es zwei Typenhierarchien (Vererbung) und andererseits besitzt die Abstraktion eine Implementierung (Komposition).
Beispiel
Das Beispiel zeigt eine einfache Implementierung des Brückenmusters.
// bridge.cpp
#include <iostream>
class Implementor { // (1)
public:
virtual void implementation() const = 0;
virtual ~Implementor() = default;
};
class ImplementorA: public Implementor {
public:
ImplementorA() = default;
void implementation() const {
std::cout << "ImplementatorA::implementation" << '\n';
}
};
class ImplementorB: public Implementor {
public:
ImplementorB() = default;
void implementation() const {
std::cout << "ImplementatorB::implementation" << '\n';
}
};
class Abstraction { // (2)
public:
virtual void function() const = 0;
virtual ~Abstraction() = default;
};
class RefinedAbstraction: public Abstraction {
public:
RefinedAbstraction(Implementor& impl) :
implementor(impl) {
}
void function() const {
std::cout << "RefinedAbstraction::function\n";
implementor.implementation();
}
private:
Implementor& implementor;
};
int main() {
std::cout << '\n';
ImplementorA implementorA;
ImplementorB implementorB;
RefinedAbstraction refinedAbstraction1(implementorA); // (3)
RefinedAbstraction refinedAbstraction2(implementorB); // (4)
Abstraction *abstraction1 = &refinedAbstraction1;
Abstraction *abstraction2 = &refinedAbstraction2;
abstraction1->function();
std::cout << '\n';
abstraction2->function();
std::cout << '\n';
}
Die Klasse Implementor
(1) ist die Schnittstelle für die Implementierungshierarchie und die Klasse Abstraction
(2) die Schnittstelle für die Abstraktion. Die Instanzen redefinedAbstraction1
und redefinedAbstraction2
erhalten ihre Implementierung in ihrem Konstruktor (3 und 4).
Der folgende Screenshot zeigt die Ausgabe des Programms.
Verwandte Muster
- Das Adapter Pattern, wenn es als Objektadapter implementiert wird, ähnelt dem Bridge Pattern, verfolgt aber eine andere Absicht. Der Zweck des Bridge Patterns ist es, die Schnittstelle von der Implementierung zu trennen, während der Zweck des Adapters darin besteht, eine bestehende Schnittstelle zu verändern.
- Die Abstract Factory kann eine Bridge erstellen und konfigurieren.
In C++ wird oft eine vereinfachte Version des Bridge Patterns verwendet.
Das Pimpl-Idiom
Der Kerngedanke des Pimpl-Idioms ist, dass die Implementierung der Klasse hinter einem Zeiger versteckt wird.
Hier ist ein Rezept für die Implementierung des Pimpl-Idioms:
- Verschiebe private Daten und Mitgliedsfunktionen der Klasse (public class) in eine eigene Klasse (pimpl class).
- Deklariere die pimpl class im Header der öffentlichen Klasse.
- Deklariere den Zeiger vom Typ pimpl class in der public class.
- Definiere die pimpl class in der Quelldatei der public class.
- Instanziiere die pimpl class im Konstruktor der public class.
- Die Mitgliedsfunktionen der public class verwenden die Funktionen der pimpl class.
Bartlomiej Filipek liefert in seinem Blogbeitrag "The Pimpl Pattern - what you should know" ein schönes Beispiel für das Pimpl Idiom:
// class.h
class MyClassImpl;
class MyClass
{
public:
explicit MyClass();
~MyClass();
// movable:
MyClass(MyClass && rhs) noexcept; // (2)
MyClass& operator=(MyClass && rhs) noexcept; // (3)
// and copyable
MyClass(const MyClass& rhs); // (4)
MyClass& operator=(const MyClass& rhs); // (5)
void DoSth();
void DoConst() const;
private:
const MyClassImpl* Pimpl() const
{ return m_pImpl.get(); } // (6)
MyClassImpl* Pimpl() { return m_pImpl.get(); } // (7)
std::unique_ptr<MyClassImpl> m_pImpl; // (1)
};
// class.cpp
class MyClassImpl
{
public:
~MyClassImpl() = default;
void DoSth() { }
void DoConst() const { }
};
MyClass::MyClass() : m_pImpl(new MyClassImpl())
{
}
MyClass::~MyClass() = default;
MyClass::MyClass(MyClass &&) noexcept = default;
MyClass& MyClass::operator=(MyClass &&) noexcept = default;
MyClass::MyClass(const MyClass& rhs)
: m_pImpl(new MyClassImpl(*rhs.m_pImpl))
{}
MyClass& MyClass::operator=(const MyClass& rhs) {
if (this != &rhs)
m_pImpl.reset(new MyClassImpl(*rhs.m_pImpl));
return *this;
}
void MyClass::DoSth()
{
Pimpl()->DoSth();
}
void MyClass::DoConst() const
{
Pimpl()->DoConst();
}
Dies sind die wichtigsten Ideen seiner Implementierung. Ich habe ein paar Zeilenmarkierungen hinzugefügt:
- Der pimpl ist ein
std::unique_ptr<MyClassImpl>
(1) - Die Klasse unterstützt die Kopier- und Move-Semantic (2 - 5)
- Die privaten
Pimp()
-Mitgliedsfunktionen vonMyClass
geben einenconst
und einen non-const
MyClassImp
l-Zeiger zurück (6 und 7)
Was sind die Vorteile des Pimpl Idioms? Es wäre einfacher, die Implementierung MyClassImpl
in die Abstraktion MyClass
einzubinden.
Vor- und Nachteile
Beginnen möchte ich mit den Vorteilen
Vorteil
- Binäre Kompatibilität: Wenn man die Implementierung ändert, wird die Schnittstelle für den Client, der die Abstraktion verwendet, nicht verändert.
- Kompilierzeit: Änderungen der Implementierung erfordern nicht, dass der Client, der die Abstraktion nutzt, neu kompiliert werden muss. Aus diesem Grund wird das Pimpl Idiom oft als Compilation Firewall bezeichnet. Diesen Vorteilen besitzen auch Module in C++20.
- Erweiterbarkeit: Es ist ziemlich einfach, die Implementierung während der Laufzeit auszutauschen. Im Allgemeinen besteht kein Bedarf an Virtualität.
Nachteile
- Performanz: Die Zeigerumlenkung verursacht zusätzliche Laufzeitkosten.
- Die Großen Sechs: Man muss die Großen Sechs berücksichtigen (siehe "C++ Core Guidelines: Die Nuller-, Fünfer- oder Sechserregel"). Da die Abstraktion einen
std::unique_ptr
hat, unterstützt sie keine Kopiersemantik. Das bedeutet für den konkreten Fall: - Man musst die Kopier-Semantik implementieren, wenn man sie braucht.
- Beim Implementieren der Kopier-Semantik, bekommt man nicht automatisch die Move-Semantik.
- Speicherzuweisung: Das Pimpl Idiom erfordert eine Speicherzuweisung. Diese ist in eingebetteten Systemen möglicherweise nicht möglich und kann eine Speicherfragmentierung verursachen.
Wie geht's weiter?
Das Decorator Pattern ist ein häufig verwendetes Strukturmuster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software". Seine Aufgabe ist es, ein Objekt dynamisch mit Verantwortlichkeiten zu erweitern. Ich werde den Decorator in meinem nächsten Artikel genauer vorstellen. ()