Sonntag, 24. Februar 2008

Was sind das für Zustände

In jeder Einführung zur objektorientierten Programmierung wird erklärt, daß ein Objekt einen Zustand besitzt, der durch die Werte der Attribute des Objekts bestimmt ist, und Methoden, die diesen Zustand manipulieren können. Was man häufig findet, sind Methoden, deren Wirkung von dem Zustand des Objekts abhängt. Beispielsweise könnte eine Klasse Wasser eine Methode "wird von Stein getroffen" besitzen. Diese Methode gibt bei einer Temperatur unter 0°C "knirsch" aus, zwischen 0°C und 100°C aber "platsch".

Bestimmte Anwendungen neigen besonders zu solchen Abhängigkeiten. Komplexe Eingabemasken aktivieren oder deaktivieren Eingabeelemente basierend auf Attributen. In Workflowsystemen ist es geradezu das Ziel, daß das Verhalten von dem inneren Zustand abhängt.

Wenn man sich den Code solcher Anwendungen ansieht, findet man häufig erstaunlich komplexe Abfragen, die dazu dienen, den aktuellen Zustand zu bestimmen. Besonders gruselig wird es, wenn Flags eingeführt werden und die Abfrage dann über diverse Flags gemischt mit natürlichen Werten gemacht wird.

Dieser ad hoc Umgang mit Zustand führt dazu, daß immer wieder subtile Fehler auftreten und die Stabilität der Anwendung nur sehr langsam konvergiert. Da eine solche Vorgehensweise durch Tools wie Visual Basic gefördert wird, ist es oft erstaunlich teuer, wenn man damit einen unerfahrenen Entwickler eine Maske entwickeln läßt.

Wo liegen die Probleme?

  1. Die Anzahl der möglichen Zustände ist nicht klar definiert. Aus willkürlicher Kombination von Merkmalen zur Bestimmung des Zustands ergibt sich häufig eine viel zu große Anzahl von möglichen Zuständen.

  2. Die Vollständigkeit der zu betrachtenden Übergänge ist nicht einfach festzustellen. Es kann leicht passieren, daß ein Zustand überhaupt nicht erreicht werden kann.

  3. Es kann sein, daß sich die Definitionen zweier Zustände unabsichtlich überlappen. Wenn der eine Zweig bei "grün" durchgeführt wird, der andere aber bei "rund" und beide führen zu jeweils unterschiedlichen Folgezuständen, so hängt der resultierende Zustand bei Objekten, die sowohl "grün" als auch "rund" sind von der Reihenfolge der Abarbeitung ab, was insbesondere zu interessanten Bugs führt, wenn die Abarbeitung in parallelen Threads erfolgt.

Also, was tun? Sobald Methoden von dem Zustand der Objekte abhängig werden, sollte man ein Zustandsdiagramm malen - entweder mit einem UML-Tool oder auf einem Schmierzettel. Darin werden alle möglichen Zustände festgehalten und die Bedingungen unter denen ein Zustand in einen anderen wechselt. Die Anzahl von Zuständen muß endlich sein, andernfalls müßten auch unendlich viele Verzweigungen in der Programmlogik notwendig sein. Dadurch, daß Bedingungen zum Zustandswechsel angegeben werden, wird ein entscheidender Wechsel in der Perspektive durchgeführt. Ursprünglich haben wir immer den aktuellen Zustand überprüft, wenn wir eine Entscheidung in der Programmlogik durchgeführt haben. Jetzt betrachten wir die Ereignisse, die einen Wechsel des Zustands bedingen. Dadurch wird unser drittes Problem automatisch gelöst. Das zweite Problem läßt sich so auch einfacher überprüfen und das erste wurde bereits durch die Auflistung der für den Ablauf notwendigen Zustände angegangen.

Jetzt führen wir eine Enumeration für den Zustand in die Klasse ein und können danach einfache Entscheidungen im Programmablauf durchführen. Oder wir verbinden den Code direkt mit dem Zustand und kommen zu dem GoF Zustandmuster.

Dieses Zustandsattribut erscheint redundant zu sein. Eigentlich läßt es sich aus den übrigen Attributen berechnen. Machen wir das, so führen wir durch die Hintertür in abgeschwächter Form wieder unsere alten Probleme ein. Was aber Sinn macht, ist, Zusicherungen einzubauen, die überprüfen, ob der aktelle Zustand mit dem errechneten Zustand übereinstimmt. Ist dies nicht der Fall, liegt ein Programmfehler vor.

Ein Zustandsdiagramm läßt sich leicht in Code überführen. Es gibt auch Tools, die den Code automatisch aus einem UML-Diagramm generieren. Dadurch, daß wir die Zustandsbehandlung von der übrigen Logik getrennt haben, haben wir eine erhebliche Vereinfachung erreicht. Jetzt wird die Eingabemaske auch in der geschätzten Zeit fertig und im Workflow bleiben keine Aufträge mehr überraschend hängen.


Sonntag, 17. Februar 2008

Wo war doch nochmal diese Klasse?

Was meinen Schreibtisch betrifft, so bin ich nicht einer von denen, die alles sofort sorgfältig abheften. Vielmehr bevorzuge ich eins dieser sehr persönlichen Ablagesysteme, die aus zwei bis drei Stapeln Papier bestehen. Diese wirken wie ein Cache. Alles was aktuell oder häufig gebraucht ist, befindet sich oben griffbereit. Alles was sich nach unten hin absetzt, ist offensichtlich lange nicht gebraucht worden und kann alle paar Monate entsorgt werden. Dies ist für die persönliche Ablage sehr effektiv.

Wenn wir aber ein Softwaresystem betrachten, so gibt es zwei entscheidende Unterschiede. Erstens arbeiten gewöhnlich mehrere Leute daran, die mit auf den Charakter Einzelner zugeschnittenen Ablagesystemen überwiegend nichts anfangen können. Zweitens sind selten angefaßte Quellen nicht notwendigerweise überflüssig. Gerade Kernkomponenten eines Systems werden oft nur sehr selten angefaßt, da sie im Gutfall so ausgereift sind, daß sich jeder blind darauf verläßt, und andernfalls traut sich keiner mehr, da etwas zu verändern.

Spätestens, wenn wir mehr Zeit mit Suchen verbringen als mit Kodieren, ist es Zeit darüber nachzudenken, wie man Ordnung in die Softwarequellen bringt.

Im Prinzip gibt es zwei Möglichkeiten Ordnung in die Softwarequellen zu bringen:

  • Namensgebung

  • Modulbildung

Namensgebung

In früheren Zeiten gab es für große Softwareprojekte Namenskonventionen, die es erlaubten, trotz meist nur kurzer Namen aus diesen direkt die Funktion des benannten Gegenstands zu erkennen. Ein bekanntes Beispiel sind die X$-Tabellen in Oracle, die den V$-Views zugrunde liegen. Eine Tabelle heißt x$kcbwait. An den ersten beiden Buchstaben (kc) läßt sich sofort erkennen, daß sie Teil des Cache-Layers ist.

Heute ist die Welt etwas einfacher geworden. Moderne Programmiersprachen unterstützen deutlich längere Namen und Namensräume. Namensräume erlauben es für einen Namen einen Kontext zu definieren, in dem er gebraucht wird. Derselbe Name kann dann in unterschiedlichen Kontexten verwendet werden ohne daß es zu Problemen kommt. Meist sind die Namensräume hierarchisch organisiert.

Module

Die andere Strukturierungsmöglichkeit ist die Aufteilung in Module. In der Entwicklungsumgebung werden dazu unterschiedliche Projekte angelegt. Daraus entstehen dann separate Dateien, z.B. jar-Dateien (Java) oder Assemblies (dotnet).

Ein Wildwuchs in der Zerlegung in Modulen ist der sichere Weg ins Desaster. Wenn hier kein klares Konzept vorliegt, wird die in der Überschrift zitierte Frage zum Leitthema der Entwicklung.

Die Aufteilung in Module bietet sich an, wenn die gleiche Funktionalität an verschiedenen Stellen installiert werden soll, z.B. sowohl in einem Client-Programm wie in einer Webserveranwendung. Hier empfiehlt es sich, zuerst etwas wie ein UML-Deployment-Diagramm zu zeichnen.

Ein anderer Grund, einzelne Module zu bilden, ist es, geschlossene Funktionalität so zusammenzufassen, daß die Module in verschiedenen Versionen der Software unverändert übernommen werden kann, wenn sich die notwendigen Änderungen nur auf andere Module beziehen. Wenn bei der Planung der Softwareänderung darauf geachtet wird, welche Komponenten betroffen sind, kann man sich viel Arbeit bei dem Verzweigen der Konfigurationen sparen.

Gute Module zeichnen sich durch klar definierte Schnittstellen aus, die sie nach außen veröffentlichen. Nicht alle Programmiersprachen liefern hier eine gute Unterstützung.

Es ist extrem empfehlenswert, die Namensräume mit den Modulen zu synchronisieren. D.h. der Namensraum sollte klarstellen, in welchem Modul sich die Klasse befindet.

Java

In Java gibt es Packages. Diese bieten sowohl hierarchische Namensräume als auch einen Ansatz der Modularisierung. Der vom Compiler erzeugte Bytecode ist in einem Verzeichnisbaum abgelegt, der genau die Namenshierarchie der Packages entspricht. Die Quellen müssen nicht so geordnet sein. Dies ist eine prima Methode Verwirrung zu stiften. Gleichzeitig kann mit der Package-Struktur der Zugriff auf Klassen geregelt werden. Wenn kein Zugriffsrecht (nicht private, public oder protected) definiert ist, ist der Zugriff nur innerhalb des Packages erlaubt. Hierbei spielt die Hierarchie der Packages leider keine Rolle, sondern der gesamte Namensraumteil des Klassennamens muß übereinstimmen. Dafür kann aber auch von einem anderen jar aus zugegriffen werden, wenn dort der gleiche Namensraum existiert.

Damit eignet sich diese Zugriffsreglung kaum für die Unterstützung von Modulbildung. Die einzige sinnvolle Anwendung ist es, den Zugriff für eng gekoppelte Klassen zu regeln, wie sie z.B. in vielen der GoF-Patterns zu finden sind, wo eine Klasse des Musters mehr über einen Partner wissen muß, als man guten Gewissens public machen möchte.

Das OSGi-Framework geht diese Schwäche der Java-Programmiersprache an und ist dabei so erfolgreich, daß Ideen davon möglicherweise in eine der nächsten Versionen der Sprache wandern werden.

C#

In C# gibt es Namensräume die unabhängig von anderen Eigenschaften flexibel angelegt werden können. Für die Zugriffsreglung gibt es das Schlüsselwort internal. Die damit gekennzeichneten Elemente sind nur innerhalb der Assembly zu sehen. Insofern kann dies dazu genutzt werden, den Zugriff zwischen Assemblies einzuschränken, um zu einem klaren Interface zu kommen. Wenn man also den Verdacht hat, daß andere auf ein Element zugreifen wollen, daß man verändern möchte, so kann man es auf internal setzen und sieht beim nächsten Build, wer betroffen ist.

Fazit

Moderne Entwicklungsumgebungen haben meist sehr gute Werkzeuge, die es erlauben im Code zu navigieren. Daher scheint die Ordnung nicht ein so großen Problem zu sein. Es zeigt sich aber, daß ab einer gewissen Größe des Projekt diese Unterstützung plötzlich zusammenbricht. Hinzu kommen Dinge, die die Entwicklungsumgebungen nicht so gut in Griff haben wie z.B. Konfigurationsdateien (Spring, JEE-Deploymentdeskriptoren ...). Daher sollte von Anfang an darauf geachtet werden, daß man jede Klasse unmittelbar lokalisieren kann, wenn man nur den Explorer dazu zur Verfügung hat. Die dadurch gesparte Zeit kann den Unterschied zwischen Sein und Nichtsein ausmachen.

Sonntag, 3. Februar 2008

Refactoring von Datenbanken

Scott Ambler hat eine Menge für die Verbreitung agiler Vorgehensweisen bei der Softwareentwicklung getan. Es gibt aber einen Punkt, in dem ich jedes Mal, wenn ich in Konferenzen mit ihm zusammentreffe, erhebliche Meinungsunterschiede feststelle. Dabei handelt es sich um den agilen Umgang mit Datenbanken. Bislang sind wir noch nicht zu einer gemeinsamen Sichtweise zu gekommen. In einem Punkt stimme ich jedoch zu. Wenn ein Datenbankschema ein Problem zeigt, so sollte man versuchen, es zu beheben. Und es ist sicher Zeit, die hierzu notwendigen Techniken zu katalogisieren, wie es Fowler für das Refactoring von Software gemacht hat.

Was ist speziell an Datenbanken?

Nicht speziell ist, daß es häufig zahlreiche Nutzer einer Datenbank gibt, die zum Teil noch nicht einmal bekannt sind. Dieses Problem gibt es auch in der Softwareentwicklung, wenn man sich auf das heutzutage so populäre Paradigma der Serviceorientierung einläßt.

Was aber besonders ist, ist daß die Datenbank Daten enthält, die wesentlich länger Bestand haben als alle Programme, die damit umgehen. Diese Daten sind ebenso wertvoll wie empfindlich. Hier gilt das Zweite Gesetz der Thermodynamik. Wenn nicht Energie hineingesteckt wird, nimmt die Entropie unweigerlich zu, was bedeutet, daß Information unwiderbringlich verloren geht. Daher lassen sich viel Änderungen in der Datenbank im Gegensatz zu Softwarerefactorings nicht rückgängig machen. Zum Beispiel kann man auf die Idee kommen, zwei Felder "Vorname" und "Nachname" zu einem Feld "Name" zusammenzufassen. Versucht man, nach diesen Vorgang wieder rückgängig zu machen, wird man feststellen, daß es keinen zuverlässigen Algorithmus gibt, für beliebige Namen Vor- und Nachnamen zu trennen. Es gibt Menschen mit nur einem Namen. In China wird gewöhnlich der Nachname zuerst genannt ...

Ein weiteres Problem ist, daß manche wünschenswerten Änderungen kaum durchführbar sind, da eine zu große Menge an Daten zu reorganisieren wäre, daß das nicht in dem verfügbaren Wartungsfenster durchführbar ist.

Diese Probleme gelten natürlich nur, wenn die Datenbank bereits in Produktion ist. Während der Entwicklung an einem Neusystem darf man fröhlich mit der Datenbank experimentieren.

Testen von Datenbanken

Wie immer bei Refactorings empfiehlt es sich, einen guten Satz von Tests zu haben, um sicherzustellen, daß nach der Änderung noch alles funktioniert. Wie kann ich nun ein Testset erstellen, um die Funktion einer Datenbank sicherzustellen?

Scott Ambler empfiehlt hier Tools wie DbUnit. Dabei handelt es sich um eine Erweiterung von JUnit speziell für Datenbanken. Dies ist sicher der richtige Weg, um die korrekte Funktion von Triggern und Stored Procedures sicherzustellen. Viel wichtiger für die Datenintegrität sind aber korrekte, durch Constraints abgesicherte, Relationen zwischen den Tabellen. Wenn ich aber hier Unittests schreibe, die z.B. versuchen, einen abhängigen Datensatz zu löschen und überprüfen, ob eine Exception geworfen wird, so teste ich überwiegend die Datenbanksoftware und kaum mein Datenbankschema. Die Frage ist, ob das Constraint richtig gesetzt ist, und nicht, ob Oracle die Überprüfung des Constraints richtig implementiert hat.

Hier scheint mir die einzige richtige Vorgehensweise zu sein, das Datenbankschema in einem Datenbankentwurfstool zu entwerfen, das in der Lage ist, diesen Entwurf automatisch mit der Implementierung abzugleichen.

Wann riecht die Datenbank komisch?

Bei der Software hat sich der Begriff "Codesmell" eingebürgert für eine Implementierung, die nicht optimal ist und durch Refactoring verbessert werden sollte. Hier einige Beispiele für Datenbankgerüche:

  • Feld- und Tabellennamen: Ähnlich wie in der Software sollten alle Datenbankelemente einen Namen haben, der klar ausdrückt, worum es sich handelt. Bei der Datenmodellierung sollten nur Begriffe verwendet werden aus dem mit dem Kunden abgestimmten Sprachgebrauch. Dazu kommt noch die Notwendigkeit einer einheitlichen Namenskonvention für Schlüssel- und Referenzfelder. Wenn hier gesündigt wird, wird die Datenbank unlesbar.

  • Normalisierung: In aller Regel sollte eine relationale Datenbank normalisiert sein. Das heißt zunächst einmal, daß eine Information immer nur genau einmal abgelegt wird. Es ist ein leider weitverbreiterter Irrglaube, daß Normalisierung zu schlechterer Performance führt. Dies ist mag bei Lesezugriffen sogar manchmal zutreffen, ist aber auch hier meist beherrschbar. Die Ausnahme bilden nur Datawarehouses, bei denen viele Ad-Hoc-Abfragen gemacht werden, die sich vorher nicht tunen lassen. Das Schreiben in eine richtig normalisierte Datenbank ist aber häufig dramatisch schneller. Ich habe schon zahlreiche Perfomanceengpässe in Datenbankanwendungen durch zusätzliche Normalisierung beheben können. Das wichtigste aber ist, daß bei unzureichender Normalisierung sich rasch Widersprüche in den Daten ergeben, wenn irgendeine auf die Datenbank zugreifende Anwendung nicht sauber programmiert ist.

  • Nicht zusammengehörende Daten in einer Tabelle: Manchmal werden in einer Tabelle Daten aus zwei verschiedenen Kontexten zusammen abgespeichert, nur weil ihre Struktur ähnlich ist. Dies macht die Datenbank nicht nur schwerer verständlich sondern erschwert auch die richtige Setzung von Constraints.

  • Zusammengehörende Daten in unterschiedlichen Tabellen: In Datenbanken, die über eine lange Zeit gewachsen sind, finden sich häufig identische Konzepte an unterschiedlichen Stellen. Diese sollten zusammengeführt werden.

Beispiel: Umbenennung einer Spalte

Scott Ambler schlägt hier vor, eine zusätzliche Spalte mit dem neuen Namen einzuführen, die Daten zu kopieren und mit Triggern sicherzustellen, daß alle Änderungen in einer Spalte auch auf die andere Spalte angewandt werden. Wenn nach einiger Zeit alle Anwendungen umgestellt sind, kann die alte Spalte entfernt werden.

Diese Lösung mag funktionieren, mir erscheint sie allerdings alles andere als optimal. Ich sehe folgende Probleme:

  • Das Kopieren der Daten ist bei großen Tabellen sehr aufwendig.

  • Durch die zusätzlichen Daten kann es zumindest bei Oracle zu einem Überlauf der Datenblöcke kommen, was zu den berüchtigten "chained rows" führt - sehr schlecht für die Datenbankperformance.

  • Alle Anwendungen, die mit "select *" arbeiten (schlechter Stil, kommt aber vor) bekommen sofort Probleme.

  • Die Trigger, auch wenn sie so geschrieben sind, daß sie nicht zu einer Endlosschleife führen (weil sie sich gegenseitig triggern), stellen einen vermeidbaren Overhead dar.

Mein Vorschlag hier ist, die Spalte nur umzubennen (geht bei neueren Oracle-Versionen zum Glück ohne großen Aufwand) und Altanwendungen eine View zur Verfügung zu stellen, in der die Spalte ihren alten Namen besitzt. Wenn alle Anwendungen die gleichen Datenbankaccounts verwenden, muß ich allerdings die Tabelle umbenennen, da View und Tabelle nicht den gleichen Namen haben könnnen. Das ist aber kein großes Problem, da die Zugrffe auf die Tabelle eh angepaßt werden. Wenn ich die Accounts unterschieden kann, kann ich die View auch in ein anderes Datenbankschema legen und mit privaten Synonymen arbeiten. Je nachdem wie die Datenbank aufgesetzt ist, muß hier geringfügig variiert werden.

Kapselung der Datenbank

Wenn niemand direkt Zugriff auf die Daten hat, ist man natürlich wesentlich freier in den Änderungsmöglichkeiten. Dies kann z.B. dadurch erreicht werden, daß nur Zugriff über Stored Procedures gestattet wird. Dies ist allerdings in vielen Fällen so nicht durchsetzbar, da dann alle neuen Datenbankzugriffe über die Datenbankgruppe implementiert werden müssen. Außerdem funktionieren die schicken O/R-Mapper nicht mehr so schön.