Entwurfsmuster in der Softwareentwicklung
Wer kennt sie nicht, die gutaussehenden Geheimagenten im eleganten Anzug, die mit teuren Sportwagen zu ihrem Einsatz fahren, der die ganze Welt retten wird. Und wer kennt sie nicht, die bösen, glibberigen Aliens, die von weither gekommen sind, um die Menschheit zu eliminieren. Oder diese Szene, wenn der Bösewicht eigentlich längst gewonnen hätte, aber, bevor er handeln kann, natürlich einen so langen Monolog halten muss, dass ein beliebiger Superheld das Blatt im allerletzten Moment noch wenden kann.
Es handelt sich hier um Filmklischees oder Stereotypen im Film. Hollywood-Autoren erfinden ihre Handlungen praktisch nie vollständig neu. Stattdessen verwenden sie Muster, die sich bereits in früheren Geschichten als besonders spannend erwiesen haben. Diese Muster geben dann bereits viele Entscheidungen vor, sodass uns die Szenen im Grunde bekannt vorkommen, und lassen Raum für einige neue Elemente, um die Spannung weiter zu steigern.
Motivation
Auch in der Softwareentwicklung ist das Wiederverwenden von Mustern ein wichtiges Prinzip! Es ist daher nicht notwendig, das Rad für jedes Projekt neu zu erfinden. Zumindest einem Teil des Problems in seiner elementaren Weise ist jemand sicherlich früher schon einmal begegnet und kann dort verwendete Lösungswege, die sich bereits als sinnvoll erwiesen haben, aufgreifen. Das spart Zeit (und damit Kosten) und sorgt dafür, dass bewährte Strukturen verwendet werden können.
Ein weiterer großer Pluspunkt von Mustern ist die Übersichtlichkeit von Software. Wenn bestimmte Konzepte immer gleich aufgebaut werden, dann ist es einfach, sie auch wiederzuerkennen. So wird die Software leichter verständlich. Auch Neueinsteiger (w/m/d) haben es dann einfacher, eine Übersicht über Programme zu erhalten und Verfahren zu lernen, die sie dann selbst – wer kann es erraten? – wiederverwenden können.
Wiederverwendung ist also offensichtlich nicht nur für den einzelnen Entwickler (w/m/d) eine erstrebenswerte Sache, sondern auch für das Unternehmen und hilft (wenn die Ideen öffentlich gemacht werden) sogar allen anderen, die auf ein ähnliches Problem stoßen.
Nun ist es aber nicht trivial, objektorientierte Software und insbesondere wiederverwendbare objektorientierte Software zu entwerfen. Der Entwurf, der geschaffen werden soll, muss speziell genug sein, das aktuelle Problem zu lösen, aber allgemein genug auch zukünftigen Anforderungen zu begegnen. Vererbungshierarchien und Beziehungen zwischen den Objekten müssen dazu in einer geeigneten Form abstrahiert werden. Alles in allem ein komplexes Problem. [1]
Glücklicherweise haben sich damit schon viele Experten beschäftigt und Erfahrungen, die sie und andere Entwickler gemacht haben, in Form von sogenannten Entwurfsmustern festgehalten. Diese benennen, erläutern und bewerten systematisch wiederkehrende Entwürfe in objektorientierten Systemen. [1]
Dieser Blogartikel soll eine grobe Übersicht über die wichtigsten Entwurfsmuster bieten. Im Folgenden gehe ich zunächst auf den Begriff Entwurfsmuster ein und stelle dann einige Entwurfsmuster vor. Dabei orientiere ich mich an dem Buch „Entwurfsmuster – Elemente wiederverwendbarer objektorientierter Software“ [1].
Der Begriff „Entwurfsmuster“
Was ist nun eigentlich ein Muster genau? Hierzu gibt es, weil Muster in den verschiedensten Kontexten verwendet werden, auch die verschiedensten Definitionen. Eine davon, mit der in [1] gearbeitet wird, besagt Folgendes: „[Ein Muster] beschreibt ein in unserer Umwelt beständig wiederkehrendes Problem und erläutert den Kern der Lösung für dieses Problem, so dass Sie diese Lösung beliebig oft anwenden können, ohne sie jemals ein zweites Mal gleich auszuführen“.
Was jetzt aus der einen Perspektive als Muster erscheint, sieht aus einer anderen aus wie ein primitiver Grundbaustein. In der Softwarearchitektur unterscheidet man deshalb zwischen verschiedenen Abstraktionsebenen.
Auf der niedrigsten Abstraktionsebene können einzelne Prozeduren und Funktionen wiederverwendet werden. Häufig werden diese auch in programmiersprachenspezifischen Bibliotheken und Frameworks zusammengefasst. Auch sogenannte Idiome, wie beispielsweise Namenskonventionen, sind auf dieser Ebene verankert.
Entwurfsmuster beschreiben dann auf einer abstrakteren Ebene die Zusammenarbeit zwischen Objekten und Klassen und präsentieren maßgeschneiderte Lösungen, um ein allgemeines Entwurfsproblem in einem bestimmten Kontext zu lösen [1].
Noch abstrakter wird es, wenn man sich Softwarearchitekturen ansieht, die dann den grundlegenden Aufbau eines Softwaresystems inklusive seiner Untersysteme beschreiben und auch die Beziehungen zwischen diesen festlegen. [2]
Die Grenzen zwischen den Abstraktionsebenen sind allerdings fließend, sodass Muster nicht zwingend genau einer Ebene zugeordnet werden können.
Beschreibung von Entwurfsmustern
Entwurfsmuster werden typischerweise durch vier grundlegende Elemente definiert [1]:
- Der Name des Musters
- Das Problem, das das Muster löst
- Eine Lösung für dieses Problem
- Konsequenzen, die sich aus der Verwendung des Musters ergeben
Der Übersicht halber werden die Muster häufig noch nach ihren Aufgaben unterteilt. Es gibt Muster, die sich mit der Erzeugung von Objekten beschäftigen, Muster, deren Aufgabe sich an der Struktur von Klassen und Objekten orientiert und solche, die Verhalten beschreiben. Im Folgenden werden diese drei Aufgabenklassen genauer beschrieben und Beispiele von entsprechenden Mustern vorgestellt.
In der Beschreibung von Entwurfsmustern kommt häufig der Begriff Klient vor. In diesem Zusammenhang meint Klient den Programmteil, der auf die Klassen und Objekte des Musters zugreifen muss.
Erzeugungsmuster
Bei Erzeugungsmustern geht es um die Erzeugung von Objekten – so viel ist klar. Aber worauf kommt es dabei an? Erzeugungsmuster bestimmen, was erzeugt wird, wer es erzeugt, wie es erzeugt wird und wann es erzeugt wird. Das wiederkehrende Leitmotiv der hier beschriebenen Muster ist, den Prozess der Erzeugung zu verstecken. So wird das System unabhängig davon, wie ein Objekt erzeugt, zusammengesetzt und repräsentiert wird. Das ist besonders dann wichtig, wenn Systeme mehr von Objektkompositionen als von Vererbung abhängen.
Ein wichtiges und obendrein gut verständliches Beispiel für ein Erzeugungsmuster ist das Muster mit dem Namen Singleton. Dieses löst das Problem, wenn es von einer bestimmten Klasse genau ein Exemplar geben soll, auf das immer wieder zugegriffen werden kann. Die Lösung: Der Klient greift dann nicht auf ein Exemplar direkt zu, sondern auf eine „Exemplar-Funktion“ der Singletonklasse, die beim ersten Zugriff das Exemplar erstellt und dann immer wieder dasselbe zurückgibt (siehe Abb.1).
Abb.1: Das Singleton-Muster [1]
Und welche Konsequenzen hat die Anwendung dieses Musters? Geht man davon aus, dass die Alternative eine statische Klasse mit globalen Variablen und Operationen ist, dann ist das Singleton-Muster ein guter Weg, die Zugriffskontrolle auf ein Exemplar einfach zu halten und den Namensraum nicht zu überfrachten. Auch lassen sich Operationen auf einem Exemplar flexibler einsetzen als Klassenoperationen und falls doch mehrere Instanzen der Klasse gebraucht werden, ist die Singleton-Lösung auch einfacher erweiterbar. Ein Nachteil ist, dass aus der Schnittstelle nicht gleich klar wird, ob die Klasse ein Singleton verwendet, worunter Übersichtlichkeit und Wartbarkeit leiden. [1,3]
Neben dem Singleton-Muster werden in [1] und [4] auch noch die folgenden, wichtigen Erzeugungsmuster vorgestellt.
- Die Abstrakte Fabrik (auch Abstract Factory oder Kit) stellt eine Schnittstelle bereit, mit der Familien voneinander abhängiger Objekt erstellt werden können, ohne ihre konkreten Klassen zu nennen.
- Der Erbauer (engl. Builder) löst die Konstruktion eines komplexen Objekts von dessen Repräsentation, sodass derselbe Konstruktionsprozess verschiedene Repräsentationen erzeugen kann.
- Die Fabrikmethode (auch virtueller Konstruktor oder engl. Factory Method) ermöglicht es einer Klasse, die Erzeugung von Objekten an Unterklassen zu delegieren.
- Der Prototyp wird zum Erzeugen neuer Objekte geklont, sodass sie seiner Art entsprechen.
Erzeugungsmuster können in Konkurrenz stehen, wenn sie ähnliche Probleme lösen, wie das Fabrikmethodenmuster und die abstrakte Fabrik in Kombination; dann muss für das jeweilige Projekt entschieden werden, welches Muster den größten Mehrwert bietet. Sie können sich aber auch ergänzen; so ergänzt zum Beispiel das Singletonmuster einen Erbauer oder Prototypen gut. [1]
Strukturmuster
Strukturmuster beschreiben die Komposition von Klassen und Objekten, um größere Strukturen zu bilden. Ein Strukturmuster kann entweder klassen- oder objektbasiert sein. Klassenbasierte Strukturmuster verwenden Vererbung, um Schnittstellen und Implementierungen zusammenzuführen. Objektbasierte Strukturmuster zeigen Mittel und Wege, Objekte zusammenzuführen, um neue Funktionalität zu gewinnen. [1]
Als Beispiel sei das Strukturmuster mit dem Namen Proxy genannt. Das Problem, das hier angegangen wird, ist die Zugriffskontrolle auf ein Objekt, weil dieses zum Beispiel in einem anderen Adressraum liegt, oder für bestimmte Zugriffe gelockt werden muss. Die Lösung, die durch ein Proxy geboten wird, ist ein vorgelagertes Stellvertreterobjekt, wie in Abb.2 zu sehen ist. Dieses nimmt Anfragen entgegen, reagiert wenn möglich selbst und leitet ansonsten die Anfragen an das eigentliche Objekt weiter, sobald dieses verfügbar ist. Konsequenzen, die sich hieraus ergeben sind Übersichtlichkeit, Speicherplatzgewinn und dass diese Lösung vielfältig verwendbar ist. [1]
Abb.2: Das Proxy-Muster [1]
Neben dem Proxy-Muster gibt es noch eine Reihe weiterer Strukturmuster, von denen die Folgenden in [1] und [4] genauer beschrieben werden:
- Der Adapter (auch Wrapper oder auf Deutsch Umwickler) ist, wie der Name schon vermuten lässt, dafür zuständig, die Schnittstelle einer Klasse so anzupassen, dass sie der vom Klienten benötigten Schnittstelle entspricht.
- Die Brücke (auch Handle, Body oder Bridge) soll eine Abstraktion von ihrer Implementierung entkoppeln – weil zum Beispiel die Implementierung von mehreren Objekten benutzt werden soll, sodass beide unabhängig voneinander geändert werden können.
- Ein Dekorierer (oder engl. Decorator) soll ein Objekt dynamisch um Zuständigkeiten erweitern, was eine flexiblere Alternative zu Unterklassenbildung darstellt.
- Die Fassade ist dazu da, eine einheitliche Schnittstelle zu einer Menge von Klassen innerhalb eines Subsystems bereitzustellen.
- Ein Fliegengewicht (engl. Flyweight) kann eingesetzt werden, um Objekte mit kleinster Granularität (wie Buchstaben in einem Editor) gemeinsam zu verwenden, um große Mengen davon effizient nutzen zu können.
- Das Kompositum (oder engl. Composite) dient dazu, Objekte mit Teil-Ganzes-Hierarchien zu Baumstrukturen zusammenzuführen, bei denen Kompositionen und einzelne Objekte einheitlich behandelt werden können.
Bei genauerer Betrachtung der Strukturmuster fällt auf, dass die Strukturen häufig relativ ähnlich aussehen. Ein- und Mehrfachvererbung bei klassenbasierten Mustern und Kompositionen bei objektbasierten Mustern sind wichtige Konzepte, die immer wieder auftauchen. Umso wichtiger ist es, den Zweck der Muster genau zu beachten, um das richtige Muster für ein Problem zu finden.
Verhaltensmuster
Verhaltensmuster befassen sich mit Algorithmen und der Zuweisung von Zuständigkeiten zu Objekten. Die Muster lenken den Fokus dabei explizit auf die Interaktion zwischen den Objekten und weg von Kontrollflüssen, weil diese zur Laufzeit häufig schwer nachvollziehbar sind.
Als Beispiel sehen wir uns das Muster mit dem Namen Iterator (oder auch Cursor) an. Dieses löst das Problem, den sequentiellen Zugriff auf die Elemente zusammengesetzter Objekte (also zum Beispiel verschiedener Listen) zu ermöglichen, ohne die zugrundeliegende Repräsentation dieses Objekts offenzulegen. Die Lösung beinhaltet das zusammengesetzte Objekt (Aggregat), das die Schnittstelle zum Erzeugen eines Iterators bietet. Sobald dieser erzeugt wurde, verwaltet er die aktuelle Position im Aggregat und kann das in der Traversierung nachfolgende Objekt ermitteln. Abb.4 zeigt die Interaktion zwischen den Klassen. [1,4]
Abb.3: Das Iterator-Muster [1]
Konsequenzen, die sich aus der Verwendung des Iterators ergeben sind, dass die Methode der Traversierung variiert werden kann, dass die Aggregatsschnittstelle vereinfacht wird und dass sogar mehrere Traversierungen parallel ermöglicht werden können. [1]
Verhaltensmuster gibt es besonders viele, die für alle möglichen Problemstellungen Lösungen anbieten. In den Büchern [1] und [4] sind die Folgenden genannt.
- Der Befehl (oder Command) soll dazu dienen, Befehle als Objekte zu kapseln. So können Operationen in einer Queue gespeichert und im Zweifel auch rückgängig gemacht werden.
- Der Beobachter (oder Observer) stellt 1-n Abhängigkeiten zwischen Objekten dar, sodass die Änderung des Zustands eines Objekts dazu führt, dass alle abhängigen Objekte benachrichtigt werden.
- Ein Besucher (engl.: Visitor) kann verwendet werden, wenn eine Objektstruktur viele Klassen von Objekten mit unterschiedlichen Schnittstellen enthält und Operationen auf diesen Objekten ausgeführt werden sollen. Der Besucher kapselt die Operationen als eigene Objekte.
- Interpreter definieren eine Grammatik für eine einfache Sprache und geben an, wie man Sätze in dieser Sprache bildet und interpretiert.
- Das Muster Memento ist dazu da, den internen Zustand eines Objekts zu erfassen und externalisieren, sodass das Objekt in diesen Zustand zurückversetzt werden kann.
- Die Schablonenmethode (engl. Template) stellt das Skelett eines Algorithmus‘ bereit, sodass einzelne Schritte von Interklassen überschrieben werden können, ohne die Struktur zu ändern.
- Das Strategie-Muster (engl. Strategy) dient dazu, Familien von Algorithmen zu definieren, jeden einzelnen zu kapseln und sie austauschbar zu machen.
- Ein Vermittler (oder Mediator) wird verwendet, wenn eine Menge von Objekten vorliegt, die in wohldefinierter, aber komplexer Weise zusammenarbeitet. Der Vermittler definiert dann ein Objekt, welches das Zusammenspiel der Objekte kapselt.
- Das Zustands-Muster (engl. State) ermöglicht es Objekten, ihr Verhalten zu ändern, wenn ihr Zustand sich ändert. Das sieht dann aus, als hätten die Objekte ihre Klasse geändert.
- Eine Zuständigkeitskette (engl.: Chain of Responsibility) ist dazu da, mehrere Objekte, die eine bestimmte Anfrage erledigen könnten, miteinander zu verketten, und die Anfrage dann die Kette entlangzuschicken, bis ein Objekt sie bearbeitet.
Als besonders wichtiges Prinzip für Verhaltensmuster stellt sich die Kapselung von Aufgaben heraus. Es fällt außerdem auf, dass die Muster sich gegenseitig gut ergänzen und verstärken und auch mit Erzeugungs- und Strukturmustern kombinierbar sind. [1]
Zusammenfassung und Buchempfehlungen
In diesem Artikel wurde erklärt, was Entwurfsmuster sind und wieso es so wichtig ist, sie zu benutzen. Es wurde eine Aufteilung der wichtigsten Entwurfsmuster nach Aufgabengebieten vorgestellt und einzelne Muster wurden als Beispiele beschrieben.
Jedem, der sich für gute Softwarearchitektur interessiert, kann ich das hier referenzierte Buch [1] als Grundlage und Nachschlagewerk und das Buch [4] als etwas humorvolleren Einstieg sehr ans Herz legen. Auch im Internet gibt es diverse Blogs und Seiten, die Entwurfsmuster beschreiben. Besonders übersichtlich erscheint mir die Seite [3]. Wer, eher ein auditiver Lerner ist, kann sich auch zunächst auf YouTube einen guten Überblick verschaffen. Ich kann hier die folgenden Kanäle empfehlen: [5,6].
Zum Schluss möchte ich noch ein Zitat aus „A Pattern Language“ von C. Alexander et al., einem der ersten Bücher zu diesem Thema aus dem Jahr 1997, anbringen, das vielleicht nicht nur Neueinsteiger wie mich dazu bringt, noch einmal einen Gedanken daran zu verschwenden, wie Muster eigentlich eingesetzt werden sollten:
Es ist möglich, Gebäude durch das lose Aneinanderreihen von Muster zu bauen. Ein so konstruiertes Gebäude stellt eine Ansammlung von Mustern dar. Es besitzt keinen inneren Zusammenhalt. Es hat keine wirkliche Substanz. Es ist aber auch möglich, Muster so zusammenzufügen, dass sich viele Muster innerhalb desselben Raums überlagern. Das Gebäude besitzt einen inneren Zusammenhalt; es besitzt viele Bedeutungen, auf kleinem Raum zusammengefasst. Durch diesen Zusammenhalt gewinnt es an Substanz. [übersetzt aus 7]
Quellen
- Gamma, R. Helm, R. Johnson, J. Vlissides: „Entwurfsmuster – Elemente wiederverwendbarer objektorientierter Software“. Addison-Wesley Verlag, 1996.
- Arndt, C. Hermanns, H. Kuchen, M. Poldner: „Working Paper No. 1 – Pest Practices in der Softwareentwicklung“ Westfälische Wilhems-Universität Münster, 2009.
- Internet: Design Pattern – Studienprojekt von Philipp Hauer [besucht am 09.08.2019] https://www.philipphauer.de/study/se/design-pattern.php
- Freeman, E. Robson, K. Sierra, B. Bates: „Entwurfsmuster von Kopf bis Fuß“. O’Reilly Verlag, 2015.
- Internet: Youtube-Kanal „moinLeude“ [besucht 09.08.2019] https://www.youtube.com/channel/UCR9ywxacKRacmq2VvBbZGZA/videos
- Internet: Youtube-Playliste „Design Patterns Video Tutorial“ [besucht 09.08.2019] https://www.youtube.com/watch?v=vNHpsC5ng_E&list=PLF206E906175C7E07
- Alexander, S. Ishikawa, M. Silverstein, M. Jacobson, I. Fiksdahl-King, S. Angel „A Pattern Language“, Oxford University Press, New York, 1977
Bildquelle: pixabay
Softwareentwicklung