Das Adapter Pattern ist ein strukturelles Entwurfsmuster, das zwei inkompatible Schnittstellen miteinander verbindet. Es ermöglicht, dass Klassen, die nicht direkt zusammenarbeiten können, dennoch zusammenarbeiten. Der Adapter dient als Vermittler und übersetzt die Methodenaufrufe von einer Schnittstelle in die andere. Dabei bleibt die bestehende Implementierung unverändert, und der Adapter bietet eine einheitliche Schnittstelle für den Client.
Was ist das Adapter Pattern?
Das Adapter Pattern wird verwendet, wenn eine Klasse oder ein System eine Schnittstelle erwartet, aber eine andere Schnittstelle vorliegt. Es übersetzt die Aufrufe zwischen zwei inkompatiblen Schnittstellen, sodass diese zusammenarbeiten können. Der Adapter ist dafür verantwortlich, die Methode des einen Systems so umzuwandeln, dass sie mit der Schnittstelle des anderen Systems kompatibel ist.
Dieses Muster wird oft verwendet, wenn ein bestehendes System mit neuen Komponenten oder Bibliotheken integriert werden soll, die nicht mit der aktuellen Architektur übereinstimmen. Der Adapter sorgt dafür, dass das bestehende System weiter funktioniert, ohne dass die ursprüngliche Implementierung geändert werden muss.
Struktur des Adapter Patterns
- Target: Die Schnittstelle, die der Client erwartet. Der Adapter sorgt dafür, dass der Client mit dieser Schnittstelle interagieren kann.
- Adapter: Die Klasse, die die inkompatible Schnittstelle in die gewünschte Ziel-Schnittstelle übersetzt. Sie implementiert die
Target
-Schnittstelle. - Adaptee: Die bestehende, inkompatible Klasse, die in das Zielinterface integriert werden muss.
- Client: Der Client verwendet die
Target
-Schnittstelle, ohne zu wissen, dass ein Adapter verwendet wird.
Beispiel des Adapter Patterns in C++
Stellen wir uns vor, wir haben ein System, das mit einer bestehenden Bibliothek arbeitet, die für das Zeichnen von Formen verantwortlich ist. Das System verwendet eine Schnittstelle Shape
, aber die Bibliothek stellt die Form als OldShape
zur Verfügung, die nicht kompatibel ist. Wir verwenden das Adapter Pattern, um eine Verbindung zwischen diesen beiden Schnittstellen herzustellen.
#include <iostream>
// Ziel-Schnittstelle, die der Client erwartet
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
// Adaptee: Die bestehende, inkompatible Klasse
class OldShape {
public:
void render() const {
std::cout << "Zeichne Form mit alter Bibliothek." << std::endl;
}
};
// Adapter: Übersetzt die alte Schnittstelle in die neue
class ShapeAdapter : public Shape {
private:
OldShape* oldShape; // Hält eine Instanz der inkompatiblen Klasse
public:
ShapeAdapter(OldShape* oldShape) : oldShape(oldShape) {}
// Implementiert die Methode der Ziel-Schnittstelle
void draw() const override {
oldShape->render(); // Übersetzt den Aufruf
}
};
// Client-Code
int main() {
OldShape* oldShape = new OldShape(); // Erstellen einer Instanz der alten Form
Shape* shape = new ShapeAdapter(oldShape); // Adapter wird verwendet
shape->draw(); // Aufruf der Ziel-Schnittstelle, die durch den Adapter übersetzt wird
delete oldShape;
delete shape;
return 0;
}
Erklärung des C++-Beispiels
In diesem Beispiel haben wir zwei inkompatible Schnittstellen: die Shape
-Schnittstelle, die der Client erwartet, und die OldShape
-Schnittstelle, die die bestehende Bibliothek bereitstellt. Die Struktur des Adapter Patterns ist folgendermaßen:
- Target: Die
Shape
-Schnittstelle stellt eine Methodedraw()
zur Verfügung, die vom Client erwartet wird. - Adaptee: Die
OldShape
-Klasse stellt eine Methoderender()
zur Verfügung, die aber inkompatibel mit derShape
-Schnittstelle ist. - Adapter: Die
ShapeAdapter
-Klasse implementiert dieShape
-Schnittstelle und enthält eine Instanz derOldShape
-Klasse. Die Methodedraw()
des Adapters ruft intern die Methoderender()
der alten Klasse auf und übersetzt so den Aufruf in die erwartete Form. - Client: Der Client verwendet die
Shape
-Schnittstelle und ruft diedraw()
-Methode auf, ohne zu wissen, dass diese durch einen Adapter in die inkompatibleOldShape
-Schnittstelle übersetzt wird.
Beispiel des Adapter Patterns in Python
Das Adapter Pattern ermöglicht es, eine Schnittstelle einer Klasse an eine andere anzupassen, ohne dass der bestehende Code verändert werden muss. Es wird oft verwendet, wenn du eine Schnittstelle eines Objekts mit einer anderen Schnittstelle kompatibel machen möchtest.
Hier ist ein einfaches Beispiel für das Adapter Pattern in Python:
Beispiel: Adapter Pattern in Python
Angenommen, wir haben eine Klasse OldSystem
, die eine Methode old_method()
hat. Wir möchten diese Methode aber durch eine neue Methode new_method()
in einer anderen Klasse NewSystem
verwenden, ohne den bestehenden Code zu ändern. Wir können ein Adapter-Pattern verwenden, um dies zu ermöglichen.
# Die alte Klasse, die wir anpassen möchten
class OldSystem:
def old_method(self):
return "Daten von der alten Methode"
# Die neue Klasse mit einer anderen Schnittstelle
class NewSystem:
def new_method(self):
return "Daten von der neuen Methode"
# Der Adapter, der die alte Methode auf die neue Schnittstelle abbildet
class Adapter:
def __init__(self, new_system: NewSystem):
self.new_system = new_system
def old_method(self):
# Hier wird die neue Methode der neuen Klasse aufgerufen
return self.new_system.new_method()
# Anwendung
old_system = OldSystem()
print("Altes System:", old_system.old_method())
new_system = NewSystem()
adapter = Adapter(new_system)
print("Adapter auf Neues System:", adapter.old_method())
Erklärung:
- OldSystem hat eine Methode
old_method()
, die du verwenden möchtest. - NewSystem hat eine Methode
new_method()
, die die neue Funktionalität bereitstellt, aber eine andere Schnittstelle hat. - Adapter ist eine Klasse, die die alte Schnittstelle (
old_method()
) auf die neue Schnittstelle (new_method()
) mappt.
Ausgabe:
Altes System: Daten von der alten Methode
Adapter auf Neues System: Daten von der neuen Methode
Durch den Adapter kannst du die alte Schnittstelle verwenden, aber die Logik des neuen Systems dahinter aufrufen. So bleibt der bestehende Code unverändert und funktioniert weiterhin, während du die neue Funktionalität verwendest.
Vorteile des Adapter Patterns
- Kompatibilität herstellen: Das Adapter Pattern ermöglicht es, inkompatible Schnittstellen miteinander zu verbinden. Es übersetzt die Anforderungen des Clients in eine Form, die das bestehende System versteht.
- Wiederverwendbarkeit: Der Adapter ermöglicht die Wiederverwendung bestehender Code-Teile, ohne dass diese geändert werden müssen. So bleibt die ursprüngliche Implementierung intakt.
- Erweiterbarkeit: Wenn ein neues System oder eine neue Schnittstelle hinzugefügt werden muss, kann ein Adapter erstellt werden, ohne dass der Client-Code angepasst werden muss.
- Flexibilität: Der Adapter ermöglicht es, verschiedene Systeme zu integrieren, selbst wenn diese unterschiedliche Schnittstellen verwenden. Neue Adapter können für verschiedene Systeme erstellt werden.
Nachteile des Adapter Patterns
- Zusätzliche Komplexität: Das Hinzufügen von Adaptern erhöht die Anzahl der Klassen im System. Dies kann die Komplexität erhöhen, besonders bei vielen inkompatiblen Schnittstellen.
- Leistungseinbußen: Der Adapter fügt eine zusätzliche Abstraktionsebene hinzu, die unter Umständen die Leistung beeinträchtigen kann. Besonders in leistungskritischen Systemen sollte der Einsatz von Adaptern sorgfältig abgewogen werden.
- Schwierigkeiten bei der Wartung: Wenn viele Adapter verwendet werden, kann es schwieriger werden, das System zu warten, da viele Klassen miteinander verbunden sind.
Welche Designprobleme löst das Adapterpatten?
Das Adapter-Pattern löst mehrere wichtige Designprobleme in der Softwareentwicklung, insbesondere wenn es um die Integration und Kompatibilität von Systemen geht. Hier sind die wichtigsten Probleme, die das Adapter-Pattern adressiert:
1. Unterschiedliche Schnittstellen
Problem: Du hast zwei Klassen oder Systeme, die unterschiedliche Schnittstellen bieten, aber sie sollen miteinander kommunizieren oder zusammenarbeiten. Wenn die Schnittstellen inkompatibel sind, musst du entweder den bestehenden Code ändern oder eine andere Lösung finden, um die Zusammenarbeit zu ermöglichen.
Lösung durch Adapter: Das Adapter-Pattern ermöglicht es, eine Klasse oder ein System so anzupassen, dass es mit einer anderen Schnittstelle kompatibel wird. Der Adapter übersetzt die Anfragen von einer Schnittstelle in die andere, ohne den Code der ursprünglichen Klassen zu ändern.
Beispiel: Ein altes System erwartet eine Methode old_method()
, aber ein neues System stellt eine Methode new_method()
zur Verfügung. Der Adapter sorgt dafür, dass der Aufruf der alten Methode intern die neue Methode aufruft, sodass die Systeme zusammenarbeiten können.
2. Vermeidung von Änderungen am bestehenden Code
Problem: Oftmals kannst du bestehende Bibliotheken oder Systeme nicht ändern, weil sie entweder von anderen Teilen der Anwendung abhängt oder du keinen Zugriff auf den Quellcode hast. Das bedeutet, dass du den bestehenden Code nicht direkt anpassen kannst, um ihn an die neuen Anforderungen oder Systeme anzupassen.
Lösung durch Adapter: Anstatt den bestehenden Code zu ändern, kannst du einen Adapter erstellen, der als Zwischenschicht fungiert. Der Adapter übersetzt die Aufrufe und sorgt dafür, dass die inkompatiblen Klassen miteinander arbeiten können. Auf diese Weise bleibt der bestehende Code unverändert und du kannst die neue Funktionalität problemlos integrieren.
3. Integration von Legacy-Systemen
Problem: In vielen Fällen existiert ein Legacy-System (Altsystem), das mit modernen Systemen oder neuen Technologien integriert werden muss. Diese Systeme sind oft inkompatibel oder haben unterschiedliche Schnittstellen, was die Integration schwierig macht.
Lösung durch Adapter: Das Adapter-Pattern ist besonders nützlich, wenn du ein Legacy-System mit modernen Systemen verbinden musst. Der Adapter fungiert als Brücke zwischen den verschiedenen Systemen, indem er die Anfragen in das jeweils benötigte Format übersetzt.
Beispiel: Ein altes System, das Daten im XML-Format liefert, und ein neues System, das JSON benötigt. Der Adapter kann die XML-Daten in JSON umwandeln, sodass das neue System mit den Daten arbeiten kann.
4. Flexibilität und Erweiterbarkeit
Problem: Wenn du ein System erweitern musst, das bereits viele verschiedene Komponenten verwendet, möchtest du nicht jede dieser Komponenten anpassen, um sie miteinander kompatibel zu machen.
Lösung durch Adapter: Durch die Verwendung des Adapter-Patterns kannst du die Funktionalität erweitern, ohne den bestehenden Code zu ändern. Neue Komponenten können einfach durch einen passenden Adapter integriert werden, ohne das gesamte System umstrukturieren zu müssen.
5. Erleichterung der Wiederverwendbarkeit
Problem: Du möchtest eine bestehende Klasse oder Komponente in einem neuen Kontext oder mit anderen Komponenten verwenden, aber die Schnittstelle ist nicht kompatibel.
Lösung durch Adapter: Ein Adapter macht eine bestehende Klasse oder ein System wieder verwendbar, indem es eine Schnittstelle bereitstellt, die mit anderen Systemen kompatibel ist. So kannst du das bestehende System in neuen Szenarien einsetzen, ohne Änderungen am ursprünglichen Code vorzunehmen.
Das Adapter-Pattern löst Probleme im Zusammenhang mit:
- Inkompatiblen Schnittstellen zwischen Klassen oder Systemen.
- Vermeidung von Änderungen am bestehenden Code und Integration von Legacy-Systemen.
- Erweiterbarkeit und Flexibilität von Softwarearchitekturen.
- Erleichterung der Wiederverwendbarkeit bestehender Systeme in neuen Kontexten.
Es sorgt dafür, dass Systeme mit unterschiedlichen Schnittstellen miteinander arbeiten können, ohne die bestehenden Klassen oder Codes zu verändern, und ermöglicht so eine elegante Lösung für Kompatibilitätsprobleme.
Fazit
Das Adapter Pattern ist ein sehr nützliches Designmuster, das die Integration inkompatibler Schnittstellen ermöglicht. Es wird oft eingesetzt, um die Zusammenarbeit von verschiedenen Systemen oder Bibliotheken zu erleichtern, die ursprünglich nicht kompatibel sind. In C++ kann dieses Muster leicht umgesetzt werden, indem eine Adapter-Klasse implementiert wird, die die Schnittstellenübersetzung übernimmt.
Die Vorteile des Patterns liegen in der Flexibilität und Wiederverwendbarkeit, die es bietet. Entwickler können bestehende Implementierungen weiterhin verwenden, ohne Änderungen am ursprünglichen Code vornehmen zu müssen. Allerdings muss der Einsatz von Adaptern sorgfältig abgewogen werden, da sie zusätzliche Komplexität und potenzielle Leistungseinbußen verursachen können.
Zurück zur Liste der Pattern: Liste der Design-Pattern