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.

In Pocket speichern vorlesen Druckansicht

(Bild: momente/Shutterstock.com)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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:

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
  • Container-Adapter

Die Container-Adapter std::stack, std::queue und std::priority_queue 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.

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 von Implementation 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.

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:

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.

  • 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

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.

Das Brücken-Muster hilft dabei, die Schnittstelle von ihrer Implementierung zu trennen. Ich werde es in meinem nächsten Artikel genauer vorstellen. (map)