logo
falkoriemenschneider.de
Software: Projektmanagement, Architektur, Methoden.
home
blog

30. jul
23. jun
25. mai
24. jan

2009

Weblog Archiv 2010

ACID in verteilten Systemen, 30.7.2010

In verteilten Systemen (a.k.a SOAs) braucht das Thema Datenkonsistenz erschreckend mehr Aufmerksamkeit als in einem System ohne Verteilung, das sich einer ausgereiften Datenbank bedienen kann.

Datenkonsistenz? Da erinnern wir uns doch mal schnell an die Informationssysteme Vorlesung und das strapazierte Beispiel von der Überweisung eines Geldbetrags zwischen zwei Konten. Die Überweisung ist nur dann korrekt ausgeführt, wenn beiden Konten jeweils ein Buchungseintrag hinzugefügt wurde, nämlich eine Belastung auf dem Quellkonto und eine Gutschrift auf dem Zielkonto. Führt ein Fehler dazu, dass z.B. die Gutschrift auf dem Zielkonto fehlt, obwohl die Belastung auf dem Quellkonto vorhanden ist, dann ist -- den Daten zufolge -- Geld "verschwunden". Das widerspricht offensichtlich der beabsichtigten Realität, d.h. es gibt eine Inkonsistenz. Und selbst wenn beide Einzelbuchungen erfolgen, kann eine Abfrage, die zeitlich dazwischenkommt, ein -- so nicht reproduzierbares -- inkonsistentes Bild hervorbringen.

Datenbanktransaktionen und das ACID Paradigma sind in einem nicht-verteilten System das geeignete Mittel, die typischen vier Anomalien (Lost update, Dirty read, Non-repeatable read und Phantom read) zu verhindern und Fehlern in Teilen mit Rückabwicklung des Ganzen zu begegnen (a.k.a Rollback). In einem verteilten System hingegen haben wir möglichweise mehrere Datenbanken, und wenn sich dann eine aus fachlicher Sicht geschlossene Operation über mehrere Teilsysteme erstreckt, wobei Zugriffe auf die jeweiligen Datenbanken stattfinden, dann reichen uns die so bequemen Datenbanktransaktionen innerhalb der beteiligten Teilsysteme nicht mehr aus.

Nehmen wir als Beispiel eine Reisebuchung. Sie besteht aus drei Einzelbuchungen, die bei unterschiedlichen Unternehmen stattfinden, denn wir benötigen für unsere Reise ein Hotel, einen Flug und einen Mietwagen. Wir stellen uns die Reise in einem Webportal zusammen, geben unsere Kreditkartendaten ein, und drücken auf den "Jetzt alles verbindlich buchen!" Knopf. Was wir hier benutzen, ist ein verteiltes System, das aus wenigstens fünf Teilsystemen besteht. Das Webportal muss für die Reise an jedes der drei Buchungssysteme eine Nachricht schicken, und unsere Kreditkarte wird durch eine weitere Nachricht an den Server des Kreditkartenunternehmens belastet. Aus Benutzersicht handelt es sich um eine geschlossene Operation. Mit einem Flug ohne Hotel und Mietwagen ist uns wahrscheinlich nicht geholfen. Und eine Belastung unserer Kreditkarte ohne eine einzige erfolgte Buchung wäre sehr ärgerlich.

Pessimistic vs. Optimistic Locking

Wir erwarten also bestimmte transaktionale Eigenschaften, aber in diesem Beispiel längst nicht alle vier. Wir hätten gerne A(tomicity), also Ganz-oder-Gar-nicht. Für C(onsistency), also dass unsere Kreditkarte nur um den Betrag belastet wird, der auch tatsächlich den erfolgreichen Buchungen entspricht, wären wir auch dankbar. Auf I(solation) können wir dagegen verzichten. Aber D(urability), also dass die beteiligten Systeme unsere Buchungen nicht zwei Stunden später wieder "vergessen" haben, ist uns wieder sehr wichtig.

Ein Ansatz, der uns bei der Datenkonsistenzwahrung in verteilten Systemen viel Komfort verspricht, sind verteilte Transaktionen, z.B. realisiert durch ein 2PC Protokoll und überwacht durch einen zentralen Transaktionskoordinator. Um diesen einzusetzen bedarf es aber einiger starker Voraussetzungen:

  1. die beteiligten Teilsysteme "sprechen" das Protokoll
  2. die zeitlich enge Kopplung ist akzeptabel
  3. die Kosten durch Lizenzanschaffung und Betrieb des hochverfügbaren Koordinators sind tragbar

a) und b) sind meist nicht sicherzustellen, speziell im SOA Umfeld gilt "loose coupling", womit auch zeitliche Entzerrung durch Asynchronität gemeint ist, als Erfolgsfaktor bei der technischen Realisierung. Kurz: in der Praxis müssen wir i.d.R. auf verteilte Transaktionen verzichten.

Die Konsequenz ist, dass wir uns selbst wieder um Datenkonsistenz Gedanken machen müssen. Konkret bedeutet das zweierlei: erstens sollten wir solchen Entwurfsgrundsätze folgen, die das generelle Problem mildern, und zweitens müssen wir diejenigen Interaktionen zwischen Teilsystemen identifizieren, die bei Fehlschlägen oder durch fehlende Isolation Inkonsistenzen verursachen können. Langlaufende fachliche Transaktionen sind dafür gute Kandidaten. Für jedes einzelne Inkonsistenzrisiko müssen wir uns dann eine Lösung überlegen.

Mühsam, oder? Genau. Aber niemand hat behauptet, dass der Bau verteilter Systeme ein Kinderspiel ist. Der Rest dieses Textes beleuchtet einige praktikable Lösungsideen im Hinblick auf die Eigenschaften des ACID Paradigmas, die wir uns damit ein bißchen zurückerobern können.

Entwurfsgrundsatz: Zustandsminimierung
Je weniger Teilsysteme ihren eigenen Datentopf pflegen, desto weniger Inkonsistenzen sind zu erwarten. Im -- aus Konsistenzgesichtspunkten -- idealen Fall gibt es im Gesamtsystem nur eine Datenbank, die von einem Teilsystem verwendet wird. Aus Gesichtspunkten der Robustheit und Performanz ist das natürlich nicht sehr günstig, und in dem obigen Reisebuchungsszenario ohnehin unerreichbar. Wenn wir aber ohne größere Schmerzen aus zwei oder mehr Datentöpfen einen machen können, ist das sicher ein Gewinn.

Entwurfsgrundsatz: Transaktionale Teilsysteme
Wenn ein Teilsystem nach Bearbeitung eines synchronen Requests den Erfolg vermeldet, oder asynchron die Nachrichtenentnahme aus einer Queue durch Commit bestätigt, dann muss es alle seine lokal nötigen Operationen erfolgreich abgeschlossen haben. Gilt diese Implikation bei allen Teilsystemen, dann haben wir eine wichtige Voraussetzung für mehr Verlässlichkeit.

Entwurfsgrundsatz: Idempotenz
In einem verteilten System muss ein Client bei asynchroner Kommunikation, die keine Zustellgarantie z.B. durch ein MOM bietet, mit Nachrichtenverlust rechnen. Selbst wenn die Zustellung garantiert funktioniert hat, kann der Client während der Ausführung nachfolgender Interaktionen abstürzen, und damit "vergessen", welche Nachrichten bereits erfolgreich zugestellt oder sogar bearbeitet wurden. Kurz: wir müssen damit rechnen, dass die gleiche fachliche Nachricht n-mal an ein Teilsystem übermittelt wird. Es ist daher eine enorme Entlastung, wenn jedes Teilsystem sich bzgl. der empfangenden Nachrichten idempotent verhält, also die 2te bis n-te Nachricht keine Wirkung auf den Empfänger mehr hat. Idempotenz muss je nach Art der Operationen gesondert "eingebaut" werden. So sind lesende Zugriffe ohne weiteres Zutun idempotent, genauso wie das Setzen von Werten. Das Hinzufügen oder Löschen von Geschäftsobjekten ist von sich aus erstmal nicht idempotent.

Kompensation
Atomarität bedeutet, dass entweder alle Interaktionen zwischen Teilsystemen ausgeführt wurden oder gar keine. Da im Verlauf einer Transaktion bis zur letzten Interaktion mit einem Fehler gerechnet werden muss, müssen wir eine Möglichkeit haben, alle bisher erfolgreichen Aktionen "zurückdrehen" zu können. Dieses "Zurückdrehen" kann nur durch Versendung kompensierender Nachrichten erfolgen. Im o.g. Reisebuchungsbeispiel muss daher zu jeder Buchungsoperation auch eine Stornooperation existieren. Hierbei kommt es uns nun zugute, dass unsere Teilsysteme für sich transaktional zuverlässig arbeiten.

Der Koordinator der langlaufenden Transaktion muss also Buch führen, was bereits ausgeführt wurde, und bei einem Fehler mechanisch mit einer (umgedrehten) Folge von Kompensationen reagieren. Da diese Funktionalität gut generalisierbar ist, wird die Ausführung solcher Prozesse gerne an Workflow- oder BPEL-Server übertragen, die zudem auch eine Überwachung von z.B. über Tage laufende Prozesse ermöglichen.

Kompensation

Mittels Kompensation und transaktionalen Teilsystemen erreichen wir so immerhin A(tomicity) und D(urability). C(onsistency) wird dadurch aber noch nicht durchgehend gesichert.

Versionszähler (a.k.a Optimistic Locking)
Im Rahmen einer langlaufenden Transaktion kann es erforderlich sein, ein Geschäftsobjekt zu Beginn über ein Teilsystem zu lesen, dieses im Laufe des Bearbeitungsprozesses zu verändern, und es schließlich geändert an das entsprechende Teilsystem zurückzugeben. Was ist, wenn es konkurrierende Bearbeitungsprozesse gibt, die das gleiche Geschäftsobjekt verändern? Wir stehen vor einem Lost update.

Um Daten vor konkurrierenden Änderungen zu schützen, gibt es zwei Techniken: Sperren und Versionszähler. Gerade in der unvorhersagbaren Welt verteilter Systeme sind Sperren selten sinnvoll. Sie behindern Parallelität (und damit mindern sie Durchsatz), und sie könnten Datensätze unakzeptabel lange blockieren, wenn z.B. der Prozess, der die Sperre angefordert hat, "verstorben" ist. Daher wird statt eines Sperrvermerks ein Versionszähler zu betroffenen Geschäftsobjekten hinzugefügt. Stimmt die Version, die kurz vor Abschluss des Prozesses im Geschäftsobjekt zu finden ist, nicht mehr mit der überein, die zu Beginn des Prozesses gelesen wurde, kann man auf einen Störenfried schließen, der offensichtlich schneller war... und muss den Verlierer dieses Wettlaufs zurückrollen. Dadurch erhalten wir aber ein gutes Stück C(onsistency).

Pessimistic vs. Optimistic Locking

Schwebeverarbeitung
Bisher gab es keine Isolation langlaufender Transaktionen. Es fehlt also die Illusion, dass sie nacheinander statt nebeneinander ablaufen. Im Reisebuchungsszenario ist diese Eigenschaft verzichtbar, bei der Bearbeitung bspw. eines Versicherungsvertrags möchte man aber während des Bearbeitungsprozesses verhindern, dass mit der unsauberen Fassung gearbeitet wird. Wir fürchten also ein Dirty read, und verlangen Isolation.

Hier hilft uns eine simple Idee, die mir als "Schwebeverarbeitung" bekannt ist. Der Bearbeitungsprozess erzeugt zunächst eine Kopie des Geschäftsobjekts, die sogenannte "Schwebe". Alle Operationen richten sich an diese Schwebe, und erst der fachliche Abschluss führt zur Ersetzung des bisherigen Originals durch den Stand der Schwebe. Diese Idee bringt uns I(solation) im Bezug auf das betroffene Geschäftsobjekt. Sollte man mehrere Geschäftsobjekte in verschiedenen Teilsystemen bearbeiten müssen, so ist die Schwebeverarbeitung in allen Teilsystemen zu implementieren.

Schwebeverarbeitung

Aus den Lösungsideen geht zum einen hervor, dass wir nicht auf Konsistenz verzichten müssen. Es wird aber auch deutlich: die Anstrengungen, die man für transaktionale Eigenschaften in verteilten Systemen unternehmen muss, können beachtlich sein. Unnötige Verteilung, weil es gerade en-vogue ist, verbietet sich daher. Dort, wo Verteilung unvermeidlich ist, können wir uns meistens genügend ACID erkämpfen, um der Fachlichkeit gerecht zu werden. Mit ziemlicher Sicherheit werden Fachbereichler bzgl. ACID schnell maximale Ansprüche stellen. Dann muss der Softwarearchitekt zusammen mit Fachbereichlern und Projektleitung Kompromisse aushandeln, denn er muss auch Entwicklungskosten, Performanz, Robustheit und Wartbarkeit im Auge behalten.


Große Softwareprojekte und Lernen, 23.6.2010

Die halbe Wahrheit ist: ein Softwareprojekt erzeugt ein Softwareprodukt. Die andere Hälfte ist: notwendigerweise entsteht dazu immer eine Projektorganisation, also eine Struktur aus Rollen und Teams und den Prozessen, die von Mitarbeitern ausgeführt werden, damit am Ende ein auslieferfähiges Release entsteht. Große Softwareprojekte sind komplex, und folgerichtig ist auch die Organisation komplex. Außerdem sind Softwareprojekte per Definition einzigartig, jedes Projekt "erfindet" also seine individuelle Projektorganisation, obwohl es Ähnlichkeiten gibt.

Einzigartig und Komplex. Das klingt nicht danach, als würden die beteiligten Menschen sofort alles richtig machen können. Und daher ist es keine Überraschung, dass kaum ein Drittel aller Softwareprojekte, die der CHAOS Report 2009 der Standish Group untersucht, die ursprünglichen Ziele trifft. Alle anderen Projekte verpassen die Ziele oder erbringen gar kein nutzbares Ergebnis. Die Ursachen von Schwierigkeiten entstammen der Gemengelage aus Zeit- und Kostendruck, unklarer oder unrealistischer Zielsetzung, sich ändernden Rahmenbedingungen, mangelndem Rückhalt in der durchführenden Organisation oder auch Qualifikationsdefiziten der beteiligten Projektmitarbeiter.

Fakt ist: diese Gemengelage existiert schon lange, und wir beklagen sie, aber sie wird auch in der Zukunft immer bleiben. Wir -- als Softwareingenieure -- können mit dem Finger auf andere zeigen und behaupten: unter diesen Bedingungen kann man keinen Erfolg haben. Aber ändern wird sich dadurch nichts.

Nehmen wir es daher hin: große Softwareprojekte sind riskant, weil sie in widriger Umgebung starten, schwierig zu organisieren sind, und praktisch niemand der Beteiligten mit diesem individuellen Vorhaben Erfahrung hat, denn ein Projekt ist eine einmalige Angelegenheit. Aber wir sind dieser Situation nicht ausgeliefert, ganz und gar nicht. Sie entspricht grundsätzlich all jenen Situationen, in denen wir lernen müssen. Und da Menschen zumindest in ihren jungen Jahren ständig lernen, besitzen wir alle längst die Schlüsselqualifikation, um große Projekte in den Griff zu kriegen.

Wie funktioniert praktisches Lernen? In meinen eigenen Worten etwa so:

  1. Begreifen, was wir wollen
  2. Abgucken, wie andere dort hinkommen
  3. Selbst handeln
  4. Vergleichen unseres Ergebnis mit unserem Ziel
  5. Verändern unserer Handlungsidee, damit's beim nächsten Mal besser klappt
  6. Wiederholen
Das Entscheidende für eine Verbesserung ist die Rückkopplung durch 4. und 5. Wenn wir uns iterative und insbesondere agile Entwicklungsprozesse ansehen, dann erkennen wir sofort, das genau diese Rückkopplung zentraler Bestandteil ist. Das Lernen ist hier schon eingebaut. Wir sollten uns Rückkopplungen aber an viel mehr Stellen zunutze machen als nur an einem Iterationsende, denn wir wissen: je schneller wir Feedback zu einer Handlung bekommen, desto genauer können wir unser Handeln reflektieren, und desto leichter können wir Qualitätsmängel beseitigen. Der richtige Einsatz von Rückkopplungen sollte uns in unserem Projekt ultimativ sogar erlauben, Fehler zu vermeiden anstatt sie hinterher finden und beseitigen zu müssen.

Bis hierher habe ich nur eine abstrakte Idee genannt, was auch große Projekte beherrschbar macht. Ich möchte nun mit ein paar Beispielen zeigen, wie konkrete Rückkopplungen aussehen und wirken:

Schätzungen und Ist-Aufwandserfassung
Aufwandsschätzungen, die auf Basis einer Grobbeschreibung eines künftigen Softwaresystems entstehen, besitzen eine Genauigkeit von [0,25;4], mit anderen Worten: jede von einem Entscheider akzeptierte Aufwandszahl ist mit extremer Unsicherheit behaftet. Man könnte auch sagen: wir wissen nicht, wie teuer es wird. Zum Teil rührt diese Unsicherheit aus all den in dieser frühen Phase noch nicht getroffenen Entscheidungen her, zum Teil aber auch, weil das Team seine Produktivität, d.h. wieviel nutzbare Funktionalität pro PT herausspringt, nicht kennt. Unterschiedliche Teams sind unterschiedlich produktiv, sehr schlechte Teams verbrauchen ca. fünfmal mehr Aufwand als sehr gute. Um eine vernünftige Idee zu bekommen, ob ein Budget reicht, muss das Team früh eine Teilfunktionalität schätzen, erstellen, den tatsächlichen Aufwand festhalten und auf dieser Datenbasis die Budgethöhe plausibilisieren. Diese Rückkopplung kann sehr viel Geld sparen, wenn man z.B. durch sie feststellt, dass der Business Case nicht mehr aufgeht. Läuft das Projekt weiter, so lernt das Team mit dieser Rückkopplung, besser zu schätzen. Das Projekt wird vorhersagbar.

Benutzertests
Wenige Softwareentwickler haben ein Talent, Benutzerschnittstellen so zu bauen, dass Nicht-Technik-Verliebte sie als intuitiv bedienbar empfinden. Hat man eine große Zahl von Benutzern als Zielgruppe für ein Softwaresystem, so ist Benutzertauglichkeit kritisch: lehnen die Benutzer die Software ab, droht dem System das frühe Aus. Das Team muss also lernen, wie eine gute UI aussieht, und dazu braucht es als Rückkopplung Benutzertests. Und zwar sehr früh, denn UI-Entwicklung ist erstaunlich aufwändig und mit UI-Prototypen findet man nicht selten wesentliche Anforderungen an das System, die sonst übersehen würden. Wartet man zwei bis drei Iterationen, bis genügend Funktionalität durch Benutzer erreichbar ist, hat man vielleicht wertvolle Zeit verschenkt.

Continuous Integration
Große Software besteht aus vielen tausend Artefakten, von denen einige wenige eng mit der Arbeitsumgebung des Entwicklers verknüpft sind. Ein Entwickler kann sicherstellen, dass die Funktionalität, die er überblickt, auch nach seiner Codeänderung in seiner Umgebung in Ordnung ist, aber er kann kaum umfassend garantieren, dass nach seinem Checkin sämtliche Funktionalität auch in einer vollkommen unabhängigen Umgebung vorhanden ist. Continuous Integration hilft: hat sich etwas im Versionskontrollsystem geändert, dann wird das System wenige Minuten später vollständig gebaut und automatisiert getestet, und zwar in einer eigenständigen Umgebung. Fällt hierbei ein Fehler auf, wird der Entwickler automatisch informiert. Diese Rückkopplung erfolgt in weniger als einer Stunde, und der Entwickler kann ohne Rüstzeit die Fehlerbeseitigung vornehmen.

Frühe Lasttests
Ob ein System der Last im Wirkbetrieb gewachsen ist, kann man nur spät auf einer wirkbetriebsnahen Testumgebung feststellen. Es gibt allerdings eine ganze Reihe von Architekturschwächen und Programmierfehlern, die sich erst unter Last bemerkbar machen, aber sehr viel früher aufzudecken sind. Da reicht heute i.d.R. der PC des Entwicklers und ein OpenSource Lastgenerator (z.B. JMeter), damit das Projekt früh lernt, wie die Architektur verbessert werden muss. Diese Rückkopplung verhindert, dass späte weitreichende Umbaumaßnahmen erforderlich werden, um das gewünschte Skalierungsverhalten zu erzielen.

Ich könnte diese Beispielbeschreibungen fortsetzen, doch ich beschränke mich auf ein paar weitere Schlagworte zu Maßnahmen, die uns beim Lernen helfen: Code Reviews, Unittests, Softwaremetriken, Lessons Learned Meetings, Feedbackgespräche unter vier Augen, kurze Releasezyklen, System- und Integrationstests, Security Audits, Architekturdurchstich, Laufzeitmonitoring, ja selbst so etwas Selbstverständliches wie ein Compiler erzeugt laufend Rückkopplungen Richtung Entwickler.

Nur wenige Maßnahmen sind an ein Iterationsende gebunden, manches kann als fester Prozessschritt eingeführt, anderes als Einzelaufgabe behandelt oder gar durch geeignete Werkzeuge automatisiert werden. Als Projektleiter habe ich eine Ahnung, wo die Fallstricke liegen können, doch erst die konkreten Rückkopplungen retten mein Team vor dem Unfall.

Große Softwareprojekte sind schwierig, aber nicht unbeherrschbar. In jedem einzelnen muss das Team erst lernen, es zu beherrschen. Das Team muss sich also die Frage stellen, wie es effizient lernen kann. Und das wichtigste Mittel dazu sind gezielt gesetzte Rückkopplungen.


Raus aus dem Wolkenkuckucksheim, 25.5.2010

Softwarearchitekten besitzen viel Erfahrung mit dem Bau komplexer Softwaresysteme. Das ist der Grund, warum sie in einem Projekt die Verantwortung für das technische große Ganze tragen sollen. Damit haben sie primär zwei Aufgaben: erstens erarbeiten sie im Team mit Analysten und Entwicklern die Architektur, darunter die fachliche und technische Zerlegung des Gesamtsystems sowie kritische technische Lösungen. Und zweitens stellen sie sicher, dass die Ideen und Festlegungen tatsächlich im Code ankommen. Ist Letzteres nicht gewährleistet, kann man sich Ersteres im Grunde sparen.

Aber wie schafft der Architekt dies? Welche Mittel hat er dazu?

Er kann ein Softwarearchitekturdokument schreiben, z.B. ein 100-seitiges Papier, das die Aufgabenstellung (also Architekturziele, Rahmenbedingungen, architekturrelevante Anforderungen), und die Lösung (z.B. fachliche Zerlegung, technische Schichten, Produkt- und Werkzeuginfrastruktur, Lösungen zu Einzelaspekten) enthält. Das ist aufwändig, doch gerade im Hinblick auf die Aufgabe, die ja die Triebkraft hinter der Wahl bestimmter Lösungen ist, ist es wertvoll.

Nun muss nur noch jeder Entwickler das Dokument vollständig lesen, verstehen und während seiner Arbeit stets in allen Aspekten berücksichtigen. Hier hakt es in der Praxis bereits gewaltig. Denn das Dokument kann nur "flach" beschreiben, wie das System aussehen soll. Die Architektur wird nicht plastisch. Es fehlt etwas, das man "anfassen" kann oder sogar beim Ablauf beobachten könnte. Kurz: ein Dokument ist zur Anleitung, wie ein System konkret gebaut werden soll, eine Ergänzung, reicht aber nicht aus.

Das nächste, etwas mächtigere Mittel, um das System im Sinne der Architektur zu formen, ist ein lauffähiger Prototyp, der an exemplarischen Anwendungsfällen zeigt, wie das System zu bauen ist. So ein Prototyp ist auch im Rahmen von frühen Lasttests zwecks Prüfung technischer Lösungen nützlich. Und beim Bau des Prototyps entstehen nebenbei Entscheidungen für oder gegen bestimmte Werkzeuge, die wiederum wichtig sind, damit es leicht ist, im Sinne der Architektur zu programmieren. Der Quellcode des Prototypen kann prima für gemeinsame Walkthroughs von Entwicklern und Architekten genutzt werden. Es entsteht eine befruchtende Diskussion, aus der das ganze Team etwas über das System, seine Architektur und wie sie zu verwirklichen ist, lernt.

Teams lernen während eines Projekts, wie sie ihre Projektarbeit besser gestalten. Das gilt in gleichem Maße für Architekturentscheidungen, die sich als "gut" oder "verbesserungswürdig" erweisen können. Geänderte Architekturentscheidungen sollten sich möglichst bald auf bereits bestehende Systemteile auswirken, d.h. dass Entwickler von diesen Änderungen "entwicklerfreundlich" informiert werden. Der Architekt benötigt also einen Update-Mechanismus. Mails oder ein persönlicher Hinweis sind zur Erzeugung der Aufmerksamkeit geeignet, der Inhalt, also was warum geändert wurde, ist in einer zentralen Wikiseite mit Einträgen in der Reihenfolge Neu-Alt-NochÄlter schon besser aufgehoben. Entstehen größere Baustellen, so ist statt einer Mail ein Eintrag in ein Ticketingsystem ratsam, der die Anpassung an eine geänderte Entscheidung zum Inhalt hat. So ein Eintrag kann im Rahmen der Iterationsplanung dann als Aufgabenpaket berücksichtigt werden.

Wir sind mit den o.g. Mitteln schon soweit, dass Entwickler die Architektur effizient verstehen lernen und Änderungen an dieser mitbekommen. Aber wie können Architekten feststellen, ob die tatsächliche Realisierung mit der Architektur zusammenpasst?

Ein wirksames, lange bekanntes Mittel sind Code Reviews. Architekten überprüfen durch das Lesen von Quellcode, ob die Implementierung die Lösungen der Softwarearchitektur berücksichtigt. Ist das an einer Stelle nicht der Fall, so kann das ein guter Anlass für den Architekten sein, mehr über das System und die bisherige Lösungsidee zu lernen. Das verbessert die Architektur und den Architekten. Es kann natürlich auch sein, dass der Entwickler jetzt mehr über die Architektur lernen kann. Das verbessert den Entwickler. Es ensteht zudem ein wertvoller Eindruck von der allgemeinen Codequalität, wie ihn Tools bis heute nicht vermitteln können.

Code Reviews haben aber den Nachteil, dass sie in großen Teams nur noch stichprobenartig erfolgen können. Ihre Durchführung skaliert nicht besonders gut. Durch die zusätzliche Verwendung von statischer Codeanalyse lassen sich einige wenige Aspekte vollautomatisch prüfen. Dazu zählt die Begrenzung auf zulässige Abhängigkeiten für Komponenten oder die Verwendung bestimmter Muster. Die statische Codeanalyse wird die Architekten aber auch darauf hinweisen, wo viel oder komplexer Code entsteht. Das erleichtert die Auswahl, was von einem Menschen via Code Review geprüft werden sollte.

Reviews und automatische Prüfungen schlagen erst an, wenn ein Missstand besteht, und somit eine Behebung erforderlich ist. Damit wirken sie eigentlich zu spät, um Fehler gar nicht erst entstehen zu lassen. Um Fehler und Inkonsistenzen auszuschließen, setzen Architekten Model-driven software development (MDSD) ein. Gemäß dieses Ansatzes werden bestimmte Aspekte des Systems wie z.B. seine Gesamtstruktur, Formulare und deren Abfolgen, persistente Datenstrukturen und Schnittstellen zu Fremdsystemen in Modellen beschrieben. In diesen Modellen spielen technische Entscheidungen keine Rolle. Erst bei der Erzeugung von Code aus den Modellen werden die fachlichen Modellinhalte mit Technik versehen. Dieser generierte Code kann z.B. mittels Vererbung manuell ergänzt werden. Auf diese Weise sind Fehler ausgeschlossen und späte Änderungen technischer Lösungen erfordern lediglich die Anpassung des Generators sowie die Neugenerierung des Codes. Bei MDSD handelt es sich damit um ein äußerst mächtiges Mittel, um einer Architektur zur konsistenten Implementierung zu verhelfen.

Ein Softwareteam besitzt also eine ganze Reihe von Werkzeugen, um einer Architektur Leben einzuhauchen und vor allem die ungewollte Abweichungen von richtigen Lösungsideen zu vermeiden. Es ist neben der Gestaltung des Softwaresystems eine sehr wichtige Aufgabe von Softwarearchitekten, diese Werkzeuge einzuführen und ihre Verwendung zur Gewohnheit des Teams werden zu lassen. Denn nur dann bleibt die Architektur kein Wolkenkuckucksheim.


Risiken und Nebenwirkungen von Wiederverwendung, 24.1.2010

Die Situation kommt sicherlich häufiger vor: Ein Softwareprojekt übergibt erfolgreich seine erste Version an den Betrieb, und es dauert nicht lange, da steht bereits das nächste fachlich und technisch ähnliche Projekt vor der Tür. Im letzten Projekt hatte man sich einige Mühe gegeben, eine saubere Infrastruktur bestehend aus Entwicklungsumgebung, Buildverfahren, Generatoren, Laufzeitprodukten, Bibliotheken und kodierten querschnittlichen Lösungen zu etablieren. Und was liegt näher als diese Infrastruktur gleich weiter zu verwenden?

Für die Widerverwendung gibt es jetzt zwei Wege. Man kann a) alle Projekte dieselbe Infrastruktur gleichzeitig nutzen lassen, oder b) man erschafft einen dauerhaften Branch, den man dann den Wünschen des neuen Projekts ohne sonderliche Rücksicht anpasst.

Der Reflex des guten Softwareingenieurs ist klar: Keine Duplikation, um keinen Preis! Das würde zu Mehrfachpflege führen, oder schlimmer noch: man lässt zeitweise die Divergenz zu und muss sich dann wieder mit der Konsolidierung mühen. Dieser Reflex ist gesund, aber man sollte eine bewusste Entscheidung treffen, ob man eine gemeinsame Infrastruktur oder verschiedene ähnliche haben möchte, denn die technischen und organisatorischen Konsequenzen, die dem Nachgeben des Reflexes folgen, können teurer und unerfreulicher sein als das divergierende Nebeneinander. Letztlich ist hier eine Entscheidung mit strategischem Weitblick und langfristiger Kosten-Nutzen-Betrachtung nötig.

Konsequenzen bei Zusammenhalten:

  • Fachprojekte müssen sich bewusst für Releases der Infrastruktur entscheiden und sind dann von dieser abhängig.
  • Änderungen an der Infrastruktur können nicht mehr ad-hoc aus Projektsicht durchgeführt werden, sondern müssen durch einen Change Prozess. Teilnehmer des Entscheider-Boards sind Experten der Infrastruktur und je ein Vertreter der Fachprojekte.
  • Dementsprechend müssen beschlossene Änderungen in Releases gebündelt werden, was die Beweglichkeit stark einschränkt.
  • Durch z.T. widersprüchliche Anforderungen an Features wird die Infrastruktur weit generischer und damit wahrscheinlich schwerer zu verstehen und schwerer zu ändern.
  • Es muss eine explizite Qualitätssicherung der Infrastruktur geben.
  • Die Kosten der Pflege und Weiterentwicklung müssen getragen werden. Beteiligen sich Projekte anteilig? Oder gibt es ein fixes Budget der Kostenstelle? Gibt es eine eigene Organisationseinheit, in der die Pflege und Weiterentwicklung gebündelt wird?
Konsequenzen bei Nichtzusammenhalten:
  • Neue Features und Bugfixes, die für n Fachprojekte relevant sind, müssen n-mal nachgepflegt werden. Der Aufwand steigt linear mit der Anzahl der parallelen Infrastrukturen.
  • Die Informationen, was sich in welchem Branch geändert hat, müssen verfolgt werden. Projekte müssen sich entscheiden, was sie übernehmen wollen und das jeweils selbst organisieren.
  • Es kann zu inkonsistenten Weiterentwicklungen kommen, die dauerhaft verschiedenes Verhalten bedeuten. Das verteuert möglicherweise die Einarbeitung von Mitarbeitern, die zwischen Projekten mit ähnlichen, aber nicht gleichen Infrastrukturen wechseln sollen.
Die Frage ist letztlich: wieviele technisch ähnliche neue Projekte wird die Organisation in Zukunft haben, die den weitgehend unveränderten Einsatz dieser Infrastruktur erfordern?

Wenn die Antwort ist: ja, noch einige, dann sollte man über eine gemeinsame Infrastruktur nachdenken.
Wenn die Antwort ist: nicht viele oder nur mit stärkeren Abweichungen untereinander, dann würde ich die Finger davon lassen und die projektweise Weiterentwicklung nebst informellem Austausch vorziehen.