Sonntag, 14. Dezember 2008

Ein ungleiches Paar

Viele meinen, daß objekorientierte Programme und relationale Datenbanken nicht gut zusammenpassen. Da aber relationale Datenbanken da sind und die objektorientierte Programmierung auch der Standardansatz der letzten zehn Jahre ist, müssen sie irgendwie miteinander auskommen. Ich will hier ein paar Gedanken über die Probleme dabei beschreiben und auch ein wenig erläutern, warum ich der Meinung bin, daß es in den meisten Fällen derzeit keinen guten Grund zur Scheidung gibt.

Datentypen

Eins der wohl lästigsten Probleme sind die elementaren Datentypen. Insbesondere die numerischen Typen verhalten sich in den gängigen Programmiersprachen deutlich anders wie die in den üblichen relationnalen Datenbanken. Während es in den Programmiersprachen meist zwei oder drei ganzzahlige Typen mit unterschiedlichem Werteumfang gibt, können in den Datenbanken die ganzzahligen Werte bis zu einer herstellerabhängigen Obergrenze jede Länge besitzen. Die Obergrenze des Zahlenbereichs ist bei Programmiersprachen meist vom binären System bestimmt, in Datenbanken aber vom dezimalen. Da den kompatiblen Datentyp bei einer streng typisierten Sprache wie Java oder C# zu finden, erfordert einige Rechnerei. Einiges an Schwierigkeiten kann man sich auch einhandeln, wenn man in der Datenbank z.B. für Schlüssel unsinnig breite numerische Felder definiert, die dann in kein Standardganzzahltyp der Programmiersprache mehr passen.
Bei Gleitzahlwerten ist der Unterschied nicht ganz so gravierend. Aber man sollte beachten, daß die Datenbanken diese mit einer definierten Anzahl von dezimalen Nachkommastellen speichern. Diese eignen sich zum Speichern auch etwas besser wie die Fließkommazahlen, die für numerische Berechnungen optimiert sind, wie sie im Programm aber nicht in der Datenbank stattfinden.

Nullwerte

In Datenbanken können Felder als optional definiert werden. Sie können dann auch den Wert "Null" besitzen, also leer sein. Objektorientierte Sprachen haben meist Probleme mit solchen Werten umzugehen. Leicht bekommt man dann eine "NullPointerException" oder ähnliches. Das bedeutet, daß der Code mit großen Mengen von Sicherheitsabfragen gespickt ist, die ihn unübersichtlich machen. Wer einmal mit den ado.net datasets in der Version 1.0 zu tun hatte, kennt eine in dieser Beziehung besonders unelegante Lösung.
In funktionalen Sprachen kennt man die maybe-Monade. Sie bietet eine besonders elegante Lösung mit Null-Werten umzugehen. Z.B. Scala implementiert dieses Konzept mit Option.

Ressourcen

Die Kommunikation mit der Datenbank benötigt Ressourcen, die alloziert und wieder freigegeben werden wollen. Dies sind insbesondere Datenbankverbindungen und Cursoren. Für Entwickler, die sich daran gewöhnt haben, daß moderne Programmiersprachen sie von diesen Problemen bezüglich Speicherverwaltung entlasten, ist das eine ungewohnte Erfahrung. Man sollte darauf achten, daß die Anwendung diese Ressourcen immer in derselben Umgebung freigibt, wo sie sie alloziert hat. In C# hilft hier das using-Konstrukt, was in Python jetzt als with übernommen wurde. In C++ hilft der alte Trick, die Allozierung in einem Konstruktor vorzunehmen und im korrespondierenden Destruktor die Freigabe. Wird die Klasse als lokale Variable instanziert, erfolgt die Freigabe automatisch, wenn der Scope verlassen wird.

Transaktionen

Einer der stärksten Punkte der Datenbanken sind Transaktionen. Zusammenhängende Änderungen werden nur zusammen sichtbar. Wenn während einer Reihe von Änderungen ein Fehler auftritt, kann der alte Zustand sofort wieder hergestellt werden (rollback). Allerdings haben viele Leute am Anfang Probleme mit der Handhabung von Transaktionen. Wenn sie nicht sorgfältig implementiert sind, kann es hier zu vielen schwer zu verfolgenden Fehlern kommen. Eine große Hilfe hier ist es, wenn die Transaktionsbehandlung deklarativ erfolgt, wie es z.B. bie JEE der Fall ist.
Transaktionen sind aber nicht nur etwas trickreich in der Anwendung, sie sind auch der wohl beste Ansatz, um mit bestimmten Problemen der Nebenläufigkeit umzugehen. Das ist nämlich der Grund, warum Datenbanken das Transaktionskonzept eingeführt haben. Viele Anwender greifen parallel auf dieselben Daten zu. Weil das so gut funktioniert, wird in letzter Zeit auch häufiger diskutiert, Transaktionen auch auf andere Daten anzuwenden (transactional memory).

SQL

Der Zugriff auf relationale Daten erfolgt heute beinahe ausschließlich mit SQL. Ingres hatte zu Beginn eine andere Sprache namens QUEL, die z.B. Date in seinem Standardwerk "Database Systems" als SQL überlegen dargestellt hat, aber die Frage ist jetzt entschieden. Das besondere an SQL ist, daß man nicht beschreiben muß, wie der Computer etwas machen soll, sondern nur, was man haben will. Das ist vielen unheimlich, die nur an prozedurale Sprachen gewöhnt sind. In Wahrheit ist es aber viel flexibler und ausdrucksstärker als jede prozedurale Lösung. Ich kenne Fälle, wo 1000 Zeilen prozeduraler Code durch 40 Zeilen SQL ersetzt werden konnten. Im Gegensatz zu der prozeduralen Lösung war die SQL-Lösung sogar wartbar, zumindest für Leute, die die Sprache beherrschen. Da SQL völlig entkoppelt ist von der endgültigen physikalischen Speicherform, kann man in der Datenbank eine Menge Optimierungen vornehmen, die die Reaktionsgeschwindigkeit der Datenbank dramatisch verbessern, ohne das der SQL-Code angefaßt werden müßte.
Leider versuchen eine ganze Menge von Frameworks SQL zu verstecken. Das führt dann dazu, daß man gegen eine weniger ausdrucksvolle API programmiert und das Framework das dann in SQL umsetzt. Dies geschieht genauso in Hibenate wie auch in LINQ. Da ich ziemlich viel Erfahrung mit SQL habe, habe ich immer eine Abneigung dagegen, eine Zwischenschicht mit List und Tücke dazu zu bringen, das geeignete SQL zu produzieren. Insgesamt macht die Zwischenschicht das optimieren des Datenbankzugriffs schwieriger und machmal unmöglich (Stichwort: hints). Ich weiß, daß man in Hibernate auch direkt SQL verwenden kann. Die Anwendung wird aber nicht wartbarer dadurch, daß man mehrere unterschiedliche Zugriffsarten gleichzeitig verwendet (bei Hibernate: HQL, Criteria und SQL).
Ein Problem mit SQL ist, daß es erst zur Laufzeit der Programms ausgewertet wird und dazu noch von einem anderen System (der Datenbank). Hier sind Unit-Tests unbedingt notwendig, um ungültiges SQL zu vermeiden. Erfreulich wäre auch, wenn es Editoren gäbe, die mit eingebettetem SQL umgegehn könnten und Code-Vervollständigung und -Prüfung bieten würden und vielleicht sogar Unterstützung für Refactoring. Man wird ja noch träumen dürfen.

Datenübergabe

Die aus der Datenbank bezogenen Daten werden über einen sogenannten Cursor zurückgegeben. Der Zugriff darauf entspricht im wesentlichen einer Queue. Man holt sich einen Datensatz nach dem anderen. Für die Weiterverarbeitung ist aber meist eine Übernahme in andere Datenstrukturen wie Listen, Hashes oder Arrays günstiger. Wenn man alles in so eine Struktur auf einmal kopieren will, so hat man das Problem, daß das viel Speicher und viel Zeit kosten kann. Besser ist es mit "faulen" Datenstrukturen zu arbeiten, die intern erst die Daten laden, wenn sie benötigt werden. Sprachen, die sich an der funktionalen Programmierung orientieren haben dieses Konzept bereits integriert.

Architekturschichten

Eins der größten Probleme mit dem Datenbankzugriff sind die sich widersprechenden architekturellen Ziele, die es zu beachten gilt. Einmal möchte man die ganzen technologischen Aspekte, die ich bisher geschildert habe, in eine separate Schicht unterbringen. Zum anderen scheint es nahezuliegen, ein Objekt selber dafür verantwortlich zu machen, sich zu speichern und zu laden (z.B. durch Konstruktor). Die letzte Betrachtungsweise führt zu einem Muster, das Fowler als "Active Record" bezeichnet. Es ist z.B. in der Datenbankschicht von Ruby on Rails verwendet worden. Das funktioniert aber in komplizierteren Fällen nicht, da die Anforderungen an ein Objektmodell etwas andere sind wie die eines relationalen Datenmodells. Die Unterschiede zu erläutern würde heute hier zu weit führen und ich hebe sie mir für ein andermal auf. Jedenfalls wir dann eine Schicht benötigt, die die Abbildung zwischen relationalem und Objektmodell implementiert.

Lebenszyklen

Datenbanken lassen sich wesentlich einfacher ändern wie die auf sie zugreifenden Programme. Damit können diese Programme aber sehr leicht funktionsunfähig werden. In der Datenbank reicht ein einfacher "alter .."-Befehl, der - vorausgesetzt man besitzt die Rechte - jederzeit ausgeführt werden kann. Die Anwendung muß geändert, kompiliert und installiert werden. Häufig ist daher auch zu beobachten, daß Teile des Systems, die weniger stabil sind, in die Datenbank ausgelagert werden (z.B. als Stored Procedures). Das halte ich nicht für eine besonders gute Idee, wenn andere Designaspekte dagegen sprechen.
Andererseits sind die Datenbanken insgesamt wesentlich langlebiger wie die Anwendungen, die darauf zugreifen.

Anwendungsübergreifend

Viele Entwickler wünschen sich, daß statt relationalen Datenbanken objektorientiere Datenbanken eingeführt würden. Es gibt einige Gründe, warum das bislang nicht geschehen ist. Da sind sicher zuerst einmal die großen Investitionen, die in die bestehenden Datenbanken geflossen sind. Dazu kommt aber auch, daß das Objektmodell einer Anwendung nicht unbedingt kompatibel sein muß zu dem einer anderen, die auch auf dieselben Daten zugreift. In der Datenbank wird statisches Wissen abgelegt. Objekte sollen aber auch miteinander über Methoden agieren können. Für die Datenmodellierung spielt nur eine Rolle, was abgelegt, für die Objektmodellierung auch wie damit umgegangen werden soll. Wenn eine Datenbank nur einer einzelnen Anwendung dient, mag eine objektorientierte Datenbank eine gute Lösung sein. Für einen Datenbestand, der seinen Wert unabhängig von den Anwendungen hat, ist die relationale Darstellung immer noch die beste weil flexibelste und gleichzeitig stabilste. Das ist insbesondere in Unternehmensanwendung fast immer der Fall.

Montag, 1. Dezember 2008

Die Linke weiß, was die Rechte tut

Neulich habe ich bereits über IT-architektonischen Aspekte der Datensicherheit geschrieben. Diesmal will ich noch einen weiteren Aspekt ergänzen.

Gerade als Softwareentwickler erlebt man heutzutage immer wieder kuriose Auswüchse der Datensicherheit: "Ihr Software arbeitet fehlerhaft. Aus Datenschutzgründen kann ich Ihnen nichts genaueres sagen, aber ich erwarte, daß der Fehler bis morgen behoben ist." Dabei ist das meiner Ansicht nach größte Problem ganz wo anders.

Wer mit vertraulichen Daten umgeht, muß sich verpflichten, die geltenden Datenschutzbestimmungen einzuhalten. Wenn die Kenntnis dieser Bestimmungen abgefragt werden, so kommen dann Fragen vor wie folgende: "Eine Kollegin ruft sie an und will die private Telefonnummer eines Kunden wissen. Geben sie sie ihr?" Da der Mechanismus dieser Fragen leicht zu durchschauen ist, antwortet man sofort mit "nein". Was ist aber, wenn ich die Nummer selber gut gebrauchen könnte für etwas was nicht mit der Aufgabe zu tun hat, die mir Zugang zu den Daten gestattet. Es ist nicht zu erwarten, daß meine eine Gehirnhälfte diese Nummer kennt, die andere sie aber nicht benutzt.

Wenn also jemand vertrauliche Daten benutzt, so muß das nicht daran liegen, daß er Sicherheitslücken ausnutzt, er kann auch ganz legal, aber in anderem Zusammenhang, Zugang gehabt haben. Derartige Kompetenzüberschneidungen geschehen immer wieder fahrlässig. Es ist aber durchaus vorzustellen, daß jemand dies bewußt nutzt.

Kritisch kann so etwas auch werden, wenn ein Dienstleister Aufträge von verschiedenen Firmen gleichzeitig entgegennimmt. Wenn dasselbe Callcenter für konkurrierende Firmen arbeitet, so ist es wahrscheinlich, daß die Kundendaten der einen Firma benutzt werden, um Kunden für die andere zu werben.

Sonntag, 23. November 2008

Zielgruppen für Softwaretools

Hersteller von Produkten und insbesondere diejenigen die dafür werben, denken in Zielgruppen. So ist es offensichtlich, daß der typische Leser der ADAC Motorwelt sich für Treppenlifte interessiert. Werbung im Fernsehen muß hingegen die 19- bis 49-jährigen ansprechen, die die Zeit für Medien heutzutage eher im Internet verbringen. Genauso wird kommerzielle Software auch für bestimmte Zielgruppen entwickelt und angepriesen.

Entwickler sind ein zu vermeidendes Übel

Manager mit dieser Einstellung sind immer schon ein beliebtes Ziel von Toolherstellern. Bereits vor 20 Jahren wurden CASE-Tools mit dem Anspruch vertrieben, daß die Fachleute für die Geschäftsprozesse diese direkt eingeben könnten, ohne das ein "Entwickler" von Nöten sei. Bei diesen Tools meine ich immer zu merken, daß das Hauptziel bei der Konzeption war, es dem Vertriebsmitarbeiter zu ermöglichen, innerhalb einer halben Stunde eine Adreßverwaltung zusammenzuklicken, so daß die Manager, den dies vorgeführt wird, den Eindruck haben, sie könnten das auch.

Leider sind all diese Tools nicht annähernd so allgemein einsetzbar, wie die Phantasie der Nutzer potentieller Anwendungen reicht. Sobald aber etwas gefordert wird, daß die Toolentwickler noch nicht berücksichtigt haben, so sind komplizierte und schlecht wartbare Hacks an der Tagesordnung, die die vorgesehene Funktionsweise des Tools umgehen und die erst die vom Kunden gewünschte Funktionalität möglich machen.

Häufig sind derartige Tools auch schlecht in den Entwicklungsprozeß einzubinden. Wiederkehrende Aufgaben lassen sich nicht automatisieren, die Ergebnisse lassen sich schlecht in ein Konfigurationsmanagementsystem speichern und automatisierte Modultests sind auch nicht vorgesehen.

Die Behauptung, daß Programmierung nicht notwendig sei, erweist sich als Taschenspielertrick. Statt in einer Textdatei mit einem in einer Programmiersprache geschriebenen Programm ist dieselbe Information in einer Vielzahl von Menüs verstreut, so daß man rasch die Übersicht verliert, wo man was angepaßt hat. Das kann man schon ausprobieren, wenn man rasch mal eine "einfache" Access-Anwendung zusammenklickt.

Turings Fluch

Eine Programmiersprache nennt sich Turing-vollständig, wenn man damit alle denkbaren Programmieraufgaben lösen kann. Dazu braucht es nicht viel: Logische Verzweigung und Schleife reichen im Prinzip schon. Es gibt daher nur wenige Beispiele für Sprachen, die nicht Turing-vollständig sind. Egal, welches Werkzeug verwendet wird, als Entwickler kann man seinem Manager selten mit gutem Gewissen sagen, daß etwas damit nicht möglich ist. Man kann Texteditoren mit der C++-Template-Sprache schreiben, Datenbanksysteme mit Excel-VBA und Schachprogramme mit XSLT. Diese Sprache sind natürlich alle für etwas anderes gedacht und am Ende werden die Programme weder wartbar noch effizient sein, aber prinzipiell möglich sind sie doch.

Manchmal geht die Sache auch schleichend. BPEL ist dazu gedacht, Webservices zu einem Geschäftsprozeß zusammenzufassen, nicht dazu komplexe Algorithmen zu implementieren. Aber wenn man erst einmal eine Schleife und eine Verzweigung eingebaut hat, so gibt es kein klares Haltesignal, bis man doch den Algorithmus zusammengebastelt hat. Das Ergebnis ist dann sehr unübersichtlich, aber es ist da und läuft und es Bedarf einer mutigen Entscheidung, die Anwendung so umzubauen, daß dieser Algorithmus in einer geeigneteren Sprachen neu implementiert wird und das BPEL-Programm wieder wartbar wird.

Marketing-Nebel

Manchmal werden an sich nützliche Dinge durch das Marketing so verzerrt, daß sie nur noch für das sehr geübte Augen zu erkennen sind. Ein Beispiel ist das bereits letztes Mal erwähnte LINQ von Microsoft. Microsoft hat eine lange Geschichte von vergeblichen Ansätzen, einen eigenen O/R-Mapper herzustellen. Über Risiken und Nebenwirkungen solcher Tools könnte ich gelegentlich mal einen eigenen Beitrag schreiben, aber bei der Java-Konkurrenz haben sich diese Tools spätestens seit Hibernate etabliert. Also wurde LINQ als die ultimative Lösung für die Datenbankeinbindung in C#-Programme verkauft. Realisiert ist das ganze in einem Mix aus Spracherweiterungen und Bibliotheken. Ob der ursprüngliche Zweck wirklich viele überzeugt, wird die Zeit zeigen. Tatsache ist, daß einige nützliche Dinge dabei entstanden sind. Das attraktivste ist wohl die sogenannte "List-Comprehension". Diese ist bei Haskell- und Python-Programmierern seit langem bekannt und beliebt. Hier wurde aber eine Syntax gewählt, die vor allem Datenbankentwickler ansprechen soll, da sie an SQL angelehnt ist. Zum einen paßt diese Syntax recht schlecht in die übrige von C#, das doch eher von dem mathematisch knappen Stil von C beeinflußt ist als von dem an der natürlichen Sprache angelehnten von SQL. Zum anderen werden viele Entwickler übersehen, welche Möglichkeit hier versteckt sind.

Alternative Zielgruppe

Wahrscheinlich würde einiges besser, wenn die Entscheidungen für Softwaretools mehr von denjenigen gemacht würden, die sie auch benutzen müssen. Dies müßte dann aber auch zu den Anbietern durchdringen. Derzeit sieht es so aus, daß ein großer Teil der kommerziellen Softwaretools mit Blick auf Manager als Entscheidungsträger hin entwickelt und vertrieben werden. Besser sieht es manchmal bei Open Source Programmen aus, auch wenn hier immer mehr einen kommerziellen Hintergrund haben, der zu den gleichen Auswüchsen führt.

Sonntag, 16. November 2008

Eine für Alles, Alles in Einer

Wie regelmäßige Leser meines kleinen Blogs sicher schon festgestellt haben, ist eines der Gebiete, für die ich mich interessiere, das der Programmiersprachen. Welche Möglichkeiten gibt es in den verschiedenen Programmiersprachen und wozu kann man sie sinnvoll einsetzen?

Diesmal geht es mir um das Wachstum von Sprachen. Was man immer wieder sehen kann, ist, daß eine Sprache klein und einfach zu erlernen anfängt und dann immer weitere Bereiche gefunden werden, wo es doch so schön wäre, wenn man die Sprache ein klein wenig erweitern würde, damit dieses oder jenes vereinfacht würde. Es gibt natürlich auch die Sprachen, die gleich ganz groß angefangen haben: ADA und PL/1. Die erste wird von kaum jemandem freiwillig benutzt. Sie ist aber Voraussetzung in Ausschreibungen von militärischen Anwendungen. Wenn man sie mal begriffen hat, soll sie gar nicht so schlecht sein. Die zweite sollte mal Cobol ablösen. Sie hat aber die meisten Entwickler von Anfang an abgeschreckt und ist rasch in einer Nische verschwunden.

Sprachwachstum

Auch Java ist im Laufe der Zeit gewachsen und in den Diskussionsrunden der Gemeinde der Java-Entwickler bemerkt man die Wachstumsschmerzen. Allerdings ist Java im Wachstum gehemmt, da es lange dauert, bis ein JCP sich durchgesetzt hat. Anders ist das bei C#. Hier steht kein kompliziertes Gremium dahinter sondern die Entwickler der Sprache werden nur durch die Marketingabteilung getrieben. Eigentlich mag ich die Sprache. Sie ist zwar Java sehr ähnlich, konnte aber schon zu Beginn von einigen Jahren Erfahrung mit Java profitieren und einiges besser machen. Allerdings sind einige neue Ideen zuerst nur halbherzig implementiert worden und erst im Laufe der Zeit brauchbar geworden. Wie in Java haben es die Entwickler nicht hinbekommen, echte Closures in der ersten Version zu implementieren. Während in Java bis zur jetzigen Version nichts in dieser Richtung da war, hatte C# zu Beginn eine halbherzige Lösung namens delegate. Diese wurde erst in der zweiten Version brauchbar und in der dritten richtig rund.

Daneben gab es in der dritten Auflage der Sprache ein Feature namens LINQ. Dazu wurde die Sprache so verbogen, daß funktionale Konzepte wie Monaden darin untergebracht worden konnten. Die Prototypen wurden in F#, einer funktionalen Sprache für das dotnet Framework, entwickelt und dann zu C# migriert. In meinen Augen hat LINQ bislang nicht viel Gutes gebracht. Es hat die Sprache aufgeblasen, verhindert aber die Portabilität von Lösungen, die darauf aufbauen. Die Datenbankanbindung funktioniert nur für SqlServer. Für Oracle gibt es keinen Support und wohl auch keine wirkliche Nachfrage. Noch schlimmer ist, daß Microsoft wegen LINQ die Unterstützung von XQuery in seinen Produkten nicht weiterentwickelt hat. XPath und XSL sind auf der Version 1 stehengeblieben, während der Rest der Welt beginnt, sich an den neuen Möglichkeiten der Version 2 zu erfreuen. Jetzt droht die vierte Auflage. In seinem Blog zeigt sich der erfahrene C#-Trainer Frank Quednau einerseits erfreut über die neuen Möglichkeiten, andererseits bezweifelt er, daß die Sprache dann noch in einem einwöchigen Kurs unterrichtet werden kann. Das ist für mich aber ein ganz großes Problem. Das bedeutet, daß es dann schwer sein wird, in einem größeren Projekt genügend Leute zu finden, die die Sprache vollständig beherrschen. Dann beginnen wieder die projektinternen Vorschriften, welche Sprachkonstrukte nicht benutzt werden dürfen. Wir kennen das von C++. Dies ist für die fortgeschrittenen Programmierer frustrierend, da sie sich gezwungen sehen, in ihren Augen schlechteren Code zu schreiben, nur damit das durchschnittliche Teammitglied eine Chance hat, diesen zu verstehen. Allerdings wird dieser Code dann auch nicht gerade schöner, denn wie das Gehirn nun mal arbeitet, werden dann nicht Lösungen implementiert, die zu den eingeschränkten Möglichkeiten passen, sondern Rückportierungen der neuen Konstrukte.

Mehrsprachlichkeit

Gerade bei dotnet ist es eigentlich überhaupt nicht notwendig, alles einer Sprache aufzubürden. Die Grundidee war eigentlich, daß sich verschiedene Sprachen eine Umgebung teilen können, in der Komponenten, die in den unterschiedlichen Sprachen geschrieben werden, zusammenarbeiten. Das funktioniert ja auch ziemlich gut, so gut sogar, daß die Java-Welt nach anfänglicher Ablehnung dieses Konzept jetzt begeistert übernommen hat. Warum macht man es sich das nicht zunutze und entwickelt verschiedene kleinere Sprachen, die für den jeweiligen Anwendungsfall optimiert sind? Dies gilt ganz besonders für Sprachkonstrukte, die offensichtlich überwiegend für Codegenerierung entwickelt wurden. Der normale Entwickler braucht doch nicht die Dinge zu kennen, die die diversen Wizzards in VisualStudio verwenden. Schon dadurch hätte die Sprache entschlackt werden können.

Auch wenn man dann mehr Programmiersprachen kennen muß, so hat doch jede ihren Kontext. Mehrere Programmiersprachen sind für heutige Projekte ja sowieso schon die Normalität. SQL, HTML, XSL etc. werden neben der Hauptentwicklungssprache meist noch verwendet.

Mittwoch, 29. Oktober 2008

Daten allüberall

Das zweite Jahr dieses Blogs will ich mit einem Thema beginnen, daß in den letzten Wochen heiß diskutiert wurde. Immer wieder tauchenDaten an Stellen auf, wo sie eigentlich nicht hin gelangen sollten. Gründe gibt es dafür eine ganze Menge. Heute will ich ein paar Aspekte behandeln, die mit der Architektur der beteiligten Systeme zusammenhängen.

Verteilte Systeme

Als ich vor gut 20 Jahren damit anfing, Geld mit IT zu verdienen, ging gerade die große Zeit der Mainframes zu Ende. Auf einem Mainframe war die Sache im Prinzip noch einfach. Es gab eine große Datenbank und alle speicherten sämtliche Daten in diese Datenbank. Man wußte immer genau oder Daten sind.

Dann kam die Idee auf, daß Systeme billiger und flexibler gebaut werden könnten, wenn man sie auf mehrere kleinere Rechner verteilt.

Diese Idee ist noch recht harmlos, wenn nur die Anwendung verteilt werden, die Daten aber immer noch in einer einzigen großen Datenbank liegen, auf die alle Zugriff haben. Aber meist halten die einzelne Anwendung ihre eigenen Daten. Da sich aber die Bedürfnisse der einzelnen Anwendungen bezüglich Daten überlappen, befinden sich plötzlich dieselben Daten auf verschiedenen Rechnern.

Mit zunehmender Anzahl von Speicherorten wird die Angriffsflächegrößer. In den meisten Fällen dürften auch die Zugriffsrechte in den verschiedenen Anwendungen voneinander abweichen. Wenn ich an die gesuchten Daten in einer Anwendung nicht herankomme, kann ich es ja mit einer anderen versuchen.

Service orientierte Architekturen

In den letzten Jahren ist dieser Ansatz einen Schritt weiter getrieben worden. Anstelle von Anwendungen werden nun Services verteilt, die erst anschließend zur Anwendung zusammengebracht werden. Jetzt hat man Schnittstellen, über die man die interessantesten Dinge erreichen kann. Häufig wird - entsprechend den Versprechungen der Industrie - bei dem Entwurf der Services der Schwerpunkt auf Wiederverwendbarkeit gelegt. Das führt dann dazu, daß die Services die Vereinigungsmenge aller Daten liefern, die alle denkbaren Nutzer brauchen könnten. Die logische Folge ist, daß die meisten Nutzer mehr Daten erhalten, als sie eigentlich benötigen würden.

Das zweite Problem sind die Zugriffsrechte auf die Services. Üblicherweise wird nur pauschal überprüft, ob die Anwendung, die diesen Service nutzt, dazu berechtigt ist. Eine weitere Differenzierung findet nicht statt. Die Anwendung hat aber wahrscheinlich unterschiedliche Typen von Nutzern. Einige sind berechtigt mehr zu sehen, andere weniger. Für alle liefert der Service aber die gleichen Daten, nämlich die, die der Benutzer mit den meisten Rechten sehen darf.

Caching

Um die Performance des Systems zu erhöhen, wird gerne Caching eingesetzt. Hierbei entstehen weitere Orte, an denen sich Daten aufhalten, deren Verbleib nicht so genau überwacht wird.

Systemweites Rollenkonzept

Viele Probleme in Griff zu bekommen, ist es meines Erachtens notwendig, ein systemweites Rollenkonzept zu haben. Dies bedeutet, daß das System allen Benutzer eine oder mehrere Rollen zuweist, die von allen beteiligten Komponenten ausgewertet werden können. Bei allen zuübertragenen Daten, muß definiert sein, welche Rollen Zugriff darauf gestatten. Um das zu erreichen, ist es notwendig, daß das System nicht nur über eine zentrale Authentifizierungskomponente verfügt (Single Sign On), sondern auch über eine zentrale Autorisierungskomponente, die die Rollen vergibt.

Sonntag, 28. September 2008

Wie man ein Dreirad zum Rennwagen umbaut

Gewisse Leute vergleichen gerne die Softwareentwicklung mit der Automobilproduktion. Dieser Vergleich hinkt auf jedem Bein. Meist wird gefragt, warum sich Software nicht am Fließband herstellen läßt, möglichst von billigen angelernten Kräften. Ein anderer Vergleich dreht sich darum, daß meist fehleranfälligere Software produziert wird wie Automobile (nicht das Automobile nicht fehlerhaft aus der Fabrik kommen würden, ich fahre ein solches Modell).

Bei der Produktion von Software haben wir aber einen Vorteil gegenüber der Automobilproduktion. Wir können mit dem Plan für ein Dreirad anfangen und am Ende doch einen Rennwagen bekommen.

Neue Software

Wenn Software komplett neu erstellt wird, dann werden die Prozesse, die die Software unterstützen soll, bislang vermutlich manuell bewältigt. Man kann jetzt versuchen, eine komplette Lösung, die alle denkbaren Spezialfälle abdeckt, zu entwerfen und umzusetzen. Der Vertrieb wird dafür sein, da so eine Lösung viel Umsatz verspricht. Das Management auf Kundenseite wird möglicherweise auch dafür sein, da anscheinend große Lösungen dem Management besser stehen als pragmatische.

Eine allesumfassende Lösung bedeutet aber eine Reihe von Problemen. Zuerst dauert die Erstellung entsprechend lange. Während dieser Zeit gibt es für die alten Prozesse keinerlei Unterstützung, was Geld kosten dürfte. Die Spezifikation verliert sich oft in den Details, die sich in der Praxis dann oft als irrelevant herausstellen oder ganz anders aussehen. Zuletzt gibt es gewöhnlich auch ein technologisches Risiko. Da sich die Technologie immer noch schnell entwickelt, kann man es sich gewöhnlich nicht leisten, nur auf bewährte und bekannte Komponenten zu setzen. Bei neuen Komponenten ist die Belastbarkeit aber nicht bekannt. Das kann bedeuten, daß die Entwicklung länger als geplant dauert oder daß am Ende die gewünschte Performance und Stabilität nicht zu erreichen ist.

Alternativ kann man das System so konzeptionieren, daß es gut mit manuellen Prozessen zusammenarbeitet. Das empfiehlt sich immer, denn es wird immer Sonderfälle geben, die niemand bedacht hat oder deren Umsetzung in Software einfach zu teuer wäre. Nun kann man sich den Prozeß herauspicken, der bei einer Automatisierung am meisten bringt verglichen zum Implementierungsaufwand und -risiko.

Wenn sich der Ansatz bewährt, kann man so Schritt für Schritt weitermachen. Wenn sich aber unvorhergesehene Probleme auftun, ist es noch nicht zu spät, mit einem anderen Ansatz neu anzufangen.

Die Wahrscheinlichkeit ist hoch, daß man so beginnend von einem ganz einfachen Ansatz (Dreirad) im Laufe der Zeit eine bestens getunete Software bekommt, die Formel 1 Qualitäten besitzt.

Altsysteme

Heutzutage ist es wahrscheinlicher das eine Software erstellt wird, um ein bestehendes System abzulösen anstatt einen manuellen Prozeß. Auch in diesem Fall ist es möglich, schrittweise vorzugehen. Mir fallen da zwei Ansätze ein, wobei das Ergebnis unter Umständen bei beiden Ansätzen ähnlich aussehen kann.

Der erste Ansatz beschäftigt sich mit den Verantwortlichkeiten. Ein System ist gewöhnlich für mehr als eine Sache verantwortlich. Wenn ein kleines Webshopsystem sowohl den Kundenbestand als auch das Lager verwaltet, so läßt sich zuerst eine dieser Verantwortlichkeiten herauslösen. Hierzu muß das Altsystem natürlich soweit angepaßt werden, daß sich die entsprechenden Schnittstellen ergeben.

Der andere Ansatz dreht sich um die Datenhoheit. Daten werden oft in mehreren Systemen gleichzeitig gehalten. Aber eins ist verantwortlich für die Daten. Der Datenbestand in den anderen Systemen stellt im wesentlichen ein Cache dar, um die Zugriffe auf die Originaldaten zu minimieren. Eventuell kommt auch eine geänderte Sichtweise hinzu. Auf jeden Fall lassen sich die Daten aller weiteren System aus denen des Systems mit der Datenhoheit immer wiederherstellen. Was man jetzt tun kann, ist, die Datenhoheit von dem Alt- auf das Neusystem zu verschieben. Dazu muß sichergestellt werden, daß das neue System in jede Datenänderung einbezogen wird. Wenn das neue System über die Daten verfügt, können z.B. alle Schnittstellen, die mit diesen Daten bedient werden schrittweise vom Alt- auf das Neusystem übertragen werden. Zu Beginn hält das Neusystem nur Daten, hat aber noch keine Funktion. Am Ende hat das Altsystem nur noch Daten, die aber dort keiner mehr braucht. Es kann jetzt abgeschaltet werden.

Insbesondere bei dem Ansatz, bei dem die Daten zeitweise doppelt gehalten werden, hat man eine hohe Sicherheit und kann im Prinzip noch lange Zeit den Umstieg wieder rückgängig machen. Das wird erst dann schwierig, wenn sich die Funktionalität im Neusystem fortentwickelt hat und im Altsystem nicht mehr nachgezogen wurde. Zu diesem Zeitpunkt hat sich das Neusystem aber schon bewährt.

Schiefe Metapher

Natürlich ist ein solcher evolutionärer Ansatz nicht in der Softwareindustrie erfunden worden. Wenn man sich z.B. die ersten Automobile anschaut, so erkennt man die Evolution vom Fahrrad und der Kutsche zum Automobil. Das Problem an dem Bild ist, daß die Softwareentwicklung der Entwicklung eines neuen Modells in der Automobilindustrie entspricht. Der Produktionsprozeß in der Automobilindustrie ist am ehesten mit der Installation der fertigen Software zu vergleichen. Das andere Problem ist, daß es sich bei der Software, deren Preis und Risiken diskutiert werden meist um Individualanfertigungen handelt, wo es definitionsgemäß nur wenig Routine gibt.


Sonntag, 14. September 2008

Eines sie alle zu binden

Unternehmensdatenmodelle

Stabsstellen sind doch etwas schönes. Budgets scheinen keine Rolle zu spielen. Und man kann andere so schön ärgern, die von den Ergebnissen abhängig sind, denn das Management steht ja dahinter. Irgendwann vor langer Zeit ist ein kleiner Datenmodellierer auf die Idee gekommen, einem Manager das nahezubringen, was er so tut. Da sich Manager nicht mit kleinen Dingen abgeben, kamen sie auf die Idee, ein allumfassendes Unternehmensdatenmodell zu schaffen. Von nun an sollten sich alle Anwendungen im Unternehmen an dieses eine Datenmodell halten. Alle weitere Datenmodellierung wäre nicht mehr notwendig und alles würde großartig zusammenarbeiten.

Nun wäre ich der letzte, der etwas gegen Datenmodellierung sagen würde. Ein aussagefähiges Datenmodell ist nicht nur die Grundlage eines erfolgreichen Projekts, sondern besitzt in meinen Augen auch so etwas wie Schönheit.

Das Problem mit den ganz großen und alles lösenden Ansätzen ist nur, daß sie im allgemeinen zu scheitern pflegen. Dies liegt im wesentlichen an drei Dingen. Zuerst leben wir in einer Zeit, in der sich Dinge schnell ändern - schneller als man ein ganz großes Datenmodell erstellen kann. Zweitens sind die Anforderungen der einzelnen Nutzer zu individuell. Wenn für einen die Adresse nur ein Stück Text ist, das auf Adreßetiketten gedruckt werden muß, ist für einen anderen Nutzer jedes Detail interessant, da er z.B. darauf aufbauend die Kreditwürdigkeit eines Kunden bestimmen will (wie auch immer man zu diesen Methoden steht, sie sind in vielen Anwendungen implementiert). Ein gutes Datenmodell zeichnet sich dadurch aus, daß der Detailierungsgrad angemessen ist. In dem genannten Beispiel muß aber berücksichtigt werden, daß der Datenaustausch nur in eine Richtung funktioniert, da der andere Partner mit weniger Information auskommt. Der dritte Punkt ist die Beschränktheit der Beteiligten, was Kommunikation und Verständnis betrifft. Die Korrektheit eines Datenmodells beweißt sich erst durch die Benutzung. Auf der grünen Wiese erstellt, wird es sehr instabil sein.

Wenn nun ein Fehler in so einem Datenmodell festgestellt wird, müssen alle Anwendungen, die davon abgeleitet sind, korrigiert werden. Ansonsten bilden sich unterschiedliche Abkömmlinge, die genauso wenig zuverlässig miteinander kommunizieren können, wie es der Fall war vor der Einführung des einheitlichen Datenmodells.

Da die mit einer Änderung des Unternehmensdatenmodells verbundenen Kosten hoch sind, führt das zu einer Trägheit bei der Anpassung der Modelle, die die Grundlage der Anwendungen bilden. Es gibt große Konzerne, die schon früh in solche Modelle investiert haben und heute darunter leiden, daß alle ihre Anwendungen noch die Situation in den 90ern widerspiegeln, obwohl sich der Markt, in dem sich diese Unternehmen befinden, drastisch geändert hat.

SOA

Heutezutage will jeder IT-Leiter seine IT auf SOA (Service Oriented Architecture) aufbauen. Diese Services sollen zweierlei leisten. Zum einen sollen sie unabhängig entwickelt und gepflegt werden können. Zum anderen sollen sie von vielen Anwendungen genutzt werden können. Ich will hier nicht weiter darauf eingehen, inwieweit diese Ansprüche erfüllt werden können. Mich interessiert hier nur die Sicht des Datenmodelliers.

Datenmodellierung findet hier bei den in den Schnittstellen ausgetauschten Daten statt. Statt ER-Modelle für Datenbanken werden jetzt XML-Schemata modelliert, wobei es natürlich eine Frage ist, ob XML immer die beste Lösung ist.

Im Prinzip kann jeder Service sein eigenes Datenmodell entwickeln. Dies bedeutet, daß der Benutzer zuerst die Daten zwischen der Darstellung des Services und seiner internen Darstellung transformieren muß. An dieser Stelle setzt die Handlung von "Die Rückkehr des Unternehmensdatenmodellierers" ein. Der Gedanke ist doch bestechend. Wir bauen ein großes Datenmodell, auf das alle Services aufbauen. Dann entfällt diese lästige Transformiererei. Alle sprechen eine gemeinsame Sprache und die Datentypen passen aufeinander.

Leider haben wir hier wieder alle oben beschriebenen Probleme. Die Services sind hochgradig gekoppelt. Eine unabhängige Entwicklung ist nicht mehr möglich. Wenn das zentrale Datenmodell ausgetauscht wird, funktioniert zuerst einmal nichts mehr, bis alle Services angepaßt sind.

Dies widerspricht grundsätzlich der Idee, die Services zu entkoppeln.

Wenn man doch in diese Richtung gehen will, so sollte man nicht die Datenmodelle durch neue Versionen ersetzen, sondern die Versionen nebeneinander leben lassen. Eine alte Version wird erst entfernt, wenn alle Nutzer auf die neue Version gewechselt sind. Dadurch müssen zwar wieder Transformationen durchgeführt werden, wenn die kommunizierenden Services unterschiedliche Versionen eingesetzt werden, doch der Aufwand dürfte meist überschaubar sein und es ist auch eine Wiederverwendung denkbar.

Allgemein gegen präzise

Ein weiterer Aspekt ist, daß ein allgemeines Schnittstellenmodell nur den kleinsten gemeinsamen Nenner darstellen kann. Das gleiche Element mag für die eine Schnittstelle optional sein, während es für die andere notwendig ist. Die eine Anwendung braucht eine Historisierung der Daten, die andere genau das neueste Element. In einem für alle nutzbarem Modell kann das Element nur optional sein und mehrfach vorkommen dürfen. Validierungen aufgrund des Datenmodells werden dadurch ungenauer. Die individuelle Anforderungen der einzelnen Schnittstellen müssen anderweitig dokumentiert werden, was sich häufig als Fehlerquelle erweist.

Zusätzliche Aspekte, die hier zu berücksichtigen sind, sind Datensicherheit und Vertraulichkeit. Nur bestimmte Systeme dürfen mit personenbezogenen Daten umgehen. Diese sind in einer allgemeinen Schnittstelle, die von allen genutzt werden soll, mit enthalten.

Logisch gegen technisch

Es ist wichtig, daß alle miteinander sprechen können. Daher ist ein - nicht zu detailliertes - logisches Datenmodell für alle sicher sinnvoll. Die technische Umsetzung in eine konkrete Schnittstelle erfolgt aber besser individuell.


Montag, 8. September 2008

Noch mehr Methoden

Gestern habe ich über Attribute und Methoden von Objekten gesprochen. Zwei Punkte möchte ich noch ergänzen.

Hilfsmethoden

Dieser Punkt ist leicht neben dem gestrigen Thema, da die Methoden überhaupt nicht von dem Objekt abhängen. Das Objekt wird der Methode als unsichtbarer (Ausnahme: Python) erster Parameter übergeben. Einige Methoden benutzen diesen Parameter aber nicht. Daher können sie auch, bei entsprechender Deklaration, so daß der Compiler einverstanden ist, ohne das Objekt direkt als Methode der Klasse verwendet werden. In diesem Fall dient die Klasse nur als Schublade für die Methode. Eigentlich handelt es sich bei einer Klasse, die ausschließlich solche Methoden enthält um ein Modul. Derartige Methoden werden häufig als Utility- oder Hilfsmethoden bezeichnet.

Wenn die Methoden Parameter haben, könnten sie auch als Methode eines der Objekte übergeben werden, die als Parameter dienen. Die ist in Ruby immer möglich. In anderen Programmiersprachen kann es aus zwei Gründen scheitern. Erstens ist nicht immer jeder Parameter ein Objekt. Das ist z.B. das Thema einer Mathematik-Bibliothek, wie sie sich u.a. bei Java findet. Zweitens kann es sich um ein Bibliotheksobjekt handeln, das nicht erweitert werden kann, da es final oder sealed ist. C# behilft sich hier mit extension methods, die bei Verwendung ähnlich aussehen als ob die Klasse erweitert worden wäre. In Wirklichkeit wird aber eine separate Klasse mit Hilfsmethoden verwendet. Dies sieht zwar hübsch aus, hat aber seine Tücken. Insbesondere ist das Verhalten bei Vererbung nur zu verstehen, wenn man weiß, was dahinter steht.

Zugehörigkeit von Methoden

Der aufmerksame Leser hat bei den vorherigen Ausführungen vielleicht schon gemerkt, daß es oft ein wenig willkürlich ist, welcher Parameter einer Methode herausgehoben wird, indem man die Methode der Klasse seines Typs zuordnet. Ein Beispiel, über das ich immer wieder stolpere ist die join-Methode, die sich in verschiedenen Bibliotheken findet. Sie dient dazu aus einem Array einen String zu machen, indem sie die Stringrepresentation der Arrayelement mit einem Separatorstring verknüpft. Es gibt also die join-Methode, ein Array und einen Separatorstring. Für mich erschiene es natürlich, diese Methode dem Array zuzuordnen und den Separatorstring als Parameter zu übergeben. Der Autoren etlicher Bibliotheken sehen das aber umgekehrt und machen die join-Mehtode zu einer Methode der String-Klasse. Das ist eine völlig legitime Sichtweise.

Solche Probleme hat man natürlich nicht, wenn die Methoden unabhängig von den Klassen definiert werden, wie es in CLOS der Fall ist.


Sonntag, 7. September 2008

Das Objekt an sich

Während ich mich noch an Zeiten erinnern, als Objekte noch als unheimliche, neue Technologie galten, hat sich der objektorientierten Ansatz in den letzten Jahren weitgehend durchgesetzt. Wenn man aber verschiedene Programmiersprachen vergleicht, so erkennt man, daß das Verständnis, was ein Objekt ist, doch deutlich variiert. Egal was man verwendet, so erscheint es mir doch immer hilfreich, zu verstehen, welche Möglichkeiten es gibt und was die konkrete Implementierung bedeutet.

Im Lehrbuch findet man ungefähr folgendes: ein Objekt besitzt einen Zustand und Methoden. Es ist die Instanz einer Klasse und diese Klassen können Eigenschaften von anderen Klassen durch Vererbung übernehmen. Wenn man eine Klasse implementiert, so definiert man den Zustand gewöhnlich durch Attribute.

Attribute

Zunächst ist es hilfreich zu unterscheiden, daß es zwei Arten von Attributen gibt. Die ersten definieren die Identität des Objekts. Sie sind unveränderlich und werden deshalb bei der Erzeugung des Objekts definiert. Wenn ein Objekt z. B. eine Person repräsentiert, werde das Geburtsdatum ein solches Attribut. In einigen Programmiersprachen werden solche Attribute als konstant oder statisch gekennzeichnet.

Die zweite Art von Attributen ändert sich während der Lebenszeit des Objektes. Man kann die Unterscheidung hier noch weiter treiben, wenn man feststellt, daß sich mit Änderung des Attributs in einigen Fällen das Verhalten des gesamten Objektes ändert. Wenn sich die Temperatur eines Raumes in angemessenem Maße ändert, bleibt die Funktion des Raumes doch erhalten. Wenn sich aber der Zustand eines Autos von fahrbereit in Schrott ändert, werden dieselben Methoden anschließend zu unterschiedlichen Ergebnissen führen.

Wenn alles ein Objekt ist, dann stellen Attribute immer nur Beziehungen zu anderen Objekten dar. Dies würde prinzipiell zu einer unendlichen Rekursion führen. Daher gibt es in den meisten Programmiersprachen primitive Typen wie Zahlen und Wahrheitswerte, die nur ihren Wert repräsentieren. Will man das vermeiden, so führt man Klassen ein, deren Objekte einen bestimmten Wert repräsentieren, diesen aber nicht als Attribut besitzen. Ein derartiges Objekt besitzt nur eine Identität aber keine Attribute.

Eine weit verbreitete Ansicht ist, daß man nei direkt auf Attribute zugreifen, sondern immmer Zugriffsfunktionen nutzen soll. Einige Programmiersprachen erzwingen diese Art der Programmierung, zum Beispiel Ruby und Smalltalk. Welche Gründe gibt es dafür? Die Antwort hängt von der Art des Attributs ab. Attribute die die Identität des Objekts definieren, sollten nur einmal bei der Erzeugung des Objekts gesetzt werden können. Später sollte nur Lesezugriff möglich sein. Attribute die den Zustand eines Objekts beschreiben, sollten die direkt gesetzt werden, sondern ändern sich nur in Folge von Ereignissen. Wenn das Objekt nur ein Container für das Attribut ist, so brauchen wir in der Regel Schreib- und Lesezugriff, aber es kann ein Objekt gegeben, die über die Änderung des Wertes informiert werden wollen, wie zum Beispiel die Java Bean Spezifikation beschreibt.

Ein anderer Grund, Zugriffsfunktionen zu verwenden, ist der, daß man die innere Repräsentation der Werte ändern können will, ohne die Schnittstelle zu beeinflussen. Werte, die man nach außen sichtbar macht, müssen unter Umständen intern gar nicht vorhanden sein. Das Lehrbuchbeispiel hierzu sind kartesische und Winkelkoordinaten, von denen man nur ein Paar abspeichern muß, während sich das andere ergibt.

Methoden

Neben Attributen besitzt ein Objekt auch Methoden. Es empfiehlt sich, streng zu trennen zwischen Methoden, die eine Information über das Objekt zurückgeben und Methoden, die das Objekt ändern. Das ist eine Regel, die von keiner mir bekannten Programmiersprache erzwungen wird, aber die Verständlichkeit der Schnittstelle deutlich erhöht. Niemand wird damit rechnen, daß eine Wiederholung von Abfragen andere Werte zurückgegeben werden.

In einigen Programmiersprachen werden Methoden als Objekte behandelt. Sie können dann in Variablen abgespeichert werden oder als Rückgabewert von Methoden auftauchen. Zum Beispiel bei Python erfolgt der Zugriff auf Methoden völlig identisch wie der Zugriff auf Attribute.

In einigen Programmiersprachen, die auch den direkten Zugriff auf Attribute kennen, gibt es spezielle Methoden, die sich mit der gleichen Syntax aufrufen lassen, mit der auch auf Attribute zugegriffen wird. Dadurch wird es transparent, ob ein Attribut direkt angesprochen wird oder über Zugriffsmethoden. Populärstes Beispiel hierfür sind die Properties von C#.

Programmiersprachen, die keine strikte Typprüfung durchführen, erlauben es in der Regel, eine Methode zu definieren, die immer dann aufgerufen wird, wenn die eigentlich gefragte Methode nicht definiert ist. Das macht unter anderem die Programmierung von Proxies zu einer trivialen Übung.

Wenn eine strikte Typisierung gefordert ist, gibt es häufig die Möglichkeit, daß eine Zusage gemacht wird, daß eine bestimmte Menge von Methoden unterstützt wird. Dies geschieht durch die Interfaces, die man zum Beispiel in Java und C# findet. Man kann dann Objekte benutzen, die nur eine bestimmte Funktionalität zusagen, ohne den genauen Typ zu kennen.

Ausblick

Ich will hier kein Lehrbuch schreiben, sondern nur meine Sicht der objektorientierten Programmierung vorstellen. Einige Aspekte habe ich bislang weggelassen. Der wichtigste ist natürlich die Vererbung. Darauf will ich bei gegebener Zeit zurückkommen.


Sonntag, 10. August 2008

Anforderungen und Strategie

Der erste Schritt, um ein neues System zu erstellen, ist die Anforderungsanalyse. Manch ein System kommt nie darüber hinaus. Manchmal macht das Ergebnis mehr Probleme, als es weiterhilft. Über das Thema gibt es genug zu schreiben, um Bücher zu füllen. Ich will hier nur zwei Gedanken beisteuern.

Geschäftsstrategie

Firmen holen sich meist unterschiedliche Berater ins Haus, wenn sie über die Geschäftsstrategie nachdenken und wenn sie ein neues IT-System planen. Die Unterstützung im Management wie die Erwartungshaltung sieht auch unterschiedlich aus. Dies führt dazu, daß bei dem Entwurf eines neuen IT-Systems häufig die bisherigen Vorgehensweisen ziemlich ungefragt erfaßt werden. Dazu kommen noch ein paar neue Anforderungen. Dies führt dann schon von Anfang an zu eigenartigen Lösungen, die sich nur aus der Firmengeschichte heraus erklären lassen.

Eigentlich müßte nach einer Bestandsanalyse zuerst untersucht werden, inwieweit die bestehenden Geschäftsprozesse angepaßt werden sollten, um die Grundlage für ein funktionsfähiges neues IT-System zu bekommen. In der Regel gibt es aber mindestens zwei Hindernisse. Zum einen haben die meisten IT-Kräfte von den Geschäftsabläufen zu wenig Ahnung, als daß sie kompetente Vorschläge machen könnten. Zum anderen sind sie mit Mitarbeitern des Kunden konfrontiert, die in ihrer existierenden Welt existieren und weder Interesse noch Kompetenz haben, diese zu ändern.

Am schlimmsten wird es, wenn man als IT-Analyst mit verschiedenen Abteilungen mit unterschiedlichen Interessen konfrontiert wird. Es ist praktisch aussichtslos, diese alle unter einen Hut zu bekommen. Damit das Ergebnis noch umsetzbar bleibt, Bedarf es der Entscheidung aus dem Management über Prioritäten. Nicht alles kann in einer ersten Stufe des Systems umgesetzt werden.

Sicht von unten

Eine andere Gefahr ist, daß man sich als Analyst nur indirekt mit dem Umfeld beschäftigt. Wenn man nur Dokumente liest und mit Analysten des Kunden oder Management redet, wird man viele wichtige Informationen nicht bekommen. Es lohnt sich immer, sich mal einen Tag zu den zukünftigen Anwendern zu setzen und zu beobachten, wie diese ihre Arbeit wirklich erledigen. Häufig wird man da pragmatische Lösungen abseits der Theorie entdecken oder auf Probleme stoßen, die so nirgendswo dokumentiert sind, mit denen die Anwender sich aber jeden Tag herumschlagen. Das neue System sollte die von den Anwendern gefunden Lösungen nicht verbauen ohne eine gangbare Alternative zu liefern. Und wenn die Probleme aus der Praxis erst bei der Einführung auf das neue System stoßen, kann es manche böse Überraschung geben.

Leider wird der direkte Kontakt zwischen IT-Analysten und Endanwendern in vielen Fällen nicht freiwillig gewährt.

Was einen guten Analysten ausmacht

Ein guter Analyst wird möglichst viel von dem Geschäft seines Kunden zu verstehen suchen. Dies bedeutet auch, daß er zuerst auch Fachliteratur darüber liest. Dies ist notwendig, um die richtigen Fragen zu stellen. Und es ist wichtig Fragen zu stellen. Zuerst gibt es ein Verständnisproblem, da die Bedeutung vieler Dinge, von denen der Kunde spricht, nicht klar ist. Man darf nie Annahmen über die Bedeutung eines neuen Begriffs machen, sondern muß unbedingt nachfragen. Es hilft nichts, den alleswissenden Berater zu spielen und am Ende des Tages eine schlechte Analyse abzuliefern.
Am Ende muß der Analyst in der Lage sein, dem Management des Kunden klar die Auswirkungen des von ihm beschriebenen Systems auf die Geschäftsabläufe des Kunden darzustellen. Ein neues System wird hier Auswirkungen haben und es ist wichtig, schon in dieser Phase darauf hinzuweisen. Wenn das Management damit nicht leben will, ist es sicher günstiger, bereits hier abzubrechen, als wenn das System in Betrieb geht. Wenn das IT-Vorhaben aber genügend Unterstützung im Management hat, ist es beruhigend, sich dessen zu versichern.

Sonntag, 3. August 2008

Zerfallende Architekturen

Erster Akt: Festlegung der Architektur

In der Theorie würde man bei einem neuen Softwareprojekt zuerst einmal hingehen und die fachlichen Anforderungen sammeln. In einem nächsten Schritt würde man sich hinsetzen und die technischen Alternativen zur Umsetzung dieser Anforderungen bewerten und die geeigneste auswählen und damit die Umsetzung beginnen. Soweit die Theorie.

In der Wirklichkeit ist die zu verwendende Technik schon häufig von Beginn an festgelegt. Vielleicht hat der Kunde eine Vorgabe gemacht oder das eigene Team hat sich spezialisiert oder ein Techniker ist bei der Angebotserstellung zwischen Tür und Angel gefragt worden, was im so spontan einfallen würde.

Zweiter Akt: Beginn der Umsetzung

Wir habe eine fachliche Beschreibung, z.B. in Form eines Satzes von Anwendungsfällen (Use Cases) und wir haben eine technische Architektur. Das müßte doch reichen, mit der Umsetzung zu beginnen. Also bekommt jeder Entwickler die technische Architektur vorgesetzt und einen Anwendungsfall zur Umsetzung, jeder einen anderen.
Mit der Zeit kommen bei der Entwicklung Fragen an die Architektur auf, die in dem bisherigen Bild noch nicht enthalten waren. Also wird da ergänzt. Inkrementelle Arbeitsweise ist doch seit Jahren angesagt und nicht nur ich sage immer wieder, daß das die einzige Art ist, die realistisch ist.

Dritter Akt: Erste Risse

Irgendwann kommt dann das unvermeidliche. Jemand fordert eine Änderung an der Architektur an, jemand anderes stellt fest, daß diese Änderung entscheidende Voraussetzung seiner Implementierung untergräbt.

Dann kommen immer häufiger Argumente gegen diskutierte Änderungswünsche, die da etwa so lauten: "Wir können das nicht machen. Wir wissen nicht ob das jemand so braucht, wie es jetzt ist."

Vierter Akt: Verzweiflungstaten

Irgendwann kommt der Punkt, an dem die Rücksichtsmaßnahmen auf bestehende Arbeit als nicht mehr praktikabel angesehen wird. Dann wird ohne Rücksicht auf Verluste aufgeräumt. Das ist eine Krise, die entweder zum baldigen Ableben des Projekts führt oder aber heilsam sein kann. Teuer ist sie in jedem Fall.

Was hier fehlte

Um Zeit, Geld und Nerven zu sparen, ist ein entscheidender Schritt vor Beginn der Umsetzung notwendig. Die fachlichen Anforderungen müssen auf die Architektur abgebildet werden. Es muß genau klar sein, welche Bedürfnisse jede fachliche Anforderung an die Architektur hat.

Es sollten dann Komponenten entstehen, die konkrete Verantwortlichkeiten haben. Die Entwickler werden sich dann eher an den Verantwortlichkeiten der Komponenten orientieren als an den ursprünglichen fachlichen Anforderungen. Diese treten dann wieder bei der Integration des Gesamtsystems zutage.
Ein Vorteil hier ist, daß frühzeitig ähnliche technische Anforderungen von unterschiedlichen Anwendungsfällen identifiziert werden können. Doppelarbeit kann so vermieden werden und noch wichtiger nicht kompatible Lösungsansätze.

Desweiteren sollte ein Bild der Gesamtarchitektur existieren, etwa in der Größe einer Wandtafel. Dieses Bild sollte so aufgehängt werden, daß jeder Entwickler es immer sehen kann. Anhand dieses Bildes kann man dann den Ablauf der Implementierung eines Anwendungsfalls durchspielen und so Probleme und Lösungsmöglichkeiten erkennen.

Sonntag, 27. Juli 2008

Gelegenheit macht Generierung

Ich muß bekennen, Routineentwicklungsaufgaben finde ich langweilig und meist nicht wert, dafür aufzustehen. Aber für langweilige Aufgaben sind doch die Maschinen erfunden worden. Also warum nicht den Computer die Arbeit machen lassen. Das ist der Grund, warum ich zum überzeugten Metaprogrammierer geworden bin.

Aber Vorsicht: Metaprogrammierung macht süchtig. Schon die Jungs in der Zeit von Flower power haben nicht nur LSD eingeworfen. Einige waren auch - wie man hört - süchtig nach M4. Dabei handelt es sich um einen vielseitigen Makroprozessor, der auch zur Codegenerierung verwendet wird. Leute, die mal sendmail konfiguriert haben, wissen, von welchem Werkzeug ich schreibe.

Die Kanonen: MDA und DSLs

Das ist zunächst MDA. MDA steht für "Model Driven Architecture", wobei mir niemand bekannt ist, der erklären kann, wie es zu dem "A" dabei gekommen ist. Die Architektur steckt im Modell und was getrieben werden soll ist die Generierung eines fertigen Systems. Der MDA-Ansatz wird von der OMG vorangetrieben, die uns CORBA und UML gebracht hat. Die Idee dabei ist, ein Modell basierend auf dem UML-Metamodell zu erstellen und daraus über eine oder mehrere Transformationen am Ende ausführbaren Code zu bekommen. Klingt kompliziert, ist kompliziert, aber es gibt einige vielversprechende Realisierungsansätze z.B. im Eclipse Modeling Project.

Der andere Ansatz sind DSLs. Hier baut man sich eine Grammatik auf für eine Sprache, die es möglichst einfach erlaubt, die für das konkrete Problem relevante Information zu erfassen. Dann kann man z.B. mit einem Parser Generator wie Antlr die Information in eine geeignete interne Darstellung einlesen, die es erlaubt mit einer Templateengine daraus Code zu erzeugen.

Besonders vorteilhaft sind diese Ansätze, wenn man unterschiedlichen Code aus derselben Information erzeugt. So kann man aus derselben Information die DDL für die Datenbankobjekte, die Zugriffsschicht für die Datenbank und die XSD für die XML-Darstellung erzeugen.

Die Spatzen

Oft ist es überhaupt nicht notwendig, sich über eine Sprache Gedanken zu machen, in der man die Information darstellen will. Vielmehr finden sich immer wieder strukturierte Dokumente, wie z.B. Excel-Tabellen, in der Information abgelegt ist, die man brauchen kann, um die Programme zu schreiben. Diese Dokumente sind viel zu schade um nur abgeschrieben zu werden. Notfalls müssen sie ein wenig nacheditiert werden. Danach geht es ans parsen.

Parsen

Dazu reicht in einfachen Fällen, wie csv-Dateien oft schon die split-Funktion, die in vielen Programmiersprachen zu finden ist. Bei Textdateien braucht man meist reguläre Ausdrücke. Manchmal hat man es auch mit XML-Dateien zu tun. Da tut es ein Sax-Parser.

Die Daten, die beim Parsen anfallen, müssen jetzt in geeignete Datenstrukturen untergebracht werden. Man kann da, wenn man gelehrt klingen will, von einem Meta-Modell sprechen. Meist braucht man aber nur wenige Listen und Dictionaries (Hashtable oder wie das Ding auch immer genannt wird), manchmal auch verschachtelt.

Transformieren

Gelegentlich wird nach dem ersten Einlesen noch eine einfache Transformation gebraucht, z.B. wenn Beziehungen zwischen den Elementen hergestellt werden müssen, wozu man die vollständig gefüllten Dictionaries braucht.

Generieren

Jetzt kann Code generiert werden. Es geht hier nicht um ganze Systeme, wie z.B. bei dem MDA-Ansatz. Vielmehr freuen wir uns schon über Codefragmente. Schön ist es, wenn es ein abgeschlossenes Dokument ergibt. Es ist aber auch nicht schlimm, wenn das Ergebnis noch nicht 100%ig ist. Wenn hunderte Zeilen Code generiert werden, dann aber 5 Zeilen manuell hinzugefügt werden, ist immer noch viel gewonnen.

Der so generierte Code kann auch manuell weiterverarbeitet werden. Dann kann der Codegenerator nach einmaliger Benutzung erst einmal eingemottet werden. Der Code ist aber sicher hilfreich als Vorlage für den nächsten Fall. Wenn sich die zugrundeliegenden Dokumente häufig ändern, lohnt es sich, darauf zu achten, daß man für die Einarbeitung der Änderungen die Codegenerierung nutzen kann.

Die Werkzeuge

Codegeneratoren kann man mit allen möglichen Werkzeugen bauen. Wenn die Daten in einer Exceltabelle vorliegen, benutze ich auch schon mal einen Excelausdruck und kopiere die resultierende Spalte. Wenn die Daten in einer Datenbank liegen, läßt sich Code auch mit SQL erzeugen.

Meist benutze ich eine Skriptingsprache. In früheren Jahren war das Perl. Heute benutze ich Ruby oder Python. Pythonprogramme sind meist lesbarer und haben die bessere Unterstützung für die Verarbeitung von XML. Ruby hingegen hat seine Stärken u.a. bei der Codegenerierung. Man kann leicht sehr mächtige Templates mit dem #{}-Ausdruck schreiben, der es erlaubt Codefragmente in das Template einzubetten. Damit sind auch rekursive Templates kein Problem.

Das Ergebnis

Mit recht wenig Aufwand kann man Code aus allen möglichen Dokumenten generieren. Abschreibefehler entfallen dabei. Der Code ist völlig konsistent. Dadurch fallen Feher schnell auf und entdeckte Fehler müssen nur an einer Stelle korrigiert werden.

Natürlich braucht es etwas Übung, aber es lohnt sich. Man bekommt nicht nur schneller bessere Ergebnisse. Die Arbeit macht auch mehr Spaß.

Sonntag, 20. Juli 2008

Grenzen der Abgrenzung

Eine Anwendung besteht oft aus einer Vielzahl von Systemen, die zusammenarbeiten müssen. Da gibt es z.B. eine Webanwendung, die Bestellungen entgegennimmt. Diese Information wird an ein Ordermanagementsystem übergeben, das wiederum die Information an ein ERP-System, ein Logistkiksystem und ein Kundenmanagementsystem weiterleitet. In komplexen Fällen kommen leicht ein Dutzend beteiligter Systeme zusammen.

Wenn sich Anforderungen ändern, hat das gewöhnlich Auswirkungen auf mehrere Systeme. Gewöhnlich werden die einzelnen Systeme unabhängig voneinander von verschiedenen Teams entwickelt und anschließend integriert. Jedes Team ist natürlich daran interessiert, seine Umgebung möglichst stabil zu halten, da nur so die Termine zu halten sind. Wie kann man sich möglichst gut mit Änderungen aus den umgebenden Systemen umgehen?

Eine umfassende Darstellung verschiedener Szenarien findet sich in dem Buch von Eric Evans "Domain Driven Design". Ich werde dies hier nicht wiederholen, sondern mich auf wenige Aspekte beschränken.

Politik

Der Ausmaß von Änderungen für ein System ist oft Verhandlungssache. Manchmal kann die Notwendigkeit in Frage gestellt werden. Oft geht es aber um verschiedene Lösungsmöglichkeiten, die sich dadurch unterscheiden, wie sehr die einzelnen Systeme betroffen sind. Das endet dann in einer Pokerpartie zwischen den Projektleitern. Die Karten werden dadurch bestimmt, wer mehr zu verlieren hat, wenn es bei der Integration zu Problemen kommt. Der Rest ist Bluff.

Technische Abgrenzung

Wie die meisten technischen Themen - mit Ausnahme des Baues eines Perpetuum Mobiles, d.h. einer risikofreien Energieversorgung - ist hier die Lösung verhältnismäßig einfach. Hier gibt es z.B. die Möglichkeit nach innen eine einheitliche technische Schnittstelle zu haben und nach außen hin flexibel zu sein, indem man einen Enterprise Service Bus einsetzt. Es gibt eine großes Angebot von Systemen, die gewöhnlich mit einer Vielzahl von fertigen Adaptern kommen.

Auch wenn man nicht ein solches Produkt einsetzt, so empfiehlt es sich immer, die technische Schnittstelle von der fachlichen zu entkoppeln. Man kann dann ein System so modular aufbauen, daß verschiedene technische Schnittstellen (z.B. SOAP über HTTP gegen Queueing System gegen Tuxedo ...) und verschiedene fachliche Schnittstellen einfach durch Konfiguration zusammengebracht werden können. Dies führt zu einer hohen Wiederverwendbarkeit der technischen Schnittstelle.

Unterschiedliche Sichten

Verschiedene Systeme benötigen unterschiedliche Sichten auf die Daten. So sind die Bedürfnisse an die Daten für ein Logistiksystem offensichtlich andere als für das System, daß die Rechnung stellt.

In vielen Fällen gibt es eine Menge von Daten, die z.B. im Ordermanagementsystem zusammengebracht wird, von der die Daten, die die übrigen Systeme benötigen. in Form von verschiedenen Untermengen abgeleitet werden können. Manchmal sind kompliziertere Abbildungen notwendig. Solange es aber möglich eine Abbildung zu finden, ist bewiesen, daß die Sichten der einzelnen System kompatibel sind.

Man kann sich die Arbeit vereinfachen, falls man genügend Einfluß auf die Entwicklung der einzelnen Systeme hat, indem man ein übergeordnetes Modell bildet. Jedes System beschränkt sich dann darauf, ausschließend Untermengen dieses übergeordneten Modells zu verwenden und bestenfalls einfache technische Transformationen durchzuführen. Dies bedeutet, daß dieses Modell dann die zentrale Instanz ist. Wenn Änderungen in der Gesamtanwendung erforderlich sind, müssen sie hier eingepflegt werden und jedes System muß entsprechend angepaßt werden, wenn es die entsprechenden Elemente übernommen hat.

Antikorruptionsschicht

Wenn es nicht möglich ist, alle Systeme an ein gemeinsames Modell anzupassen, z.B. weil ein Altsystem dabei ist, so kann man zumindestens verhindern, daß sich Konzepte aus dem einen System in das andere einschleichen. Das entsprechende Muster bezeichnet Evans als "Antikorruptionsschicht". Was man damit nicht verhindern kann, ist, daß fachliche Anforderungen der so abgeschotteten Systems erfüllt werden müssen. Das bedeutet dann, daß man nicht nur sein System anpassen muß, sondern auch die Antikorruptionsschicht.

Außerdem muß man bedenken, daß eine zusätzliche Schicht auch ein zusätzlicher Ort von Fehlern ist. Wenn man eine schlecht konstruierte, instabile Antikorruptionsschicht hat, kann es sein, daß sich die Probleme dadurch vergrößern, da das System jetzt nicht nur geändert werden muß, wenn von außen neue Anforderungen kommen, sondern auch dann, wenn Probleme in der Zwischenschicht entdeckt werden.

Während die Antikorruptionsschicht häufig das Leben deutlich vereinfacht, ist für eine gute Umsetzung auch Aufwand notwendig. Es sollte also überlegt werden, ob die Probleme, die damit gelöst werden können, so groß sind, daß sie das Risiko und den Aufwand wert sind.

Sonntag, 13. Juli 2008

Vorsicht Meeting

Heute geht es nicht um interessante Technik, sondern um das, womit die meisten sich mindestens so häufig beschäftigen, wie die "eigentliche Arbeit": Meetings. Eigentlich ist Kommunikation doch der wichtigste Teil eines Softwareprojekts, der den Unterschied zwischen Erfolg und Mißerfolg eines Projekts ausmacht. Warum haben Meetings bei so vielen so einen schlechten Ruf?

Typen von Meetings

Ohne Anspruch auf Vollständigkeit hier einige Typen von Meetings:

  • Routinemäßiges Teammeeting: Kann nützlich sein, wenn es knapp und mit strengen Regeln geführt wird (Dialy Scrum Meeting). Negative Faktoren sind hier:

    • schwache Moderation, Diskussionen kommen auf, die anderswo geführt werden sollten

    • es werden immer wieder die gleichen Probleme angesprochen, aber niemand unternimmt etwas zur Lösung

    • die Teammitglieder haben das Gefühl, daß ihre Arbeit beurteilt wird. Probleme werden unter den Tisch gekehrt

    • es wird versucht, hier Beschlüsse zu fassen. Beschlüsse brauchen Vorbereitung, damit keiner sich überfahren fühlt

  • Strategiemeetings: eine Strategie soll verabschiedet werden. Negative Faktoren:

    • die Beschlußvorlage wird zu spät verteilt, so daß sich die Teilnehmer nicht vorbereiten können

    • es wird versäumt zu Beginn des Meetings Einigung über Ziel und Notwendigkeit des Themas zu erzielen

    • unterschiedliche Meinung werden zuerst unterdrückt und kommen dann in endlosen Diskussionen zum Vorschein. Besser: Raum für die Präsentation von abweichenden Vorschlägen geben

    • Teilnehmer mit abweichenden Ansichten erst im Meeting einfangen zu wollen, führt selten zum gewünschten Erfolg. Bestenfalls bekommt man sein Meeting durch, aber die Fragen werden immer wieder hochkommen. In der Politik wird so etwas schon im Vorfeld zu klären versucht.

  • Meeting mit Kunden. Negative Faktoren:

    • problematische Themen werden vorher nicht offen zwischen den Teilnehmern auf Anbieterseite besprochen

    • die Rollen sind nicht klar. Wer hat welche Kompetenzen für Zusagen? Wer darf nur Fragen stellen? Wer nur beobachten?

    • der Erfolg oder Mißerfolg wird gegenüber dem Kunden einzelnen Teammitgliedern zugewiesen. Schuldfragen interessieren nur intern.

Kommunikationsfallen

Hier typische Kommuniktationsfallen, die häufig in Meetings beobachtet werden können:

Ein Vorlage wird vorgestellt. Wenn Gegenvorschläge kommen, reagiert der Vorstellende gereizt. Warum kann das nicht funktionieren? Wenn die Vorlage nicht zur Diskussion stehen soll, braucht es kein Meeting. Wenn der betreffende die notwendige Kompetenz hat, verkündet er sie einfach und stiehlt den Teilnehmern nicht die Zeit. Wenn er zum Meeting einlädt, signalisiert er, daß die Vorlage ein erster Entwurf ist, die im Meeting weiterentwickelt werden soll.

Die Diskussion dreht sich im Kreis über immer neue, meist kleinteilige Probleme. Wird hier etwa über eine Lösung diskutiert, bevor das Problem genau definiert wurde?

Einzelne Teilnehmer halten lange Vorträge und beschweren sich bei Unterbrechungen. Während es als unhöflich gilt, Leute zu unterbrechen, so ist es zumindest wenig souverän, Nachfragen abzuwürgen. Warum sollte jemand einem langen Vortrag zuhören, wenn er schon nicht von der Prämisse überzeugt wurde. Wenn dies eine Taktik ist, die Oberhand durch Redezeit zu gewinnen, wird es meist eher das Meeting sprengen als zum Erfolg zu führen.

Machtkämpfe und pragmatischen Suche nach Lösungen für Probleme vertragen sich selten.

Kommunikationstechniken

Viel Firmen geben erheblich mehr Geld dafür aus, ihre Mitarbeiter in Kommunkationstechniken zu schulen wie in konkreten technischen Fertigkeiten. Ich bin kein großer Freund von diesen Kursen, aber die dort gelehrten Methoden fuhren erstaunlicherweise häufig zu Erfolgen.

Warum werden die Techniken so selten angewandt? Ein Kommunikationskoffer mit den entsprechenden Spielzeugen (Karten, Stifte, Heftmittel etc.) ist meist verfügbar. Aber so ein Meeting erfordert von dem Einladenden mehr Vor- und insbesondere Nacharbeit. Außerdem taugen sie nicht zu Machtspielchen und die Ergebnisse sind nachprüfbarer.

Teilnehmerliste

Wichtig ist es, die richtigen Leute zu einem Meeting einzuladen. Es ist wenig hilfreich, in einem Meeting etwas zu beschließen, was von Dritten erwartet wird, die möglichst davon nicht unterrichtet werden.

Es ist aber auch nicht sinnvoll, Leute einzuladen, an deren Meinung man nicht interessiert ist, es sei denn, man will diese Leute loswerden.

Sonntag, 6. Juli 2008

Flexible Programmiersprachen

Während sich einige Programmiersprachen wie z.B. SQL auf spezielle Gebiete beschränken, sind die meisten Programmiersprachen, die wir täglich benutzen prinzipiell für alle Probleme einsetzbar. Leute, die ihre theoretischen Kenntnisse deutlich machen wollen, sprechen hier von Turing-vollständig. Dieser Ausdruck basiert auf den Arbeiten des englischen Mathematikers Alan Turing, der schon vor dem Bau der ersten Computer theoretisch gezeigt hat, wie man mit einem Mindestsatz von Operationen alle vorstellbaren mathematischen Probleme lösen kann. Aber nicht jede Programmiersprache macht es dem Benutzer gleich leicht, ein Problem zu formulieren. Einige Programmiersprachen sind optimiert, um bestimmte Probleme zu lösen, während andere Dinge nur schwer umzusetzen sind. So kann man mit FORTRAN relativ gut numerische Algorithmen implementieren. Eine Textverarbeitung oder Datenbank möchte ich aber eher nicht damit schreiben. Andere Programmiersprachen sind leicht zu lernen, wie z.B. BASIC, womit es aber schwer ist, größere Programme lauffähig und wartbar hinzubekommen.

Maschine gegen Logik

Bei der Entwicklung der Programmiersprachen gibt es zwei unterschiedliche Ansätze. Diese entsprechen auch den unterschiedlichen Abstammungen der Informatiklehrstühle der Universitäten. Bevor sich die Informatik als eigenständige Disziplin etabliert hatte, waren die späteren Informatiker entweder Elektroingenieure oder Mathematiker. Demnach gab es zwei unterschiedliche Herangehensweisen. Die einen hatten ihre Schaltungen und versuchten damit etwas sinnvolles anzufangen. Die anderen hatten ihre Logik, Lambdacalculus, Kategorientheorie etc. und versuchten diese Theorien irgendwie auf die Maschinen abzubilden. Das sieht man auch den Programmiersprachen bis heute an. Zum einen gibt es da die Sprachen wie C. C stammt in direkter Linie von der Maschinensprache ab. Ich bezeichne es gerne als prozessorarchitekturunabhängigen Assembler. Damit lassen sich recht einfach Programme schreiben, die sehr effizient mit der Maschne umgehen. Auf der anderen Seite gibt es Sprachen wie LISP und Smalltalk. Diese beruhen auf wenigen, aber mathematisch fundierten Grundprinzipien. Die Effizienz kann zwar auch von Programmierer beeinflußt werden, liegt aber zuerst einmal bei dem Implementierer der Compiler und der Laufzeitumgebung.

Kontrollstrukturen

Wichtige Elemente von Programmiersprachen, sind zum einen Funktionen und zum anderen Kontrollstrukturen. In der Regel wird eine Programmiersprache mit einer Bibliothek von Funktionen ausgeliefert. Der Benutzer kann aber - und muß, wenn er etwas sinnvolles bewerkstelligen will - aber eigene Funktionen hinzufügen. Bei den Kontrollstrukturen ist das aber bei Sprachen wie C oder auch Java nicht möglich. Wenn es noch keine foreach-Schleife gibt, um über eine Sammlung von Objekten zu iterieren, so muß man halt auf eine neue Version der Sprache warten, die dieses Kontrollstruktur hinzufügt.

Bei Smalltalk z.B. ist das aber anders. Diese Sprache ist das extremste Beispiel für das "alles ist ein Objekt"-Prinzip. Kontrollstrukturen basieren hier auch auf Objekten. Eine If-Else-Struktur basiert auf Objekten vom Typ Boolean. Dieser Typ besitzt Methoden IfTrue: oder IfElse:. Diesen Methoden werden Codeblöcke als Argumente mitgegeben, die abhängig vondem Wert des Objekts ausgeführt werden. Bei den Codeblöcken handelt es sich um dasselbe, was wir letzte Woche als Closure kennengelernt haben.

Warum geht ähnliches nicht mit Java? Wenn wir versuchen, ein Commandobjekt zu bauen, das ein Attribut des Typs bool besitzt und zwei mit der Signatur eines Commandobjekts, so kann man eine Verknüpfung erreichen, die in eine If-Else-Verzweigung simuliert. Dies wird gelegentlich auch so implementiert, wenn man den Programmablauf zur Laufzeit zusammensetzen muß.

Das Problem hier ist, daß das alles extrem umständlich wird, da man den Objekten alles explizit mitgeben muß, was sie benötigen. In einem normalen If-Else-Konstrukt sind aber alle Elemente des Kontextes, in dem das Konstrukt exisitert verfügbar, ohne das sie extra hinkopiert werden müßten.

Wenn ich Closures in meiner Programmiersprache unterstütze, so kann die Sprache jederzeit um neue Kontrollstrukturen erweitert werden. Recht verbreitet sind hier individuelle Schleifen, die über eine Sammlung von Objekten iterieren und hierbei die innere Struktur der Sammlung kennen. Siehe z.B. folgenden Ruby-Code:

a = [ "a", "b", "c", "d" ]
a.collect {|x| x + "!" } #=> ["a!", "b!", "c!", "d!"]

Flexibilität für alle?

Wenn man die Sprache so leicht erweitern kann, so kann man sehr kompakten und übersichtlichen Code schreiben. Wie alles ist aber auch dies nicht umsonst zu haben. Leute, die von der C-Denkschule kommen, müssen zuerst umlernen. Außerdem gilt hier noch mehr als in anderen Fällen, daß es immer schwerer wird, neue Mitarbeiter für ein bestehendes Projekt einzuarbeiten, da zu Beginn die allgemeinen Kenntnisse der Programmiersprache nur eine Grundlage geben, aber bei weitem nicht ausreichen, den Programmcode lesen zu können.
Insbesondere LISP-Programmierer sind berüchtigt dafür, daß sie für jedes Projekt praktisch eine eigene Programmiersprache entwickeln, die auf Lisp aufbaut und die typische Klammerstruktur besitzt, aber über ganz eigene Kontrollstrukturen verfügt.
Der Vorteil aber ist, daß, nachdem die Grundlagen geschaffen sind, die Arbeit deutlich vereinfacht wird und die Anzahl von Fehlern dadurch zurückgeht.
Der Trend in den modernen Programmiersprachen geht eindeutig in diese Richtung. Schon C# 3.0, Ruby und Python bieten viele Möglichkeiten, ganz zu schweigen von aufkommenden Sprachen wie Scala und F#.

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.

Sonntag, 25. Mai 2008

Enten und andere Typen

In Java-Kreisen wird gerne versucht, Ruby herunterzureden. Die Argumente dabei sind oft nicht sonderlich gut durchdacht und haben mehr mit diffusen Ängsten als mit wirklicher Kenntnis der Sachverhalte zu tun. Diese Art wertloser Diskussion ist oft anzutreffen, wenn es um zwei wirklich oder vermeintlich konkurrierende Technologien geht. Es geht hier nicht darum, die bessere zu finden, sondern darum den Wert der Investition zu erhalten, die jemand in das Erlernen einer der Alternativen gesteckt hat.

Ein Argument, daß von den Java-Befürwortern gegen Ruby of aufgebracht wird, ist, daß durch das Java-Typsystem viele Fehler schon vom Compiler gefunden werden, die in Ruby erst zur Laufzeit auftreten, was das Debuggen entsprechend teurer machen soll. Ich habe neulich ein paar hundert Zeilen lang ein etwas komplizierteres Ruby-Programm geschrieben und dabei darauf geachtet, welche Probleme auftraten und diese mit meinen Javaerfahrungen verglichen.

Duck-Typing

Häufig wird gesagt, Sprachen wie Ruby seien "untypisiert". Dies stimmt so nicht. Da Ruby objektorientiert ist, erlaubt es natürlich die Definition von Klassen und Klassen sind Typen. Was anders behandelt wird, ist die Deklaration von Variablen und Parametern. Diesen können erst einmal alle Objekte unabhängig von Typ zugewiesen werden, im Laufe der Programmausführung auch unterschiedliche Typen für dieselbe Variable. Der Typ wird erst überprüft, wenn eine Methode des Objekts aufgerufen wird. Und hier geschieht das ganz pragmatisch. Wenn die Methode vorhanden ist, so wird sie aufgerufen, ansonsten wird eine spezielle Methode aufgerufen, die alle anderen Fälle abhandelt. Diese Typisierung wird oft als Ducktyping bezeichnet: egal, was der Zoologe dazu sagen würde, wenn es nach Ente schmeckt und eine knusprige Entenhaut besitzt, dann wird es wohl Ente sein.

Jetzt gibt es die Behauptung, daß hierdurch sicheres Refactoring erschwert würde. Da ist etwas dran, doch ist die Lage nicht so hoffnungslos. Schließlich wurde das erste brauchbare Tool zum Code-Refactoring für Smalltalk entwickelt, das ein vergleichbares Typsystem besitzt.

Wie sieht es nun mit den Programmierfehlern aus? In der Tat kommen immer wieder lästige Fehler vor, die Java verhindert hätte. Allerdings handelt es sich fast nie darum, daß ein Objekt einen falschen Typ hätte. Vielmehr führen Tippfehler dazu, daß zwei Objekte da sind, wo nur eins sein sollte. Diese Art von Fehler wird in Sprachen wie Lisp und Smalltalk dadurch verhindert, daß Variablen zuerst deklariert werden müssen, ehe sie verwendet werden können. In Skriptsprachen, zu denen Ruby gehört, entsteht eine Variable in dem Augenblick, in dem sie zum ersten Mal verwendet wird. Gegenüber Python ist Ruby da sogar noch im Vorteil, da in Ruby der Gültigkeitsbereich der Variable im Namen angegeben wird (durch kein, ein oder zwei @).

Was in der Argumentation unterschieden werden sollte, ist Deklaration und Typisierung von Variablen. Wie immer erschwert es auch hier die Argumentation, wenn zwei Dinge, die nur lose etwas miteinander zu tun haben, immer wie eins behandelt werden.

Und wie war es sonst mit Ruby?

Mit Ruby schreibt man drastisch weniger Code als mit Java. Dadurch macht man natürlich auch weniger Fehler. Allerdings gibt es auch voll typisierte Sprachen, die wesentlich dichteren Code zu schreiben erlauben wie Java (z.B. Scala).

Ruby-Code ist vergleichsweise langsam. Das ist nichts Neues und macht häufig auch nichts aus. Allerdings dürfte sich die Lage demnächst auch verbessern, wenn nicht bei nativem Ruby, dann sicht durch jRuby. Letzteres scheint heute schon in vielen Bereichen schneller zu sein. Es benutzt die JVM, die ein ausgereiftes Memorymanagement Codeoptimierung mitbringt.


Sonntag, 18. Mai 2008

Ein Bild sagt mehr als tausend Worte ...

Dies ist ein beliebter Spruch, mit dem die Hersteller von graphischen Tools zur Programmentwicklung werben. Was aber bedeutet das für den Arbeitsalltag?

Lesen von Graphiken

Der zitierte Spruch bedeutet zwar, daß in einem Bild die Information von mehr als tausend Worten steckt. Dies bedeutet aber nicht notwendigerweise, daß diese Information schneller aufzunehmen wäre. Auch in ein Kunstwerk kann man sich verlieren und darin immer wieder neue Dinge entdecken. So ist das auch, wenn endlich die Tapete aufgehängt wird, nachdem der Architekt nach wochenlanger Arbeit vom DIN-A0-Plotter mit dem neuen Datenmodell zurückgekommen ist. Nach Tagen findet man dann in nur wenigen Minuten das Objekt, mit dem man gerade arbeitet und kann dann rasch feststellen, in welchem Zusammenhang es sich befindet. Vorausgesetzt die Verbindungslinien sind nicht zu lang und klar voneinander getrennt.

Meist wäre dem Betrachter wahrscheinlich eher mit einem gut verlinkten Dokument mit zusätzlichen Suchfunktionen gedient, mit denen der die Verbindungen zwischen zwei Objekten, die nächsten Nachbarn und ähnliches übersichtlich angezeigt bekäme.

Allerdings kann ein Übersichtsdiagramm, daß z.B. die wesentlichen Komponenten eines Systems und ihre Zusammenarbeit darstellt, ganz neue Einblicke ergeben.

Erzeugen von Graphiken

Der Umgang mit einem graphischen Werkzeug ist in der Regel wesentlich anstrengender wie das Schrieben von Texten. Täglich mehrstündiger Umgang mit der Maus führt bei den meisten Benutzern zu schmerzhaften Verspannungen. Außerdem ist die Informationseingabe meist deutlich langsamer. Besser sieht es aus, wenn eine textuelle Eingabe möglich ist, die dann von einer Software visualisiert wird. Dieser Ansatz ist aber eher selten.

Ebenen des Einsatzes von graphischen Werkzeugen

  • Skizzieren: Hier wird eine Idee graphisch dargestellt, wobei es nicht auf die Details ankommt, sondern nur das Prinzip dargestellt werden soll. Meist ist es hierbei schneller, die Skizze händisch zu erstellen. Ein Werkzeug, daß bei der Eingabe zuviel überprüft ist weniger hilfreich.

  • Modellieren: Hier ist es wichtig, daß der modellierte Aspekt vollständig und korrekt erfaßt wird. Es sollte aber nur auf einer bestimmten Abstraktionsebene gearbeitet werden. Eine Detaillierung bis zu den technischen Details gehört hier nicht hin. Da das Modell abstrakter sein sollte als das modellierte System, ist es zwar manchmal möglich, eine technische Implementierung automatisch zu erstellen (MDA-Ansatz), aber der umgekehrte Weg ist wenig sinnvoll.

  • Visualisieren: Die Graphik ist hier nur eine alternative Darstellung des Programms. Viele Werkzeuge erlauben Änderungen in beiden Darstellungen.

UML-Dilemma

Die oben beschriebenen Einsatzmöglichkeiten werden alle mehr oder weniger gut von den gängigen UML-Werkzeugen unterstützt. Dabei gibt es aber das Problem, daß die Validierung der Diagramme im einen Fall notwendig, im anderen aber eher weniger sinnvoll ist. Dies könnte dadurch geregelt werden, daß es konfigurierbare Sätze von Validierungsregeln gibt, die konfiguriert werden könnten. Dies ließe sich z.B. gut mit OCL durchführen. Die meisten UML-Werkzeuge sind aber nicht entsprechend flexibel.

Graphisches Programmieren

Immer wieder kommen Werkzeuge auf den Markt, die eine graphische Benutzeroberfläche zur Programmerstellung bieten. Derzeit ist das z.B. für BPEL sehr beliebt. Diese Tools haben gewisse Probleme:

  • Meist ist zusätzliche Information hinter den Symbolen verborgen. Was zuerst sehr einfach aussieht, entpuppt sich dann doch als komplex.

  • Sich wiederholende Aufgabe lassen sich auf der graphischen Ebene schlecht automatisieren. Hier muß die Textebene verwendet und auch verstanden werden.

  • Manchmal werden vom graphischen Editor Dinge nicht unterstützt, die in der Textdarstellung eigentlich möglich sein müßten. Ob das resultierende Programm dann lauffähig ist, ist zunächst unklar.

Versionierung und Vergleich

Für alle in der Entwicklung wichtigen Dokumente ist eine Versionierung notwendig. Je nach Tool ist es unklar, was in die Versionierung übernommen werden soll. Binäre Dateien sind wenig hilfreich.

Ein Vergleich zwischen den verschiedenen Versionen einer Graphik ist schwierig. Manche graphischen Werkzeuge bieten Unterstützung an. Diese arbeitet aber häufig auf eigenen Repositories und läßt sich schlecht mit der Versionierung sonstiger Dokumente verbinden. In anderen Fällen kann man die Dateien als Text vergleichen. Dann muß man aber die Struktur dieser Dateien verstehen (z.B. XMI für UML-Werkzeuge).

Zusammenfassung

Während Graphiken häufig sehr nützlich sind, um Ideen zu vermitteln, gibt es doch auch gravierende Probleme, wenn ausschließlich damit gearbeitet werden soll.

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.