Entwurfsmuster in der Softwareentwicklung: Das etwas andere IKEA-Regalsystem

Seite 2: Entkopplung von Klassen

Inhaltsverzeichnis

Das Erzeugungsmuster Factory-Method ähnelt einer Fabrik, die für die Produktion von Objekten zuständig ist. Ein Objekt erzeugt sich bei dieser Methode nicht selbst und ist auch nicht von einer konkreten Klasse abhängig – so wie es beim Singleton der Fall ist. Stattdessen erstellt die statische Factory-Methode ein Objekt in einer Factory-Klasse (Listing 1).

class Client {
     Server server = Factory.createServer()
}

class Factory {
    static Server createServer() {
	return new Server()
    }
}

Listing 1: Einfaches Factory Pattern

Problematisch ist jedoch die Abhängigkeit des Objekts zu einem speziellen Typen. Aus diesem Grund arbeitet das Erzeugungsmuster Factory-Method anstelle von konkreten Typen mit Interfaces. Weiterhin kann die Factory-Methode in der -Klasse einen Parameter enthalten, der die Art des zu erzeugenden Objekts bestimmt. Somit lassen sich beispielsweise, unter Einsatz von globalen Variablen, in der Testumgebung andere Objekte erzeugen als außerhalb der Testumgebung (Listing 2).

class Client {
     I_Server server = Factory.createServer(isInTestMode)
}
class Factory {
    static I_Server createServer(boolean testMode) {
	if (testMode) {
	     return new MockServer()
	}
	return new Server()
    }
}

Listing 2: Factory Pattern mit Interfaces

Anstelle von Interfaces ist die Verwendung von Unterklassen möglich. Mit dem Erzeugungsmuster Factory-Method lassen sich die mitunter schwierig lesbaren Konstruktoren selbst formulieren. Zudem verwaltet es Singletons und gibt je nach Parameter verschiedene Klassen zurück.

Die Notwendigkeit von zusätzlichen Factory-Klassen sowie die benötigte Abhängigkeit zu Interfaces beziehungsweise Oberklassen stellen die einzigen großen Nachteile des Musters dar. Auf der anderen Seite können Entwicklerinnen und Entwickler durch den Einsatz von Factory-Methoden erfahrungsgemäß ihren Code besser testen und warten. Ohne dieses Konstrukt wäre ein einfaches Austauschen der Klassen nicht ohne Weiteres möglich. Häufig verwendet man in der Praxis die Dependency Injection – wie auch im Java-Framework Spring. Sie führt im weitesten Sinne das Erzeugungsmuster automatisiert aus. Die Besonderheit hierbei ist, dass dabei das Erzeugungsmuster Factory-Method im Hintergrund läuft und sich Developer nicht um die Objekterzeugung kümmern müssen.

Zu den zweiten grundlegenden Entwurfsmustern gehören die Strukturmuster. Sie spezifizieren die Beziehungen zwischen einzelnen Entitäten. Ziel ist es, den Entwurf der Software zu erleichtern. In der Praxis verwendet man dafür häufig die Strukturmuster Adapter und Proxy.

Nutzer verwenden das Strukturmuster Adapter, wenn die Schnittstelle einer existierenden Klasse nicht zu der benötigten Schnittstelle passt. Die Folge daraus ist die Verwendung eines Adapter-Objekts. Häufig ist das bei Bibliotheken der Fall, da sie in der Regel unveränderbar sind. Es bietet sich auch das Nutzen von wiederverwendbaren Klassen an, die mit nicht vorhersehbaren anderen unabhängigen Klassen zusammenarbeiten sollen.

Ein Beispiel ist die Entwicklung einer App, die die aktuellen Lottozahlen im XML-Format vom Lotto-Server herunterlädt und sie in Listenform darstellt. Nach einer Aktualisierung zeigt sie die letzten Lottozahlen grafisch aufbereitet an. Um diese Ausgabe zu ermöglichen, verwenden Entwicklerinnen und Entwickler eine bereits existierende Grafik-Bibliothek. Sie benötigt jedoch Daten im JSON- und nicht im XML-Format. Da die App die Daten vom Lotto-Server aber im XML-Format abruft und sie das Format nicht ändern kann, ist ein Adapter notwendig. Die jeweilige Client-Klasse, die die Daten vom Lotto-Server abruft, benötigt einen Adapter. Er parst die XML-Daten ins JSON-Format, damit die Grafik-Bibliothek arbeiten kann. Entwicklerinnen und Entwickler haben die Möglichkeit, den Adapter als Objekt-Adapter oder als Klassen-Adapter umzusetzen.

Beim Objekt-Adapter-Ansatz stellt die existierende Client-Klasse – hier die Lotto-App – ein Interface zur Verfügung (Abbildung 3). Der Adapter implementiert sie und kommuniziert mit einer Service-Klasse – hier GrafikLib. Er hält eine Referenz zur Service-Klasse, bringt die Daten in das korrekte Format und ruft anschließend die passende Service-Methode auf.

Der Objekt-Adapter implementiert ein Objekt, Klassen sind beliebig austauschbar (Abb. 3).

Die Client-Klasse ist aufgrund der Nutzung von Interfaces nicht an einen speziellen Adapter gebunden. Je nach Anforderung ist es möglich, den Adapter durch einen anderen auszutauschen, ohne den Client-Code anzupassen. Das Schema entspricht dem Open/Closed-Prinzip.

Voraussetzung für den Einsatz eines Klassen-Adapters ist die Möglichkeit der Mehrfachvererbung (Abbildung 4). Hierbei erbt er die Eigenschaften und Methoden sowohl von der Client- als auch von der Service-Klasse. Der Adapter überschreibt dann die jeweiligen Methoden, was die Zusammenarbeit mit verschiedenen Klassen ermöglicht. Anstelle der Client-Klasse verwendet der Entwickler nur noch die Adapter-Klasse. Dadurch entsteht eine hohe Abhängigkeit zwischen den Klassen, weshalb ein flexibler Austausch ohne größere Code-Anpassungen nicht möglich ist.

Der Klassen-Adapter ist abhängig von Klassen und erbt ihre Eigenschaften und Methoden (Abb. 4).

Der Einsatz des Strukturmusters Adapter erhöht die Komplexität des Codes, da Entwickler neue Interfaces und Klassen einführen müssen. Je nach Anwendungsfall und Möglichkeit ist es sinnvoll, die Service-Klasse so abzuändern, dass sie zum restlichen Code passt. Dennoch ermöglicht das Entwurfsmuster Adapter eine große Wiederverwendbarkeit und Flexibilität beim Programmieren.