Passt eine gute Lösung auf eine Vielzahl von Problemen, erspart das eine lange, anstrengende und potenziell fehlerbehaftete Suche nach dem schon bekannten Weg. In der Softwareentwicklung können Entwurfsmuster wertvolle Erfahrungen speichern und nachnutzbar machen.
Welcher Softwareentwickler kennt das nicht: Man steht vor einer größeren Programmieraufgabe, und es beschleicht einen das Gefühl, eine ähnliche Aufgabe schon einmal gelöst zu haben. Wie genau, das will einem jedoch beim besten Willen nicht mehr einfallen. Nun muss man also erneut Energie in einen eigentlich nicht neuen Entwurf stecken. Zu schade, denn wie schön wäre es gewesen, hätte man von der zuvor gemachten Erfahrung profitieren können oder noch besser: könnte man sogar aus dem Erfahrungsschatz einer ganzen Community schöpfen! In diesem Artikel geht es um diesen Erfahrungsschatz und damit um Entwurfsmuster in der objektorientierten Softwareentwicklung.
Vor etwa 80 Jahren entstanden mit der Z3 und dem Eniac die ersten programmierbaren Maschinen und bald darauf auch die ersten einfachen Programmiersprachen, später folgten die sogenannten Hochsprachen. Es entwickelten sich Programmierkonzepte wie Kontrollstrukturen, Variablen, Prozeduren und Funktionen und schließlich auch die objektorientierte Programmierung. In rasantem Tempo erweitert sich permanent die Liste der Programmiersprachen, etwa um mit technischen Entwicklungen Schritt zu halten, eine bestimmte fachliche Domäne zu adressieren oder das Programmieren zu vereinfachen.
Es mag der Eindruck entstehen, dass man sich lediglich für die richtige Programmiersprache zu entscheiden braucht, um gute Software zu schreiben. In gewisser Hinsicht stimmt das sogar, denn gute Bibliotheken und Frameworks nehmen Entwicklern unliebsame oder langweilige Tätigkeiten ab, sodass weniger menschliche Fehler passieren. Dennoch ist die Softwareentwicklung eine kreative Tätigkeit, bei der es konkrete Anforderungen auf eine abstrakte Art und Weise zu lösen gilt, sodass ein Rechner die Lösung wiederholt ausführen kann. Oft gibt es viele völlig verschiedene Wege, um Anforderungen technisch umzusetzen. Aber welcher Weg ist der richtige? Gibt es überhaupt einen richtigen Weg, oder sind alle Wege gleich gut, solange sie dasselbe Ziel erreichen?
Nun, das hängt von mehreren Faktoren ab. Für meinen Arbeitsweg mit dem Auto im Großraum Stuttgart haben sich für mich drei Routen etabliert. Bevor ich losfahre, befrage ich einen Kartendienst und wähle eine davon nach geringster voraussichtlicher Fahrtzeit aus. Diesen Vorteil (Zeitersparnis) erkaufe ich durch potenzielle Nachteile (zum Beispiel eine längere oder bergige Strecke und dadurch mehr Benzinverbrauch, eine Strecke mit mehr Ampeln und dadurch mehr Stress). Außerdem basiert meine Entscheidung auf der aktuellen Verkehrssituation. Die kann sich jedoch schnell ändern, etwa durch einen Verkehrsunfall auf meiner Strecke, und somit meine Wahl verschlechtern. Die Entscheidung für eine der Möglichkeiten lässt sich später zwar noch ändern, das zöge jedoch zusätzliche Kosten nach sich – etwa einen Umweg. Das Beispiel lässt sich auf die Softwareentwicklung übertragen:
- Jede Entwurfsentscheidung hat Vorzüge und Nachteile.
- Selbst eine gut begründete Entwurfsentscheidung kann sich in Anbetracht künftiger Änderungen als unpraktisch erweisen.
- Eine Entwurfsentscheidung lässt sich später revidieren, was jedoch zusätzliche Kosten verursacht.
Es zeigt sich, dass Probleme nicht für sich allein stehen, sondern manche davon kleinere oder größere Gemeinsamkeiten aufweisen. Das ist eine gute Nachricht für die Problemlöser, denn ähnliche Probleme lassen sich durch ähnliche Lösungen – Patterns genannt – bewältigen. Für diese Entwurfsmuster gibt es allerdings keine allgemeine Definition. Wesentlich mitgeprägt wurde der Begriff 1977 von Christopher Alexander: “Jedes Muster beschreibt ein in unserer Umwelt beständig wiederkehrendes Problem und erläutert den Kern der Lösung für dieses Problem, sodass Sie diese Lösung beliebig oft anwenden können, ohne sie jemals ein zweites Mal gleich auszuführen.” [1]
Wer jetzt glaubt, dass es in dem Zitat um Muster in der Informatik geht, irrt. Christopher Alexander war Architekt. Seine Muster bezogen sich auf Entwürfe für Gebäude oder Städte. Wenn ein Handwerker einen neuen Holzfußboden in ein Zimmer legen soll, dann folgt die Problemlösung ebenfalls einem technischen Muster: Der Untergrund ist vorzubereiten (sauber, gerade, gegebenenfalls gedämmt); die Bretter müssen einen bestimmten Abstand zur Wand haben; es gilt, die Sockelleisten zu montieren. In manchen Parametern weichen die Lösungen jedoch voneinander ab: Zimmer haben unterschiedliche Maße und Winkel; Parkett ist dicker als Laminat; Parkett lässt sich sägen, Laminat kann man schneiden; Sockelleisten können je nach Wandbeschaffenheit genagelt oder müssen verschraubt werden.
Auch in anderen Domänen gibt es Muster. In der Mode denke man an die Abendgarderobe oder Business Casual. In der Literatur kennt man die Spannungskurve für die Handlung beim Drama. Knapp 20 Jahre nach Christopher Alexanders Veröffentlichung wurde der Ansatz auf die Informatik übertragen. Die sogenannte Viererbande (Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides) verschriftlichte die Entwurfsmuster für objektorientierte Software [2]. Mittlerweile gibt es Mustersammlungen für Softwarearchitekturen, Enterprise Application Architecture [3], Enterprise Integration [4], Cloud Computing [5], Workflows [6] oder den Entwurf von APIs [7], um nur einige zu nennen. Das ist nur logisch, denn Musterkataloge sind eine Sammlung von Erfahrungen vieler Köpfe. Sie verbessern die Kommunikation unter Experten, zeigen unterschiedliche Alternativen für die Problemlösung auf, erhöhen die Flexibilität einer Lösung oder verbessern die Wiederverwendung.
Was ist ein guter Entwurf?
Aber zurück zu objektorientierten Entwurfsmustern. Objektorientierte Programmierung lebt von Abstraktionen in Form von Klassen und ihren Objekten, der Kapselung (also Information Hiding und Schnittstellen), der Vererbung, Polymorphie und Assoziationen (etwa die Aggregation). Wer diese Konzepte oder einen Teil davon verstanden hat, kann objektorientiert entwickeln. Objektorientierung allein führt jedoch noch nicht zu guten Softwareentwürfen: Das Zerlegen eines Systems ist und bleibt schwierig.
Es gilt, unheimlich viele Entwurfsentscheidungen zu treffen, die bestimmte Konsequenzen nach sich ziehen. Welche Klassen soll das System haben und in welcher Granularität? Wo und wie sollen welche Klassen instanziiert werden? Wie viele Instanzen darf es für welche Klassen geben? Welche Schnittstellen soll es geben? Welche Art von Beziehungen sollen die Klassen und Objekte zueinander haben? Wie kann man Komponenten flexibel zusammensetzen? Wie lassen sich Algorithmen leicht wiederverwenden?
Bei der Softwareentwicklung können wir uns auf eine Konstante immer verlassen: die Veränderung. Wenn Anwendungen längere Zeit in Gebrauch bleiben, dann entsteht zwangsläufig der Bedarf, sie aus verschiedensten Gründen zu ändern: Kunden wünschen eine neue Funktion, die Schnittstelle eines Drittsystems ändert sich, die neue Version einer genutzten Bibliothek verlangt ein anderes Datenformat. Die Liste lässt sich quasi beliebig verlängern. Gute Entwürfe gehen vorausschauend mit möglichen Veränderungen um und erhöhen den Grad an Wiederverwendung.
Dabei haben sich bestimmte Prinzipien herauskristallisiert, die zu guten Entwürfen führen. Das Prinzip, auf eine Schnittstelle hin zu programmieren statt auf eine konkrete Implementierung, hilft bei der Entkopplung von Klassen und führt so zu mehr Robustheit. Die Implementierung einer Schnittstelle kann man leicht austauschen, ohne die aufrufende Klasse (den Klienten) anpassen zu müssen.
Ein weiteres Prinzip ist es, Objektkomposition gegenüber der Klassenvererbung zu bevorzugen. Sowohl Komposition als auch Vererbung ermöglichen das Erweitern einer Anwendung durch neues Verhalten. Bei der Klassenvererbung überschreibt eine Kindklasse das Verhalten der Elternklasse. Das ist eine statische Änderung, die bereits zur Kompilierzeit feststeht. Die Komposition hingegen delegiert die Ausführung eines Algorithmus an ein anderes Objekt. Auf diese Weise lässt sich ein Verhalten oder sogar ein ganzer Algorithmus zur Laufzeit austauschen.
Das dritte hier zu nennende Prinzip ist das Open/Closed-Prinzip. Es besagt, dass ein Entwurf offen für Erweiterungen sein soll und geschlossen für Veränderungen. Anders ausgedrückt: Man soll das Verhalten einer Anwendung leicht erweitern können, ohne bestehenden Code verändern zu müssen. Diese und weitere Prinzipien liegen den Entwurfsmustern zugrunde.
Aber aufgepasst! Kein Entwickler soll Prinzipien und Muster exzessiv anwenden. Es ist weder nötig noch möglich, an jeder Stelle allen Prinzipien zu folgen oder ein Muster an das nächste zu reihen. Das führt nur zu sehr komplexem Code. Prinzipien muss man an manchen Stellen verletzen, Muster haben Grenzen. Ein Entwickler sollte danach streben, Prinzipien und Muster dort einzusetzen, wo zukünftige Erweiterungen zu erwarten sind. An anderen Stellen im Code, die voraussichtlich stabil bleiben, kann man mit weniger Flexibilität gut leben. Technische und fachliche Erfahrung hilft bei diesen Entscheidungen maßgeblich.
Objektorientierte Entwurfsmuster teilt man in drei Kategorien ein: Erzeugungsmuster, Verhaltensmuster und Strukturmuster. Erzeugungsmuster kümmern sich darum, neue Objekte von Klassen anzulegen. Strukturmuster beschreiben Möglichkeiten, Klassen oder Objekte geschickt so miteinander zu assoziieren, dass beispielsweise größere, aber flexible Strukturen entstehen oder gute Schnittstellen zu Klassen, Datenstrukturen oder Systemen eingeführt werden. Der Fokus von Verhaltensmustern hingegen liegt auf Algorithmen, Zuständigkeiten für und Interaktionen zwischen Objekten. Im Folgenden sehen wir uns anhand von Leitfragen beim Softwareentwurf beispielhaft Entwurfsmuster aus diesen drei Kategorien an.
Welche Klassen?
Die Frage klingt zunächst trivial, entpuppt sich bei genauerer Betrachtung aber als ein ernsthaftes Problem. Um ein einzelnes Objekt anzulegen, benutzt man den Konstruktor der Klasse und übergibt, falls nötig, die entsprechenden Parameter zur Initialisierung. Gilt es jedoch, komplexere Strukturen anzulegen, verliert man auf diese Weise an Flexibilität. Möchte man in Zukunft fallbasiert unterschiedliche Objekte anlegen, muss man unschöne Abfragen einbauen und die Anwendung verändern.
Angenommen, wir entwickeln ein Textverarbeitungsprogramm, konkret dessen Werkzeugleiste. Wir betrachten lediglich einen Teilaspekt: Wir wollen Schalter für verschiedene Zwecke hinzufügen, und es soll unterschiedliche grafische Darstellungen (Light, Dark) je nach Vorliebe des Benutzers geben. Im einfachsten Fall entsteht eine Lösung wie in Abbildung 1. Das ist erst einmal völlig in Ordnung, denn irgendwo müssen schließlich Entscheidungen getroffen werden.
Betrachten wir den initialen Entwurf durch die Brille der Erweiterbarkeit. Mögliche zukünftige Erweiterungen können die Einführung neuer Stile, neuer Knöpfe oder eines völlig anderen Layouts der Werkzeugleiste sein. In jedem Fall müssten wir unsere Hauptklasse »TextProcessingApp« anfassen. Ein besserer Entwurf wäre es, die Erstellung der Objekte in eine Fabrik auszulagern, also in eine Klasse, deren Zweck die Erstellung von Objekten ist (Abbildung 2).

Abbildung 2: Das Abstract-Factory-Muster delegiert die Erzeugung von Objekten an ein separates Fabrikobjekt. Familien von Produkten lassen sich an einer zentralen Position erstellen und neue Produktfamilien einfach einführen.
Dieses Erzeugermuster nennt sich Abstract Factory. Jeder Stil wird durch eine eigene Fabrik abgebildet, deren gemeinsame Schnittstelle eine abstrakte Klasse ist. Jede Fabrik erstellt Familien von Objekten (hier: Werkzeugleiste und zugehörige Schalter). Die Hauptklasse ist nun nicht mehr selbst dafür zuständig, die Objekte zu erzeugen. Stattdessen wird ihr eine konkrete Fabrik übergeben, die sie zum Erzeugen der Werkzeugleiste verwendet. Auf diese Weise lässt sich ein neuer Stil einfach hinzufügen, indem man eine zusätzliche Fabrik und neue Produkte (Werkzeugleiste, Button) anlegt. Ein Stil lässt sich auch zur Laufzeit austauschen: Man muss lediglich der Hauptklasse die entsprechende Fabrik zuweisen und die Werkzeugleiste neu zeichnen.
Die Hauptklasse besitzt keine statischen Abhängigkeiten mehr zu den konkreten Implementierungen der Produkte. Der Nachteil dieses Ansatzes liegt darin, dass das Einführen ganz neuer Produkte (etwa Dropdowns) zu einer Änderung der abstrakten Fabrik und aller konkreten Fabriken führt.
Welche Schnittstellen?
In einer komplexen Software gibt es viele verschiedene Arten von Schnittstellen. Objekte haben eine Schnittstelle, Systeme und Subsysteme ebenfalls. Es gibt interne und externe Schnittstellen, aktuelle und veraltete. Sehen wir uns das am Beispiel einer Art Objektschnittstelle an.
Wenn man in einer Textverarbeitung ein Dokument öffnet, dann muss es erst vollständig geladen sein, damit das Programm es anzeigen kann (Abbildung 3). Nun lässt sich Text schnell einlesen, Bilder jedoch nicht. Enthält ein Dokument mehrere große Bilder, dauert der Ladevorgang entsprechend lange, was unschön für den Benutzer ist. Besser wäre es, wenn die Software den Text sofort anzeigt und die Bilder eines nach dem anderen einfach nachlädt. Besonders elegant klappt das, wenn bereits Metadaten der Bilder vorliegen, insbesondere die Maße, sodass sich der Text dann schon korrekt formatieren lässt.
Das Strukturmuster Proxy schafft hier Abhilfe (Abbildung 4). Unser »BildProxy« ist ein Stellvertreterobjekt für das echte Bild. Proxy und Bild implementieren dieselbe Schnittstelle, der Proxy kontrolliert aber den Zugriff auf das Bild. Er verhält sich genauso wie das echte Bild, enthält Bildinformationen und kann auf diese Weise einfache Anfragen beantworten (Höhe, Breite). Er lädt das tatsächliche Bild aber nur dann vollständig, wenn es wirklich angezeigt werden soll. So kann man auch in anderen Kontexten ein Lazy Loading umsetzen und das Laden des Originals so weit wie möglich hinauszögern. Je nach Implementierung kann man vor dem Klienten sogar gänzlich verbergen, dass er nur mit dem Proxy und nicht mit dem Original kommuniziert.

Abbildung 4: Das Proxy-Muster ermöglicht Lazy Loading. Im Beispiel kontrolliert der Proxy den Zugriff auf ein großes Bild. Er liefert einfach Daten wie die Maße sofort. Das echte Bild kann später oder parallel geladen werden.
Flexible Komponenten
Manchmal steht man vor dem Problem, dass es Komponenten mit mehreren kombinierbaren Ausprägungen geben soll. Unsere Textverarbeitung soll verschiedene Komponenten wie Text, Tabellen, Bilder und geometrische Formen grafisch darstellen können. Da liegt es nahe, jeder Komponente eine Klasse zu spendieren. Wir erhalten also die Klassen »Text«, »Tabelle«, »Bild«, »Rechteck«, »Kreis« und so weiter, die alle von der abstrakten Klasse Komponente erben.
Nun kommt die Anforderung hinzu, um jede Komponente einen schönen dicken Rahmen ziehen zu können. Geben wir dem üblichen objektorientierten Reflex nach, diese Anforderung per Klassenvererbung zu lösen, stehen wir plötzlich mit doppelt so vielen Klassen da: »TextMitRahmen«, »TabelleMitRahmen«, »BildMitRahmen« und so weiter. Als wäre das nicht schon aufwendig genug, bemerkt der Kunde, dass größere Texte, Bilder oder Tabellen nicht mehr auf den Bildschirm passen, und wünscht sich eine Scrollbar für diese Fälle.
Mit der Klassenvererbung explodiert spätestens jetzt die Anzahl der Klassen (Abbildung 5): »Text«, »TextMitRahmen«, »TextMitScrollbar«, »TextMitRahmenUndScrollbar« und so weiter. Was ist, wenn jetzt noch jemand auf die Idee kommt, den Komponenten Überschriften zu geben? So kann es nicht weitergehen. Was wir hier eigentlich tun, ist, Objekten zusätzliches Verhalten in verschiedenen Kombinationen zu geben.

Abbildung 5: Die objektorientierte Herangehensweise kann zu einer inflationären Ausweitung der Klassenanzahl führen.
Das Strukturmuster Decorator Pattern löst das Problem durch Anwendung des Prinzips Komposition über Vererbung. Wie vorher sind die grafischen Komponenten Kinder einer abstrakten Klasse (Abbildung 6). Die grafischen Erweiterungen (»Rahmen«, »Scrollbar«, »Überschrift«) werden jedoch bei Bedarf als Komposition hinzugefügt. Man spricht dabei von sogenannten Dekorierern.

Abbildung 6: Das Decorator-Muster erweitert die Funktionalität einer Klasse, ohne die Methoden mithilfe von Vererbung zu überschreiben. Im Beispiel werden grafische Komponenten flexibel mit zusätzlichen Elementen wie einem Rahmen oder einer Scrollbar erweitert.
Es gibt einen abstrakten Dekorierer, ebenfalls als Kindklasse der Komponente, von dem die konkreten Dekorierer erben. Der Dekorierer hält eine Referenz auf eine grafische Komponente (echte Komponente oder Dekorierer) und kann somit sowohl deren Logik als auch die eigene dekorierende Logik ausführen. Nun kann man Dekorierer beliebig kombinieren und einer konkreten Komponente hinzufügen. So erzeugt »new Scrollbar(new Text())« einen Text mit einer Scrollbar und »new Überschrift(“Ein Bild”, new Rahmen(3, new Bild()));« ein Bild mit Überschrift und Rahmen.
Das Decorator Pattern erzeugt hier Veränderlichkeit hinsichtlich unterschiedlicher konkreter Ausprägungen der grafischen Komponenten. Es lassen sich sehr einfach neue grafische Aspekte hinzufügen, ohne das bestehende Programm anpassen zu müssen. Das ermöglicht, auf sehr elegante Art und Weise die verschiedenen Aspekte zu kombinieren, ohne dadurch eine explosionsartige Vermehrung der Klassen zu erzwingen oder komplexe Fallabfragen innerhalb der Klassen auszulösen. Die Komponenten lassen sich sogar zur Laufzeit mit zusätzlichem Verhalten erweitern.
Da die Dekorierer geschachtelt werden, muss der Entwickler die Reihenfolge im Blick haben, denn die Ausführung der Logik folgt der definierten Abfolge. Das Muster birgt außerdem Gefahren. Zum einen erben Komponenten und Dekorierer von derselben abstrakten Komponentenklasse, obwohl Dekorierer eigentlich gar keine Komponenten sind. Das muss man berücksichtigen, wenn aus bestimmten Gründen die Objektidentität eine Rolle spielen sollte. Zum anderen können viele kleine Dekoriererklassen entstehen, die sehr ähnlich aussehen, nur wenig Funktionalität enthalten und deren Objekte verschachtelt sind. Das kann für ungeübte Entwickler schwer zu verstehen sein und erschwert außerdem das Debugging.
Algorithmen wiederverwenden
Bleiben wir beim Beispiel der Textverarbeitung. Ein Dokument besteht aus Absätzen. Jeder Absatz lässt sich verschieden ausrichten: linksbündig, rechtsbündig oder zentriert. Es gibt aber noch weitere Komponenten, die man auf diese Weise formatieren kann, zum Beispiel Tabellenzellen, Kopfzeilen oder Bilder.
Naiv könnte man die Algorithmen für die Ausrichtung in eine Klasse »Absatz« stecken. Sie erhält eine Variable mit der Art der Ausrichtung und wählt abhängig davon den passenden Algorithmus aus. Das führt allerdings zu einer Codeduplizierung in den entsprechenden Klassen, etwa für Tabellenzellen und so weiter. Also erstellen wir lieber eine abstrakte Klasse »Komponente«, von der die Klassen »Absatz«, »Tabellenzelle«, »Kopfzeile« und »Bild« erben (Abbildung 7). Status und Logik der Ausrichtung liegen in der Klasse Komponente und werden von allen vier Klassen verwendet.

Abbildung 7: Die naive Lösung für die Ausrichtung sieht einfach aus, schränkt aber die Flexibilität ein. Zudem erzwingt sie komplizierte Ausnahmen, wenn weitere Anforderungen hinzukommen.
Was nach einem logischen Entwurf klingt, schränkt jedoch Flexibilität und Erweiterbarkeit ein. Um eine neue Ausrichtungsart einzuführen, den Blocksatz, müssen wir jetzt die Klasse »Komponente« ändern. Es entsteht ein neuer Ausrichtungsstatus, und es gilt, den bestehenden Algorithmus anzupassen. Das ist zwar unschön, aber machbar. Jedoch stehen bald weitere Anforderungen vor der Tür, die unseren Entwurf strapazieren wie eine von der Art der Komponente abhängige Ausrichtung, etwa bei Tabellenzellen oben linksbündig oder mittig rechtsbündig. Außerdem kommen weitere ergänzende Formatierungsarten wie Schriftart oder Aufzählung hinzu.
Es gibt jetzt also drei Familien von Algorithmen (Ausrichtung, Schriftart, Aufzählung), von denen jeweils eine herangezogen wird. In der Vererbungshierarchie müsste man nun anfangen, in den konkreten Komponenten Ausnahmen vom Standardverhalten einzurichten, indem man Methoden überschreibt: Ein Bild hat keine Schriftart, eine Tabellenzelle nutzt die genannten besonderen Ausrichtungen. Vererbung ist hier nicht der richtige Ansatz und führt zu Codeduplikaten, wenn mehrere Komponenten dieselben Ausnahmen implementieren. Außerdem muss man bei der Einführung neuer Algorithmen in den drei Familien mehrere Klassen adaptieren.
Wie finden wir einen Ausweg aus dieser Misere? Sie erinnern sich: Komposition über Vererbung! Das Verhaltensmuster Strategy ist hier der richtige Weg (Abbildung 8). Wir verlagern die Familien verwandter Algorithmen in separate Klassen. Für jede Familie gibt es eine abstrakte Oberklasse, von der die konkreten Implementierungen mit den jeweiligen Algorithmen erben. Die Komponenten besitzen nun für jede Algorithmenfamilie eine Member-Variable und rufen die Logik bei Bedarf auf.

Abbildung 8: Das Stragegy-Muster kapselt Familien von Algorithmen. Das ermöglicht, Algorithmen leicht zuzuweisen und auszutauschen, sogar zur Laufzeit. Das gezeigte Beispiel weist grafischen Elementen verschiedene Formatierungsalgorithmen zu.
Der Entwickler kann den Komponenten nun flexibel ihr Formatierungsverhalten zuweisen, die Konfiguration lässt sich sogar noch zur Laufzeit ändern. Die Auswahl eines Algorithmus erfolgt nicht mehr auf Basis von Bedingungsanweisungen, sondern über Zuweisung. Neue Algorithmen oder deren Varianten lassen sich einführen, ohne den bestehenden Code anpassen zu müssen.
Allerdings gilt es zu bedenken, dass die Algorithmen den Komponenten irgendwo zugewiesen werden müssen. Der Client-Code muss also die Algorithmen, deren Unterschiede und möglicherweise Implementierungsdetails kennen. Ein weiterer Nachteil: Die Algorithmen einer Familie teilen sich notwendigerweise eine Schnittstelle. Daher müssen einfachere Algorithmen unter Umständen eine komplexere Schnittstelle implementieren als eigentlich nötig. Beispielsweise bekommen sie Parameter übergeben, die sie gar nicht benötigen. Darüber hinaus führt das Muster zu einer erhöhten Anzahl von Objekten in der Anwendung.
Einschränkungen?
Die Muster funktionieren weitgehend unabhängig von konkreten objektorientierten Programmiersprachen, und es gibt Codebeispiele für viele Sprachen [8]. Manche der Muster, zum Beispiel Adapter oder Fassade, lassen sich auch bei prozeduralen Sprachen einsetzen.
Es gibt einige wenige Fälle, bei denen sich Muster in einer objektorientierten Sprache leichter oder schwerer umsetzen lassen. Zwei Beispiele: Das Singleton-Muster kann man in Python auf recht unterschiedliche Arten implementieren (mit einer Meta-Klasse oder den Konstruktoren »__new__« oder »__init__«), die für Ungeübte teils schwer zu lesen sind. In Java gibt es keine Mehrfachvererbung, weshalb sich das klassenbasierte Adapter-Muster nicht realisieren lässt. Das sind jedoch eher Ausnahmen.
Entwurfsmuster lassen sich universell einsetzen und in jedem Softwareprojekt nutzen, unabhängig von der Größe oder der Fachdomäne. Auch die gewählte Entwicklungsmethodik spielt keine Rolle. Ob man nun nach V-Modell oder Scrum entwickelt, beeinflusst solche codenahen Entwurfsentscheidungen nicht. Relevanter ist da schon die persönliche Qualifikation. An den Umgang mit Entwurfsmustern nicht gewohnte Entwickler werden zunächst etwas mehr Probleme haben, den Programmcode zu verstehen oder so zu erweitern, dass das Muster nicht verletzt wird. Das sollte sich jedoch nach einer Phase der Einarbeitung legen.
Aus der Praxis kommt nicht selten der Einwand, dass Entwurfsmuster zu schlechter Performance führen. Hier gilt es zu differenzieren. Einerseits ist es tatsächlich so, dass manche Muster zu einer deutlich höheren Anzahl von Objekten führen, zu zusätzlichen Klassen und Assoziationen. Das Ziel ist ja schließlich eine bessere Erweiterbarkeit und nicht bessere Performance. Andererseits gibt es viele verschiedene nicht funktionale Anforderungen, und die Geschwindigkeit steht in vielen Fällen gar nicht an erster Stelle. Eine robustere Software oder einfachere Softwarewartung sind häufig wichtiger.
In der Welt der Embedded Software kann es aber in der Tat auf Millisekunden ankommen – denken Sie an die Steuerung von Ventilen oder Gerätesteuerungen im Fahrzeug (Airbag, ABS). Die Entscheidung zwischen Flexibilität und Performance ist immer ein Trade-off, man kann nicht beides in vollem Maß bekommen. Höhere Flexibilität führt zu Geschwindigkeitseinbußen. Mit allgemeinen Aussagen zur Performance einer Lösung sollte man dennoch vorsichtig sein. Letztendlich können nur Messungen der konkreten Umsetzung von Algorithmen in einer konkreten Umgebung Unterschiede in der Laufzeit offenbaren. Alles andere ist Herumstochern im Nebel.
Wohin geht die Reise?
Wie eingangs erwähnt, wurden in den letzten Jahren neue Muster entwickelt oder entdeckt. Das liegt zum einen am beschleunigten technischen Fortschritt: Dadurch gibt es immer wieder zusätzliche Bereiche, für die Lösungen gefragt sind. Mit dem Einsatz von KI in der Softwareentwicklung und dem Aufkommen vom Quantencomputing sind bereits spannende Gebiete am Horizont zu erkennen. Zum anderen gibt es viele Experten, die gern Wissen und Erfahrungen dokumentieren und teilen möchten, nicht selten aus der wissenschaftlichen Community. Man kann also davon ausgehen, dass in Zukunft weitere Muster zum Wissensschatz hinzukommen.
Die Liste der objektorientierten Entwurfsmuster ist hingegen stabil, und das wird voraussichtlich auch so bleiben. Der Bedarf ist eher anderer Natur: Zwar gehören Entwurfsmuster zu einer guten softwaretechnischen Ausbildung, doch es gibt noch viele Entwickler, denen sie nicht geläufig sind. Der Mangel an IT-Fachkräften verstärkt diesen Effekt noch. Dadurch gibt es unter den Entwicklern immer mehr Quereinsteiger, denen bestimmte Teile der Ausbildung fehlen. Diese Tatsache und der Umstand, dass es in der Softwareentwicklung oft sehr schnell gehen muss, führen zu Entwürfen, die man im weiteren Verlauf eines Projekts oder über die Lebensdauer eines Produkts oft adaptieren muss. Das verursacht bei der Softwarewartung einen ärgerlichen, unnötigen Mehraufwand.
Schriftlich festgehaltene Entwurfsmuster gibt es schon seit fast 30 Jahren. Sie umfassen eine Sammlung von Lösungswegen, die sich für wiederholt auftretende Probleme etabliert haben. In welcher anderen Branche würde man schon freiwillig auf einen so breiten Erfahrungsschatz verzichten? Darum empfehle ich jedem Entwickler, seine Axt zu schärfen, bevor er in den Wald geht – in diesem Fall mit dem Wissen über objektorientierte Entwurfsmuster. Diese Investition zahlt sich schnell aus. (jcb/jlu)
Der Autor
Prof. Dr. Mirko Sonntag lehrt Softwaretechnik an der Hochschule Esslingen, mit den Gebieten Programmieren, Softwaretechnik, Software Testing und Advanced Software Engineering. Zusätzlich forscht er am Fraunhofer Anwendungszentrum KEIM an neuen Mobilitätskonzepten. Darüber hinaus unterstützt er die beruflichen Weiterbildung durch Schulungen im Bereich Softwaretechnik an der Technischen Akademie Esslingen (TAE).
Infos
- Christopher Alexander et al., “A Pattern Language”: https://en.wikipedia.org/wiki/A_Pattern_Language
- Erich Gamma et al., “Design Patterns”: https://en.wikipedia.org/wiki/Design_Patterns
- Martin Fowler et al., “Patterns of Enterprise Application Architecture”: https://martinfowler.com/books/eaa.html
- Gregor Hohpe et al., “Enterprise Integration Patterns”: https://ptgmedia.pearsoncmg.com/images/9780321200686/samplepages/0321200683.pdf
- Christoph Fehling et al., “Cloud Computing Patterns”: https://link.springer.com/book/10.1007/978-3-7091-1568-8
- Nick Russel et al., “Workflow Patterns: The Definitive Guide”: https://mitpress.mit.edu/9780262029827/workflow-patterns/
- Olaf Zimmermann et al., “Patterns for API Design”: https://www.oreilly.com/library/view/patterns-for-api/9780137670093/
- Beispiele: https://refactoring.guru/design-patterns/examples







