Sonntag, 29. Juni 2008

Fernwirkungen

Immer mal wieder hat man das Problem, daß an einem Ort sich entscheidet, was an einem anderen Ort passieren muß. Rede ich in Rätseln? Gut, also ein Beispiel. Nehmen wir den SAX-Parser aus meinem letzten Beitrag. Das Starttag mit seinen Attributen bestimmt, was später daraus werden soll. Oft muß aber zuerst alles abgearbeitet, was zwischen Start- und Endetag eingeschlossen ist, ehe am Endetag die beabsichtigte Wirkung auftreten soll. Wie macht man sowas?

Befehlsmuster (Command Pattern)

Wenn man seinen Gamma im Kopf hat, wird man zuerst darauf kommen, eine Befehlsklasse zu definieren. Die Basisklasse hat nur eine Methode die meist einen Namen wie "run" oder "execute" besitzt. Der Empfänger braucht nur diese Methode zu kennen. Für alle unterschiedlichen Aktionen, die ausgeführt werden sollen, muß eine Ableitung dieser Klasse erzeugt werden. Diese wird dann bei der Behandlung des Starttags instantiiert und auf den Stack gelegt, der das Parsen durch den SAX-Parser kontrolliert. Am Endetag wird dann das Befehlsobjekt vom Stack entfernt und die Befehlsmethode aufgerufen.

Alle benötigten Werte müssen in das Befehlsobjekt hineinkopiert werden, so daß sie am Ziel verfügbar sind.

Closure

Den größten Teil des Aufwands kann man der Programmiersprache überlassen, wenn sie Closures unterstützt. Was sind Closures? Closures stammen aus der funktionalen Programmierung, finden sich in vielen Programmiersprachen wie Python, Ruby, C# 3.0 und vielleicht in der nächsten Version von Java.

Hier zunächst ein Python-Beispiel:

>>> def foo(a):

def bar(b):
return a+b

return bar

>>> x = foo(11)
>>> x(12)
23


Was passiert hier? Innerhalb von der Funktion foo wird die Funktion bar definiert und als Ergebnis von foo zurückgegeben. Das Ergebnis kann jetzt selber aufgerufen werden. Das spannende ist der Parameter von foo. Er befindet sich innerhalb des Kontextes, in dem bar definiert wird, gehört aber selber nicht zu bar. Am Ende von foo wird der Kontext vernichtet, in dem a ursprünglich definiert war. Das bedeutet aber nur, daß die Referenz von foo zu a verloren geht. Das in a enthaltene Objekt hat jetzt immer noch eine Referenz zu bar und kann so überleben. Es ist aber nicht mehr direkt erreichbar sondern in bar eingeschlossen. Daher der Name Closure. Die Runtime sorgt dafür, daß die innere Funktion Referenzen zu allen benötigten Objekten in dem Kontext erhält, in dem sie definiert worden ist.

Damit haben wir aber unser Befehlsmuster ohne eine Klassenhierarchie definieren zu müssen und ohne manuell die benötigten Werte kopieren zu müssen.

In unserem Beispiel des SAX-Parsers würden wir also eine Closure auf den Stack legen und bei der Behandlung des Endetags davon entfernen und ausführen:

saxStack.pop()()


Sonntag, 22. Juni 2008

Gezähmte Ente

Neulich habe ich ein kleines Skript geschrieben, daß über einen SAX-Parser Informationen aus einer XML-Datei in eine Objektstruktur lesen sollte, um Code generieren zu können. Dabei habe ich festgestellt, daß bei dem Handler für einen SAX-Parser einige interessante Probleme auftreten können, die sich mit dem richtigen Werkzeug sehr elegant lösen lassen. Einen Aspekt will ich heute behandeln, einen anderen in Kürze an derselben Stelle.

Ein SAX-Parser liest die XML-Datei einmal durch und löst dabei für Start- und Ende-Tags Events aus. Dadurch, daß man die Events behandelt, läßt sich extrem effizient eine XML-Datei parsen.

Ich hatte nun beim Endetag das Problem, daß ich abhängig vom Typ des Tags einmal ein Objekt an eine Liste anhängen mußte oder das andere Mal dieselben Daten einem vorher erzeugten Objekt zur weiteren Verarbeitung übergeben mußte. Der Methodenaufruf war identisch, das zweite Objekt war aber keine Liste. Ich habe die Methode bei diesem Objekt trotzdem wie bei der Liste "append" genannt und brauchte hier keine weitere Unterscheidung zu machen.

Dies konnte ich nur machen, da ich das Skript in einer Sprache schrieb, die "Duck-Typing" verwendet (in diesem Fall Python), also einen Methodenaufruf durchführt, wenn Name und die Anzahl der Parameter übereinstimmen.

Interfaces

Was für Optionen hätte ich aber gehabt, wenn ich Java (oder C#) verwendet hätte?

  • Ein neues Interface definieren mit der betreffenden Methode

Dies wäre die geeignete Vorgehensweise, wenn ich über den Code beider Klassen verfügen könnte. Dies geht hier aber nicht, da die List-Klasse zu der Bibliothek der Sprache gehört. Außerdem würden am Ende Interfaces für alle denkbaren Verwendungen bei zentralen Klassen definiert werden, was den Code auch nicht lesbarer macht.

  • Das Interface für Listen in meiner Klasse implementieren

Das habe ich in ähnlichen Fällen auch schon gemacht. Dabei entstehen eine Reihe von zusätzlichen Methoden in meiner Klasse, die nichts tun als eine "nicht implementiert"-Ausnahme zu werfen. Außerdem ist die Klasse definitiv keine Liste, doch die Deklaration sagt jetzt anderes. Kommentare sind hier dringend notwendig.

  • Neues Interface und Adapter für List-Klasse

Da ich die List-Klasse nicht ändern kann, kann ich nur einen Adapter schreiben, der das Protokoll umsetzt, also nach außen das für diese Verarbeitung definierte Interface anbietet und den Aufruf dann auf das List-Objekt umlenkt. Umständlich, aber die wohl sauberste Lösung.

Structural Typing

Genau zu dieser Zeit fand ich einen Beitrag von Frank Sommers, in dem es um "structural typing" in Scala geht.

Scala erlaubt es, einen Parameter einer Funktion so zu definieren, daß definiert wird, welche Signatur ein diesem Parameter zugewiesenes Objekt unterstützen muß. In diesem Fall würde also an der Stelle, wo das Objekt zugewiesen wird, definiert, daß alle Objekte, die eine Methode "append" anbieten (mit dem geeigneten Parameter) hier akzeptiert werden. Dies ist im Prinzip dasselbe wie beim "Duck-Typing", aber der Compiler kann die Zuweisung überprüfen.

Wenn es mit Vorsicht verwendet wird, ist dies sicher ein Feature, das es erlaubt Programme leichter zu schreiben und wohl auch verständlicher. Wenn aber zwei Klassen ein Protokoll teilen, so ist ein klassischen Interface sicher die bessere Wahl.

Anmerkung: Z.Z. hat die Scala-Implementierung wohl noch ein kleines Problem mit der Threadsicherheit (siehe).



Sonntag, 8. Juni 2008

Generisch vs. generiert

Häufig wird Code gebraucht, der in ähnlicher Form immer wieder auftaucht. Nur in Details unterscheidet er sich. Dies bringt zwei Probleme mit sich: Erstens ist es eine stumpfsinnige und aufwendige Arbeit, ihn zu schreiben und zweitens ist es sehr schwer, ihn zu pflegen, da man dazu die kleinen Unterschiede in all dem Code überblicken muß.

Für stumpfsinnige Arbeiten sind eigentlich Maschinen erfunden worden. Und so haben sich zwei Ansätze etabliert, mit dem Problem umzugehen: generischer und generierter Code.

Generischer Code

Hier wird der immer wiederkehrende Teil von dem, was den Unterschied ausmacht getrennt. Ein allgemein geschriebener Code (Framework) wird über zusätzliche Daten gesteuert. Diese Daten können in einer Datei (heutzutage meist XML) vorliegen oder werden zur Laufzeit gewonnen z.B. durch Reflection auf andere Codeteile oder durch Abfrage des Datenkatalogs einer Datenbank.

Ist das Framework hinreichend ausgereift, kommt man rasch ans Ziel, eine übersichtliche Anwendung zu bekommen. Das Problem ist aber, daß das Framework alle möglichen Spezialfälle berücksichtigen muß, so daß die Konfiguration auch häufig komplizierter ist, als sie im konkreten Fall sein müßte. Wenn Parameter konfiguriert werden müssen, die nur in anderem Zusammenhang notwendig werden, geht dadurch wieder ein Teil der erstrebten Herausstellung der Teile, die variieren, verloren.

Ansonsten wirkt das Framework wie ein Interpreter. Es gibt einen Overhead durch das Interpretieren der Steuerdaten und Probleme werden erst zur Laufzeit sichtbar.

Generierter Code

Bei der Codegenerierung wird ein Werkzeug benutzt, um den sich wiederholenden Code zu erzeugen. Dies geschieht zur Zeit der Erstellung des Codes. Zur Laufzeit verhält sich das System ähnlich wie ein händisch geschriebenes.

Codegenerierung kann im einfachsten Fall aus der Verwendung der Suche/Ersetzen-Funktion des Editors bestehen und im kompliziertesten Fall aus einem kompletten MDA-Ansatz. Je einfacher der Ansatz, desto weniger Gründe gibt es, ihn im Zweifelsfall nicht zu benutzen. Allerdings kann auch ein gut geplanter Einsatz von MDA sich auszahlen.
Eine häufige Frage ist, wie sich generierter mit manuell erstelltem Code zusammenfügt. Hier spielt die Frage eine Rolle, ob bei späteren Änderungen sich die Generierung wiederholen läßt ("Roundtrip"). Auch wenn dies nicht möglich ist, so gewinnt man bei der Erstellung des Codes häufig dennoch soviel Zeit, daß es sich lohnt. Allerdings bleibt dann das Wartungsproblem, da man dann im weiteren den generierten Code warten muß.
Am besten ist es, wenn der generierte Code und der manuell erstellte so getrennt sind, daß die Generierung unabhängig erfolgen kann. Microsoft hat einige Merkmale von C# nur in Hinblick auf dieses Problem eingeführt (z.B. partial classes).
Der Vorteil von Codegenerierung ist, da es sich um eine Art von Compilierung handelt, daß viele Fehler schon zur Zeit der Erstellung entdeckt werden können.

Hybride

Wie auch sonst gibt es zwischen dem reinen Interpreteransatz und dem Compiler Mischlösungen. Z.B. werden in Ruby on Rails die Datenbankzugriffsmethoden zur Laufzeit generiert. D.h. der Aufwand zur Interpretation fällt nur einmal an.

Beispiel Datenbankschnittstelle

Bei den meisten OR-Mapper arbeiten überwiegend interpretierend. Dadurch werden Probleme im Mapping häufig erst zur Laufzeit sichtbar, was zu einem erhöhtem Testbedarf führt. Im Prinzip könnte der Datenbankzugriffscode auch generiert werden, wenn der Generator Zugriff auf eine repräsentative Datenbankinstanz hat. Wenn der Generator funktioniert, gibt es dann keine Probleme mit Schreibfehlern bei Name von Datenbankobjekten mehr. Mit sind allerdings keine Tools bekannt, die diesen Ansatz verwenden, abgesehen kleinerer selbstgeschriebener.