Follow my new blog

Posts mit dem Label Software als System werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Software als System werden angezeigt. Alle Posts anzeigen

Donnerstag, 6. Oktober 2011

Skalierbare Softwarebausteine - Teil 2

In meinem Traum sehen fundamentale Softwarebausteine ganz einfach aus, wie ich im ersten Teil dieser kleinen Artikelserie beschrieben habe:

image

Es sind potenziell zustandsbehaftete und potenziell nebenläufige Funktionseinheiten auf beliebig vielen Abstraktionsebenen, die Input-Daten in Output-Daten transformieren. So einfach ist das :-)

Dabei könnte ich nun stehenbleiben und hübsche Diagramme mit solchen Funktionseinheiten malen. Das würde mir das Nachdenken über Softwarestrukturen schon erleichtern.

Doch ich will mich nicht in Wolkenkuckucksheim einrichten. Bubbles don´t crash. Der Traum muss also einen Realitätsbezug bekommen. Die konzeptionellen Funktionseinheiten müssen ausführbar sein; irgendwie müssen sie an Code angebunden werden.

Die Form einer normalisierten Funktionseinheit

Wenn solche traumhaften Flüsse ausführbar sein sollen, stellt sich als erstes Frage, welche Form eine Funktionseinheit in Code haben könnte. Bisher habe ich als Übersetzung Event-based Components favorisiert: Jede Funktionseinheit wird als Klasse implementiert, die für jeden Input-Stream eine Methode anbietet und für jeden Output-Stream einen Event.

Das finde ich inzwischen zu aufwändig. Es funktioniert ordentlich, doch es macht ziemliche Mühe und schränkt ein. Der Zwang zur Klasse ist in der Community auch immer wieder auf Stirnrunzeln gestoßen.

Deshalb jetzt ein Vorschlag mit kleinerem Fußabdruck:

delegate void FunctionalUnit<in TInput, out TOutput>(
    IMessage<TInput> input,
    Action<IMessage<TOutput>> output);

interface IMessage<out T> {
    string StreamQualifier { get; }
    T Data { get; }
}

Wie ist das? Deutlich einfacher, oder? Sie können eine Funktionseinheit bauen, wie Sie mögen, solange es einen Adapter gibt, der sie an diesen Delegaten anpasst. Funktionen, Methoden, auch EBC-Klassen sind möglich. Nur ein Beispiel:

class Program
{
    static void Main(string[] args)
    {
        FunctionalUnit<string, IEnumerable<string>> fuSplit;
        fuSplit = (inputMsg, outputCont) =>
            {
                var outputData = Split(inputMsg.Data);
                var outputMsg = new Message<IEnumerable<string>>(
                                           "", outputData);
                outputCont(outputMsg);
            };

        fuSplit(new Message<string>("", "a;b"),
                  (m) => Console.WriteLine(string.Join("/", m.Data)));
    }


    static IEnumerable<string> Split(string config)
    {
        return config.Split(';');
    }
}

Die Implementation der Funktionseinheit ist eine Funktion. Um die herum wickelt Main() einen Adapter, der den Input an FunctionalUnit übersetzt in das Funktionsargument und ihren Rückgabewert in den Aufruf des Continuation-Delegaten.

Dasselbe ist möglich mit einer Methode oder mit einer EBC-Klasse. Sollte es dabei um mehrere Input- und/oder Output-Streams gehen, kann der StreamQualifier von IMessage<> zur Unterscheidung herangezogen werden.

An dieser Stelle nehmen Sie bitte vor allem die Botschaft mit: Sie können Funktionseinheiten implementieren, wie Sie mögen.

Es gibt keinen Vorzug mehr für Klassen. Einzig, Ihre Implementation muss sich an den Delegaten adaptieren lassen. Das geht aber zumindest für statische und Instanz-Methoden/Funktionen und für EBC-Klassen.

Die Beschreibung von Flüssen

Dass Funktionseinheiten jede Form haben können, wird Sie etwas aufatmen gelassen haben. So ganz entfinstert wird Ihre Miene allerdings noch nicht sein, weil der Preis für die Freiheit der Form ein höherer Koordinationsaufwand zu sein scheint. Es müssen ja nun Adapter für Funktionseinheiten zusammengesteht werden, damit es zu einem Fluss kommt.

Das ist richtig. Deshalb wird nun die Frage nach einer Beschreibung von Flüssen drängender. Bisher konnte sie recht simpel in C# als Event-Event-Handler Zuweisungen von EBC-Funktionseinheiten übersetzt werden. Das geht nun nicht mehr so einfach. Eine DSL wäre zur Unterstützung schön; aus der könnte womöglich der ganze Adapter-Code und das Koordinieren von Funktionseinheiten generiert werden.

Ja, das könnte man. Aber ich möchte etwas anderes vorschlagen.

Aber erst einmal: Wie könnten Flüsse beschrieben werden? Sie könnten grafisch notiert werden. Dafür ist aber ein recht aufwändiger Designer nötig. Oder sie könnten textuell notiert werden. Ich halte letzteres für ohnehin nötig, selbst wenn es einen Designer gäbe.

ebc.xml (ebclang.codeplex.com) ist ein Versuch gewesen, Flüsse textuell zu beschreiben. Dazu gibt es auch einen Visualizer und einen Compiler. Aber XML scheint mir zu umständlich. Da steckt viel Rauschen drin.

Dann habe ich mit einer DSL experimentiert, die ich ebc.txt genannt habe. Die Transformation einer Konfigurationszeichenkette der Form “a=1;b=2” in ein Dictionary ließe sich damit so beschreiben:

ToDictionary {
  this.in –(string)-> (Split)
          –(string*)-> (Map)
          –(KeyValuePair<string,string>*)–> (Build)
          –(Dictionary<string,string>)-> this.out
}

Das ist doch lesbar, oder? Ich benutze diese Notation jedenfalls recht gern, um mal kurz in einer Email oder einem Diskussionsgruppenbeitrag einen Flow zu beschreiben.

Allerdings ist auch diese Notation noch recht länglich, finde ich. Für die Menschen-Mensch-Kommunikation ist sie ok. Doch auch sie enthält noch Rauschen.

Deshalb komme ich auf ebc.xml zurück, nur ohne XML. Die einfachste, maschinenlesbare Form der Beschreibung eines Flow ist eine Tabelle:

ToDictionary
this.in, Split
Split, Map
Map, Build
Build, this.out

Die erste Zeile bezeichnet die koordinierende Funktionseinheit, alle weiteren Zeilen verbinden Output mit Input.

Dass hier keine Typen mehr für Input/Output angegeben sind, ist nicht weiter schlimm. Entweder man rüstet das nach


this.in, Split: string
Split, Map: IEnumerable<string>

oder man überlässt die Prüfung auf Passgenauigkeit einer Übersetzungs- oder Laufzeitinstanz. Letzteres finde ich völlig ausreichend.

Mit solchen “Verbindungstabellen” lassen sich nun trefflich auch tiefe Fluss-Hierarchien beschreiben:

image

Jedes Komposit wird durch eine Tabelle beschrieben:

R
R, T
T.a, W
T.b, V
W, S.c
V, S.d
S, R

W
W, X
X, Y
Y, W

V
V, Z
Z, Y
Y, V

Wo Input- bzw. Output-Streams implizit bestimmbar sind, da müssen sie nicht angegeben werden, z.B. bei

R
R, T

Hier ist klar, dass R sich auf das Komposit bezieht und deshalb auf der linken Seite des Tupels ein Input-Stream gemeint ist. Rechts steht nicht das Komposit selbst, sondern eine enthaltene Funktionseinheit. Deren Input-Stream ist gefragt; doch da sie nur einen hat, muss der nicht näher benannt werden.

Wenn mehrere Input-/Output-Streams vorhanden sind, ist allerdings eine Qualifizierung nötig wie bei:


W, S.c

Solche Tabellen sind recht bequem hinzuschreiben. Das hat schon ebc.xml gezeigt. Als einfachste Notationsform mögen sie deshalb genügen. Minimal sind sie jedoch nicht.

Die Arbeitspferde einer hierarchischen Struktur sind die Blätter. Hier: S, T, X, Y, Z. Die Komposite bilden nur Kontexte, in denen sie zu “Kooperativen” zusammengefügt sind. Da die keine weitere eigene Aufgabe haben, können sie spätestens zur Laufzeit wegfallen als eigenständige Funktionseinheiten.

Das obige System kann auch in nur einer Tabelle so beschrieben werden:

/R, /R/T
/R/T.a, /R/W/X
/R/W/X, /R/W/Y
/R/W/Y, /R/S.c
/R/S, /R
/R/T.b, /R/V/Z
/R/V/Z, /R/V/Y
/R/V/Y, /R/S.d

Hier sind alle Blatt-Funktionseinheiten qualifiziert durch einen Pfad, der beschreibt, in welcher Schachtelung von Kompositen sie stecken. Damit lassen sich auch Y in W und Y in V unterscheiden.

Die Daten von T.a gehen nun direkt nach W/X, statt zuerst an W und dann an X.

An dieser Stelle halten Sie bitte wieder einen Moment inne und lassen Sie die Botschaft einsinken: Die Struktur beliebig komplexer Software lässt sich durch eine einzige Tabelle beschreiben.

Egal wieviele Ebenen, wieviele Komposite und Blätter: Ein solches System können Sie immer abbilden auf eine simple Tabelle der Output-Input-Beziehungen.

Damit meine ich nicht, dass Kontrollanweisungen wie if oder while überflüssig werden bzw. auch irgendwie über eine Tabelle verknüpft werden sollen. Die stecken ja in den Blättern der Holarchie und sind unsichtbar. Implementationsdetails. Mir geht es nur um die Funktionseinheiten der beschriebenen Form. Wie groß Sie die auf unterster Ebene machen, ist Ihre Sache. Je mehr Sie in Blättern verstecken, desto weniger profitieren Sie vom hiesigen Ansatz.

Höhepunkt: Flüsse zur Laufzeit

Und nun zum eigentlichen Punkt, den ich mit dem vorherigen und diesem Artikel machen will. Der Höhepunkt meines Traums ist eine Runtime Engine für Flüsse. Denn die wird sehr einfach möglich durch diese einfachste Beschreibung von Flüssen.

Ich bin der Meinung, dass wir Flüsse nicht mehr übersetzen sollten. Wir sollten sie auch nicht mehr von Hand “verstöpseln”. Stattdessen sollten wir sie ganz, ganz simpel über eine Tabelle beschreiben, die Blätter (Operationen) benennen – und eine Runtime Engine “interpretiert” dann die “Flusstabelle”.

image

Wie aus Ihren Funktionen, Methoden, EBC-Klassen Operationen werden, ist ein anderer Belang. Der finde ich gerade nicht so spannend. Dafür kann ein Container geschrieben werden, bei dem Sie Ihre Implementationen registrieren können und der sie hin FunctionalUnit-Adapter wickelt. Er könnte Operationen aus Assemblies sogar eigenständig sammeln.

Dito ist die Herkunft der einen, das ganze System beschreibenden Tabelle für mich zweitrangig. Die kann aus mehreren Tabellen generiert worden sein. Oder sie kann aus einer ebc.txt DSL übersetzt worden sein. Auch das ist eine Funktionalität, die sich kapseln lässt.

Nein, zentral ist für mich die Runtime Engine zur Interpretation von Flows. Da spielt die Musik. Und jetzt festhalten:

  • Die Runtime ist der zentrale Ort, an dem der Fluss (oder die Hierarchie der Flüsse) belauscht werden kann. Die Runtime leitet Output an Input weiter. Sie hat also jede Nachricht ausdrücklich in der Hand. Ihr können wir unsere Wünsche äußern, welche Nachrichten wir abgreifen wollen. Vielleicht wollen wir sie nur protokollieren, vielleicht wollen wir sie aber auch manipulieren. Alles ist möglich durch den zentralen Eingriffspunkt Runtime. Stellen Sie sich eine Logging vor, das sie zur Laufzeit mit Pfaden einschalten können: “log /*/T.*”, d.h. logge alle Nachrichten, deren StreamQualifier dem Pattern entspricht.
  • Die Runtime ist der zentrale Ort, an dem alle Flüsse in ihrer Geschwindigkeit kontrolliert werden können. Wenn wir wollen, können wir sie komplett anhalten – und dann wieder starten. Oder wir verlangsamen sie nur, um den Fluss vielleicht grafisch visualisiert mitverfolgen zu können.
  • Die Runtime ist der zentrale Ort, an dem wir Performancedaten messen lassen können. Was sind die Durchflusszeiten, wie groß sind die Nachrichten in Strom /R/S.c durchschnittlich usw.
  • Wenn die Runtime Output an Input “von Hand” zustellt, dann kann sie diese Übertragung selektiv aussetzen, während wir eine Operation zur Laufzeit ersetzen.
  • Oder wir können der Runtime zur Laufzeit eine Änderung des Flusses mitteilen. Der ist ja nur eine Tabelle, die wir aktualisieren können.
  • Und schließlich bekommt die Runtime jede Exception mit. Wenn wir wollen, stellen wir in der Runtime ein, was mit Exceptions passieren soll, die bei der Verarbeitung ganz bestimmter Streams auftreten.
  • Ach, bevor ich es vergesse: Natürlich ist die Runtime auch die Instanz, von der wir uns wünschen können, ob Funktionseinheiten synchron, async+sequenziell oder parallel ausgeführt werden sollen.

Sie sehen, ich träume von großen Vorteilen, wenn wir beginnen, Software als Hierarchie von Datenfluss-Funktionseinheiten zu betrachten.

Technisch ist das alles grundsätzlich möglich. Ich habe es in einem Spike schon ausprobiert. Hier und da lauern sicherlich noch Teufelchen im Detail. Aber wenn wir das Ergebnis für wünschenswert halten, dann werden wir Wege finden, es vom Traum zur Realität werden zu lassen.

Mit diesen beiden Postings wollte ich dazu meine Gedanken ein wenig ordnen. Ein Codeplex-Projekt für Implementation einer Runtime ist schon aufgesetzt. Jetzt muss ich nur noch die Zeit finden, sie schrittweise auch zu implementieren. Vielleicht machte ich daraus eine Application Kata mit vielen Iterationen. Dann können andere parallel und auf anderen Plattformen es auch angehen.

Einstweilen lassen Sie mich wissen, ob Sie mitträumen oder es sich für Sie wie ein Albtraum anhört…

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Mittwoch, 5. Oktober 2011

Skalierbare Softwarebausteine - Teil 1

Wie können wir Software mit evolvierbarer Struktur herstellen? Wie kann Software selbst so agil werden wie unsere Prozesse es schon geworden sind? Diese Frage treibt mich immer noch um. Die bisher angebotenen Lösungsansätze aus Richtung Clean Code finde ich noch nicht ausreichend. TDD und SOLID sind ganz gute Ansätze… nur fehlt ihnen aus meiner Sicht ein umfassender Blick auf Software. Das SRP in SOLID ist zwar grundlegend – doch DI ist demgegenüber merkwürdig sprachspezifisch, finde ich. Und TDD fokussiert auf Code, so dass von TDD wenig Hilfe zu erwarten ist, wenn noch gar kein Code oder eine Code-Idee vorhanden ist.

Ich glaube deshalb weiterhin, dass wir noch einen Schritt zurücktreten müssen. Wir müssen eine allgemeinere Vorstellung von Software entwickeln, in die dann TDD und SOLID usw. hineinpassen. TDD und SOLID sollen also nicht einfach ersetzt werden, sondern einen soliden Platz bekommen.

Warum die real existierende Objektorientierung uns behindert

Die Objektorientierung, so könnten Sie sagen, ist doch aber schon so eine allgemeine Vorstellung von Software. Ja, das stimmt. Irgendwie ist sie das. Und dann doch wieder nicht. Zumindest nicht die real existierende Objektorientierung, die sich von einer idealen ursprünglichen stark unterscheidet. Die real existierende Objektorientierung (REOO) hat aus meiner Sicht einfach einige große Mängel:

  • Sie REOO skaliert konzeptionell nicht. Wir können Software mit ihr nicht vom kleinsten bis zum größten Baustein durchgängig beschreiben. Dass Software aus Bausteinen unterschiedlicher Größe/Granularität zusammengesetzt ist, sollte außer Zweifel stehen. Damit meine ich nicht, dass Bausteine mal mehr und mal weniger LOC haben. Ich meine ihre konzeptionelle Größe, ihren Funktionsumfang. Eine ganze EXE umfasst mehr Funktionalität als eine Klasse in der EXE.
    Mir fällt es jedenfalls sehr schwer, Software aus Objekten unterschiedlicher Granularität vorzustellen. Der UML offensichtlich auch, sonst würde sie Klassendiagrammen nicht auch noch Komponenten- und Paketdiagramme beistellen. Weder Komponenten noch Pakete gehören jedoch zur Objektorientierung. Wie ihre Namen schon sagen, geht es nicht um Objekte (oder Klassen als deren Schemata), sondern um Komponenten und Pakete.
  • Die REOO ist fixiert auf ein essenziell statisches Bild der Realität. Nomen est omen: Die Realität besteht aus Objekten, aus “Dingern”, die erstmal sind, deren Existenz eine Dauer hat. Diese “Dinger” haben dann vor allem einen Zustand. Die Praxis der REOO versucht sie vor allem anhand von Daten zu identifizieren, für die sie verantwortlich sind. Objekte sind mithin Zuständigkeitsaggregate. Sie bündeln den Umgang mit Daten. Das wird positiv formuliert als Kapselung.
    Ich verstehe, dass diese Sichtweise Appeal hat. Menschen mögen Greifbares. Mit Lego kann jedes Kind umgehen, mit Mathematik jedoch nicht; die ist viel abstrakter. Also mögen Softwareentwickler auch lieber umgehen mit Greifbarem, eben mit Objekten.
    Wenn ich die Wurzel der Softwareentwicklung im Maschinenbau und der Elektrotechnik verorte (und nicht in der Mathematik), dann finde ich das auch nur konsequent. Die vormaligen Hardwarebausteine sind in die Software übertragen worden: aus konkreten Zahnrädern und Hebeln und Transistoren und Widerständen sind Objekte geworden.
    Oder ich könnte philosophisch werden und die Softwareentwicklung an die Griechen binden. Demokrit mit seinen Atomen und Platon mit seinen Ideen haben die Welt bestehend aus klar umrissenen Entitäten aufgebaut gesehen. Die einen waren anfassbar, wenn auch seeeeehr klein. Die anderen abstrakt, dafür beliebig groß. In jedem Fall haben beide ihre “Weltenbausteine” als unwandelbar angesehen, als statische Bausteine.
  • Die REOO verbindet Objekte über Abhängigkeiten. Ein Objekt, das viel kann, ist von Objekten abhängig, die weniger können. Dadurch entsteht eine Abhängigkeitshierarchie, über die die Gesamtleistung vertikal verteilt ist. Auf jeder Ebene passiert ein bisschen.
    Das hat die REOO als Problem erkannt – im Zusammenhang mit der Testbarkeit. Als Gegenmaßnahme empfiehlt sie IoC und DI und Attrappen. Damit werden Objekte auf jeder Ebene einzeln testbar.
    Ich halte das für eine Symptombehandlung. Die Abhängigkeiten bleiben bestehen. Funktionalität ist weiterhin vertikal in der Hierarchie verstreut.

Meine Kritik an der real existierenden Objektorientierung ist also sehr fundamental. Sie macht uns die Beschreibung von Lösungen schwer, weil wir die mit ihren Mitteln nicht leicht auf beliebigen Abstraktionsebenen denken können. Sie fixiert sich auf ein Weltbild, das es uns schwer macht, das zu beschreiben, was Software essenziell repräsentiert: Prozesse. Und die Ergebnisse der Objektorientierung, der Code, hat eine Form, die ganz fundamental anti-agil ist, also im Widerspruch zur zentralen Erkenntnis der Softwareentwicklung steht, dass Software sich ständig anpassen muss.

Aber nun genug des OO-Bashings :-) Es soll nur als Motivationshintergrund dienen, warum ich immer noch und wieder an einem anderen Software-Weltbild bastle. Nicht, dass mit REOO nix ginge. Doch, klar, es geht was. Mit Pferdefuhrwerken ging auch etwas. Jahrhunderte lang. Und dann kam der Trecker. Nun geht noch mehr.

Vorschlag für ein alternatives Weltbild

Damit ich nicht den Anschein erwecke, eine rundum-sorglos Lösung zu bieten – Silverkugeln gibts ja nicht und sind in unserer Branche bei vielen auch nur im Anschein verhasst –, spreche ich lieber mal von einem Traum.

Ja, ich habe einen Traum, in dem wir Software viel einfacher entwickeln. Das ist natürlich auch irgendwie dem verhaftet, was ich in 30 Jahren Softwareentwicklung erfahren habe. Wie Software in 100 Jahren entwickelt wird, kann ich mir wohl nicht vorstellen. Doch für Sie mag mein Traum dennoch esoterisch anmuten. Macht ja aber nichts, ist eben nur ein Traum :-) Träumen Sie doch mal mit. Lassen Sie sich darauf ein. Schicken Sie Ihre Skepsis für eine kleine Weile auf Urlaub. Die kann sich ja mal den Versuchen zur Rettung von Euro und Europa zuwenden.

In meinem Traum entwickeln wir Software anders, weil Software darin ein anderes Weltbild unterliegt. Software oder die Welt besteht darin nicht aus “Dingern”, sondern aus Prozessen. Im Kern steht nicht etwas Statisches, sondern Dynamik. Es geht ums Tun, um Bewegung, um Wandlung. Softwareentwickler fragen nicht, nach “Objekten”, sondern nach “Aktivitäten”.

Nach diesem Weltbild besteht Software aus diesem:

image

Das ist eine Aktivität. Da passiert etwas. Das ist neutral gesprochen eine Funktionseinheit. Kent Beck würde es vielleicht Element nennen.

“Baustein” möchte ich eigentlich ungern dazu sagen, da “Baustein” schon wieder Statik suggeriert wie “Objekt”. Aktion, Aktivität oder eben Funktionseinheit scheinen mir passender.

So eine Funktionseinheit “macht ihr Ding”, indem sie eingehende Daten verarbeitet. Sie steht also für das alte EVA-Prinzip: Eingabe-Verarbeitung-Ausgabe.

Daten können aus verschiedenen Richtungen in die Funktionseinheit einfließen (Input). Das, was sie daraus macht, kann in verschiedene Richtungen aus ihr herausfließen (Output).

Und wenn eine Funktionseinheit nicht reicht, dann arbeiten mehrere zusammen in einem Fluss. Der Output der einen wird zum Input der anderen:

image

Das war´s. So sieht Software aus. Auf jeder Abstraktionsebene. Damit lässt sich eine ganze Anwendung beschreiben wie auch der kleinste Teil einer Anwendung.

Zustand

Input ist ein Trigger für Funktionseinheiten. Kommt Input an, tun sie etwas. Allerdings muss sich eine Aktivität nicht ausschließlich auf einfließenden Input beziehen. Sie kann auch ein Gedächtnis haben, d.h. Zustand. Aus dem kann sie lesen und den kann sie verändern.

Zustand macht es zwar schwerer, über das Ergebnis einer Aktion nachzudenken, aber ich halte Zustand für einen so natürlichen Bestandteil der Welt, dass wir ihn nicht mit Macht ausschließen sollten. Das rückt Funktionseinheiten natürlich in die Nähe von REOO Objekten – doch der Unterschied besteht für mich im Fokus. Bei REOO steht Zustand eher am Anfang, bei meiner Träumerei eher am Ende.

Im einfachsten Fall ist solcher Zustand lokal. Eine Aktion hat individuellen Zustand:

image

Wenn Aktionen kooperieren sollen, dann kann es allerdings nötig sein, dass sie Zustand gemeinsam nutzen. Das ist dann shared state und sollte explizit gemacht werden. Das wird spätestens dann wichtig, wenn der Zugriff darauf gleichzeitig erfolgen soll. Dann muss er nämlich synchronisiert werden.

image

Nebenläufigkeit

Ohne weitere Angaben arbeiten Aktionen sequenziell und synchron. Während eine Funktionseinheit Input verarbeitet, tut das keine andere. Wenn sie Output produziert, stößt sie damit eine empfangende Funktionseinheit an, die ihn als Input verarbeitet, bevor die produzierende weitermacht.

image

Sequenzielle und synchrone Verarbeitung lässt sich gut denken und nachverfolgen. Aber in der realen Welt kommt sie eher nicht vor. Und sie nutzt die verfügbaren Prozessorressourcen womöglich nicht optimal.

Es ist deshalb konsequent, Aktionen auch nebenläufig betreiben zu können. Sie laufen dann auf (mindestens) einem eigenen Thread (und womöglich auf einem eigenen Prozessorkern).

image

Sobald der Fluss von Input-Output durch eine Aktion mit eigenem Thread läuft, findet die weitere Verarbeitung auf diesem Thread statt, bis wiederum eine Aktion mit eigenem Thread angestoßen wird usw.

image

Dass Aktionen nebenläufig betrieben werden können, bedeutet jedoch nicht, dass Nebenläufigkeit auch innerhalb von Aktionen stattfindet. Die scheint mir problematisch, weil sie dazu führt, dass Input in anderer Reihenfolge als der, in der er eintrifft, verarbeitet wird. In meinem Traum ist daher die Verarbeitung innerhalb einer Funktionseinheit immer noch sequenziell. Input wird in der Reihenfolge seines Eintreffens verarbeitet.

Zumindest sollte das wohl der Default sein. Wenn in ganz bestimmten Situationen eine Aktion davon profitiert, in sich ebenfalls nebenläufig zu sein, d.h. Input auf mehreren Threads parallel zu verarbeiten, dann sei das so. Es könnte so ausgedrückt werden:

image

Aktionen sind zunächst also synchron und sequenziell, dann können sie auch asynchron sequenziell sein und schließlich vollständig parallel.

Unabhängigkeit

Es ist vielleicht keiner Erwähnung wert, weil der Fluss von Input-Output es so natürlich macht, doch ich sage es lieber einmal ausdrücklich: Aktionen sind von einander unabhängig.

Zwei verbundene Aktionen wissen nichts von einander. Der Producer (generiert Output) weiß nicht, wer seinen Output weiterverarbeitet. Der Consumer (verarbeitet Input) weiß nicht, von wem sein Input stammt.

Das (!) ist ein fundamentaler Unterschied zur REOO. Und deshalb ist es wohl gut, dass ich ihn hier nochmal betone ;-)

Eine Gesamtleistung als Ergebnis einer Kooperation mehrerer Funktionseinheiten erfordert keinerlei Abhängigkeiten zwischen den Kooperationspartnern. Sie wissen nicht einmal, dass sie kooperieren.

Jede Aktion innerhalb einer “Kooperative” bekommt Input “von irgendwoher” und produziert Output “für unbekannt”.

Ein Zusammenhang von Funktionseinheiten wie dieser:

image

sieht also eher so aus:

image

Schachtelung

Um größere und sehr große Systeme zu beschreiben, müssen Aktionen hierarchisch angeordnet werden können. Aus großer Flughöhe sieht ein System dann eigentlich immer so aus:

image

Wenn man niedriger fliegt, kommen Details in den Blick:

image

Und wenn man noch tiefer runter geht, d.h. in das System hineinzoomt, dann kommen noch mehr Details in den Blick:

image

So kann es beliebig tief gehen. Software kann also als Baum dargestellt werden:

image

Das sieht nun wieder wie eine REOO Objekthierarchie aus. Das macht auch nichts, solange klar ist, dass der “Verfeinerungsbaum” einem wesentlichen Prinzip folgt: alle Aktionen, die nicht Blatt sind, haben ausschließlich die Aufgabe, Aktionen zu “Kooperativen” zu kombinieren.

In Nicht-Blättern stecken keine Algorithmen. Sie entscheiden nichts, sie enthalten keine Schleifen. Sie sorgen nur dafür, dass Output der einen Aktion zu Input für eine andere wird. That´s it.

Ebenfalls betonenswert: Funktionseinheiten auf allen Ebenen sehen gleich aus. Sie verarbeiten Input zu Output mit eventuellen Seiteneffekten. Und sie sind gleichzeitig Teil von darüber gelagerten Funktionseinheiten, von denen sie in einen Kooperationszusammenhang gestellt werden, wie sie Ganzes sind in Bezug auf Funktionseinheiten, die sie selbst zu einem Kooperationszusammenhang zusammenstellen.

Software ist damit grundsätzlich selbstähnlich aufgebaut; man könnte vielleicht sogar sagen, fraktal. Software ist eine Holarchie und die Funktionseinheiten sind ihre Holons.

Darüber habe ich früher schon öfter geschrieben wie hier oder hier oder 2005 hier. Aber auch wenn ich riskiere, damit zu langweilen, ist mir diese Sichtweise so wichtig, dass ich sie wiederhole. Wir tun uns einen Gefallen, wenn wir dahin kommen, Software so zu sehen. Aller Softwareentwurf kann durch solche Regelmäßigkeit nur einfacher werden.

Normalisierung

Zum Abschluss für heute noch eine Verallgemeinerung. Aktionen habe ich als Funktionseinheiten mit beliebig vielen Input- und Output-Strömen beschrieben. So soll es auch sein.

Für etwas mehr Regelmäßigkeit können Sie jedoch reduziert werden. Das hat später Vorteile, wie Sie sehen werden, wenn es an die Implementierung geht.

Die “Einheitsdarstellung” oder normalisierte Form für Aktionen ist diese:

image

Normalisierte Funktionseinheiten haben genau 1 Input-Strom und genau 1 Output-Strom. Immer.

Statt Input/Output entlang verschiedener Ströme fließen zu lassen, gibt es je nur einen Strom, auf dem Input/Output-Daten mit einem Qualifier näher beschrieben sind; der ordnet sie logisch einem der vielen früheren physischen Ströme zu.

Wie und ob sie sich von Input triggern lassen, sei dahingestellt. Ob sie Output herstellen, sei dahingestellt. Mir geht es um ihre Form. Alle Funktionseinheiten aller Ebenen können ohne Verlust an Flexibilität und Individualität gleich aussehen.

Zwischenstand

Soweit mein Traum von einer universellen, skalierbaren Grundstruktur von Software. Sie besteht aus einer beliebig tiefen Hierarchie von potenziell zustandsbehafteten und potenziell nebenläufigen Aktionen. Software tut etwas. Sie verarbeitet Daten. Deshalb wird sie beschrieben durch Tätigkeiten einheitlicher Form.

Klar, das ist Flow-Design nochmal beschrieben. Mir scheint eine so knappe Darstellung allerdings sinnvoll als Grundlage für das, was ich im Folgenden beschreiben möchte. Sie fasst den Stand der Flow-Design Überlegungen zusammen – allemal für die, die nicht alles verfolgen, was bisher dazu geschrieben wurde.

Ganz ohne Neuigkeit ist dieser Artikel andererseits aber auch nicht. Die Nebenläufigkeit ist jetzt “offiziell” im Bild und die Normalisierung. Der nächste Artikel wird zeigen, dass sie nicht nur konzeptionell nett ist.

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Dienstag, 6. September 2011

Softwareevolution in Vielfalt

In einem früheren Posting habe ich über Rahmenbedingungen für die Evolution von Software nachgedacht. Den dort genannten möchte ich nun noch eine hinzufügen: die Vielfalt.

Ich glaube, dass Evolution eines Ganzen schwieriger ist, als die Evolution von Vielem, das eine Summe hat.

Das Leben auf der Erde ist nicht ein Organismus (auch wenn es die Gaia-Hypothese gibt), sondern es besteht aus unzähligen. Und es evolviert in jedem einzelnen.

Genauso ist ein Betriebssystem wie Unix oder Windows nicht “ein Ding”, sondern besteht aus Hunderten oder gar Tausenden kleinen “Dingen”, die erst in Summe das ergeben, was wir mit dem Namen “Unix” oder “Windows” betiteln.

Mein iPhone ist einerseits ein “Ding”, eine Hard-/Softwareplattform. Andererseits ist es etwas, das sich entwickelt. Die Summe einer wachsenden Zahl von Teilen, den Apps. Mein iPhone evolviert sozusagen; es passt sich meinen Bedürfnissen ständig an. Das ist ganz einfach möglich, weil es nicht monolithisch ein “Ding” ist wie mein vorheriges Handy. Die Evolvierbarkeit steckt in der Möglichkeit zur Vielfalt.

Ich kann meine Schreib-Anforderungen iterativ immer besser erfüllen, weil ich meine Schreib-Apps getrennt von den Diagramm-Apps und die wiederum getrennt von den Musik-Apps “weiterentwickeln” kann. Ich kann mit “Notizen” beginnen, zu “Nebulous” weitergehen, auf “Pen & Paper” umsatteln und schließlich bei bei “Simplenote” enden.

Die Erfüllung anderer Anforderungen, ist von dieser Weiterentwicklung nicht betroffen. Ich kann auch jederzeit meinen Fokus wechseln. Mal möchte ich beim Schreiben vorankommen, mal möchte ich die Fotonachbearbeitung verbessern.

Diese Offenheit zur Evolution eines Ganzen durch feingranulare Evolution einer Vielfalt von Summanden, halte ich für eine Voraussetzung für evolutionären Erfolg. Unix hat es in Software vorgemacht, der PC hat es in Hardware nachgemacht, das Web hat dieses Prinzip ins Extreme getrieben, das iPhone setzt wieder darauf.

Voraussetzung für eine derartige Evolution der Vielfalt ist eine Plattform. Bei Unix war es der Kernel, würde ich sagen, beim PC der Bus mit seinen Slots für die Erweiterungskarten, beim Web TCP+DNS+HTTP+HTML, beim iPhone Hardware+iOS+AppStore.

Und was bedeutet das für Ihre Anwendung?

Wenn der Kunde kommt und sagt, er wolle “eine Anwendung”, dann nehmen Sie das “eine” nicht so genau. Ihr Kunde will einen Anforderungsberg abgetragen bekommen. Klar. Aber letztlich ist ihm egal, ob das mit 1 Anwendung (lies: EXE) geschieht, solange es praktikabel und nachhaltig geschieht.

Denken Sie also nicht reflexhaft an 1 großes Fenster für die GUI, in dem sich irgendwie alles abspielt. Denn das zwingt den Code in schnell in 1 großen Kasten, in dem er leicht verklebt.

Stattdessen überlegen Sie, wie Sie die Lösung als Summe einer Vielfalt von Apps beschreiben können. Ja, ich meine App in der Linie von iPhone oder iPad Apps. Das sind kleine, sehr fokussierte Programme. Sie dienen einem überschaubaren Zweck. Ihre Usability ist zu dessen Erfüllung maximiert.

Deshalb sehen die Apps auch alle sehr unterschiedlich aus. Eine Foto-App ist ganz anders als ein Spiel und das unterscheidet sich von der Mindmap-App. Das nehmen wir ihnen nicht übel, sondern begrüßen es. Auch, dass wir auf dem iPhone nur jeweils 1 App zur Zeit sehen, stört uns meist nicht. Im Gegenteil: so fokussieren wir uns.

Ich glaube, dass wir diesen Ansatz auf unsere “Geschäftsanwendungen” übertragen sollten. Wir sollten sie zerschlagen in eine Vielfalt von Apps. Und jede dieser Apps kann separat evolvieren.

Beispiel Faktura. Landläufig würde ein Softwarehaus für die Anforderungen, die hinter dem Begriff stehen, eine Anwendung bauen. Eine EXE, die auf allen Desktops im Büro installiert wird. (Und vielleicht noch ein SQL Server auf einem Server-Rechner.) Alle Funktionen würden über ein Hauptfenster erreicht und liefen im Faktura-Prozess ab.

Jede Änderung würde dann notwendig die eine Codebasis betreffen. Unschöne Kopplungen würden bei aller Mühe immer wieder entstehen. Die Evolvierbarkeit würde schnell sinken. (Ja, dagegen kann man sich mit Prinzipien und Praktiken stemmen, doch die grundlegend monolithische Sicht auf die Anforderungen macht es schwer für sie zu greifen.)

Wie anders würde die Entwicklung verlaufen können, wenn das Projekt mit evolvierender Vielfalt angegangen würde. Dann gäbe es eine App für die Rechnungslegung, eine App für die Stammdatenpflege, eine App für den Zahlungseingang, eine App für das Mahnwesen und eine App für den Chef. Vielleicht auch noch eine App für den Import und Export. Oder noch eine App für die Gestaltung unterschiedlicher Rechnungsvorlagen.

Es gäbe nicht mehr eine große Codebasis. Es gäbe ja nicht mehr eine Anwendung, sondern viele, ein Anwendungssystem. Die könnten Codeteile gemeinsam verwenden. Aber sie wären grundsätzlich sehr deutlich getrennt. Änderungen würden dann nicht mehr zu ungewollten Kopplungen in der Codebasis führen, da sie meist nur eine App zur Zeit beträfen. An manchen Apps würde mehr geschraubt als an anderen. Apps, die später in Angriff genommen würden, könnten intern anders aufgebaut sein, als frühere Apps, weil man schon aus Fehlern gelernt hat. Überhaupt müssten Entscheidungen nicht immer für “alles” getroffen werden; keine Notwendigkeit für 1 allumfassende Datenbank, 1 allumfassendes Datenmodell, 1 alle glücklich machende Persistenztechnologie, 1 GUI-Technologie usw.

Eine App könnte ihre Daten in SQL Server speichern, die andere in MongoDb. Die eine App könnte noch mit einer WinForms-Benutzerschnittstelle versehen sein, die andere WPF benutzen. Und wenn in einer App die Unwartbarkeit erreicht sein sollte, dann könnte die ganz unabhängig von allen anderen neu gemacht werden.

Ich kann nur Vorteile in einer solchen Vielfalt erkennen. Sie eröffnet Chancen, ohne einzugrenzen.

Es gibt lediglich eine Hürde: die in unseren Köpfen. Wir müssen Anwendungen so denken wollen. Technisch spricht nichts dagegen, sondern alles dafür. Auch sollten uns die genannten Präzedenzfälle ermutigen. Wir alle lieben Plattformen, die wir stückweise erweitern, die wir evolvieren können. Warum also nicht die Vorteile feingranularer Vielfalt für unsere eigenen Projekte nutzen?

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

PS: Einige Leser werden nun denken, ich würde damit vorschlagen, dass eine GUI-Shell oder ein Basisframework wichtig seien. Ohne etwas Gemeinsames unter allen Apps oder über allen Apps sei solche Vielfalt nicht zu machen. Nichts könnte mir jedoch ferner liegen. Anwendungsevolution durch eine Vielfalt an Apps hat keine Voraussetzung; nichts muss erstmal eingerichtet werden. Jede App kann unabhängig von anderen jederzeit begonnen werden. Das Betriebssystem als Plattform reicht.

PPS: Das Ganze nicht als Monolith zu sehen, sondern als Summe von vielen Teilen, sollte sich natürlich durch alle Ebenen einer Software ziehen. Die ganze Anwendung besteht aus unabhängig evolvierbaren Apps. Jede App besteht aus unabhängig evolvierbaren Bausteinen. Und diese Bausteine wiederum sind unabhängig evolvierbar. Apps sind mithin Ausdruck der Selbstähnlichkeit von Software.

Bausteine für die Softwareevolution

Wenn wir Software naturnah entwickeln wollen, also als evolvierbares System, wie sollte sie denn dann aufgebaut sein? In meinem vorherigen Artikel habe ich mir Gedanken über eher organisatorische Voraussetzungen für naturnahe Entwicklung gemacht. Diesmal will ich die technische Seite angehen. Oder besser: wieder angehen, denn vor Jahren schon hatte ich in diese Richtung spekuliert [1]. Damals waren meine Idee eher im Konzeptionellen verblieben - heute habe ich eine Vorstellung von ihrer Implementation.

Voraussetzung Selbstähnlichkeit

Ich glaube immer noch, dass Software nur wirklich gut evolvieren kann, wenn ihre Struktur selbstähnlich ist. Das heißt erstens, dass es in Software mehrere Ebenen gibt. Und zweitens bedeutet es, dass der grundlegende Aufbau aller Ebenen gleich ist.

Ich möchte diese Ebenen jedoch nicht Schicht/Layer nennen. Der Begriff ist überladen und deutet deshalb in die falsche Richtung. Stattdessen wähle ich Stratum; damit lehne ich mich an Abelson/Sussman an, die von Stratified Design sprechen.

Selbstähnlichkeit hat den Vorteil, dass Erfahrungen und Denken nicht ebenenspezifisch sind. Man kann auf einer beliebigen Ebene beginnen und sich sowohl nach oben wie nach unten vorarbeiten, ohne einen konzeptionellen Bruch zu erleiden.

Methoden sind etwas ganz anderes als Klassen, die wiederum etwas anderes sind als Assemblies, die wiederum ganz anders sind als Prozesse. Wenn wir mit diese Mitteln Software in unserem mentalen Modell repräsentieren, dann ist das kompliziert, weil immer wieder anders.

Eine Reduktion auf Klassen allein jedoch ist nicht möglich, da Klassen konzeptionell nichts enthalten außer Methoden. Klassen sind nicht schachtelbar. Sie sind dafür gedacht, Zustand und Funktionalität zusammenzufassen. Das war´s. [2]

Selbstähnlichkeit ist für mich die Voraussetzung, dass Systeme sich entwickeln können. Sie können in der Breite wachsen (innerhalb einer Ebene) oder sie können nach oben wachsen (Abstraktion, Aggregation) oder sie können nach unten wachsen (Verfeinerung, Spezialisierung).

Voraussetzung Unabhängigkeit

Dann glaube ich, dass die Bausteine in den Strata einer Software, unabhängig von einander sein sollten. Abhängigkeiten – statische wie dynamische – sind der Anfang allen Unwartbarkeitsübels. Dependency Injection zum Zwecke der Bereitstellung von Dienstleistungen ist keine Lösung, sondern perpetuiert das fundamentale Problem.

In der Natur finden sich keine Dienstleistungsabhängigkeiten in der Form, wie sie in Software allerorten existieren. Keinem Organismus wird eine Abhängigkeit injiziert. (Nur Parasiten injizieren sich – wir denken an Sacculina, Juwelwespe oder Ridley Scotts Alien.) Organismen brauchen einander, aber nicht in Form von “Referenzen” oder “Handles”. Sie sind vielmehr auf ihre jeweils autonom hergestellten “Outputs” angewiesen. Das ist gelebte Entkopplung.

Dasselbe gilt für den Maschinenbau oder die Elektrotechnik. Einem Motor wird kein Benzintank injiziert, auch wenn er ohne den Treibstoff nicht arbeiten kann. Benzintank und Motor werden lediglich verbunden; es gibt Berührungspunkte, aber keine Abhängigkeiten.

Allemal gibt es keine so breiten Abhängigkeiten wie die durch Interfaces üblicherweise eingegangenen. Ein Motor ist nicht von einem Chassis oder einen kompletten Restauto abhängig. Er hat verschiedene Input-Kanäle, die nur irgendwie gespeist werden müssen. In einem Motorprüfstand wird deutlich, dass das auch ganz anders als mit einem Auto geschehen kann. Dasselbe gilt für einen menschlichen Körper, der ganz ohne Herz und Lunge auskommen kann, wenn an die Gefäße eine Herz-Lungen-Maschine angeschlossen ist.

Abhängigkeiten machen Systeme starr. Evolvierbarkeit setzt daher maximale Unabhängigkeit voraus.

Voraussetzung Nachrichtenorientierung

Die Verbindung zwischen Lebewesen ist immer “nachrichtenorientiert”. Wichtiger ist aber die Nachrichtenorientierung. “Daten” fließen unidirektional von einem Lebewesen zu einem anderen, gezielt oder diffus. Folgt man dem Konstruktivismus, dann ist die Welt (und damit andere Lebewesen) gar nicht direkt erfahrbar. Wir haben lediglich eine begrenzte Wahrnehmungsbreite und empfangen Signale. Die Ausgangspunkte dieser Signale selbst haben wir nie in der Hand. Wir konstruieren sie uns aus den Signalen.

Wenn das für die Evolution der Natur so zentral ist, dann scheint es mir vorteilhaft für Software, die ebenfalls hochgradig evolvieren muss. Ihre Bausteine sollten ebenfalls nur über Nachrichten kommunizieren. Das heißt, es gibt kein Call/Response, sondern nur unidirektional fließende Datenpakete. (Inwiefern die Referenzen auf den Zustand von Bausteinen enthalten können/dürfen, lasse ich hier mal dahingestellt.)

Softwarebausteine verstehen bestimmte Nachrichten, die sie von der Umwelt empfangen. Und sie senden eine bestimmte Menge von Nachrichten an ihre Umwelt. Sendung und Empfang sind in der Natur asynchron. Zwischen Softwarebausteinen würde ich Asynchronizität jedoch nicht zwingend voraussetzen.

Die Form evolvierbarer Software

Wenn ich diese Voraussetzungen zu einer Form für Softwarebausteine zusammenfasse, dann kommt das folgende Bild heraus:

image

Ich könnte diesen Softwarebaustein “Objekt” nennen, da er zustandsbehaftet ist und über Nachrichten kommuniziert. Aber “Objekt” ist so belastet, dass ich zumindest heute auf einen neutralen Begriff ausweichen möchte. Ich nenne ihn deshalb “Holon”. Das passt zur Selbstähnlichkeit; Holons sind “Dinger”, die gleichzeitig Teil und Ganzes sind.

Jedes Holon nimmt die Umgebung durch einen “Kanal” wahr und sendet über einen weiteren “Kanal” Signale an seine Umgebung, die dann andere Holons wahrnehmen können. Wahrnehmungen (Input) verarbeitet das Holon zu Zustand und/oder Signalen (Output).

Natürlich kann jedes Holon nur eine bestimmte Menge von Signalen verarbeiten und erzeugen. Signale bzw. Nachrichten bestehen daher nicht nur aus Daten, sondern haben auch noch einen Typ oder eine Bedeutung. Die Zahl 42 mag einmal die Bedeutung einer Antwort auf die ultimative Frage haben und einmal schlicht das Alter einer Person sein.

Aus Holons lassen sich nun Verarbeitungsstrukturen auf mehreren Ebenen (Strata) bilden:

image

Meine These ist, dass Software viel besser evolvierbar wird, wenn wir sie aus Holons aufgebaut denken. Die scheinen merkwürdig eingeschränkt gegenüber unseren heutigen OOP-Objekten. Ich behaupte aber mal, dass einen eventuelle Einschränkung mehr als kompensiert wird durch das, was wir bekommen. Und das wir nur bekommen, was wir wollen – viel mehr Evolvierbarkeit –, wenn wir uns einschränken.

Wir denken mit Holons nicht mehr über Abhängigkeitsverhaue nach. Wir haben einen ganz regelmäßigen Aufbau von Software von der kleinsten Codeeinheit bis zur größten. Wir können ganz natürlich Stratified Design betreiben. Und wir bekommen Bausteine, die sich von Hause aus für asynchrone/parallel Verarbeitung eignen.

Klar, das bedeutet, wir müssen unsere Denkstrukturen ändern und Gewohntes über Bord werfen. Der in Aussicht gestellte Gewinn scheint mir jedoch groß genug, um es damit zu probieren. Und so schwer ist es ja auch nicht, es auszuprobieren. Hier das universelle Interface für Holons:

interface IHolon {
  void Process(IHolonMessage input, Action<IHolonMessage> output);
}

Im Grunde reicht sogar ein Delegat, um jeden Baustein in jedem Stratum zu beschreiben:

delegate void Holon(IHolonMessage input, Action<IHolonMessage> output);

Und die Nachrichten, die zwischen den Holons fließen, sind auch ganz einfach:

interface IHolonMessage {
    string Type {get;};
    object Data {get;};
}

Kaum zu glauben, aber ich denke, dass mit solch einfachen Mitteln sich grundsätzlich alle Softwarelösungen ausdrücken lassen. [3] Diese einfachen Mittel stellen für mich das Rückgrat evolvierbarer Software dar.

PS: Wenn in der Softwareentwicklung Eleganz wirklich einen hohen Stellenwert hat, dann wage ich mal keck zu hoffen, dass eine Softwarestruktur dieser Art doch einigen Appeal hat. Ich zumindest finde sie in ihrer Einfachheit elegant.

PPS: Ich weiß, die Holons sehen aus wie Funktionseinheiten des Flow-Designs. Natürlich gibt es da auch einen Zusammenhang. An dieser Stelle überlege ich jedoch grundlegender. Deshalb haben Holons auch nur einen Eingang und einen Ausgang.

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Fußnoten

[1] s. ältere Artikel in der Rubrik “Software als System” in diesem Blog, insbesondere diesen und diesen Artikel.

[2] Das heißt nicht, Objekte seien nutzlos für evolvierbare Software. Nein, ich denke, wir müssen keine neue Programmiersprache erfinden, sondern die vorhandenen nur besser einsetzen.

[3] Natürlich meine ich damit nicht, dass Software bis hinunter zu Ausdrücken aus Holons aufgebaut werden sollte. Mir geht es um das Herunterbrechen von Funktionalität bis zu einer Granularität, dass man begründet zuversichtlich sein kann, ein Blatt-Holon mit wenigen Zeilen konventionellen Codes umsetzen zu können.

Sonntag, 4. September 2011

Voraussetzungen für sich entwickelnde Softwarestrukturen

Liegt die Zukunft der Softwarearchitektur in Emergent Architecture und Growing Software? Diese Frage wurde in zwei Open Space Gruppen auf der SoCraTes Konferenz 2011 behandelt. Das hat mir gefallen, da ich ja mit meinen Softwarezellen auch schon einmal eine naturnahe Metapher gewählt hatte.

In den Diskussionen wurde ich das Gefühl jedoch nicht los, dass wir bei allem Appeal der Metaphern noch zu wenig darüber nachgedacht hatten, was es denn wirklich heißt, naturnah Software entstehen zu lassen. Wie entwickeln sich natürliche Systeme? Was lässt sich davon auf Softwareentwicklung übertragen? Welche Voraussetzungen sind nötig?

Struktur entspricht gegenwärtiger Umwelt

Ganz zentral für naturnahe Softwareentwicklung ist, dass wir die Rolle der Umwelt richtig verstehen. In der Natur ist alles so wie es ist, weil es so am besten zur Umwelt passt. Das ist der Kerngedanke der Evolutionstheorie, würde ich sagen. Alles an allen Organismen hat einen Zweck, den es erfüllt, so wie es ist. Da ist nichts zuviel, nichts zuwenig.

Vor allem ist nicht so entwickelt wie es ist auf Vorrat. Alles ist nur einer gegenwärtigen Umwelt mit ihren über Generationen erfahrenen Schwankungen angepasst. Kein Lebewesen besitzt eine Eigenschaft nach dem Motto “Aber was wäre, wenn Umweltparameter X in der Zukunft ganz anders aussehen würde?”

Leben ist insofern “lean”, d.h. frei von “Fett”. Es gibt keine “Eigenschaftenpolster”. Selbst die Anpassungsfähigkeit ist begrenzt, obwohl sie die wichtigste Metaeigenschaft ist.

Wenn wir wirklich naturnah Software entwickeln wollen, dann müssen wir konsequent auf den Blick in die Glaskugel verzichten. Erfahrung, die uns verleitet zu sagen, “Aber ich weiß doch, dass Kunden in den meisten Fällen noch Y wollen…”, darf nicht zu Anreicherungen in der Software führen. Das höchste der Gefühle ist, dem Kunden einen solchen Gedanken vorzulegen, um durch seine Antwort die gegenwärtige Umwelt besser zu verstehen. Das oberste Prinzip naturnaher Entwicklung ist mithin YAGNI.

Jede Entscheidung darf sich nur vom gegenwärtigen Kenntnisstand leiten lassen. Dann ist die Entscheidung erstens schnell getroffen und zweitens das Ergebnis so schlank wie möglich.

Evolution ist ohne Ziel

Wenn Evolution sich nur an der gegenwärtigen Umwelt orientiert, dann bedeutet das in verschärfter Formulierung, dass Evolution kein Ziel hat. Und das bedeutet, Evolution kann nicht vorplanen. Es gibt kein Vordenken, sondern nur Reaktion, Anpassung. [1]

Das bedeutet für die Softwareentwicklung ein viel größere als bisher übliche Konzentration auf das Hier und Jetzt. Geradezu buddhistisch muss für naturnahe Entwicklung gelten “live in the now”.

Weder Strukturen noch Schritte sollten über das unmittelbar Sichtbare und Notwendige hinaus ausgelegt sein. Zu YAGNI tritt KISS als Leitprinzip.

Evolvierbarkeit ist ein Ergebnis von Evolution

Als Ergebnis der Evolution sehen wir vor allem die umweltangepassten Eigenschaften von Lebewesen. Wir haben verstanden, dass diese Eigenschaften sich durch Feedback ausprägen. “Survival of the fittest” nennen wir das. Eigenschaften entstehen und müssen sich gegenüber der Umwelt behaupten. Passen sie nicht, sinkt die Wahrscheinlichkeit zur Weitergabe der Eigenschaften an die nächste Generation; unpassende Eigenschaften sterben aus. Das Feedback der Umwelt ist gnadenlos.

Auf die Software haben wir das inzwischen in Form iterativen Vorgehens übertragen. Wir stellen Software mit bestimmten Eigenschaften her, von denen wir meinen, dass sie zur Umwelt (den Anwendern) passen – und suchen dann das Feedback der Umwelt. So nähern nähern wir die Eigenschaften in kleinen Schritten der Umwelt an bzw. co-evolvieren Software mit der Umwelt.

Hinter den sichtbaren Eigenschaften evolvierender Systeme gibt es allerdings noch eine unsichtbare Eigenschaft: die Evolierbarkeit. Wie jede Eigenschaft unterliegt auch sie einem Zwang zur Passgenauigkeit.

Meinem Empfinden nach haben wir diese Eigenschaft als ebenfalls zu entwickeln nicht auf dem Zettel. Wir unterwerfen sie in unseren Projekten keinem Feedback. Wir unterwerfen sie nicht einmal branchenweit einem Entwicklungsdruck (oder allenfalls einem nur vergleichsweise schwachen).

Wenn wir über naturnahe Softwareentwicklung sprechen, ist das eine große Nachlässigkeit, da im Kern der gesuchten Naturnähe ja gerade die Evolvierbarkeit steht.

Wie müsste also das Feedback aussehen, um Evolvierbarkeit zu entwickeln?

Ob unsere Software schon performant, skalierbar, sicher genug ist, stellen wir fest, indem wir sie immer wieder auf die Probe stellen. “Los, zeig uns, wie schnell du bist!”, “Ha! Kommst du auch mit dieser größeren Nutzerlast zurecht?”, “Nimm diese Sql Injection Attacke, du Software, du!”

Aber wie ist es mit der Evolvierbarkeit? Was ist eine Herausforderung für sie?

Ich denke, das sollte auf der Hand liegen: Evolvierbarkeit wird durch unerwartete Anforderungen auf den Grill gelegt. “Pass dich dieser überraschenden Anforderung an, Code!”

Das erste, was Evolution lerne musste war die Unvorhersehbarkeit der nächsten Umweltveränderung. Von der hat sie ihre grundlegenden Bausteine, die Moleküle (DNA, RNA), Organellen und Zellen formen lassen. So existiert Leben am tiefsten Meeresgrund genauso wie im Darm und auf dem Mount Everest.

Für die Softwareentwicklung ist das noch eine Aufgabe, denke ich. Unsere grundlegenden Bausteine sind noch nicht wirklich evolutionsunterstützend. Allemal können wir noch viel zu leicht anti-evolutionäre Strukturen damit herstellen, wie die Massen an Brownfield-Code zeigen.

Das bedeutet, wir müssen mehr Druck auf die Evolvierbarkeit ausüben. Sonst hinkt ihre Entwicklung der anderer Eigenschaften hinterher – und behindert deren Evolution. Wir müssen also geradezu auf die Evolvierbarkeit zuallererst Evolutionsdruck ausüben, weil sie für alles andere fundamental ist. Wie soll das aber geschehen?

Nach meinem Gefühl ist das gar nicht schwer. Wir müssen erstens die Frequenz der Änderungsanforderungen drastisch erhöhen. Und zweitens müssen wir die Vorausschau drastisch begrenzen.

Ganz konkret bedeutet das für mich: Software ist jeden Tag mit überraschenden Änderungswünsche zu konfrontieren. Und jeden Tag ist Software auszuliefern. Nur so entsteht auf ihre Anpassungsfähigkeit soviel Druck, dass die wirklich geschmeidig wird. Das gilt für die allgemeine Form von “Softwarebauteilen” wie für die konkrete Struktur einer Lösung. Ohne diesen Druck werden wir nicht dahin kommen, die Prinzipien und Praktiken für dauerhafte Evolvierbarkeit zu erkennen und zu leben.

Evolution geht aufs Ganze

Wer hätte je einen Magen oder ein Gehirn oder eine Hand getrennt von einem Lebewesen sich entwickeln sehen?

Evolution findet immer am ganzen Organismus statt. Ein Lebewesen ist entweder als Ganzes an seinem Umwelt angepasst oder gar nicht. Das beste Herz nützt nichts, wenn das Nervensystem unangepasst ist.

Auf die Softwareentwicklung übertragen bedeutet das, es gibt keine Infrastrukturentwicklung. Jedes Mal, wenn sich Software der Umwelt aussetzt – also am Ende einer Iteration –, setzt sie sich als Ganzes aus. Sonst ist sie nicht “anschlussfähig” gegenüber ihrer Umwelt. Das muss sie aber sein, sonst bekommt sie kein Feedback. Und Mangel an Feedback bedeutet Stillstand und dann Tod.

Alle Arbeit an Software muss deshalb sagen können, inwiefern sie an die Umwelt anschließt, welches konkrete Bedürfnis der Umwelt sie erfüllt. Ein Team als Ganzes muss zu jeder Zeit auf die Software als Ganzes konzentriert sein. (Ein einzelner Entwickler kann sich im Rahmen dessen natürlich auf ein Detail fokussieren. Er dient damit ja einem Ganzen.)

Naturnahe Entwicklung bedeutet also Entwicklung in Durchstichen. In jeder Iteration ist eine ganze, besser angepasste Software abzuliefern. [2]

Evolution ist Verfeinerung

Schließlich scheint mir ein wesentliches Merkmal von natürlicher Entwicklung die Bewegung vom Groben zum Feinen. Evolution ist Differenzierung, ist Verfeinerung.

Die Entscheidung zwischen bottom-up und top-down Entwicklung können wir zur Ruhe betten. Diese beiden Pole sind unwichtig. Wir können die Herangehensweise wählen, wie wir mögen, wie es uns in Bezug auf eine Problemstellung passt.

Worüber wir aber nicht nachdenken sollten, das ist coarse-to-fine oder simple-to-complex Entwicklung. Software sollte schrittweise “schärfer” oder detaillierter werden.

Auch breadth-first vs. depth-first ist hier zweitrangig. Die Verfeinerung kann in der Breite in geringem Maß stattfinden oder in der Tiefe in hohem Maß. Wie es der Kunde mag. Er bestimmt die Prioritäten.

Nach jeder Iteration kann er sich wünschen, ob Feature A verfeinert werden soll oder lieber Feature X grob angegangen werden soll. Vor dem Hintergrund des täglichen Drucks auf die Evolvierbarkeit bedeutet das, der Kurs der Entwicklung kann sich prinzipiell jeden Tag ändern. Und warum auch nicht? Der Kunde, die Anwender: das ist die Umwelt. Der gilt es sich anzupassen.

Naturnahe Entwicklung findet deshalb in kleinsten Schritten statt. User Story, Use Case, ja selbst Feature sind dafür zu groß. Würden sie pro Iteration komplett umgesetzt, würde zu schnell zu viel Detail entstehen. Das wäre keine Evolution mehr, sondern Setzung.

Wenn es jedoch jeden Tag an quasi beliebiger Stelle in der Software ein Nutzenstückchen weiter geht, dann entsteht kontinuierliche Anpassungsfähigkeit. Dann kann auch nie mehr als die Arbeit eines Tages umsonst gewesen sein. Tägliches Feedback korrigiert ja den Evolutionsweg unmittelbar.

Fazit

Agile Softwareentwicklung ist schon ein Schritt in Richtung naturnahe Softwareentwicklung. Erreicht ist das Ziel mit ihr jedoch noch nicht. Wenn wir naturnahe Softwareentwicklung als Tugend ansehen, dann müssen wir ein paar Schrauben noch mehr anziehen. Evolution ist rigoroser als Agilität.

Fußnoten

[1] Ziellosigkeit mag merkwürdig klingen, da doch anscheinend mindestens der Kunde ein Ziel hat. Er möchte schließlich eine Software für einen bestimmten Zweck. Wie oft ist es aber der Fall, dass am Ende der Entwicklung genau das herauskommt, was am Anfang in den Anforderungen drin gestanden hat? Wie oft sind Anforderungen vollständig? Ich halte es deshalb für besser, die Idee von der Zielhaftigkeit im Sinne einer sichtbaren Zielmarke aufzugeben. Softwareentwicklung ist vielmehr ziellos – hat jedoch ein Ende. Beendet ist sie, wenn der Kunde sagt “Genug!”

[2] Mit “ganze Software” meine ich natürlich nicht, dass vom ersten Tag an eine komplette Buchhaltung oder eine ganze Textverarbeitung ausgeliefert werden soll. Wie sollte das auch gehen? Nein, “ganze Software” bedeutet, dass die Software für den Anwender als etwas sinnhaftes wahrnehmbar ist, zu dem er Feedback geben kann. Für eine Buchhaltungssoftware bedeutet das nicht mal, dass es ein GUI geben muss, sondern am ersten Tag vielleicht nur einen Prüfstand, mit dem der Anwender Feedback zum Datenimport geben kann.

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Montag, 6. Dezember 2010

Was ist Softwarearchitektur – Teil 2

Softwarearchitektur ist eine der beiden Entwurfsdisziplinen der Softwareentwicklung. Ihr Ergebnis ist die fundamentale Struktur einer Software im Hinblick auf die nicht-funktionalen Anforderungen.

Das ist für mich schon eine recht präzise Definition – allerdings eine allgemeine. Sie beantwortet noch nicht alle Fragen in der Praxis. Gehört die Entscheidung für IIS und gegen NT Service eine Architekturentscheidung? Ist die Entscheidung für eine Aufteilung der Rechtschreibprüfung in Textzerlegung und Wortprüfung eine Architekturentscheidung? Oder ist die Entscheidung für die Zusammenfassung von Funktionalität zu einer Komponente eine Architekturentscheidung?

Um diese und andere Fragen zu beantworten, müssen Sie erstens mein allgemeines Verständnis von Struktur kennenlernen und zweitens meine Vision davon, wie Architektur Strukturen konkretisiert.

Was ist eine Struktur?

Als Struktur verstehe ich “etwas”, das aus Elementen besteht, die in Beziehung zu einander stehen.

image

Strukturen mit vielen Elementen und/oder Beziehungen sind unübersichtlich. Deshalb gehört zu Struktur auch noch die Abstraktion. Ein System wie im Bild beschreibt eine Struktur also nur auf einer Abstraktionsebene, andere können darunter/darinnen liegen…

image

oder darüber/darum…

image

Wer eine Struktur entwirft, muss sich also drei Fragen stellen:

  1. Welche Abstraktionsebenen hat die Struktur?
  2. Welche Elemente enthält eine Abstraktionsebene (innerhalb eines womöglich umschließenden Elements einer höheren Abstraktionsebene)?
  3. Wie sind die Elemente in einer Abstraktionsebene verbunden?

Die Abstraktionsebenen und Strukturelemente der Softwarearchitektur

Mit einem allgemeinen Strukturverständnis bewaffnet können Sie nun verstehen, dass Softwarearchitektur als Entwurf auf verschiedenen Ebenen betrieben werden sollte. Ohne diese Ebenen ist die Kompliziertheit eines Systems nicht in den Griff zu kriegen.

imageDie Abstraktionsebenen der Softwarearchitektur sind für mich diese:

  1. Das gesamte Anwendungssystem
  2. Bounded Contexts
  3. Partitionen/Apps
  4. (Virtuelle) Maschinen
  5. Betriebssystemprozesse
  6. Belange/Concerns

Ein paar Worte zu den Ebenen:

Anwendungssystem

Ein Entwicklungsauftrag wird gewöhnlich auf der Ebene des Anwendungssystems erteilt: “Schreiben Sie mir eine Software, die dies und jenes kann.” Das kann eine Warenwirtschaft sein, ein TicTacToe-Spiel oder eine Textverarbeitung. Wenn implementiert, erfüllt das Anwendungssystem alle Anforderungen.

Bounded Contexts

Das Gesamtsystem zerlegt der Architekt in Bounded Contexts, d.h. funktionale Untermengen, die unterschiedliche Datenmodelle haben. Sie gehören innerhalb des Gesamtsystems, der Anwendungsdomäne verschiedenen Sub-Domänen an. Bei einem Faktura-Anwendungssystem könnten das Rechnungslegung und Rechnungsverfolgung und Stammdatenverwaltung sein.

In jedem Bounded Context können Daten anders persistiert werden. Das drückt die kleine Tonne im Bild aus.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Evolvierbarkeit, Skalierbarkeit, Sicherheit

Apps

Innerhalb eines Bounded Context kann die Funktionalität weiter aufgeteilt werden auf Partitionen oder – neuerdings von mir so genannt – Apps. Jede App steht für eine klar umrissene Funktionalität, die durch ein spezifisches Userinterface bedient wird. iPhone/iPad Apps spiegeln diesen Gedanken sehr schön wider, finde ich. Oder die vielen kleinen Tools unter Unix.

Alle Apps in einem Bounded Context benutzen dieselbe Datenbasis.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Usability, Evolvierbarkeit

Maschinen

Jede App läuft auf mindestens einer Maschine – vom Server über den Desktop bis zum Smartphone –, kann aber auch mehrere Maschinen überspannen wie bei einer Web-Apps, die sowohl im Browser wie im IIS wie in einem Application Server wie in der Datenbank Codeteile hat.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Skalierbarkeit, Performance, Sicherheit, Verfügbarkeit, Robustheit

Prozesse

Auf jeder Maschine einer App läuft deren Code mindestens in einem Prozess, es können aber auch mehrere sein. Zum Beispiel könnten ein Application Server und ein Reporting Server auf derselben Maschine betrieben werden.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Skalierbarkeit, Performance, Evolvierbarkeit, Flexibilität

Concerns

Die Zerlegung des Anwendungssystems bis in Prozesse führt zu autonomen Funktionseinheiten. Die arbeiten jede für sich vor sich hin. Jede läuft auf mindestens einem eigenen Thread. Die Kommunikation ist mithin ganz fundamental nur asynchron zwischen ihnen möglich.

Diese autonomen Funktionseinheiten zu identifizieren, ist die vornehmste Aufgabe des Architekten.

Als Vorbereitung für die Modellierung und Implementierung ist das allerdings noch nicht genug, finde ich. Einen Schritt weiter sollte der Architekt noch gehen. Er sollte innerhalb der Prozesse auch noch die grundsätzlichen Concerns herausarbeiten, d.h. Bereiche, die gezielt nicht-funktionale Anforderungen realisieren. Das sind z.B. Frontend, Persistenz, Kommunikation, Sicherheit, Domänenlogik.

Die Modellierung hat es dann einfacher, zu einem Entwurf zu kommen, weil die Concerns eine Grundeinteilung für die Funktionseinheiten des Modells vorgeben. Wenn das Modell sich um Funktionalität kümmert, dann muss es wissen, um welche Funktionalität es geht. Domänenfunktionalität ist nicht die einzig zu modellierende.

Beziehungen in Abstraktionsebenen

Auf welchen Abstraktionsebenen der Architekturentwurf stattfindet, ist geklärt. Die wesentlichen Strukturelemente sind Bounded Context, App, Maschine und Prozess. Aber wie werden die mit Beziehungen innerhalb einer Ebene zu einem System verwoben? Ich denke, das sollte auch definiert sein, denn dann lassen sich weitere Fragen leichter beantworten.

Bounded Context

Bounded Contexts sind durch Datensynchronisationen miteinander verbunden. Sie teilen weder Datenbanken noch Datenmodelle, also müssen die Daten explizit zwischen Bounded Contexts synchronisiert werden. Das dauert natürlich seine Zeit. Deshalb ist über Bounded Contexts hinweg nur Eventual Consistency zu erwarten.

Fragen für die Kommunikation: Wie soll die Datensynchronisation implementiert werden? Gibt es Tools (Stichwort ETL)? Wie groß das die maximale Verzögerung sein? Kann es Konflikte geben, wie werden die behandelt?

Apps

Apps müssen nicht direkt miteinander kommunizieren, sondern teilen sich eine Datenbasis. Sie ist das Bindeglied. Zentral für die Apps eines Bounded Context ist daher das eine Datenmodell.

Fragen für die Kommunikation: Welche Persistenztechnologie soll verwendet werden (RDBMS, Dokumentendatenbank, Graphendatenbank, Dateisystem usw.)? Wie sieht das Schema aus?

Maschinen

Maschinen sind über (virtuelle) Leitungen miteinander verbunden; sie teilen keinen physischen Adressraum. Die Kommunikation kann daher nur nachrichtenorientiert und asynchron ablaufen. Und ich würde noch hinzufügen: Die Kommunikation sollte zwischen Maschinen auch immer als asynchron in der Implementierung zu sehen sein.

Fragen für die Kommunikation: Welche Kommunikationstechnologie soll zum Einsatz kommen, insb. wenn die Maschinen mit unterschiedlichen Betriebssystemen betrieben werden? Wie sollen Leitungsunterbrechungen behandelt werden?

Prozesse

Prozesse laufen zwar im selben physischen Adressraum, werden vom Betriebssystem darin jedoch sauber getrennt. Die Kommunikation kann zwischen ihnen kann daher auch nur nachrichtenorientiert sein. Allerdings ist zu erwarten, dass Prozesse in der selben Maschine verlässlicher vorhanden/erreichbar sind als solche in anderen Maschinen. Deshalb ist zu überlegen, ob die Kommunikation zwischen Prozesse in derselben Maschine die Kommunikation immer als asynchron in der Implementierung zu sehen sein muss. Im Augenblick tendiere ich dahin, meine Position, dass das immer so sein muss, zu lockern. Das bedeutet jedoch: Wenn ein Architekt sich für eine synchrone Kommunikation zwischen Prozessen entscheidet, weil er sie auf derselben Maschine laufen sieht, dann können diese Prozesse später nicht auf verschiedenen Maschinen deployt werden. Denn die sollen asynchron kommunizieren (s.o.). Soviel Grundsatz sollte sein, finde ich.

Fragen für die Kommunikation: Welche Kommunikationstechnologie soll zum Einsatz kommen? Welche Hosts für den Code sollen benutzt werden (z.B. IIS, Desktopanwendung, NT Service)?

Concerns

Concerns sind funktionale Bereiche innerhalb von Prozessen. Sie kommunizieren also im selben Adressraum synchron via Stack oder globale Speicherbereiche.

Fragen an die Architektur

Mit diesen Konkretisierungen lassen sich die obigen Fragen und andere beantworten. Ist die Entscheidung für IIS oder NT Service eine Architekturentscheidung? Das ist natürlich eine Architekturentscheidung auf Prozessebene.

Wer sich über WCF vs NServiceBus unterhält, unterhält sich ebenfalls über Architektur. REST vs SOAP? Auch ein Architekturthema – mindestens auf Maschinenebene. Was ist mit OAuth? Ein Architekturthema. Was ist mit NoSql? Ein Architekturthema. Was ist mit WPF? Nur insofern ein Architekturthema als dass WPF auf bestimmten Devices (nicht) möglich sein könnte. Was ist mit Bubblesort vs Quicksort? Kein Architekturthema, auf wenn es um die nicht-funktionale Anforerung Performance gehen mag; denn die Unterscheidung ist keiner Abstraktionsebene der Architektur zuzuordnen. Code im Application Server oder im Datenbankserver laufen lassen? Ein Architekturthema.

Architektur hat mit der Verteilung von Funktionalität zu tun, um nicht funktionale Anforderungen zu erfüllen. Wenn Funktionalität verteilt wird, dann stellen sich Fragen dazu, wie die Funktionalität an ihren Orten gehostet werden soll und wie die verteilten Teile miteinander kommunizieren sollen.

Verteilt wird Funktionalität in zwei Weisen:

imageArchitektur macht zunächst auf den Abstraktionsebenen Bounded Context und App Längsschnitte durch das Anwendungssystem. Architektur schneidet es in dünne Scheiben. Die sind vollständig insofern, als dass sie vom Frontend bis zum Backend reichen.

Der Entwurf beginnt also damit zu überlegen, wie die gesamten Anforderungen in überschaubare Happen zerlegt werden können. Welche für den Anwender sinnvollen Subsysteme lassen sich finden, um nicht sofort und immer alles umsetzen zu müssen? Bounded Contexts und Apps sind daher auch fundamental für die Entkopplung von Code. Sie dienen unmittelbar der Evolvierbarkeit.

imageApp, Maschine und Prozess zerlegt die Architektur dann transversal weiter. Die funktionalen Scheiben werden “gewürfelt”, um insbesondere Skalierbarkeit, Performance und Verfügbarkeit zu erreichen. Jedes Teil stellt dann nicht mehr für sich einen Anwendernutzen her, sondern trägt nur dazu bei. (Hier ist die klassische Schichtenarchitektur anzusiedeln.)

Zusammenschau

Meine Erfahrung mit der Definition von Architektur in dieser Weise ist, dass sie sich leicht erklären lässt und Fragen beantwortet, bei denen zumindest ich früher “rumgeeiert bin”. Ich behaupte nicht, dass diese Definition vollständig ist; ich empfinde sie lediglich als pragmatisch-praktisch-gut ;-) Nützlichkeit sowie Kommunikationsfähigkeit stehen für mich hier höher als formale Reinheit.

Donnerstag, 1. April 2010

Holarchie – Event-Based Components für natürliche Softwaremodellierung

Software ist eine Holarchie, d.h. eine Hierarchie von Teilen, die gleichzeitig Ganzes sind. Das hatte ich während der Modellierung von Software mit “traditionellen” Komponenten schon fast wieder vergessen. Aber neulich schrieb mich jemand zu meiner Blogserie über das Softwareuniversum an, weil er Parallelen zu seiner Arbeit sah. Da habe dann versucht, meinen derzeitigen Ansatz der Event-Based Components (EBC) die damaligen Gedanken anzuschließen.

So sah 2005 eines meiner Diagramme für die grundsätzliche Struktur von Software aus:

image

Die Frage heute: Bin ich diesem “Ideal” nun mit EBCs ein Stück näher gekommen?  Ich will die Antwort anhand eines Beispiels versuchen, für das ich zwei Architekturen entwickle. Eine mit “traditionellen” Komponenten, eine mit EBCs.

Das Szenario ist simpel: Der Kunde wünscht sich einen Funktionsplotter. Er möchte also ein kleines Programm haben, in das er eine Formel eingeben kann – z.B. x^2+2x-3 oder Sqrt(Sin(x)*Cos(x)) –, für die dann ein Graph am Bildschirm geplottet wird.

Traditionelle Komponentenarchitektur

Wie hätte ich eine Architektur für den Funktionsplotter vor einigen Monaten geplant? Ich kann mich kaum erinnern ;-) EBCs haben sich schon so in meinem Denken breit gemacht, dass ich kaum noch ohne kann. Aber ich versuche es einmal.

Am Anfang stünde eine Softwarezellen bzw. ein System-Umwelt-Diagramm:

image

Eine Anwenderrolle greift auf die ganze Anwendung zu. Der Anwender ist abhängig von der Anwendung. Das System-Umwelt-Diagramm ist also ein Abhängigkeitsdiagramm.

Im nächsten Schritt würde ich die Softwarezelle zerlegen in Funktionseineiten. Nach dem Separation of Concerns Prinzip fielen mir da mindestens zwei ein: eine Funktionseinheit für die Interaktion mit der Benutzerrolle, ein Portal, und eine für den Rest, d.h. die Domänenlogik.

image

Bei genauerem Nachdenken würde ich dann natürlich das Portal weiter zerlegen. Eine eigene Funktionseinheit für das Plotten schiene mir angezeigt. Und auch die Logik zerfiele in mindestens zwei Funktionseinheiten:

image

Sollte ich es dabei bewenden lassen? Im Frontend ja. Aber die Logik… Nein, die ist noch zu grob. Ein Formelübersetzer ist – so nehme ich mal für dieses Beispiel an – keine so einfache Sache. Die will sauber in drei Pässe gegliedert sein. Es geht ja um nicht weniger als die Übersetzung von Quellcode (Formel) in Code (ausführbare Funktion). Da soll auch die kleine Plotteranwendung in puncto Systematik nicht hinter einem richtigen Compiler zurückstehen.

Und auch die vorgeschaltete Funktionseinheit scheint nicht sauber. Sie hat noch ein “und” in der Bezeichnung, sie tut also noch zweierlei. Zum einen veranlasst sie etwas (Übersetzung), zum anderen tut sie etwas (Funktionswerte berechnen). Das ist im Sinne des Single Responsibility Principle nicht schön.

Wenn ich diese Gedanken in die Architektur einfließen ließe, würde sie sich so weiter entwickeln:

image

Zwei Funktionseinheiten stechen darin heraus, das Rechenwerk und der Compiler. Beide haben eigentlich keine richtige Aufgabe im Sinne der Geschäftslogik. Sie sind nur Fassaden, die Details rechts von ihnen gegenüber ihren Clients verbergen. Allenfalls koordinieren sie noch die Arbeit der Funktionseinheiten, von denen sie abhängig sind. Das Rechenwerk lässt erst den Compiler die Formel übersetzen und übergibt sie dann an die Funktionswertberechnung. Der Compiler fordert zuerst den Scanner auf, die Formel in Symbole zu zerlegen. Die reicht er weiter an den Parser. Der erzeugt einen abstrakten Syntaxbaum, den der Codegenerator übersetzt.

Im Rahmen “traditioneller” Komponentenorientierung ist das völlig normal. Auf Rechenwerk und Compiler zu verzichten, würde nämlich zu ganz unschönen Abhängigkeiten führen, z.B.

image

Rechenwerk und Funktionswertberechnung hingen nun zusammen und würden nur den Parser ansteuern. Der lieferte ultimativ auch die übersetzte Funktion – aber natürlich nur unter Zuhilfenahme des Parsers, der wiederum den Codegenerator brauchte.

Nein, das sieht ungut aus. Warum soll ein Scanner den Parser kennen und am Ende sogar etwas mit dem generierten Code zu tun haben? Und warum sollte eine Funktionsberechnung einen Scanner kennen? Der ist ein Detail einer bestimmten Compilerimplementation.

Die Funktionseinheiten Rechenwerk und Compiler wären nötig in einer “traditionellen” Komponentenorientierung.

Und wo sind die Komponenten nun? Ich würde sagen, alle Funktionseinheiten verdienen es, als Komponenten realisiert zu werden. Z.B. Compiler, Scanner, Parser und Codegenerator in einer Komponente zusammen zu fassen, würde die Möglichkeit zur Parallelentwicklung begrenzen. Denn neben klaren Grenzen zwischen Funktionseinheiten geht es darum bei der Komponentenorientierung: hohe Produktivität, durch gleichzeitige Arbeit an Funktionseinheiten. Rechenwerk und Compiler sind dann zwar recht simple Komponenten, weil sie nur andere integrieren. Doch das macht nichts. Wenn sich z.B. der Ansatz zur Compilation ändert, dann kann wieder mehr Verantwortung in den Compiler wandern. Ihn als Verantwortlichkeit ausdrücklich zu definieren, trägt zur Evolvierbarkeit bei.

Fazit “traditionelle” Komponentenarchitektur

Was gibt es hervorzuheben an einer “traditionellen” komponentenorientierten Architektur? Was gibt es womöglich zu kritisieren? Das ist gar nicht so leicht zu sagen, wenn man tief drin steckt in dem Denken. Denn dann ist vieles ganz natürlich – was eigentlich Ballast ist. Und was fehlt, erkennt man nicht so genau.

In meinem ersten Posting zu EBCs habe ich fünf Aspekte “traditioneller” Komponenten genannt, mit denen ich mich schwer tue. Für das obige Beispiel möchte ich drei davon herausgreifen:

  • Komponenten haben Abhängigkeiten. Abhängigkeiten machen das Entwicklerleben schwer. Wo Abhängigkeiten ins Spiel kommen, wird allemal das Testen schwieriger.
  • Spezifikationen sind nicht kompakt. Die Spezifikation des Compilers besteht z.B. aus seinem eigenen exportierten Kontrakt sowie den importierten Kontrakten der drei Komponenten, von denen er abhängig ist.
  • Die Schachtelung von Komponenten ist schwierig oder gar unmöglich.

Insbesondere die letzten beiden Punkte machen mich bei der obigen Architektur unglücklich. Ich finde es sehr unhandlich, einen Compiler nur implementieren zu können, wenn ich insgesamt vier Kontrakte zur Hand habe. Und warum sollte eine Client-Komponente den ganzen Kontrakt einer Service-Komponente kennen, wenn sie selbst davon nur einen Ausschnitt selbst braucht? Wer ist schon so konsequent und betreibt Interface Segregation, um jedem “Kunden” einer Service-Komponente nur den für ihn passenden Ausschnitt zur Verfügung zu stellen?

Viel schlimmer aber noch ist, dass die finale Architektur nur noch eine Abstraktionsebene im Code enthält. Die Komponenten liegen alle auf demselben Niveau. Ein Betrachter des Codes hat damit keine Möglichkeit, unterschiedliche Betrachtungsabstände einzunehmen, um sich Überblick zu verschaffen oder Details zu betrachten.

Zwar habe ich mich in mehreren Schritten an diese “Blattebene” herangeschlichen. Doch die Funktionseinheiten auf darüber liegenden Abstraktionsniveaus sind nicht mehr sichtbar. Sie existieren nur noch auf einem Blatt Papier – und das ist bekanntlich geduldig.

Aber vielleicht sehen Sie es auch anders und meinen, die Architektur würde immer noch mehrere Abstraktionsebenen enthalten. Scanner, Parser und Codegenerator gehören dann zur untersten Ebene, Compiler liegt darüber und mit der Funktionswertberechnung gleich auf. Darüber dann das Rechenwerk, darüber das GUI. Ja, so könnte man das sehen:

image

Aber ist das wirklich plausibel und hilfreich? Liegt ein GUI auf einem höheren Abstraktionsniveau als ein Compiler? Ist das Abstraktionsniveau von einem Aufgabenbereich oder gar Belang abhängig? Nein, nein, damit kann ich mich gar nicht anfreunden. Außerdem würde das bedeuten, dass Kommunikation nie zwischen Funktionseinheiten auf demselben Abstraktionsniveau abliefe, sondern immer über Niveaus hinweg: der Compiler auf Level n spräche mit dem Scanner auf Level n+1 usw.

image

Das fühlt sich gar nicht gut an.

Abhängigkeiten definieren einen Baum. Entlang der Äste verläuft die Kommunikation. Schwer verständliche Schachtelung ist damit das Fundament der “traditionellen” Komponentenorientierung. Grauslich! Da hilft dann auch keine Dependency Injection.

Das führt übrigens auch dazu, dass so sinnlose Komponenten wie der Compiler oder das Rechenwerk auftauchen. Die tun nichts anderes, als die Kommunikation zwischen anderen Komponenten zu ermöglichen. Vom Standpunkt der Domänenlogik aus gesehen ist das nicht nötig. Aber die vertikalen Abhängigkeiten erzwingen das. Scanner, Parser und Compiler liegen auf demselben Abstraktionsniveau – und kennen sich daher nicht. Widersinnig, oder? Und wenn sie sich kennen wie in der verworfenen Architektur, dann ist das auch nicht glücklich. Denn warum sollte ein Scanner von einem Parser abhängen? Oder von mir aus auch umgekehrt: Warum sollte ein Codegenerator von einem Parser abhängen und der von einem Scanner?

Ein Codegenerator leistet etwas auf einem Input: er verwandelt einen Abstrakten Syntaxbaum (AST) in lauffähigen Code. Diese Leistung hat nichts, aber auch gar nichts mit der Erzeugung eines solchen AST zu tun. Warum sollte also ein Codegenerator die anstoßen?

Also muss ein übergeordneter Compiler her. Der tut nichts anderes, als die Kommunikation zwischen Scanner, Parser und Codegenerator angemessen herzustellen. Aber er ist eine Komponente mit vielen Abhängigkeiten. Er ist vergleichsweise schwierig zu testen und dabei so gar nicht durch die Problemdomäne motiviert. Eine unbefriedigende Situation. Koordinationskomponenten verrauschen das Architekturbild.

Ein Abhängigkeitsdiagramm ist einfach nicht genug, um eine Architektur zu beschreiben. Es ist nötig, um die vielen Kontrakte klar zu kriegen und die Abhängigkeiten sauber zu definieren. Aber wie die Kommunikation läuft, ist daraus nicht ersichtlich. Dafür brauchen wir mindestens einen zweiten Diagrammtyp. Z.B. ein Aktivitätsdiagramm oder ein Datenflussdiagramm müssen die Lücke füllen.

Meine bottom line: Eine unbefriedigende Situation. “Traditionelle” Komponenten sind besser als gar keine Komponenten. Sie befördern die Produktivität, die Flexibilität und die Korrektheit. Doch es bleibt ein Nachgeschmack.

Event-Based Component Architektur

Wie kann es mit der Architektur besser werden als mit “traditionellen” Komponenten? Ich versuche dasselbe Szenario mal mit Event-Based Components (EBC) zu beschreiben. Am Anfang steht wieder ein Systen-Umwelt Diagramm:

image

Das sieht der “traditionellen” Komponentenorientierung noch sehr ähnlich. Allerdings: Beachten Sie, dass auch hier schon von Nachrichten gesprochen wird und keine Abhängigkeit vorhanden sind. Die Anwendung als ganzes wird als Komponente gesehen, die asynchron gegenüber ihrer Umgebung läuft.

Das System-Umwelt Diagramm ist die höchste Abstraktionsstufe. Es besteht ja nur aus einer Funktionseinheit. Diese Funktionseinheit ist natürlich zu groß, um sie einfach so zu implementieren. Also muss ich sie zerlegen.

image

Wenn ich die Applikation “aufmache”, dann finde ich zwei Funktionseinheiten: ein Frontend für die Interaktion mit dem Benutzer und ein Rechenwerk, dass die Formel im angegebenen Wertebereich berechnet. Das ist immer noch eine Ebene, die ein Laie versteht. Eine Benutzeroberfläche ist etwas anderes als die Funktionalität, die Formeln berechnet.

Im Hintergrund habe ich noch die darüber liegende Hierarchieebene “durchscheinen lassen”. Sie soll uns erinnern, dass die beiden Funktionseinheiten Verfeinerungen sind.

Jetzt zur nächsten Ebene hinab steigen:

image

Jede Funktionseinheit der vorherigen Ebene ist verfeinert worden, d.h. zerlegt in mehrere Funktionseinheiten mit spezifischeren Verantwortungen. Diese Subfunktionseinheiten sind den übergeordneten eingeschrieben. Funktionseinheiten können also Container sein. Nicht physische Container, sondern logische, sozusagen “Verantwortungscontainer”.

Das ist ein Aspekt, der mir wichtig geworden ist. Mit EBCs ist echte Schachtelung möglich. Aber das ist keine physische Schachtelung. Wie Implementierungen von Funktionseinheiten physisch geschachtelt sind, halte ich inzwischen für einen ganz anderen Concern. Darüber muss man nachdenken – aber es hat nichts mit der Funktionalität einer Anwendung zu tun. Wie Funktionseinheiten physisch versammelt und geschachtelt werden, ist eine Sache des Entwicklungsprozesses. Darauf wirken vor allem Fragen der Parallelität von Entwicklung, von Risiko, Unsicherheit oder Komplexität/Entwicklungsdauer ein.

Aber nun weiter mit der Modellierung. Das Frontend ist abgeschlossen, doch beim Rechenwerk muss der Compiler noch zerlegt werden:

image 

In dieser Abbildung sind nun alle Abstraktionsebenen ganz natürlich ineinander geschachtelt zu sehen. Je dunkler eine Komponente, desto niedriger ihr Abstraktionsniveau. Echte Domänenfunktionalität enthalten nur die Blätter, d.h. die Funktionseinheiten mit dem niedrigsten Abstraktionsniveau.

Der Schachtelungsbaum dazu sieht so aus:

image

Abhängigkeiten verlaufen in der Vertikalen. Sie sind im Architekturdiagramm implizit. Wenn Komponenten andere enthalten, dann sind sie von ihnen abhängig. Wichtiger ist jedoch, dass die Kommunikation hier horizontal verläuft. Das Architekturdiagramm legt den Schwerpunkt nicht auf Abhängigkeiten, sondern darauf, wie die Funktionseinheiten miteinander kommunizieren.

Wo bei “traditionellen” Komponenten zwei Diagramme nötig sind, reicht bei EBCs ein Diagramm.

Fazit Event-Based Component Architektur

Vergleichen Sie selbst die Diagramme für die “traditionelle” Komponentenarchitektur und die EBC-Architektur. Welches finden Sie aussagekräftiger, verständlicher?

Die Vorteile der EBC-Architektur scheinen mir auf der Hand zu liegen:

  • Der Fokus liegt hier ganz eindeutig auf den Blättern. In denen “spielt die Musik”, dort steckt die Domänenlogik, sie mögen komplex sein. Aber sie haben keine Abhängigkeiten. Deshalb sind sie gut zu testen. Und deshalb lassen sie sich auch vergleichsweise gut wiederverwenden.
  • Abhängig sind “Zwischenebenen” oder “Platinen”. Sie sind von den eingeschachtelten Funktionseinheiten abhängig. Aber das macht nichts. Diese Abhängigkeiten sind sehr einfach. “Platinen” lassen sich aus der Beschreibung der Verbindungen zwischen ihren “Bauteilen” generieren. Das macht ihre Tests sehr einfach.
  • Die Abhängigkeiten sind implizit. Sie verwirren nicht das Verständnis. Die wichtigeren Kommunikationswege stehen im Vordergrund. Die Kommunikation ist horizontal zwischen Funktionseinheiten innerhalb eines “Containers” auf derselben Abstraktionsebene.
  • Last but not least: Die Ebenen, die eine schrittweise Verfeinerung durchläuft, bleiben in der Architektur physisch, d.h. in Form von Artefakten (“Platinen”) erhalten. Das trägt sehr zum Verständnis der Architektur bei. Zoom-in/out ist nicht nur möglich, sondern bezieht sich auf realen Code und nicht nur “Ideen”.

EBC-Architekturen manifestieren für mich die konzeptionelle Holarchie des ersten Bildes oben. Software ist ein System mit beliebig vielen Ebenen. Auf jeder Ebene arbeiten beliebig viele Funktionseinheiten zusammen. Aus Input produzieren sie Output. Sie kennen einander nicht und sind ganz regelmäßig aufgebaut. Das erleichtert ihre Produktion (“Platinen” können generiert werden, Blätter werden parallel handcodiert). Das erleichtert die Komposition von Architektur mit ihnen. Und das ermöglicht den Einsatz von Standardkomponenten.

Nach meinem Empfinden sind wir mit EBCs der lang erprobten Arbeitsweise von Elektrotechnikern und Maschinenbauern so nah wie nie zuvor. Software ist natürlich immer noch anders als physische Bauteile; aber warum soll sie nicht von denen lernen?

Ich jedenfalls kann quasi schon gar nicht mehr anders denken als in EBCs. Und mit jeder Architektur, die ich so entwerfe, wird es einfacher.

Mit EBCs sehe ich Software als Sammlung von Prozessen. Einfachen und komplizierten. Die bestehen aus Verantwortlichkeiten, die zusammen etwas verarbeiten und produzieren. Diese Verantwortlichkeiten und ihre Kooperation beschreiben EBCs.

Objekte und “traditionelle” Komponenten werden dadurch nicht überflüssig. Sie bekommen aber einen anderen Platz. Auch das macht die Architektur einfacher.

Verfeinerung der EBC-Architektur

Zum Schluss noch ein Detail, über das Sie sich keine Gedanken gemacht haben mögen. In der EBC-Architektur ist noch eine kleine Unstimmigkeit, die ich der Einfachheit halber bisher überspielt habe. In das Rechenwerk kommt eine Berechnungsanforderung rein. Die besteht aus einer Formel und einem Wertebereich, auf den die Formel angewandt werden soll, z.B. Formel “2*x” und Wertebereich 0 bis 10 in 100 Schritten.

Durch die Blätter des Rechenwerks fließt bisher nur ein Datenstrom. Das bedeutet, jede Funktionseinheit muss den Wertebereich bis zur Funktionswertberechnung durch schleifen. Was aber soll z.B. ein Scanner mit diesem Wertebereich?

Die Architektur für das Rechenwerk sähe daher besser wie folgt aus:

image

Split und Join sind Standard-EBCs. Sie werden in den Nachrichtenstrom eingesetzt, um Nachrichten zu transformieren und damit passend für folgende Funktionseinheiten zu machen. Das erhöht die composability (Komponierbarkeit) von Architekturen. Funktionseinheiten können leichter wiederverwendet werden, wenn sie ihren Kontrakt nicht an ihre Umgebung anpassen müssen, sondern umgekehrt kleine Standardadapter die Umgebung an eine Komponente anpassen.

Hier noch zwei Ideen, wie solche Standardkomponenten helfen könnten:

image

Zunächst: Bemerken Sie, wie einfach es ist, die Flughöhe zu wechseln? Im vorhergehenden Bild waren wir im Rechenwerk. Jetzt ist das Rechenwerk eine Black Box; wir fliegen höher, sehen ein bigger picture. Das Abstraktionsniveau ist gestiegen.

Auf diesem Level habe ich nun Standardkomponenten eingefügt, die die Performance und Reaktionsfähigkeit des Systems steigern sollen. Ein Cache ist zwischen Frontend und Rechenwerk geschaltet, um zu vermeiden, dass Übersetzungen und Berechnungen, die schon gelaufen sind, nochmal zeitaufwändig ausgeführt werden. Für das Frontend macht dieser Einschub keinen Unterschied. Es ist ja unabhängig von anderen Komponenten. Und auch das Rechenwerk merk davon nichts. Das ist AOP at its best ;-)

Dasselbe gilt für die Asynchronizität. Das Rechenwerk läuft nun auf einem anderen Thread als das Frontend. Berechnungen finden dem gegenüber also im Hintergrund und asynchron statt. Der Benutzer kann im Frontend andere Funktionen aufrufen, während das Rechenwerk seine Arbeit leistet. Das Frontend friert nicht ein.

Falls im Hintergrund ein Fehler auftritt, wird der als eigene Nachricht auf dem Frontend-Thread zurück gemeldet. Und Ergebnisse kommen auch auf dessen Thread an, weil sie ein Synchronization Context Switch aus dem Hintergrund in den Vordergrund holt.

Fühlen Sie die Eleganz des EBC-Ansatzes? Keine der Komponenten kennt/braucht eine andere. Die Verbesserung der nicht funktionalen Eigenschaften des Systems waren deklarativ ohne Veränderung auch nur einer existierenden Komponente möglich.

Und falls sich herausstellt, dass die Kombination Cache+Async öfter nützlich ist, kann sie ganz einfach mit einer “Platine” für die Wiederverwendung zu einer neuen Komponente zusammengeschnürt werden:

image

Wieder würde sich für die anderen Komponenten nichts ändern. Nur die Verdrahtung wäre betroffen, d.h. die “Platine”, die die Komponenten verbindet. Mit einem Architekturtool wäre das aber wohl nur eine Kleinigkeit. Eine simple und typische Refaktorisierung, z.B. “Komponenten zu Platine zusammenfassen”.

Im Sinne des SLA-Prinzips würde ich dann jedoch noch einen Schritt weitergehen:

image

Jetzt ist wieder alles auf demselben Abstraktionsniveau: ein Frontend ist verbunden mit einem Rechenwerk. Wie das genau funktioniert, ist egal. Wen das interessiert, der zoomt in das asynchrone Rechenwerk hinein.

Irgendwie geht das alles natürlich auch mit “traditionellen” Komponenten. Aber elegant finde ich es nicht mehr. Warum sollte ich mir auch nur eine Sekunde lang Gedanken über Abhängigkeiten machen? Warum sollte ich mir eine Sekunde lang Gedanken über die Größe von Interfaces machen?

Ob eine EBC-Komponente “zu groß” ist, sehe ich in einer EBC-Architektur viel leichter als in einer “traditionellen”. Ich muss nur die Input-/Output-Pins zählen. Die geben viel detailliertere und sichtbarere Auskunft über die Verantwortlichkeitsbreite einer Komponente, als ein simpler Pfeil in einem Abhängigkeitsdiagramm.

So, nun aber genug für heute. Lassen Sie sich die Diagramme einmal auf der Zunge zergehen. Ich hoffe, Sie schmecken es, wieviel einfacher, regelmäßiger, verständlicher echt holarchische Software mit EBCs sein kann.