Entwurfsmuster in der Softwareentwicklung: Das etwas andere IKEA-Regalsystem

Softwareentwicklung ohne Design Pattern ist vergleichbar mit einer Suppe ohne Salz – sie sind essenziell, doch können für Neulinge herausfordernd sein.

In Pocket speichern vorlesen Druckansicht 5 Kommentare lesen

(Bild: Black Jack/Shutterstock.com)

Lesezeit: 15 Min.
Von
  • Nils Kasseckert
Inhaltsverzeichnis

Entwurfsmuster ähneln einem modularen IKEA-Regalsystem, das in verschiedenen Größen und Farben erhältlich ist. Sie lassen sich immer gleich kombinieren und erfüllen stets denselben Zweck. Die Farbe ist mit der Programmiersprache vergleichbar, die Regalgröße mit dem Entwurfsmuster. Je nach Anwendungsfall und Projektgröße bietet sich die Verwendung eines Regals in einer anderen Farbe oder Größe an.

Konkret sind Entwurfsmuster also Elemente wiederverwendbaren Codes in der objektorientierten Programmierung (OOP). Sie haben kein festgelegtes Design und beinhalten keinen fertigen Code. Entwurfsmuster bieten Lösungsansätze für typische Probleme in der OOP-Welt und sind auch bei immer komplexer werdenden Frameworks aktuell. Eine Informatikerin oder ein Informatiker kann sie unabhängig von der jeweiligen Programmiersprache einsetzen.

Young Professionals schreiben für Young Professionals

Dieser Beitrag ist Teil einer Artikelserie, zu der die Heise-Redaktion junge Entwickler:innen einlädt – um über aktuelle Trends, Entwicklungen und persönliche Erfahrungen zu informieren. Bist du selbst ein "Young Professional" und willst einen (ersten) Artikel schreiben? Schicke deinen Vorschlag gern an die Redaktion: developer@heise.de. Wir stehen dir beim Schreiben zur Seite.

Aktuell gibt es mehr als 100 verschiedene Entwurfsmuster (Stand Januar 2024). Jedes Entwurfsmuster lässt sich einem Typus zuordnen. Die grundlegenden Typen sind: Erzeugungsmuster, Strukturmuster sowie Verhaltensmuster. Inzwischen gibt aber auch weitere Typen. Sie sind aufgrund der immer größer werdenden Menge an Entwurfsmustern hinzugekommen. Einfachheitshalber beschränkt sich dieser Artikel lediglich auf die grundlegenden Typen (Abbildung 1).

Das Entwurfsmuster gliedert sich in mehrere Teilmengen. Die Bekanntesten sind aufgelistet (Abb. 1).

Der erste Typ sind Erzeugungsmuster. Sie dienen ausschließlich der Erzeugung neuer Instanzen und ermöglichen die Erstellung eines neuen Objekts unabhängig von seiner konkreten Implementierung. Dadurch ist die Objekterstellung ausgelagert und gekapselt. Die am häufigsten genutzten Erzeugungsmuster sind Singleton und Factory-Method.

Ein Singleton kann man sich wie eine globale Variable ohne Zugriffsschutz in der OOP vorstellen. Es ist während der gesamten Laufzeit nur ein einziges Mal erzeugbar, weshalb in einem System maximal eine Instanz eines Singletons existiert. Es kapselt die Verwaltung der Instanzen und stellt einen einheitlichen Zugriffspunkt zur Verfügung.

Jedes Singleton besteht aus einer privaten statischen Variablen und einer veröffentlichten statischen Methode (Abbildung 2). Die Variable enthält die aktuelle Instanz des Objekts über seinen gesamten Lebenszyklus. Die öffentliche Methode getInstance() gibt eine statische Variable zurück. Nur das Objekt selbst darf die Instanz erzeugen. Ein privater Konstruktor stellt das sicher.

Ein Singleton besteht aus einer öffentlichen statischen Methode und einer privaten statischen Variable (Abb. 2).

Das Konstrukt ist leicht anzuwenden und zu verstehen. Zudem vereinfacht es Architekturentscheidungen. Dem stehen aber auch einige Nachteile entgegen. Zum einen benötigt eine Nutzerin beziehungsweise ein Nutzer bei jedem Zugriff auf ein Singleton den vollständigen Klassennamen. Dadurch entsteht eine starke Kopplung an den konkreten Typ, was bei automatisierten Softwaretests ein großes Problem darstellt. Schließlich ersetzen Dummy-Objekte normalerweise nicht benötigte Objekte. Das ist mit einem Singleton jedoch bei klassischen Programmiermethoden nicht möglich. Zum anderen verschlechtert der Einsatz dieses Konstruktes die Systemarchitektur. Schließlich ist für dieses Objekt keine Zugriffskontrolle möglich und jeder Nutzer kann darauf zugreifen. In der Praxis sollten Nutzer sich den Einsatz des Strukturmusters Singleton gut überlegen.

Besonders bei Einsteigern ist dieses Konstrukt aufgrund seiner Einfachheit beliebt. Es gibt zwar diverse Beispiele, die nicht auf die Softwarearchitektur achten, aufgrund der vielen Nachteile sollte man es aber nur in Ausnahmefällen einsetzen: Zum Beispiel dann, wenn die Testbarkeit oder Steuerung der Zugriffskontrolle nicht notwendig sind.

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.

Das Strukturmuster Proxy bietet sich für die Steuerung der Zugriffskontrolle oder für das verzögerte Laden von Bildern, Videos und Audiodateien an. Die Steuerung von Zugriffskontrollen ist ein klassisches Problem in der Programmierung. Ein Beispiel veranschaulicht das: In einer Datenbank existieren Daten, die nur bestimmte Nutzer sehen dürfen. Eine Zugriffskontrolle ist nur dann möglich, wenn Klassen nicht direkt auf die Datenbank zugreifen können. Auch bei der Kommunikation zwischen zwei Hardwarekomponenten bietet sich die Verwendung eines Proxys an. Zum Beispiel ist typischerweise die Anzahl der maximal übertragbaren Daten pro Zeiteinheit begrenzt und man muss sie vorher einer Plausibilitätsprüfung unterziehen.

Zwischen dem Client und dem Service (einer Datenbank oder der Hardware) führt man einen Proxy mit derselben Schnittstelle ein. Der Client stellt die benötigte Schnittstelle als Interface bereit. Der Proxy implementiert sie anstelle des eigentlichen Objekts. Er kommuniziert mit dem Service und führt davor und/oder danach weitere Operationen aus, ohne dass der Client etwas von der Zwischenschicht merkt (Abbildung 5).

Der Proxy implementiert das Interface und führt Operationen aus (Abb. 5).

Die Vorteile dieser Methode sind klar ersichtlich: Zum einen kann man den Lebenszyklus des Service-Objekts vom Proxy zentral steuern. Der jeweilige Client braucht sich darum nicht mehr zu kümmern. Besonders bei aufwendigen Operationen wie beim Aufbau einer Datenbankverbindung kann er Laufzeit einsparen. Weiterhin funktioniert der Proxy, auch wenn der Service noch nicht zur Verfügung steht und kann dementsprechend reagieren. Ein Proxy-Objekt ist austauschbar, ohne dass man dafür den Client oder Service ändern muss – dies entspricht ebenfalls dem Open/Closed-Prinzip.

Nachteilig ist, dass dieses Strukturmuster neue Klassen benötigt. Außerdem muss der Proxy die Daten mit einer gewissen Verzögerung zurückgeben können, asynchrone Programmierung ist also empfehlenswert.

Der letzte Typ der aufgezählten Entwurfsmuster sind Verhaltensmuster. Sie beschäftigen sich vorrangig mit der Modellierung von komplexem Verhalten der Software. Hierzu gehört die Zuteilung von Zuständigkeiten an Objekte, aber auch Algorithmen, um die Flexibilität einer Software hinsichtlich ihres Verhaltens zu erhöhen. Sie zu verstehen, stellt für Anfänger oftmals eine Hürde dar.

Möchten Objekte von der Zustandsänderung eines anderen Objekts erfahren, bietet sich das Verhaltensmuster Observer an. Um es besser zu verstehen, hier ein Beispiel: Es existieren die Objekte Kunde und Laden. Der Kunde möchte gerne als Erster ein neues Smartphone kaufen. Deshalb fährt er mehrfach am Tag zu einem bestimmten Laden und überprüft, ob das Gerät vorrätig ist. Da sich das neue Smartphone jedoch noch auf dem Lieferweg befindet, bleiben die Besuche ohne Erfolg. Würde der Laden eine E-Mail an alle Kunden schicken und sie darüber informieren, dass das Gerät verfügbar ist, müsste der Kunde nicht mehr so oft das Geschäft betreten. Andererseits könnten sich nicht interessierte Kunden über diese Benachrichtigung ärgern und sie als Spam einstufen

Dieser Konflikt lässt sich durch das Verhaltensmuster Observer lösen. In diesem existieren im Wesentlichen die Akteure Publisher und Subscriber. Der Publisher (der Laden) stellt Informationen zur Verfügung. Der Subscriber (der Kunde) möchte hingegen die Informationen erhalten. Der Subscriber registriert sich beim Publisher, der ihn bei definierten Ereignissen benachrichtigt (Abbildung 6).

Der Publisher schickt maßgeschneiderte Informationen an den Subscriber (Abb. 6).

Der Publisher besitzt neben seiner eigentlichen Logik verschiedene Methoden, mit denen sich Subscriber an- und abmelden. Er kann sie auch benachrichtigen. Dabei meldet sich die Subscriber-Klasse nicht direkt, sondern über ein Interface an. Das Interface definiert die Nachrichtenschnittstelle. Meist besteht sie aus einer einzigen Update-Methode mit einem Kontext-Objekt. Das Kontext-Objekt beinhaltet zum Beispiel den Eventnamen oder weitere Eventdetails. Der konkrete Subscriber implementiert das Interface. Dadurch kann er sich benachrichtigen lassen. Für die Verknüpfung beider Elemente ist eine zusätzliche Client-Klasse notwendig. Diese erzeugt sowohl die Publisher- als auch die Subscriber-Klasse und registriert den Subscriber beim Publisher.

Durch dieses Verhaltensmuster lassen sich Beziehungen zwischen Objekten zur Laufzeit abbilden. Es führt zu hoher Wiederverwendbarkeit, Modularität und Flexibilität. Klassische Einsatzgebiete sind beispielsweise die Benachrichtigung bei Netzwerkverlust oder die Durchführung einer asynchronen Kommunikation zwischen verschiedenen Objekten. Die Benachrichtigung folgt keiner Reihenfolge, sondern läuft zufällig ab. Es gibt jedoch auch Nachteile: Meldet sich der Nutzer beim Publisher nicht ab oder meldet er sich mehrfach an, kann dies bei der Software zu nicht definierten Zuständen führen. Solche Fehler sind meist nur schwer zu finden. Besonders bei großen Projekten kann die Änderung eines Objekts zu einem Domino-Effekt führen, wenn ein Subscriber gleichzeitig wiederum ein Publisher ist. Auch ein sich immer wiederholender Aufruf ist so nicht ausgeschlossen.

Wenn man in verschiedene Klassen eine ähnliche Funktion implementieren möchte, die sich nur im Detail unterscheidet, eignet sich das Verhaltensmuster Template-Method. Es kann eine Art Skelett eines Algorithmus in der Basisklasse definieren, das man in den Unterklassen in bestimmten Schritten überschreiben kann.

Um die Funktionsweise zu erklären, bietet sich ein Beispiel an: Angenommen, es existiert ein Rechnungsanalyseprogramm, das automatisiert DOC-Dateien analysiert und notwendige Daten extrahiert. Anschließend gibt es die Daten in einem Modul zurück. Nun aktualisiert jemand das Programm, damit es dieselben Funktionen auch für PDF- und CSV-Dateien bietet. Jede dieser analysierten und extrahierten Daten setzt das Programm in einer eigenen Klasse um. Hierbei fällt die große Ähnlichkeit der drei Klassen auf. Nur die Art der Extraktion der Daten unterscheidet sich, während alle anderen Operationen identisch sind.

Durch die große Ähnlichkeit der drei Klassen bietet sich das Template Method-Pattern an. Das Verhaltensmuster Template-Method vermeidet schlecht wartbaren doppelten Code (Abbildung 7). Zuerst teilt man den Algorithmus in verschiedene Schritte auf. Ein Schritt ist in diesem Beispiel die Datenanalyse, ein anderer die Datenextraktion. Beide entsprechen jeweils Methoden in der abstrakten InvoiceMiner-Klasse. Eine Entwicklerin beziehungsweise ein Entwickler implementiert dann die Methode Datenanalyse processData(data) im InvoiceMiner. Die abstrakte Methode Datenextraktion extractData() lässt sich in der jeweiligen konkreten Klasse implementieren, wie PDFInvoiceMiner.

Somit lässt sich mit dem Verhaltensmuster Template-Method gemeinsamer Code zwischen verschiedenen Klassen mit demselben Use Case teilen. Nachteilig ist die schwierigere Wartbarkeit bei vielen einzelnen Methoden. Das vorgegebene Grundgerüst beschränkt aber nicht die Funktion einer Klasse.

Der Publisher schickt maßgeschneiderte Informationen an den Subscriber (Abb. 7).

Das Thema der Entwurfsmuster ist viel größer als hier dargestellt. Es empfiehlt sich, sich mit dem Thema weiter auseinanderzusetzen. Aus meinen eigenen Erfahrungen heraus mag gerade zu Beginn die Fülle der Informationen und die Theorielastigkeit abschrecken. Es lässt sich jedoch beim richtigen Einsatz von Entwurfsmustern die Codequalität, Testbarkeit sowie Wartbarkeit erheblich verbessern. Besonders bei größeren Softwareprojekten lohnt es sich, mit Entwurfsmustern zu arbeiten.

Folgende Fachliteratur eignet sich für weiterführende Informationen:

  • Design Patterns. Elements of Reusable Object-Oriented Software; Gamma, Erich; Boston 1994.
  • Entwurfsmuster von Kopf bis Fuß; Freeman, Erich; Sebastopol: 2015.
Young Professionals schreiben für Young Professionals

Nils Kasseckert
arbeitet als Lead Developer bei einem großen Netzbetreiber. Seit 2013 betreibt er zusätzlich sein Nebengewerbe "AppSupporter" und promoviert im Bereich "Erneuerbare Energien". Er interessiert sich für Softwarearchitektur, Embedded Systems sowie jegliche neue Technologien.

(mdo)