Adapter-Muster
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 97 von 104.
Das Adapter-Muster ermöglicht es inkompatiblen Schnittstellen, zusammenzuarbeiten. Es fungiert als Brücke zwischen zwei Klassen und wandelt die Schnittstelle einer Klasse in eine Schnittstelle um, die Clients erwarten. Dies ist besonders nützlich bei der Integration von Legacy-Code oder Bibliotheken von Drittanbietern, die nicht mit der Schnittstelle Ihres Systems übereinstimmen.
Stellen Sie sich vor, Sie haben eine bestehende Klasse, die genau das tut, was Sie benötigen, aber deren Schnittstelle nicht mit dem übereinstimmt, was Ihr Code erwartet. Anstatt eine der beiden Seiten zu ändern, erstellen Sie einen Adapter, der die bestehende Klasse umschließt und die Aufrufe übersetzt:
#include <iostream>
#include <string>
// Vorhandene Klasse mit inkompatibler Schnittstelle
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// Ziel-Schnittstelle, die der Client erwartet
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// Adapter kapselt LegacyPrinter und implementiert die Printer-Schnittstelle
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // Den Aufruf weiterleiten
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// Client verwendet die erwartete Schnittstelle
adapter.print("Hello World");
}Der Adapter hält eine Referenz auf den LegacyPrinter und implementiert das Printer-Interface. Wenn print() aufgerufen wird, delegiert er an printMessage(). Der Client-Code arbeitet mit dem Printer-Interface, ohne etwas über die darunterliegende Legacy-Implementierung zu wissen.
Verwenden Sie den Adapter, wenn Sie eine bestehende Klasse verwenden möchten, deren Schnittstelle jedoch nicht Ihren Anforderungen entspricht, oder wenn Sie eine wiederverwendbare Klasse erstellen müssen, die mit Klassen mit inkompatiblen Schnittstellen zusammenarbeitet.
Aufgabe
EinfachLassen Sie uns ein Media Player Adapter-System bauen, das verschiedene Audio-Bibliotheksschnittstellen überbrückt. Stellen Sie sich vor, Sie integrieren eine veraltete Audio-Bibliothek in eine moderne Media-Player-Anwendung – die alte Bibliothek funktioniert einwandfrei, aber ihre Schnittstelle entspricht nicht dem, was Ihr Player erwartet. Genau hier glänzt das Adapter-Muster.
Sie werden Ihren Code über vier Dateien organisieren:
MediaPlayer.h: Definieren Sie die Ziel-Schnittstelle, die Ihr moderner Media-Player erwartet.Erstellen Sie eine abstrakte
MediaPlayer-Klasse mit diesen rein virtuellen Methoden:play(const std::string& filename)— spielt die angegebene Datei abstop()— stoppt die WiedergabesetVolume(int level)— stellt die Lautstärke ein (0-100)
Fügen Sie einen virtuellen Destruktor hinzu.
LegacyAudioLib.h: Erstellen Sie die bestehende "legacy" Audio-Bibliothek mit einer inkompatiblen Schnittstelle.Ihre
LegacyAudioLib-Klasse repräsentiert eine alte Bibliothek, die anders funktioniert als Ihr Player erwartet. Sie sollte Folgendes haben:loadAudio(const std::string& path)— gibtLegacy: Loading [path]ausstartPlayback()— gibtLegacy: Playback startedausstopPlayback()— gibtLegacy: Playback stoppedausadjustVolume(double percentage)— nimmt einen Dezimalwert (0.0 bis 1.0) entgegen und gibtLegacy: Volume set to [percentage]aus (zeigen Sie den Dezimalwert an)
AudioAdapter.h: Bauen Sie den Adapter, der die beiden Schnittstellen überbrückt.Ihre
AudioAdapter-Klasse sollte vonMediaPlayererben und eine Referenz auf einLegacyAudioLib-Objekt halten. Der Adapter übersetzt Aufrufe von der modernen Schnittstelle in die Legacy-Schnittstelle:play()sollte sowohlloadAudio()als auchstartPlayback()in der Legacy-Bibliothek aufrufenstop()sollte anstopPlayback()delegierensetVolume()erhält eine Ganzzahl (0-100), aber die Legacy-Bibliothek erwartet einen Dezimalwert (0.0-1.0), daher müssen Sie diesen umrechnen
main.cpp: Demonstrieren Sie den Adapter in Aktion.Lesen Sie zwei Eingaben ein:
- Einen Dateinamen zum Abspielen (String)
- Einen Lautstärkepegel (Ganzzahl, 0-100)
Erstellen Sie eine
LegacyAudioLib-Instanz und einenAudioAdapter, der diese umschließt. Verwenden Sie dann den Adapter über dieMediaPlayer-Schnittstelle (speichern Sie ihn alsMediaPlayer-Pointer), um:- Die Lautstärke auf den eingegebenen Pegel einzustellen
- Den eingegebenen Dateinamen abzuspielen
- Die Wiedergabe zu stoppen
Zum Beispiel mit den Eingaben song.mp3 und 75:
Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stoppedMit den Eingaben podcast.wav und 50:
Legacy: Volume set to 0.5
Legacy: Loading podcast.wav
Legacy: Playback started
Legacy: Playback stoppedBeachten Sie, wie der Client-Code vollständig mit der MediaPlayer-Schnittstelle arbeitet – er hat keine Ahnung, dass im Hintergrund eine Legacy-Bibliothek die eigentliche Arbeit erledigt. Der Adapter übernimmt die gesamte Übersetzung, einschließlich der Umrechnung der ganzzahligen Lautstärke in einen Dezimalwert. Dies ist der Kern des Adapter-Musters: inkompatible Schnittstellen zusammenarbeiten zu lassen, ohne eine der beiden Seiten zu verändern.
Spickzettel
Das Adapter-Muster ermöglicht es inkompatiblen Schnittstellen, zusammenzuarbeiten, indem es als Brücke zwischen zwei Klassen fungiert. Es wandelt die Schnittstelle einer Klasse in eine Schnittstelle um, die Clients erwarten.
Verwenden Sie das Adapter-Muster, wenn Sie eine bestehende Klasse nutzen möchten, deren Schnittstelle jedoch nicht Ihren Anforderungen entspricht, oder wenn Sie Legacy-Code oder Bibliotheken von Drittanbietern integrieren.
Struktur
Das Muster besteht aus drei Hauptkomponenten:
- Target Interface: Die Schnittstelle, die der Client erwartet
- Adaptee: Die bestehende Klasse mit einer inkompatiblen Schnittstelle
- Adapter: Umhüllt den Adaptee und implementiert die Ziel-Schnittstelle, wobei Aufrufe zwischen ihnen übersetzt werden
Beispiel
#include <iostream>
#include <string>
// Bestehende Klasse mit inkompatibler Schnittstelle (Adaptee)
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// Ziel-Schnittstelle, die der Client erwartet
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// Adapter umhüllt LegacyPrinter und implementiert die Printer-Schnittstelle
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // Den Aufruf übersetzen
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// Client verwendet die erwartete Schnittstelle
adapter.print("Hello World");
}Der Adapter hält eine Referenz auf den Adaptee und implementiert die Ziel-Schnittstelle. Wenn Methoden auf dem Adapter aufgerufen werden, delegiert er an die Methoden des Adaptees und übersetzt die Aufrufe nach Bedarf. Der Client-Code arbeitet mit der Ziel-Schnittstelle, ohne von der Legacy-Implementierung zu wissen.
Probier es selbst
#include <iostream>
#include <string>
#include "MediaPlayer.h"
#include "LegacyAudioLib.h"
#include "AudioAdapter.h"
using namespace std;
int main() {
// Eingaben lesen
string filename;
int volumeLevel;
cin >> filename;
cin >> volumeLevel;
// TODO: Erstelle eine LegacyAudioLib-Instanz
// TODO: Erstelle einen AudioAdapter, der die Legacy-Bibliothek kapselt
// TODO: Speichere den Adapter als MediaPlayer-Pointer
// TODO: Nutze das MediaPlayer-Interface, um:
// 1. Setze die Lautstärke auf das Eingabeniveau
// 2. Spiele den eingegebenen Dateinamen ab
// 3. Stoppe die Wiedergabe
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins via CRTPPimpl-IdiomType ErasureEnum-Klassen & starke TypisierungException-Handling in OOPEigene Exception-Hierarchien14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Method-MusterState-MusterComposite-MusterRAII als Muster3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container