Software Entwurfsmuster
Entwurfsmuster, im englischen Design oder Software Patterns genannt, sind Lösungsschablonen für häufig auftretende Probleme in der Softwareentwicklung, insbesondere im Design einer Softwarearchitektur. Sie ermöglichen es, flexible und stabile Software zu entwickeln, indem bewährte Strategien und Ansätze generisch und wiederverwendbar definiert werden. Jedes Pattern ist dabei auf einen oder zwei Aspekte der Software spezialisiert, die unabhängig beleuchtet werden.
Wichtige Grundkonzepte
Bevor man sich im Detail mit Entwurfsmustern auseinandersetzen kann, müssen verschiedene Grundbegriffe insbesondere aus der objektorientierten Programmierung klar sein. Die wichtigsten Konzepte sind Vererbung, abstrakte und konkrete Klassen, Interfaces bzw. Schnittstellen, Polymorphie und lose Kopplung bzw. hohe Kohäsion.
- Vererbung: Vererbung
ist eines der wichtigsten Grundkonzepte der Objektorientierung. Eine
Klasse wird dabei von einer anderen Basisklasse abgeleitet und hat
dadurch Zugriff auf deren Felder und Methoden. Die abgeleitete Klasse
kann die Basisklasse um eigene Felder und Methoden erweitern oder
bestehende Methoden mit eigener Logik überschreiben
- Abstrakte Klasse: Eine
abstrakte Klasse kann nicht instanziiert werden, das heißt man kann
kein Objekt der Klasse erzeugen. Stattdessen dient sie als Rumpf für
konkrete Klassen, indem sie einen bestimmten Aufbau vorgibt oder
Methoden definiert, die auf einer konkreten Klasse vorhanden sein
müssen. Diese Methoden können bereits über eine Basisimplementierung
verfügen
- Konkrete Klasse: Eine konkrete Klasse
ist eine Klasse, die sich von einer abstrakten Klasse ableitet – sie
besitzt und erweitert gegebenenfalls ihre Felder und Methoden
- Interface bzw. Schnittstelle:
Ein Interface ist eine abstrakte Klasse, die über keinerlei
Implementierung oder Logik verfügt. Das heißt, eine Schnittstelle gibt
lediglich an, was eine Klasse, die das Interface implementiert, können
muss und wie die Methodensignaturen aussehen. Eine Methodensignatur
umfasst den Namen der Methode, ihren Rückgabewert und alle
Eingabeparameter
- Polymorphie: Polymorphie
bedeutet Vielseitigkeit und meint im Kontext der Softwareentwicklung,
dass eine Variable als Wert Objekte unterschiedlichen Datentyps annehmen
kann. Als Beispiel sei die abstrakte Klasse AbstractClass mit einer Methode DoSomething und den konkreten Klassen ClassA und ClassB betrachtet. Wird eine Variable x deklariert, auf der die Methode DoSomething aufgerufen wird, ist dies sowohl möglich, wenn x ein Objekt von ClassA oder ClassB ist. Der Datentyp ist damit austauschbar
- Lose Kopplung: Generell gilt es als guter Stil, wenn Objekte und Klassen lose gekoppelt sind – sie wissen also möglichst wenig über die konkreten Implementierungen anderer Klassen und können unabhängig voneinander geändert werden. Gleichzeitig sollten sie eine hohe Kohäsion bzw. einen starken Zusammenhang haben. Das bedeutet, das alles was in einer Klasse liegt, tatsächlich miteinander zu tun hat
Entstehung und Einteilung von Entwurfsmustern
Entwurfsmuster gehen auf die “Gang Of Four” bestehend aus Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides zurück. Diese waren die ersten, die Entwurfsmuster – die es in anderen Formen bereits gab – in einem Katalog zusammengefasst haben. Ihr Buch “Design Pattern, Elements of Reusable Object-Oriented Software” erschien 1995 und ist heute immer noch lesenswert.
In ihrem Buch unterteilen die Autoren Entwurfsmuster nach zwei Kategorien: Zum einen nach ihrem Zweck (Purpose) und zum anderen nach dem Bereich, auf den sie wirken (Scope). Wird nach dem Zweck eingeteilt, ergeben sich drei verschiedene Typen: Erzeugungsmuster (Creational Patterns), Strukturmuster (Structural Patterns) und Verhaltensmuster (Behavioral Patterns). Erzeugungsmuster beschäftigen sich mit dem Klassendesign und der Erstellung bzw. Instanziierung von Objekten, während Strukturmuster das Zusammenwirken und die Beziehungen von Klassen oder Objekten zueinander definieren. Verhaltensmuster sind die größte Gruppe von Entwurfsmustern und beschreiben, was eine bestimmte Klasse oder ein Objekt tun soll. In diese Kategorie fällt auch die Kommunikation oder das Versenden von Nachrichten und Aufrufen zwischen Objekten.
Teilt man Entwurfsmuster wiederum nach ihrem Anwendungsbereich ein, wirken sich die Muster entweder auf die Klassenstruktur oder die Objektstruktur der Software aus. Während Klassenmuster sich viel mit Vererbungshierarchien der Programmklassen beschäftigen, definieren Objektmuster die Beziehungen oder die Zusammensetzung von konkreten Objekten. Klassenmuster sind zudem zu der Zeit, zu der das Programm übersetzt wird, bereits statisch festgelegt, während Objektmuster Strukturen beschreiben, die zur Laufzeit geändert werden können.
Vor- und Nachteile von Entwurfsmustern
Entwurfsmuster bringen zwei große Vorteile mit sich: Zum einen ermöglichen sie es, einen komplexen Sachverhalt losgelöst zu betrachten, zu modellieren und darüber zu diskutieren. Das erhöht die Qualität des Designprozesses und macht die Software flexibler und besser wartbar. Zum anderen sind die Lösungen, die Entwurfsmuster bieten, erprobt. Der Entwickler, der sie anwendet, kann sich sicher sein, dass die Lösung brauchbar ist und Probleme vermeidet, die ihm vielleicht nicht einmal bewusst sind.
Trotzdem sollten Entwurfsmuster nicht blind oder überladen eingesetzt werden. Insbesondere Anfänger neigen manchmal dazu, jedes neu gelernte Muster direkt auf ihr Projekt anwenden zu wollen, obwohl das nicht immer Sinn hat. Insbesondere die Verwendung vieler Entwurfsmuster, die für den expliziten Anwendungsfall gar nicht geeignet sind, bezeichnet man als “Antipattern”.
Die wichtigsten Entwurfsmuster im Überblick
Erzeugungsmuster
Die meisten Erzeugungsmuster haben das Ziel, die Konstruktion des Objektes von seiner Repräsentation zu entkoppeln, um die Außenwelt von der Implementierung unabhängig zu machen. Zwei Ideen kehren dabei immer wieder: Zum einen das Abkapseln der Information darüber, welche Klassen ein bestimmtes System nutzt, und zum andere das Verstecken des Erstellungsprozesses selbst. Erzeugende Objektmuster delegieren einen Teil der Klassenerstellung dazu zu einem anderen Objekt, während Klassenmuster häufig mit Vererbungshierarchien oder Subklassen arbeiten.
Die fünf wichtigsten Erzeugungsmuster sind:
- Abstract Factory: Eine
abstrakte Fabrik definiert eine Schnittstelle über die ähnliche Objekte
erzeugt werden können, ohne, dass der Aufrufer die konkreten Klassen
selbst benennen können muss. Angenommen, ein Programm kann mehrere
Fahrzeuge wie PKWs und LKWs erstellen. Dann könnte eine abstrakte Klasse
erstellt werden, die grundsätzlich eine Schnittstelle zum Erstellen von
Fahrzeugen bereitstellt. Für explizite Fahrzeuge werden dann konkrete
Klassen, beispielsweise eine PKWFactory oder eine LKWFactory, abgeleitet. Diese implementieren dann spezifische Logik für den jeweiligen Fahrzeugtyp
- Factory Method: Etwas
anders funktioniert das Muster Factory Method. Hier wird eine
Klassenschnittstelle definiert mit der ein Objekt erstellt werden kann.
Da es sich lediglich um eine Methode handelt, können weitere Klassen
diese Methode überschreiben und ihren eigenen Initialisierungsprozess
implementieren. So gibt es beispielsweise die Basisklasse BaseVehicle, die eine Methode Build() zur
Verfügung stellt. Eine abgeleitete Klasse LKW überschreibt diese
Methode mit eigener Logik, beispielsweise dem Hinzufügen weiterer
Fahrzeugkomponenten. Zur Laufzeit könnten Objekte, die verschiedene
Fahrzeugtypen repräsentieren, ausgetauscht werden, da sie alle dieselbe
Signatur haben
- Builder Pattern: Das Builder
Pattern oder Erbauermuster ermöglicht es, ein komplexes Objekt durch
mehrere Schritte zu konstruieren, um so den Erstellungsprozess zu
vereinfachen und die Übersichtlichkeit zu erhöhen. So gibt es auf einer
Klasse beispielsweise mehrere Methoden BuildPartA(), BuildPartB(),
etc., die nacheinander aufgerufen werden können. So wird ein Objekt
instanziiert, das ein Subset aller möglichen Komponenten beinhaltet
- Prototype Pattern: Beim
Prototype Pattern werden Objekte einer Klasse erzeugt, indem ein
typischer Prototyp als Vorlage kopiert und angepasst wird. Dies lohnt
sich beispielsweise, wenn das Erzeugen einer Kopie des Objekts
wesentlich weniger Zeit benötigt als das Objekt mit einer Factory immer
wieder neu zu erstellen
- Singleton Pattern: Das Singleton Pattern definiert, dass es von einer Klasse im gesamten Projekt nur ein Objekt geben darf, das global verfügbar ist
Strukturmuster
Strukturmuster fassen Klassen oder Objekte zu größeren Strukturen zusammen. Auf Klassenebene spielen dabei vor allem Schnittstellen und Interfaces eine wichtige Rolle, da diese die lose Kopplung erhalten – sie entkoppeln den Zugriff auf die Klasse von ihrer Implementierung. Strukturen auf Klassenebene können zur Laufzeit nicht geändert werden, sondern sind bereits zur Übersetzungs- bzw. Kompilierzeit festgelegt. Strukturmuster auf Objektebene ordnen die erstellten Objekte in eine Struktur ein, die zur Laufzeit änderbar ist.
Zu den wichtigsten Strukturmustern zählen:
- Adapter: Ein
Adapter lässt Klassen zusammenarbeiten, deren Schnittstellen eigentlich
nicht zusammenpassen. Dazu wird die Schnittstelle der Klasse, die
benutzt werden soll, durch den Adapter so angepasst, wie der Aufrufer
sie erwartet. Der Adapter stellt also die Methode AdaptedOperation() zur Verfügung, die intern dann die Operation()-Methode der ursprüngliche Klasse aufruft. Die Funktion bleibt dabei identisch, allerdings ändert sich die Signatur
- Decorator: Ein
Decorator ist eine Alternative zum Verwendung von Unterklassen. Im
Grunde geht es ähnlich wie beim Adapter darum, die Funktion eines
anderen Objektes zu verwenden, aber für den Aufrufer zu ändern. Ein
Decorator “dekoriert” (sprich erweitert) die Funktion der Klasse mit
zusätzlicher Funktionalität. Die Signatur der Methode bleibt dabei
gleich
- Proxy: Ein Proxy (auch Vertreter oder
Surrogate genannt) definiert eine Schnittstelle zu einem anderen Objekt.
Über den Proxy werden dann unter anderem die Erzeugung des Objektes,
aber auch Teile der Steuerung, Authentifizierung oder Zugriff auf das
Objekt abgewickelt. Im Gegensatz zu Adapter und Decorator bleiben sowohl
die Definition der Schnittstelle als auch ihre Funktion weitestgehend
gleich
- Facade: Eine Fassade verbirgt die Komplexität eines Systems, indem sie eine einheitliche Schnittstelle zu mehreren Schnittstellen des unterliegenden Systems anbietet. Der Konsument oder Anwender muss die unterliegende Struktur dementsprechend nicht kennen. Das unterstützt die lose Kopplung zwischen Klassen und Objekten
Wie man sieht, gibt es viele Muster, die für recht ähnliche Anwendungsfälle genutzt werden können und subtile Unterschiede besitzen. Weitere Strukturmuster, die oft verwendet werden, sind das Bridge Pattern, das Composite Pattern oder das Flyweight Pattern.
Verhaltensmuster
Verhaltensmuster beschreiben wie Objekte oder Klassen innerhalb von komplexen Kontrollflüssen miteinander interagieren. Verhaltensmuster auf Klassenebene teilen dazu den Kontrollfluss des Programms über mehrere Klassen auf oder nutzen Vererbungshierarchien, während Objektmuster beispielsweise Kompositionen oder Zusammensetzungen von Objekten nutzen, um ein bestimmtes Verhalten zu erzielen.
Einige wichtige Verhaltensmuster sind:
- Interpreter: Interpreter
kennen die meisten – es handelt sich um die Repräsentation einer
Grammatik für eine Sprache, die es ermöglicht, klare Anweisungen zu
formulieren und zu verstehen. Insbesondere für wiederkehrende Probleme
innerhalb einer Domäne oder eines Programms kann das sinnvoll sein, denn
so lassen sich Aufrufe vereinfacht darstellen und zentralisiert ändern,
indem die Bedeutung bestimmter Komponenten der Grammatik geändert wird
- Template Method: Eine
Template Method oder Schablonenmethode wird für Verhaltensweisen mit
vielen Teilschritten verwendet. Der Algorithmus definiert ein Skelett
für die Gesamtoperation und delegiert einzelne Teilschritte dann an
Unterklassen. Dadurch ist es auch möglich, Teile des Algorithmus durch
das Überschreiben der Unterklasse zu ändern, ohne den gesamten Ablauf
anpassen zu müssen. An dieser Stelle ist beispielsweise die Polymorphie
von Objekten relevant: Indem mehrere konkrete Klassen von derselben
abstrakten Klasse abgeleitet werden, können diese zur Laufzeit beliebig
ausgetauscht werden. Eine Template Method ist damit sehr flexibel und
frei konfigurierbar
- Observer: Ein sogenannter Observer oder Beobachter definiert Abhängigkeiten zwischen Objekten unter Erhalt der losen Kopplung. Dazu kann ein Objekt beliebig viele andere Objekte “abonnieren” bzw. selbst abonniert werden. Ändert sich an einem abonnierten Objekt etwas, werden alle Abonnenten durch den Observer benachrichtigt und können darauf reagieren. Die Objekte selbst müssen dabei nicht wissen, von wem sie abonniert werden
Verhaltensmuster können bisweilen recht komplex sein, decken dafür aber einen Großteil der typischen Probleme der Softwareentwicklung ab. Insbesondere das Visitor Pattern, das Mediator Pattern und das Strategy Pattern sind Verhaltensmuster, die ebenfalls häufig angewendet werden.
Fazit
Entwurfsmuster werden zum einen in Erzeugungs-, Struktur- und Verhaltensmuster und zum anderen nach Klassen- oder Objektbezug eingeteilt. Das Ziel aller Entwurfsmuster ist es, bewährte Lösungsansätze für flexible, robuste und gut wartbare Software mit wiederverwendbaren Schablonen zu formulieren. Dadurch können komplexe Probleme besser modelliert, diskutiert und verstanden werden. Weiterhin können neue Entwickler auf bewährte Prinzipien zurückgreifen und vermeiden dadurch häufige Fehler. Einige Entwurfsmuster können auf den ersten Blick recht kompliziert wirken – oft wird die genaue Verwendung erst klar, wenn man auf einige der Probleme während des Programmierens selbst gestoßen ist und man die Muster an einem Beispiel selbst implementiert hat. Dazu gibt es viele Tutorials und Bücher, die das Verständnis für Entwurfsmuster erhöhen und den Einstieg mit umfangreichen Codebeispielen erleichtern.