zurück zum Artikel

Patterns in der Softwareentwicklung: Das Adapter-Muster

Rainer Grimm

(Bild: momente/Shutterstock.com)

Die Idee hinter dem Adapter-Muster ist ganz einfach: Es wandelt eine Schnittstelle in eine andere Schnittstelle um.

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 [1]" 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

Auch bekannt als

Anwendungsfall

Die Container-Adapter std::stack [2], std::queue [3] und std::priority_queu [4]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 [5]; std::priority_queue hingegen verwendet std::vector [6]. Außerdem benötigt std::priority_queue ein binäres Prädikat. Als Default kommt std::less [7] zum Einsatz.

C++ hat weitere Adapter.

C++ unterstützt Insert-Iteratoren und Streams-Iteratoren.

Mit den drei Insert-Iteratoren std::front_inserter [8], std::back_inserter [9] und std::inserter [10] lässt sich ein Element am Anfang, am Ende oder an einer beliebigen Position in einen Container einfügen.

Stream-Iterator-Adaptoren [11] 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

Adaptor

Klasse:

Objekt:

Adaptee

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:

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.

Es stellt sich nun die Frage, in welchem Fall der Klassen-Adapter und wann der Objekt-Adapter zum Einsatz kommen sollte?

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.

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 [15] hilft dabei, die Schnittstelle von ihrer Implementierung zu trennen. Ich werde es in meinem nächsten Artikel genauer vorstellen. (map [16])


URL dieses Artikels:
https://www.heise.de/-7285630

Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Design_Patterns
[2] https://en.cppreference.com/w/cpp/container/stack
[3] https://en.cppreference.com/w/cpp/container/queue
[4] https://en.cppreference.com/w/cpp/container/priority_queue
[5] https://en.cppreference.com/w/cpp/container/deque
[6] https://en.cppreference.com/w/cpp/container/vector
[7] https://en.cppreference.com/w/cpp/utility/functional/less
[8] https://en.cppreference.com/w/cpp/iterator/front_inserter
[9] https://en.cppreference.com/w/cpp/iterator/back_inserter
[10] https://en.cppreference.com/w/cpp/iterator/inserter
[11] https://en.cppreference.com/w/cpp/iterator
[12] https://en.wikipedia.org/wiki/Bridge_pattern
[13] https://en.wikipedia.org/wiki/Decorator_pattern
[14] https://en.wikipedia.org/wiki/Proxy_pattern
[15] https://en.wikipedia.org/wiki/Bridge_pattern
[16] mailto:map@ix.de