Patterns in der Softwareentwicklung: Das Adapter-Muster
Die Idee hinter dem Adapter-Muster ist ganz einfach: Es wandelt eine Schnittstelle in eine andere Schnittstelle um.
- Rainer Grimm
In der modernen Softwareentwicklung sind Patterns eine wichtige Abstraktion mit klar definierter Terminologie und sauberer Dokumentation. Das klassische Buch "Design Patterns: Elements of Reusable Object-Oriented Software" (kurz Design Patterns) enthält 23 Muster, darunter auch das Adapter-Muster. Dessen zugrundeliegende Idee ist ganz einfach: Es wandelt eine Schnittstelle in eine andere Schnittstelle um.
Das Adapter-Muster eignet sich vor allem in Situationen, in denen Softwareentwicklern zwar eine Klasse zur Verfügung steht, die die vom Kunden benötigte Funktionalität implementiert, die zugehörige Schnittstelle aber nicht mit der Unternehmens-Policy vereinbar ist. Durch das Adapter-Muster lässt sich die benötigte Schnittstelle ganz einfach mit der bestehenden Klasse unterstützen.
Unter allen in dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software" behandelten Mustern, ist das Adapter-Muster das einzige, das nicht nur auf Klassenebene, sondern auch auf Objektebene implementiert wird.
Bevor ich die beiden gängigen Wege aufzeige, dieses Strukturmuster zu implementieren, kurz noch ein Blick auf die wesentlichen Fakten:
Das Adapter-Muster
Zweck
- Eine Schnittstelle in eine andere übersetzen
Auch bekannt als
- Wrapper
Anwendungsfall
- Eine Klasse verfügt nicht über die erforderliche Schnittstelle
- Definition einer allgemeinen Schnittstelle für eine Reihe ähnlicher Klassen
Beispiele
- Container-Adapter
Die Container-Adapter std::stack
, std::queue
und std::priority_queu
e
bieten eine angepasste Schnittstelle für die Sequenzcontainer. Der folgende Codeschnipsel zeigt die Template-Signatur der drei Container-Adapter:
template<typename T, typename Container = std::deque<T>>
class stack;
template<typename T, typename Container = std::deque<T>>
class queue;
template<typename T, typename Container = std::vector<T>,
typename Compare = std::less<typename Container::value_type>>
class priority_queue;
Per Default adaptieren std::stack
und std::queue
den Sequenzcontainer std::deque; std::priority_queue
hingegen verwendet std::vector
. Außerdem benötigt std::priority_queue
ein binäres Prädikat. Als Default kommt std::less zum Einsatz.
C++ hat weitere Adapter.
- Iterator-Adapter
C++ unterstützt Insert-Iteratoren und Streams-Iteratoren.
- Insert-Iteratoren
Mit den drei Insert-Iteratoren std::front_inserter
, std::back_inserter
und std::inserter
lässt sich ein Element am Anfang, am Ende oder an einer beliebigen Position in einen Container einfügen.
- Stream-Iteratoren
Stream-Iterator-Adaptoren können Streams als Datenquelle oder -ziel verwenden. C++ bietet zwei Funktionen an, um Istream-Iteratoren bzw. Ostream-Iteratoren zu erzeugen. Die erzeugten Istream-Iteratoren verhalten sich wie die Input-Iteratoren, die Ostream-Iteratoren wie Insert-Iteratoren.
Struktur
Die folgenden beiden Klassendiagramme zeigen die Struktur des Adapter Patterns, basierend auf Klassen oder auf Objekten (Ich bezeichne sie kurz als Klassen-Adapter und Objekt-Adapter).
Klassen-Adapter
Objekt-Adapter
Client
- Verwendet die Memberfunktion
methodA()
des Adapters
Adaptor
Klasse:
- Stellt die Funktionalität von
methodA()
durch Mehrfachvererbung zur Verfügung - Wird öffentlich von
Interface
und privat vonImplementation
abgeleitet
Objekt:
- Delegiert den Aufruf der Memberfunktion an seinen
Adaptee
Adaptee
- Implementiert die Funktionalität des Clients
Nun sollte das klassen- oder objektbasierte Adapter Pattern offensichtlich sein.
Implementierung
Klassen-Adapter
Im folgenden Beispiel passt die Klasse RectangleAdapter
die Schnittstelle des LegacyRectangle
an.
// adapterClass.cpp
#include <iostream>
typedef int Coordinate;
typedef int Dimension;
class Rectangle {
public:
virtual void draw() = 0;
virtual ~Rectangle() = default;
};
class LegacyRectangle {
public:
LegacyRectangle(Coordinate x1, Coordinate y1, Coordinate x2, Coordinate y2) : x1_(x1), y1_(y1), x2_(x2), y2_(y2){
std::cout << "LegacyRectangle: create. (" << x1_ << "," << y1_ << ") => ("
<< x2_ << "," << y2_ << ")" << '\n';
}
void oldDraw() {
std::cout << "LegacyRectangle: oldDraw. (" << x1_ << "," << y1_
<< ") => (" << x2_ << "," << y2_ << ")" << '\n';
}
private:
Coordinate x1_;
Coordinate y1_;
Coordinate x2_;
Coordinate y2_;
};
class RectangleAdapter : public Rectangle, private LegacyRectangle {
public:
RectangleAdapter(Coordinate x, Coordinate y, Dimension w, Dimension h) : LegacyRectangle(x, y, x + w, y + h) { // (1)
std::cout << "RectangleAdapter: create. (" << x << "," << y
<< "), width = " << w << ", height = " << h << '\n';
}
void draw() override {
oldDraw();
std::cout << "RectangleAdapter: draw." << '\n';
}
};
int main() {
std::cout << '\n';
Rectangle* r = new RectangleAdapter(120, 200, 60, 40);
r->draw();
delete r;
std::cout << '\n';
}
RectangleAdapter
besitzt dank Mehrfachvererbung die Schnittstelle von Rectangle
und die Implementierung von LegacyRectangle
. Außerdem passt der RectangleAdapter
die Größe des LegacyRectangle
an (Zeile 1).
Diese Implementierung des Adapter-Musters ist einer der seltenen Anwendungsfälle für private Vererbung. Dazu möchte ich noch ein paar Worte über Schnittstellenvererbung und Implementierungsvererbung ergänzen:
- Die Schnittstellenvererbung nutzt die öffentliche Vererbung. Sie trennt Nutzer von der Implementierung, damit abgeleitete Klassen hinzugefügt und geändert werden können, ohne die Nutzer der Basisklasse zu beeinträchtigen. Abgeleitete Klassen unterstützen die Schnittstelle der Basisklasse.
- Bei der Implementierungsvererbung wird gerne private Vererbung verwendet. Normalerweise stellt die abgeleitete Klasse ihre Funktionalität durch die Anpassung der Funktionalität der Basisklasse bereit. Abgeleitete Klassen unterstützen nicht die Schnittstelle der Basisklasse.
Abschließend ist hier die Ausgabe des Programms:
Objekt-Adapter
Die folgende Implementierung, der RectangleAdapter
, delegiert Aufrufe an seinen Adapter LegacyRectangle
.
// adapterObject.cpp
#include <iostream>
typedef int Coordinate;
typedef int Dimension;
class LegacyRectangle {
public:
LegacyRectangle(Coordinate x1, Coordinate y1, Coordinate x2, Coordinate y2) : x1_(x1), y1_(y1), x2_(x2), y2_(y2){
std::cout << "LegacyRectangle: create. (" << x1_ << "," << y1_ << ") => ("
<< x2_ << "," << y2_ << ")" << '\n';
}
void oldDraw() {
std::cout << "LegacyRectangle: oldDraw. (" << x1_ << "," << y1_
<< ") => (" << x2_ << "," << y2_ << ")" << '\n';
}
private:
Coordinate x1_;
Coordinate y1_;
Coordinate x2_;
Coordinate y2_;
};
class RectangleAdapter {
public:
RectangleAdapter(Coordinate x, Coordinate y, Dimension w, Dimension h) : legacyRectangle{LegacyRectangle(x, y, x + w, y + h)} { // (1)
std::cout << "RectangleAdapter: create. (" << x << "," << y
<< "), width = " << w << ", height = " << h << '\n';
}
void draw() {
legacyRectangle.oldDraw();
std::cout << "RectangleAdapter: draw." << '\n';
}
private:
LegacyRectangle legacyRectangle;
};
int main() {
std::cout << '\n';
RectangleAdapter r(120, 200, 60, 40);
r.draw();
std::cout << '\n';
}
Die Klasse RectangleAdapter
erstellt ihr LegacyRectangle
direkt in ihrem Konstruktor (Zeile 1). Eine andere Möglichkeit wäre, LegacyRectangle
als einen Konstruktorparameter von RectangleAdapter
zu verwenden.
class RectangleAdapter {
public:
RectangleAdapter(const LegacyRectangle& legRec): legacyRectangle{legRec} {}
...
};
Die Ausgabe dieses Programms ist identisch mit der des vorherigen.
Verwandte Design Patterns
- Das Brücken-Muster ist dem Objekt-Adapter ähnlich, verfolgt aber eine andere Absicht. Der Zweck des Bridge-Musters ist es, die Schnittstelle von der Implementierung zu trennen, während der Zweck des Adapters darin besteht, eine bestehende Schnittstelle zu ändern.
- Das Dekorator-Muster erweitert ein Objekt, ohne seine Schnittstelle zu verändern. Dekoratoren können zusammengesteckt werden, Brücken oder Adapter können dies nicht.
- Das Proxy-Muster erweitert die Implementierung des Objekts, für das es steht, ändert aber nicht dessen Schnittstelle.
Es stellt sich nun die Frage, in welchem Fall der Klassen-Adapter und wann der Objekt-Adapter zum Einsatz kommen sollte?
Klassen-Adapter versus Objekt-Adapter
- Klassen-Adapter
Der Klassen-Adapter wendet Klassen und ihre Unterklassen an. Er nutzt die Trennung von Schnittstelle, Implementierung und virtuellen Funktionsaufrufen. Seine Funktionalität ist fest kodiert und zur Kompilierzeit verfügbar. Der Klassen-Adapter bietet weniger Flexibilität und besitzt kein dynamisches Verhalten, wie etwa der Objekt-Adapter.
- Objekt-Adapter
Der Objekt-Adapter nutzt die Beziehung von Objekten. Die Abstraktion kann aufgebaut werden, indem Objekte zusammengesetzt und deren Arbeit delegiert wird. Diese Komposition lässt sich zur Laufzeit durchführen. Folglich ist ein Objekt-Adapter flexibler und ermöglicht es, das delegierte Objekt zur Laufzeit auszutauschen.
Wie geht es weiter?
Das Brücken-Muster hilft dabei, die Schnittstelle von ihrer Implementierung zu trennen. Ich werde es in meinem nächsten Artikel genauer vorstellen. (map)