menu

Bindeglied: Das Adapter-Entwurfsmuster

Objektorientierte Programmierung ist eine feine Sache: Zu jedem Zeitpunkt können Klassen einfach durch bessere Klassen ausgetauscht werden. Was aber tun, wenn die neue API sich signifikant von der alten API unterscheidet? Man bräuchte eine Art Universal-Adapter um die neue Klasse „einzustöpseln“. Genau diese Aufgabe erledigt das Adapter-Muster.

vom 16. Oktober 2006

Dies ist der zweite Teil einer Serie, die sich mit dem Einsatz von Entwurfsmustern in PHP beschäftigt. Der erste Teil dieser Serie ist in der letzten Ausgabe des PHP Magazins erschienen (06/03) und hat versucht, Ihnen die Grundlagen der Entwurfsmuster-Theorie näherzubringen. Er gab einige Ratschläge, die helfen sollten, für die verschiedensten Anwendungszwecke das jeweils passende Entwurfsmuster zu finden. Der Artikel erklärte und demonstrierte die Vorgehensweise der Entwurfsmuster-Adaption (bzw. Implementierung) am Beispiel des Singleton-Musters. Die hier verwendete Terminologie wurde im ersten Artikel erklärt.

Dieser Teil der Serie setzt die Grundlagen des ersten Teils voraus und baut auf diese weiter auf. Er widmet sich dem Adapter und greift damit ein immer noch einfach zu überblickendes, aber doch schon etwas komplexeres Entwurfsmuster heraus. Der Adapter gehört der Kategorie der Strukturmuster an. Im Gegensatz zum Singleton aber (welches der Kategorie der objektbasierten Erzeugungsmuster entstammt) ist der Adapter klassenbasiert. Diese Aussage wird verständlich, wenn wir uns den Aufbau und die Aufgaben des Adapters betrachten. Erinnern wir uns: Strukturmuster befassen sich überwiegend mit der Zusammensetzung von Klassen und deren Exemplaren. Der Adapter bildet einen Aufsatz auf eine  oder mehrere Klassen. Man spricht von einem klassenbasierten Entwurf, da die Beziehungen zwischen Adapter und der zu steuernden Klasse (Host) zur Laufzeit bereits festgelegt sind. Objektbasierte Entwürfe hingegen beinhalten meist dynamische Objektbeziehungen, was hier nicht der Fall ist.

 

Die Problembeschreibung

Objektorientierter Code bietet den Vorteil, dass zu jedem Zeitpunkt Klassen einfach durch bessere Klassen ausgetauscht werden. Immer wieder stößt man bei der Auswahl  geeigneter Klassen auf die Problematik, dass verschiedene in Frage kommende Klassen, obwohl sie an sich die gleichen Aufgaben erledigen, dies mit Hilfe verschiedener APIs tun. Dabei differieren nicht nur die Methoden-Syntax und die Anzahl (und Reihenfolge) der erforderlichen Parameter von Klasse zu Klasse, sondern auch die Anzahl der benötigten Methodenaufrufe. Das später vorgestellte Beispiel zeigt zwei Klassen (mathe1 und mathe2), deren Funktion jeweils die Addition zweier Zahlen ist. Während ein Exemplar von mathe1 dies mit nut einem Befehl erledigt, benötigt ein Exemplar von mathe2 für die gleiche Aufgabe bereits vier Befehle.

Damit man sich nicht bei der Auswahl einer Klasse für alle Zeiten an diese bindet, ist eine universelle API wünschenswert. Diese soll sicherstellen, dass keine Zeile des Programmcodes geändert werden muss, sollte man einmal die Klasse hinter einem Exemplar austauschen. Die Anforderungen an eine solche API sind wie folgt:

  • alle Möglichkeiten der Klassen sollen nutzbar sein
  • die API soll für alle Klassen mit gleichen Aufgaben identisch sein
  • die API soll, ebenso wie die durch sie repräsentierten Exemplare, objektorientiert sein

 

Die Problemlösung

Die bevorzugte Möglichkeit, das beschriebene Problem zu lösen, ist das Adapter-Entwurfsmuster. Der Adapter fungiert hierbei als „Übersetzer“ (oder auch Zwischenstück) zwischen einer allgemein gehaltenen (und somit universellen) API und den Methoden der zu steuernden Klasse (bzw. deren Exemplare).

Etwas abstrahiert kann man das Adapter-Muster mit einem Steckdosen-Adapter vergleichen. Hierzulande sind Stecker zweipolig mit knicksicheren Dochten. Im Ausland kann dies jedoch ganz anders aussehen. Ein Steckdosen-Adapter wird benötigt, um das heimische Elektrogerät auch im Ausland nutzen zu können. Der Stecker des Elektrogeräts findet am Adapter Anschluss. Der Adapter wiederum verfügt über mindestens einen (oft sogar mehrere), speziell auf die Form der ausländischen Steckdosen zugeschnittenen Stecker. Die „Kommunikation“ des Elektrogeräts mit der Steckdose läuft ausschließlich durch den Adapter. Vereinfacht ausgedrückt geht nichts am Adapter vorbei und der Adapter erfindet auch nichts hinzu. Ok, zugegebener Maßen „erfinden“ spezielle Adapter in der Tat etwas hinzu (etwa die bei manchen Mikrophonen benötigte Phantomspeisung), was aber nichts am Grundprinzip ändert: Die ergänzten Teile der ausgetauschten „Information“ sind für den Host (hier: die Steckdose) unerlässlich und ändern nichts am Ergebnis des Datenaustauschs. Später im konkreten Implementierungs-Abschnitt werden Sie sehen, dass auch der Adapter dem Input eventuell noch weitere notwendige Daten hinzufügen kann (vielleicht sogar muss).

Der Ablauf ist schnell erklärt und leicht verständlich: Die Applikation sendet alle Anweisungen an den Adapter in seiner eigenen Syntax. Dem Adapter muss zu diesem oder einem früheren Zeitpunkt mitgeteilt werden, welcher Host verwendet wird. Der Adapter übersetzt nun alle empfangenen Anweisungen und leitet diese an den Host weiter. Alle vom Host empfangenen Rückgaben werden wieder in die Signatur des Adapters übersetzt und zu definierten Zeitpunkten an die Applikation zurückgeliefert (Der Begriff der Signatur wurde im ersten Artikel geklärt, noch einmal kurz zusammengefasst ist die Signatur die Summe aller Ein- und Ausgabeparameter einer Methode).

 

Implementierung

Der Adapter besteht meist aus genau einer Klasse, die durch ein Exemplar repräsentiert wird. Nach außen hin existiert eine universelle API, welche die APIs der zu steuernden Klassen vollständig ersetzt.

Es ist nicht praktikabel, die Adapter-Klasse durch Vererbung von der Host-Klasse abzuleiten: Da der Host alternieren kann (wahrscheinlich sogar wird, sonst wäre der Adapter nicht zum Einsatz gekommen), ist nicht vorhersehbar, wie die Elternklasse aufgebaut ist. Der Aufbau in Frage kommender Elternklassen kann sich so stark unterscheiden, dass der Adapter im schlimmsten Fall ungewollt deren Methoden polymorph überschreibt. Auch das parent Konstrukt hilft hier nur begrenzt weiter, da gleichlautende klassenglobale Variablen ebenfalls ungewollt überschrieben werden können. Auch kann eine solche Umsetzung unter PHP4 nur inkonsequent erfolgen; die Methoden des Hosts sollten von außen nicht oder nur über den Adapter zugreifbar sein. Zwar bietet PHP5 die Möglichkeit, Methoden durch das private Konstrukt zu verstecken, aber durch die mangelnde Abwärtskompatibilität zu PHP4 ist dies zum jetzigen Zeitpunkt keine empfehlenswerte Lösung.

Die Implementierung als eigenständige Klasse umschifft all diese Klippen. Das nachfolgende Beispiel zur Implementierung stellt einen einfachen Adapter vor, der auf Basis des switch() Konstrukts arbeitet. Um das Beispiel möglichst einfach zu halten, soll der Adapter lediglich zwei Zahlen addieren und das Ergebnis der Addition zurückliefern. Listing 1 zeigt zwei Klassen, welche diese Aufgabe voneinander unabhängig auf verschiedene Weise bewältigen. Unser Adapter (Listing 2) soll diese beiden Klassen steuern können. Um das Exemplar der gerade verwendeten Mathe-Klasse vor der Applikation zu verbergen, wird dieses direkt vom Konstruktor des Adapters instanziert und anschließend in einer Klassenvariablen abgelegt. Für jede Funktion des Hosts muss eine Funktion des Adapters implementiert werden, in diesem Beispiel lediglich eine Funktion für die Addition.

Wir implementieren diese Funktion als eine einzige Methode namens addiere(), die als Parameter nur die beiden zu addierenden Zahlen erwartet. Der Adapter prüft nun per switch() welche Klasse als Host fungiert. Deren benötigte Methoden werden nun aufgerufen bevor das Ergebnis zurückgeliefert wird.

In der eben verwendeten Terminologie existiert ein wichtiger Unterschied zwischen Funktionen und Methoden, der hier deutlich zum Tragen kommt: Eine Funktion ist unabhängig von der Implementierung und kann durch mehrere Methodenaufrufe realisiert sein. Die Anzahl der für eine Funktion benötigten Methodenaufrufe kann zwischen Adapter und Host durchaus variieren. Ersichtlich wird dies in der Methode zeige() des Adapters: Der Adapter benötigt für diese Aufgabe (ebenso wie der Host mathe1) genau einen Methodenaufruf. Da mathe2 jedoch für die gleiche Aufgabe vier Aufrufe benötigt, bricht der Adapter die Aufgabe auf vier Aufrufe herunter. In den ersten beiden Aufrufen werden die Operanden übermittelt, bevor im dritten Aufruf deren Summe berechnet wird. Der letzte Aufruf fragt zum Schluss die berechnete Summe ab.

Der default: Eintrag des switch() Konstrukts kann, wie hier gezeigt, dazu genutzt werden, die Verwendung von nicht unterstützten Hosts zu erkennen und zu behandeln.

Der Einsatz des Adapters gestaltet sich denkbar einfach: Mit $mathe = new mathAdapter(‚mathe1‘); instanzieren wir das benötigte Exemplar des Adapters. Um den Adapter auf die Klasse mathe2 aufzusetzen, reicht eine Anpassung des übergebenen Parameters: $mathe = new mathAdapter(‚mathe2‘); Dem Sinn des Adapters folgend, ist der Methodenaufruf zur Berechnung der Summe von 4 und 6 unabhängig von der verwendeten Mathe-Klasse gleich: $summe = $mathe->addiere(4, 6);

Das hier vorgestellte Beispiel ist natürlich bewusst einfach gehalten. Ziel ist es, die  Funktionsweise des Adapters zu demonstrieren, ohne die Klarheit durch gesteigerte Komplexität zu trüben. Auf der beiliegenden Heft-CD finden Sie neben diesem noch einen weiteren Ansatz. Der alternative Ansatz nutzt die Methode call() zum Steuern aller Funktionen. Die jeweiligen Funktionen sind für jeden möglichen Host als eigene (private) Methode implementiert, die durch einen an call()  übergebenen Parameter aufgerufen werden. Der Vorteil liegt in einer zentralen Fehlerbehandlung, die nicht bei jedem switch() Konstrukt durchgeführt werden muss. Zusätzlich ist der Quellcode auf der Heft-CD mit phpDoc Kommentaren dokumentiert, aus Platzgründen fehlen diese in den abgedruckten Listings.

 

Adapter !== Wrapper

Oft wird der Adapter auch als Wrapper (Umwickler) bezeichnet. Manchmal stimmt diese Aussage, erledigen Adapter und Wrapper doch auf den ersten Blick scheinbar die gleichen Aufgaben. In den meisten Fällen trifft dies aber nur teilweise zu. Die alternative API, die der Adapter anbietet, muss die Funktionalität der zu steuernden Klasse (Host) zu genau 100% abdecken. Hier liegt der Unterschied zu den Wrappern, die sich oft nur einen Teil der Funktionalitäten des Hosts herausgreifen oder diese sogar noch um eigene Funktionen erweitern. Im Sprachgebrauch hat sich auch der Terminus „Abstraktion“ eingebürgert, der aber in der Regel einen Wrapper bezeichnet. Spricht man beispielsweise von einer Datenbank-Abstraktion, meint man doch meist einen Datenbank-Wrapper. Bekannte Datenbank-Wrapper wie die PEAR Pakete DB, MDB oder ADO_DB bieten eine einheitliche API für alle unterstützen Datenbanken, erweitern aber deren Leistungsumfang durch Funktionen wie Sequenz- oder auch Increment-ID-Emulation.

 

Bewertung

Die Wahl des im zweiten Teil der Serie vorgestellten Entwurfsmusters ist natürlich nicht willkürlich auf den Adapter gefallen. Der Adapter ist ein (je nach Komplexität der Hosts) einfach zu implementierendes Muster, was in praktisch jeder größeren Applikation Verwendung finden kann. Der große Vorteil besteht unbestreitbar darin, dass der Adapter selbst in fertigen Applikationen integriert werden kann, womit man sich aber auf die von der ersetzten Klasse bereits definierte API festlegen muss. Trotzdem wird dadurch ein nachträglicher Umstieg auf andere Klassen ermöglicht. So hat mir der Adapter in einem Projekt geholfen, in dem ich mich nach Fertigstellung in der Lage befand, die verwendete Template-Engine austauschen zu müssen. Wie erwartet war die API der neuen Engine nicht kompatibel, was ich mit Hilfe eines Adapters jedoch schnell und unproblematisch lösen konnte.

Aber auch die Nachteile des Adapters sollen nicht verschwiegen werden. Wie bei allen Entwurfsmustern führen die zusätzlichen Zeilen Code (und vor allem die darin enthaltenen Anweisungen) zu Performanceeinbußen. Diese mögen zwar idealerweise so klein sein, dass sie vernachlässigt werden können – Eingeständnisse an die Performance bleiben sie trotzdem. Auch die für die Umsetzung benötigte Zeit spielt eine bedeutende Rolle: Da der Adapter alle Funktionalitäten des Hosts abbilden muss, sind zwei APIs statt einer zu implementieren. Die damit verbundene Arbeit kann zu einem erheblichen Mehraufwand führen.

Der Adapter ist ein Ausweg aus Problemsituationen und sollte auch nur in diesen angewandt werden. In solchen Situationen aber (und diese treten häufiger auf, als wir manchmal zugeben möchten) ist der Adapter ein einfaches und effektives Mittel, den Tag zu retten und doch noch pünktlich Feierabend zu machen.

Die nächste Ausgabe entführt Sie in die Gefilde der Verhaltensmuster und stellt mit dem Memento ein Muster vor, welches den inneren Zustand eines Objekts erfassen kann, ohne dessen Kapselung zu verletzen. Dies ermöglicht es, ein Objekt jederzeit in einem vorherigen Zustand wiederherzustellen.

 

Listing

class mathe1 {
    function plus($zahl1, $zahl2) {
        return $zahl1 + $zahl2;
    } 
}

class mathe2 {
    var $definitionen = array();
    var $result = FALSE;

    function definiere($index, $definition) {
        $this->definitionen[$index] = $definition;
    }

    function addiere($index1, $index2) {
        $this->result = $this->definitionen[$index1] + $this->definitionen[$index2];
    }

    function ergebnis() {
        return $this->result;
    }
}

class matheAdapter {
    var $hostObject = FALSE;
    var $hostClassName = NULL;

    function matheAdapter($hostClassName) {
        if(class_exists($hostClassName)) {
            $this->hostObject = new $hostClassName;
            $this->hostClassName = $hostClassName;
        } else {
            die('Fehler beim instanzieren der Klasse "'.$hostClassName.'"!');
        }
    }

    function addiere($zahl1, $zahl2) {
        switch($this->hostClassName) {
            case 'mathe1': $result = $this->hostObject->plus($zahl1, $zahl2);
                           break;
            case 'mathe2': $this->hostObject->definiere(1, $zahl1);
                           $this->hostObject->definiere(2, $zahl2);
                           $this->hostObject->addiere(1, 2);
                           $result = $this->hostObject->ergebnis();
                           break;
            default:       die('Der Adapter hat keine Entsprechung der Klasse "'.$this->hostClassName.'" für addiere() definiert!'); 
        }

        return $result;
    }
}

 

 

Kommentar abgeben

Hinweis: Felder, welche mit einem Sternchen (*) gekennzeichnet sind, müssen ausgefüllt werden.