Entwurfsmuster und Programmiersprachen
Ich war wohl einer der ersten Käufer des Buchs "Design Patterns" von Gamma, Helm, Johnson, Vlissides. Während der Lektüre hatte ich endlich das Gefühl, richtig zu verstehen, wie objektorientierte Programmierung funktionieren kann. Seit damals ist das Buch zu einer Art Bibel der Entwickler in aller Welt geworden und wenn das Design eines Programms erklärt wird, werden die in diesem Buch geprägten Namen für Entwurfsmuster verwendet. Was allerdings oft übersehen wird, ist, daß viele der vorgestellten Muster nur elegante Wege sind, Defizite der verwendeten Programmiersprache und der zugrundeliegenden Paradigmen zu umgehen.
Obwohl die Beispiele von Gamma et. al. ursprünglich aus C++ und manchmal Smalltalk kommen, passen die Entwurfsmuster noch besser zu Java, aber das war damals noch nicht bekannt. So sind fortgeschrittene Anwendungen von C++-Templates nicht zu finden. Das bedeutet, wir haben es hier mit einem rein objektorientierten Paradigma zu tun. Daten sind in Klassen organisiert. Methoden, die auf diese Daten zu greifen sind in diesen Klassen gekapselt. Außerdem wird von einer streng typisierten Sprache ausgegangen. Der Compiler kann statisch die Typen der Funktionsaufrufe überprüfen.
1. Beispiel: Proxy
Bei einem Proxy geht es darum, zwischen einer Sende- und einer Empfängerklasse eine dritte Klasse dazwischenzuschieben, die möglichst transparent Zusatzfunktionen realisiert, wenn eine Methode aufgerufen wird. So kann der Aufruf protokolliert werden oder über ein Kommunikationsprotokoll zu einem entfernten System weitergeleitet werden. Dazu muß die Proxy-Klasse das gesamte Interface des Empfängers implementieren. Dies können häufig sehr viele Methoden sein. Dabei entsteht meist hochgradig redundanter Code, z.B. 50 mal der Aufruf des Loggers. In Sprachen die weniger strikt typisiert sind (z.B. Smalltalk oder Ruby) gibt es für jedes Objekt eine Methode, die aufgerufen wird, wenn eine Methode mit dem eigentlich verlangten Namen nicht existiert. Hier kann die Funktionalität an genau einer Stelle implementiert werden.
In Ruby würde das z.B. so aussehen:
class Myproxy
def initialize(obj)
@obj = obj
end
def method_missing (symbol, *args)
puts "Called " + symbol.to_s + " with parameters " + args.join(", ")
@obj.send symbol, *args
end
end
Wenn MyProxy mit einem beliebigen Objekt initialisiert wird, wird jeder Aufruf an MyProxy zuerst ausgegeben und dann an das ursprüngliche Objekt weitergeleitet.
Dies ist ein großer Gewinn an Les- und Wartbarkeit im Vergleich zu einer Java- oder C#-Lösung. Das gleiche gilt für die meisten anderen von Gamma "Structural Patterns" genannten Entwurfsmuster (Adapter, Decorator, Facade ...).
Natürlich läßt sich sowas auch durch Reflection simulieren. Dies sieht aber nicht sehr schön aus und vernichtet die Vorteile der Typsicherheit. Das heißt, ich bekomme das Schlechte von beiden Welten.
2. Beispiel: Iterator
Die Bedeutung das Iterator-Patterns ist bereits stark zurückgegangen dadurch, daß es heute in den aktuellen Versionen von Java, C# und vielen anderen Sprachen spezielle Schleifen gibt (foreach), bei denen der Iterator durch den Compiler verwaltet wird und für den Programmierer unsichtbar bleibt.
Eine andere Möglichkeit ergibt sich, wenn die Programmiersprache Closures unterstützt (Code-Blöcke in Smalltalk und Ruby, Delegates ode neuerdings Lamdas in C#). Dann kann man Methoden auf Collection-Objekten nutzen, die ein Closure als Argument nehmen und dieses dann auf alle Elemente der Collection anwenden. Auch hier bleibt der Iterator unsichtbar. Nur muß der Compiler davon nichts wissen, sondern die Implementierung ist allein eine Sache der Collection.
3. Beispiel: Template Method
Ein Ablauf steht fest, aber was in den einzelnen Schritte zu tun ist, variiert. Das Entwurfsmuster schlägt vor, daß eine abstrakte Klasse eine konkrete Methode mit dem Ablauf implementiert und die einzelnen Schritte als abstrakte Methoden, die dann in unterschiedlichen konkreten Klassen implementiert werden können. Diese recht komplizierte und unflexible Klassenhierarchie kann wegfallen, wenn man stattdessen der Ablaufmethode die Methoden der einzelnen Schritte als Parameter übergeben kann.
Das häufig eine Notwendigkeit besteht, eine Funktion als Parameter zu übergeben, sieht man an den zahlreichen Entwurfsmustern aus dem Bereich "Behavioral Pattern", bei den Klassen vorkommen, die nur eine Methode mit Namen wie "execute" oder "run" besitzen. Das ist nur eine etwas unschönere Schreibweise für die Verwendung einer Funktion als Parameter. Aber die Schreibweise macht den Unterschied, denn Code wird bekanntlich häufiger gelesen wie geschrieben und je besser ein Muster versteckt ist, desto schlechter ist es zu erkennen.
4. Beispiel: Visitor
Dieses Muster ist das am meisten mißverstandene aller Muster aus dem Buch. Dies liegt zuerst an dem Namen. Wie auch erklärt wird, implementiert es etwas, das "Double Dispatch" genannt wird. Unter diesem Namen ist es auch bei Ken Beck in seinem Buch "Smalltalk Best Practices" zu finden. Worum geht es? Normalerweise hängt der ausgeführte Code von dem Methodennamen und dem Typ des Empfängers ab. Beim Visitorpattern wird auch nach dem Typ des (ersten) Parameters der Methode unterschieden. Dazu braucht es eine zwischengeschaltete Klasse (Reflektor), die die erste Klassenhierarchie abbildet und dann den Aufruf weiterleitet, so daß die zweite Klassenhierarchie zum Tragen kommt.
Die Notwendigkeit für dieses Musters ergibt sich aus der Kapselung der Methoden in den Klassen. Im Common Lisp Object System (CLOS) existieren die "generic functions" außerhalb der Klassen. Hier ist es möglich zwei und mehr Parameter zu haben, deren Typ bei der Auswahl der Methode ausgewertet wird.
Zukunft der Sprachen
Was wir zur Zeit sehen, ist eine Fusion der Paradigmen in den modernen Programmiersprachen. C# ist in Version 3.0 stark zur funktionalen Sprache mutiert. Das LINQ-System ist rein funktional. Auch Java denkt über Closures nach. Sprachen wie Python und Ruby verwenden schon längst das beste aus allen Welten. Damit werden die Sprachen ausdrucksfähiger, da die Intention des Programmierers nicht mehr unter den Entwurfsmustern verschwindet. Andererseits setzen diese Sprachen auch besser ausgebildete Entwickler voraus, die souverän mit all den Paradigmen umgehen können. Für Leute, die in allen Sprachen nur Funktionsaufrufe, einfache Schleifen und if-Entscheidungen verwenden, ist dann kein Platz mehr.
Keine Kommentare:
Kommentar veröffentlichen