Sonntag, 20. April 2008

Wartbarkeit von Programmen

Zur Zeit arbeite ich mich mal wieder in ein neues Projekt ein. Unter den Anfoderungsdokumenten fiel mir eins auf über nichtfachliche Anforderungen. Ein Punkt, der dort aufgeführt ist, ist die Wartbarkeit der zu erstellenden Software. Das genannte Kriterium ist, daß sich jemand innerhalb einer Woche so weit in den Programmcode einarbeiten können soll, daß er in der Lage ist, Änderungen vorzunehmen. Was kann das bedeuten außer nicht furchtbar unstrukturierten Code zu schreiben in einer kryptischen und nicht konsequent durchgehaltenen Architektur?

Aufteilung in autonome Komponenten

Zuerst einmal stellt sich die Frage, was der neue Mitarbeiter über das System wissen muß, damit er loslegen kann. Bei einem hinreichend komplexen System, ist es in einer Woche nicht möglich, alle Komponenten kennen zu lernen. Es muß reichen, den Gesamtzusammenhang zu kennen, um dann eine Komponente identifizieren zu können, in der die Änderung gemacht werden muß. Wenn die Komponenten klare Zuständigkeiten haben, die sich möglichst auch in ihren Namen wiederspiegeln, so ist das machbar. Dann muß die zu ändernde Komponente so gut abgeschlossen sein, daß sichergestellt ist, daß die Änderung keine Rückwirkung auf andere haben kann. Bis hier ist die Sache noch recht klar.

Programmierstil

Was schlechter Programmierstil ist, kann jeder von uns wohl erkennen. Was aber ist ein guter Stil, insbesondere in Hinsicht auf das hier betrachtete Ziel? Ich kenne Projekte, die verbieten bestimmte fortgeschrittene Möglichkeiten der verwendeten Programmiersprachen. Dies erleichtert es, Mitarbeiter in das Projekt zu bringen, die mit dieser Programmiersprache nicht so gut vertraut sind. Allerdings bedeutet es, daß der Code dann entsprechend aufgeblasen wird. Statt einer kurzen Formulierung, die für denjenigen, der den Hintergrund kennt, sehr elegant ist, wird eine Menge Code geschrieben, dessen Ziel von dem Leser mühsam entschlüsselt werden muß. Dasselbe gilt für die Verwendung von APIs. Verwendung von Konstrukten, die eine höhere Abstraktion bewirken, sind für den Kenner wesentlich einfacher zu verstehen, während der Anfänger verständnislos davor steht.

Meine persönliche Ansicht hier ist die, daß alle Methoden erlaubt seien sollten, Code so knapp und klar zu machen, wie irgend möglich. Es zahlt sich immer aus, einen höheren technischen Kenntnisstand zu erwarten. Erstens ist es billiger, die Mitarbeiter einmalig zu schulen, wie jedesmal wenn der Code gelesen wird, zusätzliche Zeit aufzuwenden. Der zweite Grund ist, daß es für Leute, die die besten Methoden, ein Problem zu lösen, kennen, stattdessen eine weniger geeignetere wählen zu müssen. Im Zweifelsfall erfinden sie dann die gewünschte Technik neu, so daß sie für andere nicht mehr mit einem Lehrbuch erkennbar ist. Oder sie suchen sich ein fortschrittlicheres Team und das allgemeine Niveau des Projekts sinkt.

Verwendete Technologie

Zuerst einmal eine klare Erkenntnis: Jede zusätzliche verwendete Technologien, bedeutet zusätzlichen Einarbeitungsaufwand. Trotzdem kommt es immer wieder vor, daß Projekte mit der Zeit für dasselbe Problem unterschiedliche Lösungen verwenden. Dies kann z.B. daran liegen, daß eine Komponente zuerst gute Dienste geleitet hat. Dann aber für eine etwas unterschiedliches Problem eine zweite verwendet wurde. Wenn diese zweite daneben auch den Anwendungsbereich der ersten mit abdeckt, so könnte die erste entfernt werden. Der Aufwand wird aber häufig gescheut.

Eine andere Frage, die man betrachten muß, ist die Verbreitung der Technologie. Wenn jemand heutzutage versuchen würde, ein Projekt mit z.B. Lisp oder Scala starten würde, müßte er sich darüber im klaren sein, daß es auf dem Markt nicht sehr viele Leute gibt, die diese Technologien beherrschen. Wenn man die entsprechenden Spezialisten hat, kann man aber unter Umständen einen deutlichen Vorteil gegenüber Konkurrenten haben, der auf eine verbreitete Sprache wie Java setzt, die auch deshalb so weit verbreitet ist, weil sie für alles zu gebrauchen ist, für kaum aber wirklich ideal ist.

Sonntag, 13. April 2008

Vergeßt nicht was war

Wenn man Programmcode nach einiger Zeit betrachtet, fragt man sich nicht nur, wie denn das jetzt funktionieren soll, sondern oft auch, warum es überhaupt da ist. Hier kann es helfen, wenn man einen Blick in die Geschichte des Codes wirft. Meist liegt der Code in einem Versionskontrollsystem und man bekommt leicht eine Übersicht über alle jemals dort abgelegten Versionen. Diese Versionen kann man dann vergleichen und feststellen, wann der betreffende Codeabschnitt erstmals auftauchte. Wenn man ganz viel Glück hat, gibt es zu jeder Version einen Kommentar, der besagt warum die Änderung durchgeführt worden ist. Man kann das Glück aber auch erzwingen, indem man es zur Pflicht macht, immer einen sinnvollen Kommentar einzufügen. Wer sich nicht daran hält, zahlt in die Kaffeekasse. Dies funktioniert übrigens nur auf diese Weise und nicht durch technische Lösungen. Wenn ein Kommentar erzwungen wird, lautet er bei vielen dann doch nur "x" oder ".".

Viele Systeme

Es gibt eine große Anzahl von Versionskontrollsystemen. Unter Unix gehörten schon immer rcs und sccs zum Lieferumfang. Später entstand dann das noch heute wohl meistgenutzte cvs daraus. Dieses wurde in letzter Zeit dann von subversion abgelöst. Warum ist subversion das bessere cvs? cvs geht davon aus, daß die Struktur der Ablage im Versionskontrollsystem der des Dateisystems entspricht. Wenn eine Datei verschoben wird, verlieft man bei cvs den Zusammenhang. Außerdem werden Löschvorgänge in cvs nicht gespeichert, während subversion Dateien, die obsolet geworden sind beim nächsten Update entfernt. Diese Eigenschaften von subversion sind extrem wichtig beim Umgang mit Java-Code, da hier die logische Struktur des Codes sich in der physikalischen Ablage im Dateisystem wiederspiegelt. Dateiverschiebungen gehören somit zum Entwicklungsalltag.

Hier sollte etwas auffallen. Wenn cvs benutzt wird, besteht immer die Gefahr, daß sich etwas lokal kompilieren läßt nur wegen Dateien, die noch an der alten Stelle herumliegen, obwohl sie eigentlich schon lange in ein anderes Paket verschoben wurden. Es ist also wichtig, immer wieder alles wegzukopieren und alle Dateien frisch auszuchecken, um nicht über solche veraltete Dateien zu stolpern (gilt auch für Microsofts VCS).

Es gibt auch eine Reihe von meist sehr teueren kommerziellen Systemen. Einige besitzen sicherlich wertvolle Möglichkeiten. Das Geld dafür lohnt aber nur, wenn gleichzeitig Geld bereit liegt, die Benutzer zu schulen, da diese Systeme in der Regel recht komplex sind. Vor allem braucht es erfahrene Administratoren.

Einfache Systeme wie cvs haben noch einen Vorteil: sie speichern in Klartext ab. Das heißt, man kann über ein Archiv mit find/grep suchen. Systeme die eine raffiniertere Ablage haben, müssen solche Suchmöglichkeiten selber implementieren.

Aus vielen Teilen wird ein Ganzes

Ein System besteht gewöhnlich aus vielen hundert Dateien. Um nicht nur die Geschichte der einzelnen Dateien verfolgen zu können, sondern des gesamten Systems, muß ein Zusammenhang hergestellt werden. Dies geschieht dadurch, daß immer dann, wenn das System gebaut wird, der verwendete Stand aller Komponenten markiert wird (Tag). Anhand dieser Markierung kann dann später immer rekonstruiert werden, wie die einzelnen Dateien aussahen, wenn eine Auslieferung des Systems erstellt wurde. Diese Markierung sollte immer über das gesamte System erfolgen, selbst wenn eine Teillieferung erfolgt. In diesem Fall müssen alle nicht auszuliefernden Komponenten in der Version ihrer letzten Auslieferung einbezogen werden. Dies klingt etwas kompliziert und ist es auch. Deshalb würde ich in der Regel empfehlen, immer nur das vollständige System zu liefern. Es ist tödlich, wenn in einer Produktionsumgebung Module laufen, die nicht jederzeit aus dem Code rekonstruiert werden können.

Verzweigungen

Ab der ersten Auslieferung geht die Entwicklung nicht mehr linear voran. Es müssen Bugs in der ausgelieferten Version behoben werden und gleichzeitig neue Anforderungen für zukünftige Versionen realisiert werden. Daher werden dann, wenn Änderungen an bereits ausgeliefertem Code durchgeführt werden müssen, Verzweigungen eingeführt. Das bedeutet, daß jetzt Änderungen nicht nur basierend auf dem aktuellen Stand gemacht werden, sondern auch aufbauend auf einem alten. Dies ist eine schwierig zu beherrschende Situation, bei der es immer wieder vorkommt, daß ein Entwickler in einem anderen Zweig arbeitet wie er meint. Daher sollten Änderungen in den Zweigen auf das nötigste begrenzt werden. Verzweigungen eignen sich überhaupt nicht dazu, die Entwicklung zu parallelisieren, indem man prophylaktisch mehrere Zweige anlegt, um sie unterschiedlichen Entwicklergruppen zur Verfügung zu stellen.

Zusammenführen von Zweigen

Änderungen in Zweigen werden meist auch in den anderen Zweigen gebraucht. Eine Zeitnahe Übernahme der Änderungen ist unbedingt zu empfehlen, denn es ist sehr zeitaufwendig, nachträglich festzustellen, welche Komponenten von einer Änderung betroffen waren. Wenn nicht nach jeder Änderung direkt die Übernahme in andere Zweige erfolgt, ist darüber penibel Buch zu führen. Hier ist sehr viel Disziplin erforderlich.

Es gibt Tools, die die Übernahme (Merge) von Code von einem in einen anderen Zweig unterstützen. Diese Tools haben alle ihre Grenzen, da die zugrundeliegenden Vergleichsalgorithmen alle auf einem Textvergleich aufbauen, bei dem meist nur ausgewählt werden kann, ob Leertext mitbetrachtet wird. Dies führt zu großen Schwierigkeiten z.B. bei Konfigurationsdateien, die die IDE im Hintergrund pflegt, aber nach dem Zufallsprinzip pflegt. Auch sind die Dateien in der Regel anders aufgebaut. Code von C-artigen Sprachen kümmert sich kaum um Zeilenumbrüche. Hier ist das Semikolon das trennende Zeichen. Bei Python wäre es aber schlecht, wenn Leertext unberücksichtigt bleibt. Hier wäre es sehr hilfreich, wenn es kontextspezifische Vergleichseditoren gäbe, die mit dem Inhalt genauso gut umgehen könnten, wie heutige Programmeditoren, die ja auch im Hintergrund die Syntax analysieren.

Erleichtern kann man sich den Codevergleich, wenn man den Code vor der Ablage immer automatisch formatiert. Leider erlauben die mir bekannten IDEs nicht, dies zu erzwingen. In der alten Unix-Zeit haben wir nie direkt die Werkzeuge aufgerufen, sondern Skripts verwendet, in denen soche Funktionen leicht eingebaut werden konnten.

Noch schwieriger wird es, wenn inzwischen ein Refactoring durchgeführt worden ist. Hier hilft es mal wieder, wenn Tests existieren. Diese sind meist wesentlich einfacher zusammenzuführen. Dann wird solange Code übernommen, bis die Tests in allen Zweigen erfolgreich sind.

Wer sich einmal bindet

Das Versionskontrollsystem wird einmal am Anfang des Projekts bestimmt. Es später zu wechseln ist nur schwer möglich, da dann meist die Historie verloren geht. Das gleiche gilt für den Umgang mit dem System. Wenn nicht von Anfang an diszipliniert gearbeitet wird, ist das später kaum zu korrigieren. Wenn es mal vergessen wurde, Zweige zusammenzuführen oder falsch verzweigt wurde, kann das kaum noch korrigiert werden. Fehlende Markierungen von Versionen sind auch nicht nachträglich zu rekonstruieren.

Es gibt wenige Dinge, die sich so schlecht später korrigieren lassen, wie die Versionsführung.


Sonntag, 6. April 2008

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.