Follow my new blog

Donnerstag, 26. Januar 2012

TDD im Flow – Teil 1

Steht Flow-Design eigentlich im Gegensatz zu Test-Driven Design? Nein. Zwar bin ich überzeugt, dass TDD viel weniger wichtig ist, als viele Vertreter agiler Softwareentwicklung glauben, aber deshalb hat TDD doch seinen Platz. An einem Beispiel möchte ich das demonstrieren.

Vor einiger Zeit habe ich Gedanken zu einer Flow Execution Engine geäußert. Die habe ich inzwischen angefangen zu bauen. Ihr Name ist “PantaRhei” und der Quellcode für eine C#-Implementation liegt bei CodePlex: npantarhei.codeplex.com.

Selbstverständlich ist diese Flow Runtime selbst im Flow-Design entstanden. Ihre Aufgabe ist also, Flows wie den, der sie selbst beschreibt, auszuführen. Hier ein Ausschnitt aus dem Modell:

image

Gezeigt ist die asynchrone Verarbeitung von Nachrichten, die von außen hereinkommen:

  • Sie fließen in einen Flow, der bei der Runtime registriert ist, über die Methode ProcessAsync() hinein.
  • Dann werden sie über die Funktionseinheit Asynchronize auf einen Hintergrund-Thread gehoben.
  • Vor der Ausführung wird dann nochmal geprüft, ob die Verarbeitung parallel zu anderen Nachrichten erfolgen soll (Schedule processing).
  • Zwei Parallelverarbeitungsmodi stehen zu Auswahl: echte Parallelverarbeitung, d.h. jede Nachricht an einen Port wird parallel zu anderen Nachrichten verarbeitet, oder eingeschränkte Parallelverarbeitung, da Nachrichten an den selben Port nur sequenziell verarbeitet werden.

Der Flow im Bild ist nicht durch TDD entstanden. Klar. Den habe ich iterativ wachsen lassen: ein bisschen entwerfen (auf dem Papier), ein bisschen implementieren, dann wieder ein wenig entwerfen usw. Im Repository kann man verfolgen, wie es voran gegangen ist.

Die (Sub-)Flows sind trivial in der Implementation. Die Musik spielt in den Operationen, den Blättern des oben grob erkennbaren Schachtelungsbaums. Blätter sind z.B. Register operation, Asynchronize und Execute task.

Diese Operationen sind so einfach, dass ich sie runtergeschrieben habe. Einige ohne Tests, einige mit Tests – hinterher. Ja, der TDD-Freund wird es den Tests ansehen, dass ich sie hinterher geschrieben habe ;-) Macht aber aus meiner Sicht nichts. Ich brauche sie ja nicht, um zu entwerfen, sondern nur zur Feststellung von Korrektheit kleiner Funktionseinheiten.

Nun aber bin ich an einem Punkt, wo ich meinen Modus umschalte. Das will ich einmal dokumentieren, um zu zeigen, wie TDD und FD Hand in Hand gehen können.

Anforderungen

Ganz rechts im Bild finden Sie die Operation Sequentialize. Bei ihr geht es darum, Nachrichten zur Verarbeitung auf einen von mehreren Threads zu heben. Parallelize tut das ganz einfach: Nachrichten werden dem nächsten frei werdenden Thread zugewiesen. Das führt zu unterschiedsloser Parallelverarbeitung aller Nachrichten. Bei Sequentialize soll es dagegen etwas differenzierter vorgehen. Da sollen manche Nachrichten parallel und manche sequenziell verarbeitet werden.

Unterschieden werden Nachrichten bei Sequenzialize nach den Ports, zu denen sie fließen. Nachrichten an unterschiedliche Ports werden parallel verarbeitet, aber Nachrichten an den selben Port werden sequenziell verarbeitet. Damit wird eine häufige Fehlerquelle des concurrent programming automatisch ausgeschaltet.

Ganz einfach ließe sich die sequenzielle Verarbeitung aller Nachrichten an einen Port natürlich dadurch lösen, jeden Port mit einem eigenen Thread und einer Warteschlange auszustatten. Das würde die Verarbeitung insgesamt jedoch eher verlangsamen, dass es immer viel mehr Ports als Prozessorkerne gibt.

Also soll die sequenzielle Verarbeitung der Nachrichten an einen Port bei gleichzeitiger Parallelität aller Ports mit nur wenigen Threads realisiert werden. Sequentialize muss daher für jede Nachricht prüfen, ob sie an einen Port geht, der gerade noch mit der vorhergehenden beschäftigt ist. Falls ja, muss die Nachricht warten; andernfalls kann sie vom nächsten freien Thread verarbeitet werden.

Wenn eine Nachricht warten muss, kann eine andere, die zu einem anderen Port fließt, an den nächsten Thread zugewiesen werden. Die wartende Nachricht darf darüber natürlich nicht vergessen werden.

Lösungsidee

Mir scheint es für dieses Problem keine Lösung in der TPL zu geben, da ich ja keine fixen Task-Netzwerke aufbaue. Also muss ich selbst Infrastruktur basteln. Das sollte kein Hexenwerk sein – aber es ist schwieriger als das, was ich bisher für die Runtime zu tun hatte.

Der Rahmen ist noch simpel:

image

Zur Sequentialisierung in beschriebener Weise, müssen die Nachrichten in einen Puffer geschrieben werden: Enqueue message. Das geschieht auf dem Thread, der sie annimmt für die Verarbeitung (s. Asynchronize in Flow asynchronously).

Die vielen Threads, auf denen Nachrichten dann parallel abgearbeitet werden können, entnehmen sie aus diesem Puffer, sobald sie frei für neue Arbeit sind: Parallel dequeue.

Diese beiden Funktionseinheiten sind trivial. Die Musik spielt im Message store. Der kann nämlich nicht eine simple Warteschlange wie bei Asynchronize sein. Vielmehr muss der Message store dafür sorgen, dass Nachrichten erstens in der Reihenfolge ihres Eingangs herausgegeben werden, zweitens dabei aber eine Einschränkung für Nachrichten an denselben Port gilt.

Das ist eine Funktionalität, die ich nicht eben mal so runterschreibe. Und dazu fällt mir auch gerade kein sinniger Flow ein. Mein Gefühl ist, hier bei einer Funktionseinheit angelangt zu sein, die aus Sicht von Flow-Design ein Blatt, eine Black Box darstellt.

Dennoch hat der Message store eine Struktur: eine Datenstruktur wie auch eine Verarbeitungsstruktur. Wie komme ich an die heran?

Meine Lösungsidee für die Datenstruktur sieht erst einmal so aus:

  • Jeder Port bekommt eine eigene Warteschlange. Damit wird sichergestellt, dass pro Port die Nachrichten in der ursprünglichen Reihenfolge bearbeitet werden.
  • Allerdings hat jede solche Port-Warteschlange eine Flagge, die anzeigt, ob gerade an einer Nachricht dieses Ports gearbeitet wird. Solange das der Fall ist, kann kein Thread eine Nachricht aus dieser Warteschlange entnehmen. Sie ist dann blockiert.
  • Da es mehrere Warteschlange gibt, die mehrere Threads mit Arbeit versorgen, muss es gerecht zugehen. Kein Port darf bevorzugt werden. Deshalb sollten die Warteschlangen von freien Threads im Round Robbin Verfahren abgefragt werden. Entnimmt ein Thread aus einer Warteschlange eine Nachricht, entnimmt der nächste freie Thread aus einer anderen. Die Warteschlangen bilden deshalb einen Kreis in Form einer einfach verketteten Liste mit einem Zeiger auf die nächste zu befragende Warteschlange.

Im Bild sieht das so aus:

image

TDD-Anhänger mögen angesichts von soviel Entwurf sagen, das sei vielleicht nicht so einfach, wie es sein könnte. Darauf sage ich: Mag sein. Aber warum sollte ich nicht eine Lösungsidee haben auf der Basis der Kenntnis der Anforderungen? Warum sollte ich mich dümmer stellen als ich bin? Ob ich die Datenstruktur am Ende so umsetze, ist ja noch eine zweite Frage. Zunächst trenne ich aber die Phasen Lösungsentwurf und Implementierung. Damit entlaste ich die Implementierung. Ich habe dann nämlich schon eine Vorstellung von der Lösung – an die ich mich allerdings auch nicht sklavisch halten sollte.

Es gibt mehrere Werte, die es auszugleichen gilt. Ein Wert mag die einfachst mögliche Implementation sein. Aber ein anderer ist Geschwindigkeit. Und die ist höher, wenn ich eine (zumindest grobe) Struktur habe, auf die ich hinarbeiten kann, ein Ziel. Dann muss ich nämlich nicht dauernd refaktorisieren, weil mich Erkenntnisse während der TDD-Codierung überraschen.

Solange mein Entwurf verständlich ist und mit Augenmaß auch simpel gehalten, finde ich es völlig ok, vorauszudenken. Nein, ich finde es sogar empfehlenswert. Zu oft habe ich nämlich gesehen, dass mit TDD eine Implementation angegangen wird, ohne eine Vorstellung von der Lösung zu haben – um dann nach anfänglichen Erfolgen steckenzubleiben.

Mit einem Entwurf schaffe ich mir sozusagen selbst eine Karte für ein bis dahin unbekanntes Terrain. Und mit Karte wandert es sich leichter. Das heißt nicht, dass ich auf dem Weg nicht auf Hindernisse treffe. Aber ich kann die dann in einem bigger picture verorten und schauen, wie ich sie umwandere.

Und wie sieht meine Lösungsidee für die Verarbeitungsstruktur aus? Da kenne ich nur die Schnittstelle. Ich will die Datenstruktur als abstrakten Datentyp (ADT) realisieren. D.h. nach außen ist von einem Objektgraphen nichts zu sehen. Funktionseinheiten, die mit der Datenstruktur umgehen, sollen es so einfach wie möglich haben. Ich stelle mir das so vor:

class NotifyingMultiQueue<T> {
    public void Enqueue(T message, string queueId) {…}
    public bool TryDequeue(string workerId, out T message) {…}

    public void Wait(int milliseconds) {…}
}

  • Um eine Nachricht zur Verarbeitung auf irgendeinem Thread einzustellen, wird Enqueue() für eine Warteschlange aufgerufen.
  • Um eine Nachricht von der nächsten Warteschlange zur Verarbeitung abzuholen, wird TryDequeue() aufgerufen. Dabei ist anzugeben, wer die Nachricht dann verarbeitet, damit sichergestellt werden kann, dass keine ungewollte Parallelverarbeitung stattfindet.
    Eine erfolgreiche Entnahme aus einer Warteschlange sperrt diese durch Hinterlegung der Id des Workers, an den eine Nachricht ausgegeben wurde. Wenn der Worker dann das nächste Mal eine Entnahme tätigen will, wird die durch seine vorherige Entnahme gesperrte Warteschlange wieder freigegeben. 
  • Und falls gerade keine Nachricht zur Verarbeitung anliegt (TryDequeue() liefert false), kann ein Thread sich mit Wait() schlafen legen, bis es wieder Arbeit gibt.

Das klingt hoffentlich sinnig. Dass das grundsätzlich funktioniert, habe ich schon bei der asynchronen Nachrichtenverarbeitung überprüft. Mit so einer Schnittstelle läuft das Umheben auf andere Threads ordentlich.

Test-Driven Development

Die Anforderungen sind klar, die Schnittstelle ist einfach – aber wie sieht nun der Code dahinter aus? Den könnte ich jetzt versuchen runterzuschreiben; durch die Datenstruktur habe ich ja einen Überblick über das Nötige gewonnen. Aber dabei würde ich mich nicht wohl fühlen. Etwas mehr Systematik darf sein bei einem so wichtigen Bestandteil der Flow Runtime. Wenn die Datenstruktur nämlich nicht sauber aufgesetzt ist, dann kommt es zu Fehlern in der Arbeit mit mehreren Threads – und die sind sicher nur schwer zu reproduzieren und zu lokalisieren.

Wie aber anfangen mit TDD? Jetzt Visual Studio anwerfen und einen ersten Test für eine der Schnittstellenmethoden schreiben? Nein! Am Anfang von TDD steht die Sammlung von Testfällen. Darauf, dass die Ihnen während der Codierung einfallen, sollten Sie sich nicht verlassen. Und schon gar nicht sollten Sie erwarten, dass sie Ihnen in einer guten Reihenfolge einfallen. Damit würden Sie das TDD-Vorgehen überfrachten.

Testfälle für das Ganze, das Sie realisieren wollen, sind vor dem Beginn der Codierung festzulegen. Es sind schließlich die Akzeptanzkriterien für Ihren Code. Wenn Sie die nicht vorher kennen, lügen Sie sich schnell mit der Codierung – selbst wenn die Test-First geschieht – etwas in die Tasche. Außerdem dient die Sammlung der Testfälle dem tieferen Verständnis der Anforderungen.

Hier die ersten 4 Testfälle von 14, die mir zu dieser speziellen Warteschlange eingefallen sind:

image

Sie sind in aufsteigender “Schwierigkeit” sortiert. Mit jedem Testfall wird das Szenario etwas komplizierter. Es gibt ja mehrere Variablen:

  • Die Zahl der Einträge in einer Warteschlange
  • Die Zahl der Warteschlangen
  • Die Zahl der Worker, die auf die Warteschlangen zugreifen
  • Den Blockierungszustand
  • Benachrichtigungszustand

Allerdings gibt es noch ein zweites Sortierkriterium: den Interessantheitsgrad eines Tests bzw. die Relevanz der durch ihn abgedeckten Funktionalität. Deshalb steht am Anfang ein happy day Szenario und nicht die sonst häufig zu sehende Prüfung, ob eine leere Datenstruktur korrekt behandelt wird. Das ist erst der 9. Test.

Vor dem ersten Test bin ich so frei und setze meine Klasse mit den obigen Methoden auf. Das empfinde ich als keinen schlimmen Bruch mit der strengen TDD-Praxis. Alle Methoden werfen die NotYetImplemented Exception. So mache ich mir das Schreiben der Tests etwas einfacher und habe gleich sozusagen ein kleines Backlog im Code.

Gleichfalls lege ich mir in meiner Testklasse das System under Test (SUT) zurecht. Wie mir die Testplanung zeigt, brauche ich ja immer wieder eine Instanz von NotifyingMultiQueue, die via TryDequeue() eine Nachricht zurückgibt.

[TestFixture]
public class test_NotifyingMultiQueue
{
    private NotifyingMultiQueue<string> _sut;
    private string _result;

    [SetUp]
    public void Before_each_test()
    {
        _sut = new NotifyingMultiQueue<string>();
        _result = null;
    }
    …

Test #1: Ein Worker entnimmt aus einer Queue

Der erste Test ist ganz einfach. So soll es ja auch sein. Leicht beginnen und in kleinen Schritten vorgehen:

[Test]
public void Single_worker_takes_from_single_queue()
{
    _sut.Enqueue("a", "q1");
    Assert.IsTrue(_sut.TryDequeue("w1", out _result));
    Assert.AreEqual("a", _result);
}

Die Zeilen setzen die Testplanung treu um:

image

Ich habe länger überlegt, ob ich den ADT als Black Box testen soll. Am Ende habe ich mich dafür entschieden. Auch wenn ich eine hübsche Datenstruktur entworfen habe, will ich mich nicht daran in Tests binden, wenn es nicht absolut nötig ist. Also teste ich die Funktionalität immer durch den API hindurch. Bei der zu erwartenden Größe des Codes scheint es mir vertretbar, ganz auf Integrationstests zu setzen, auch wenn sich intern die funktionalen Strukturen ausdifferenzieren.

Allerdings muss ich dadurch meist paarweise Änderungen vornehmen: bei Enqueue() und bei TryDequeue() bzw. Wait(). Anders kann ich nicht überprüfen, ob eine interne Zustandsveränderung korrekt erfolgt. Auf beiden Seiten muss ich mich also konzentrieren, das KISS Prinzip nicht aus den Augen zu verlieren.

Für den ersten Test ist das aber noch einfach. Das Szenario kann mit einer Queue abgehandelt werden:

internal class NotifyingMultiQueue<T>
{
    Queue<T> _queue = new Queue<T>();

    public void Enqueue(T message, string queueName)
    {
        _queue.Enqueue(message);
    }

    public bool TryDequeue(string workerId, out T message)
    {
        message = _queue.Dequeue();
        return true;
    }
    …

Die Implementation muss zur Zeit nur einen einzigen Test erfolgreich machen. Wenn also nicht alle Parameter benutzt werden oder von Round Robbin nichts zu erkennen ist, dann macht das nichts. Diese Implementation ist die einfachste mögliche, zur “Testbefriedigung”.

Zumindest ist das so, wenn man den Anspruch hat, dass im Code etwas halbwegs sinniges passiert. Oft ist ja bei TDD Demos zu sehen, dass bei den ersten Tests triviale Implementationen vorgenommen werden, z.B.

internal class NotifyingMultiQueue<T>
{
    public void Enqueue(T message, string queueName)
    {}

    public bool TryDequeue(string workerId, out T message)
    {
        message = “a”;
        return true;
    }
    …

Der Test würde damit grün – ansonsten wäre aber nichts gewonnen. Es wäre kein Schritt in Richtung einer Lösung unternommen worden.

Tests sind ja aber kein Selbstzweck. Das Ziel ist nicht, Tests grün zu bekommen. Tests sind nur ein Mittel, um das Ziel zu ereichen. Das ist mit und ohne Tests grundsätzlich dasselbe: Produktionscode, der die Anforderungen erfüllt.

Es ist gut, wenn kleinschrittige Tests zu einem Wachstum der Implementation auch in kleinen Schritten führt. Triviales Wachstum aber, von dem man weiß, dass es nicht belastbar ist im Sinne der Anforderungen, sollten Sie vermeiden.

Ein Testszenario entfällt

Nicht nur Implementation kann allerdings trivial wachsen. Auch bei Tests besteht die Gefahr. Das weiß man aber nicht immer, wenn man den Test formuliert. Der folgende schien mir beim Entwurf noch sinnig:

image

Nach der einfachen, aber nicht trivialen Implementation zu Test #1 war dieser Test jedoch überflüssig. Es wäre sofort grün gewesen. Ich habe ihn deshalb ausgelassen.

Die Maxime dahinter lautet: Tests, die du keiner Änderung an der Implementation führen (d.h. die nicht zuerst rot sind), sollten nicht geschrieben werden. Sie treiben den Produktionscode nicht voran. Sie gehören zur selben Äquivalenzklasse wie ein anderer Test. Hier wäre es die Äquivalenzklasse “Ein Worker entnimmt aus einer Queue” gewesen.

Würde ich eine Queue implementieren, wäre der Test nötig. Aber ich benutze in meinem ADT eine Queue. Und deren Funktionstüchtigkeit muss ich nicht auch nochmal testen. Darauf verlasse ich mich. In Bezug auf benutzte ADTs sind meine Tests also Integrationstests.

Weiter geht es im nächsten Teil…

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

Mittwoch, 11. Januar 2012

Schätzungen auf dem Prüfstand

Kann man verlässlich schätzen, wie lange ein Team braucht, Anforderungen in einsetzbare Software zu übersetzen? Kann man das für einen Horizont von wenigen Wochen? Kann man das für Monate oder Jahre?

Diese Frage führt immer wieder zu hitzigen Diskussionen. Eine der am besten besuchten Sessions auf der letztjährigen DevCon drehte sich auch um diese Frage. Das Interesse an einer Antwort, ja geradezu an einer Erlösung von diesem Thema ist groß.

Ich habe dazu auch meine Meinung und bin stolzes Mitglied der XING-Gruppe "Stop Software Estimation Now!" :-)

image

Aber wenn ein Thema so lange und so fortschrittslos diskutiert wird, frage ich mich auch: Gibt es hinter der Frage nicht ein grundsätzlicheres Problem? Sitzen wir vielleicht einem Missverständnis auf?

Vielleicht rennen wir ja kollektiv gegen eine Wand - und sehen nicht, dass nur ein paar Schritte weiter eine Tür ist.

Solch eine Tür meine ich nun entdeckt zu haben. Ich glaube, die Diskussion zum Thema Schätzen kommt aus einem ganz simplen Grund nicht voran: Wir messen einfach nicht den Erfolg des Schätzens. Keine Partei weiß so richtig, ob Schätzen wirklich erfolgreich ist.

Der Weg aus dem Problem mit dem Schätzen besteht aus meiner Sicht daher aus zwei Schritten:

1. Festlegen, wann eine Schätzung erfolgreich ist; wir brauchen Kriterien.
2. Messen, ob eine Schätzung nach den aufgestellten Kriterien erfolgreich war.

Klingt einfach, oder? Und man fragt sich, ob das nicht immer schon so gehandhabt wurde. Ich glaube, nein. Mit dem Schätzen ist man schnell bei der Hand. Aber weder werden Schätzungsqualitätskriterien bilateral (!) festgelegt, noch wird geprüft, wie die Schätzqualität am Ende war.

Bilateral Schätzungsqualitätskriterien festlegen

Beim Schätzen sind mindestens zwei Parteien beteiligt: der Kunde und das Entwicklungsteam. Der eine hat das Geld und die Zeit, die anderen sollen sagen, was sie im Rahmen dieser Budgets leisten können. Das Team muss also abschätzen, wie viel Geld und Zeit es für einen Scope braucht oder ob es einen Scope innerhalb eines gewissen Budgets realisieren kann.

Wenn nun Geld, Zeit und Scope zu einem Vertrag zwischen diesen Parteien gehören, dann natürlich auch die Schätzung. Deshalb müssen sich beide einig darüber sein, wann eine Schätzung gut war. Sonst ist es schlecht mit der Schätzerfolgskontrolle für beide Seiten. Und ohne Erfolgskontrolle kein Lernen, um es das nächste Mal genauso zu machen, weil es gut war, oder es besser zu machen, weil es nicht gut war.

Hier sehe ich das erste Defizit: Üblicherweise werden Schätzungsqualitätskriterien nicht explizit festgelegt und schon gar nicht bilateral. Es gibt einfach keine Diskussion darüber. Die Schätzung besteht aus zwei Zahlen (“Wir brauchen M Monate und G Euro für den gegebenen Scope.”), die irgendwer irgendwie im Blick behält. Wenn M und G zur Neige gehen, schaut man, wie viel vom Scope noch übrig ist. Dann stellt man fest, dass M und G nicht reichen werden und die Nachverhandlungen beginnen.

So tut man das halt. Darüber wird vorher nicht gesprochen. Das nehmen beide Seite als normal hin – und ärgern sich doch. Oder zumindest eine Seite ärgert sich. Die andere mag es nicht kratzen, weil sie eine Horde von Anwälten für solche Nachverhandlungen beschäftigt.

Dazu kann man nun sagen: “So ist halt die Welt.” Doch das will ich nicht akzeptieren, solange diese Parteien auf der anderen Seite klagen, “alles” sei so teuer. Denn Aufwand für Nachverhandlungen aufgrund schlechter Schätzung ist unnütz, falls schlechte Schätzungen die Norm sein sollten. Es wäre dann auf die Dauer billiger das Schätzen zu verbessern. Dafür braucht man jedoch Qualitätskriterien. Die müssen bilateral und explizit definiert sein, weil sie zum Vertrag gehören. Und die müssen dann auch am Ende überprüft werden.

Explizite Schätzungsqualitätskriterien

Müssen denn die Softwarevertragsparteien aber länglich über Schätzungsqualitätskriterien sprechen? Sind die nicht offensichtlich?

Nein. Das ist ja das Problem. Darüber bestehen unterschiedliche Meinungen, über die nicht gesprochen wird.

Offensichtlich sind natürlich die Schätzwerte selbst, z.B. geschätzte Zeit und geschätztes Geld. Die werden mit dem Soll an Scope verglichen und man erfährt, ob das geschätze Budget ausgereicht hat oder nicht.

Zu diesen offensichtlichen Schätzwerten sollten dann allerdings noch mindestens drei weitere Kriterien treten:

Hinnehmbare Budgetabweichung: Es sollte ausdrücklich darüber gesprochen werden, welche Abweichung von den Schätzwerten noch als Erfolg verbucht werden darf. Sind 5% Abweichung ok oder 10% oder gar 20%? Schätzungen sind eben Schätzungen. Dass ein Team punktgenau landet, ist nicht zu erwarten. Also sollte man sich darüber unterhalten, wie groß der Landeplatz ist.

Hier sind sogar zwei Seiten zu unterscheiden: Am Anfang stehen ein Termin und ein Geldbetrag in Bezug auf einen Scope. Der Kunde ist zufrieden, wenn aus seiner Sicht beides innerhalb einer zu definierenden Abweichung eingehalten wird.

Das Team hat zur Erreichung dieser Fixpunkte aber auch noch einen Aufwand geschätzt, der durch den Geldbetrag gedeckt werden soll. Selbst wenn der Kunde also zufrieden ist, kann es sein, dass das Projekt aus Teamsicht floppt. Falls es den Aufwand erhöhen musste, um den Scope zum Termin zu liefern, ohne mehr Geld zu bekommen, ist die Schätzung auch schlecht gewesen – ohne, dass der Kunde davon etwas merken muss.

Hinnehmbarer Qualitätsverlust: Inzwischen wissen wir ja, dass es eben nicht nur um Geld, Zeit und Scope geht, sondern immer auch um Qualität. Der Kunde stellt funktionale und nicht-funktionale Anforderungen (Scope), die ein Entwicklungsteam mit unterschiedlicher interner Codequalität umsetzen kann. Inwiefern der Scope innerhalb des geschätzt nötigen Budgets umgesetzt wird, beobachtet der Kunde natürlich genau. Fällt der Erfüllungsgrad unter eine bestimmte Marke, übt der Kunde Druck aus. Darunter leidet gewöhnlich die interne Codequalität. Das sollte so nicht sein, ist aber so. Deshalb ist es wichtig, sich darüber Gedanken zu machen, ein wie großer Verlust an dieser Qualität noch als Erfolg beim Schätzen verbucht werden darf. Denn beim Schätzen besteht ja der Anspruch, dass die interne Qualität konstant über die geschätzte Dauer gehalten wird. (Dass man die interne Qualität dann auch noch messen können muss, um eine Abweichung vom Soll feststellen zu können, steht auf einem anderen Blatt.)

Hinnehmbarer Zufriedenheitsverlust: Der Kunde ist zufrieden, wenn er seinen Scope innerhalb des geschätzten Budgets bekommt. Wie ein Team das schafft, ist ihm in der Regel ziemlich egal. Überstunden, Wochenendarbeit, Urlaubssperre, Zuckerbrot, Peitsche… das schert ihn nicht. Schade – aber wohl nicht zu ändern.

Ein Team sollte für sich allerdings in dieser Hinsicht einen Anspruch definieren. Ist eine Schätzung erfolgreich, wenn zwar der Scope zum geschätzten Termin abgeliefert wird – aber die Stimmung auf Null ist? An dieser Stelle geht es mir nicht um eine Abweichung beim Aufwand, ohne dass der Kunde das zu spüren bekommt. Dafür gilt es, eine hinnehmbare Budgetabweichung zu definieren (s.o.).

Nöjd Crispare HistorikIch glaube, genauso wichtig wie die Beobachtung der Ressourcen Zeit und Geld, ist die der Motivation, der Stimmung, der Zufriedenheit, der Kompetenz (s. dazu z.B. Jeff Sutherland, “Happyness Metric – The Wave of the Future”). Alle Teammitglieder sind wertvolle Ressourcen – warum sollte man sie sonst bezahlen? Also sollte man nicht leichtfertig mit ihnen umgehen. Sie können ihren Wert nur voll einbringen, wenn sie “wie geschmiert funktionieren”.

Eigentlich mag ich diesen Ressourcen-Jargon nicht, aber an dieser Stelle scheint er mir nützlich, um den Kontakt zu anderen Schätzungsqualitätskriterien zu halten.

“Wie geschmiert funktionieren” die Teammitglieder nur, wenn sie das Gefühl haben, dass ihre persönlichen Bedürfnisse erfüllt werden. Sie haben einen Anspruch daran, wie ihr Arbeitsumfeld zur Befriedigung ihrer Bedürfnisse beitragen soll. Dazu zählt z.B. “regelmäßiges Gehalt für das Bedürfnis ‘Certainty’” oder “nette Kollegen für das Bedürfnis ‘Connection’” oder “Zeit fürs Lernen für das Bedürfnis ‘Growth’” (Bedürfnisbezeichnungen nach Tony Robbins “Why we do what we do”).

Dass nicht immer alle Bedürfnisse voll erfüllt werden können, weiß jeder. Man ist deshalb damit zufrieden, wenn sie recht verlässlich innerhalb eines gewissen Bereichs erfüllt werden. Geschieht das allerdings nicht… dann geht die Stimmung in den Keller. Unaufmerksamkeit schleicht sich ein, die Fehler nehmen zu, Demotivation zieht ihre Kreise, Dienst nach Vorschrift bekümmert den Kunden, Krankmeldungen nehmen zu, Fluktuation entsteht usw.

Das sind Entwicklungen, die nicht nur persönlich bedauerlich für die Teammitglieder sind, sondern Unternehmen Geld kosten. Diese Kosten sind allerdings meist unsichtbar für das Projekt. Wenn Teammitglieder aus Frust über das Projekt kündigen, das unter hohem Druck steht, weil ein Termin zu halten ist, dann wird die Einarbeitung eines neuen Teammitglieds nicht dem Projekt zugeschlagen – obwohl das Projekt sie verursacht.

Deshalb scheint es mir nützlich, die Zufriedenheit der Teammitglieder als Kriterium heranzuziehen. Dieser Messwert kann innerhalb des Teams erhoben werden, auch wenn niedrige Zufriedenheit außerhalb des Teams zu Kosten führt.

Ein Projekt kann also mit dem gewünschten Scope zum geschätzten Termin mit dem geschätzen Aufwand und mit hinnehmbarer Qualität abgeschlossen werden – und doch war die Schätzung insgesamt nicht erfolgreich, wenn nämlich trotz der Einhaltung dieser Kriterien die Zufriedenheit aus dem definierten Rahmen gefallen sein sollte. Das kann z.B. passieren, wenn die Einhaltung der anderen Kriterien dazu führt, dass Stress entsteht, der messbar unzufrieden macht. Das ist dann ein Raubbau an der “Ressource Entwickler”.

Fazit

Ob Schätzungen funktionieren oder nicht… Ich habe da zwar meine Meinung, doch ich empfehle ihnen heute nur: Messen Sie doch einfach mal. Aber richtig.

Setzen Sie sich im Team oder noch besser mit dem Kunden zusammen und definieren Sie die Erfolgskriterien für Schätzungen. Bleiben Sie allerdings nicht bei Geld und Zeit stehen. Weiten Sie Ihren Blick und definieren Sie auch Ihren Anspruch an die innere Qualität und Ihre persönliche Zufriedenheit.

Dann messen Sie vorher, während und nachher. Und dann vergleichen Sie die Messwerte mit Ihren vorher definierten Ansprüchen.

Wenn die Messungen innerhalb der Toleranzgrenzen sind, dann funktioniert das Schätzen. Glückwunsch.

Aber wenn sie wiederholt außerhalb der Toleranzgrenzen liegen… tja, dann funktioniert das Schätzen eben nicht. Soviel Einsicht sollten Sie dann haben und Ihre Praxis ändern.

 

PS: Dass die Diskussion über das Schätzen so hitzig verlaufen, liegt also daran, dass da unterschiedliche Wertesystem aufeinanderprallen. Die Kriterien, wann Schätzungen erfolgreich sind, differieren. Deshalb lohnt es, einen Schritt zurückzutreten und erst einmal zu schauen, was denn diese Kriterien überhaupt sind.

Montag, 9. Januar 2012

Functions considered harmful

Bisher habe ich die "traditionelle" konzeptionelle Objektorientierung als eine der Ursachen für die heutigen Probleme mit der Wartbarkeit von Software gesehen. Inzwischen regt sich jedoch in mir der Verdacht, dass das Wurzelproblem tiefer liegt.

Womöglich ist die Objektorientierung sogar zu loben, weil sie das irgendwie verstanden hatte und versucht zu helfen. Leider ist das nicht so geglückt, wie man es sich erhoffte. Warum? Weil das Wurzelproblem eben nicht behoben, sondern nur kaschiert wurde. Man hat sich nicht getraut, so tief nach unten zu graben, um es auszureißen.

Denn das Wurzelproblem scheint mir... der Funktionsaufruf.

Ja, genau, der gute alte, unscheinbare und für imperative Sprachen so fundamentale Funktionsaufruf.

Dass wir schreiben können

y = f(x)

ist der Keim vieler Wartbarkeitsübel.

Wie kann das sein?

Problem #1: Unbegrenzte Länge

Durch Funktionen gibt es keine Grenze für das, was eine Codeeinheit tun kann. Ungezügeltes Wachstum von Codeeinheiten wird durch Funktionen ermöglicht. Denn nach einem Funktionsaufruf kann es ja weitergehen im aufrufenden Code.

Funktionen kombinieren Request und Response. Damit kombinieren sie vorbereitenden Code und nachbereitenden Code im Aufrufer. Und selbstverständlich kann nachbereitender Code gleichzeitig vorbereitender Code für den nächsten Funktionsaufruf sein. Nach dem Funktionsaufruf ist vor dem Funktionsaufruf.

Wann sollte also aufrufender Code beendet sein? Wenn er inhaltlich eine Verantwortlichkeit erfüllt. Klar. Aber das ist ein Kriterium, dessen Erfüllung im Auge des Betrachters entsteht. Der eine mag es, Verantwortlichkeiten in höchstens 20 Zeilen zu formulieren, der nächste hat kein Problem, wenn es dafür 100 Zeilen braucht und wieder einem anderen sind auch 10.000 Zeilen recht.

Durch Funktionsaufrufe gibt es kein Halten in Bezug auf den Umfang des aufrufenden Codes. Die Existenz von Regeln, die versuchen, diesen Umfang direkt oder indirekt zu begrenzen, ist der beste Beweis dafür.

Und wie wäre es, wenn es keine Funktionen gäbe? Dann würde eine Codeeinheit immer nur aus soviel Code bestehen wie nötig ist, um einen Request vorzubereiten, der am Ende dann abgeschickt wird. Ohne Funktionen gäbe es keinen Response, den die vorbereitende Codeeinheit nachbereiten könnte, also wäre ihre Aufgabe mit Versand des Request abgeschlossen. Der Response würde von einer anderen Codeeinheit weiterverarbeitet.

Ohne Funktionen ist es aber natürlich sinnlos, von Request und Response zu sprechen. Einen Request gibt es nur, wenn man auch einen Response erwartet. Clients übergeben Requests an Services, die mit Responses antworten.

Ohne Funktionen gibt es nur Daten, die produziert und konsumiert werden. Producer senden Daten an Consumer. Und immer so weiter. Aus Client-Code wie diesem, der sich potenziell unendlich fortsetzt…

Client:
  Vorbereitender Code erzeugt X
  Y= f(X)
  Nachbereitender Code verarbeitet Y

wird Producer-Consumer-Code:

Producer:
  Vorbereitender Code erzeugt X
  Versenden von X

f als Prosumer:
  X nach Y transformieren
  Versenden von Y

Consumer:
  Nachbereiten von Y

Sie sehen, der Umfang jeder Codeeinheit ist ganz natürlich sehr begrenzt, wenn es keine Funktionen gibt. Es lässt sich einfach nur wenig tun, bis wieder Arbeit an eine andere Codeeinheit delegiert werden muss.

Die grenzenlose Kopplung von Vorbereitung und Nachbereitung ist das offensichtliche Problem, zu dem Funktionsaufrufe führen. Sie bläht aufrufende Codeeinheiten auf. Das erschwert schnell die Verständlichkeit und leistet einer Vermischung von Verantwortlichkeiten im Sinne des SRP Vorschub.

Problem #2: Unbegrenzte Tiefe

Unterhalb des Request/Response-Problems liegt leider noch ein weiteres, noch fundamentaleres. Das wird allerdings erst sichtbar, wenn Software weiter wächst. Es ist das Problem des Aufrufs schlechthin.

Zur Erinnerung: Aufrufe von Code sind eigentlich als Mittel zur Platzersparnis erfunden worden. Früher war Speicher eben sehr knapp. Da war jedes Mittel recht, um Bytes zu sparen. Also hat man das CALL/RET Maschinenbefehlpaar erfunden. Aus Code wie:

A
S
T
U
B
S
T
U
C
S
T
U

konnte nun werden:

A
CALL F
B
CALL F
C
CALL F

F: S
T
U
RET

6 Anweisungen statt 10. Das ist selbst mit diesem Pseudocode eine Reduktion um 40%.

Platzersparnis, nicht Wiederverwendbarkeit ist die Motivation hinter Unterprogrammen als Verallgemeinerung von Funktionen. Für Wiederverwendbarkeit wären Macros ausreichend gewesen.

Wie alles im Leben hat natürlich auch die Platzersparnis ihren Preis. Der besteht in der Trennung von Client-Kontext und Service-Code. Solange im obigen Beispiel STU zwischen A und B textuell steht, ist klar, was die Aufgabe von STU ist. STU steht im Nutzungskontext. Der Entwickler sieht zur Entwicklungszeit, was vorher passiert, was zwischendurch passiert und was nachher passiert.

Die Einführung des Unterprogramms F zerstört diese verständliche Einheit. Was zwischendurch passiert, steht nun irgendwo. Wer nun liest

A
CALL F
B

der ist darauf angewiesen, dass F ein ausdrucksstarker Name ist, um zu verstehen, was da passiert. Die vorherige Einheit zur Entwicklungszeit existiert erst wieder zur Laufzeit.

Das bedeutet: Funktionen machen den Aufrufort schwerer lesbar, weil sie dort Code durch einen Namen ersetzen. So eine Ersetzung ist sehr verlust- bzw. missverständnisgefährdet.

Und Funktionen machen Code insgesamt schwerer lesbar, weil sie einen natürlichen Zusammenhang wie

A
S
T
U
B

über die Codebasis verteilen. Auch da gibt es ja kein Halten. Die Aufruf-Schachtelung von Funktionen ist beliebig tief. Aus

A
S
T
U
B

wird zuerst

A
CALL F
B

F: S
T
U
RET

dann vielleicht

A
CALL F
B

F: S
CALL G
U
RET

G: T
X
Y
RET

und dann vielleicht

A
CALL H

H: CALL F
B
RET

F: S
CALL G
U
RET

G: T
X
Y
RET

uns so weiter…

Zur Laufzeit macht das alles keinen Unterschied. Zur Entwicklungszeit jedoch wird es immer schwieriger zu verstehen, was da eigentlich passiert. Inhaltliche Sequenzen werden aufgelöst. Es entsteht ein Granulat an Unterprogrammen, dessen Aufrufhierarchie zur Laufzeit keine formale Entsprechung zur Entwicklungszeit hat. Funktionen führen mithin zu einem fundamentalen Impedance Mismatch zwischen Entwicklungszeit und Laufzeit.

Gut, inzwischen gibt es IDEs, mit denen man die Aufrufhierarchie durchwandern kann. Aber seien wir ehrlich: das ist umständlich. Eine hierarchische Sicht von Funktionsaufrufen ist kein First Class Citizen in den populären IDEs wie eine Projektdateiansicht oder eine Klassenansicht. Und in der Tradition von C gibt es keine geschachtelten Funktionen in C++, Java, C#.

Die Schachtelung von Funktionsaufrufen ist so tief in unser aller Programmiererstammhirn eingebrannt, dass es nicht einmal eine Metrik dafür gibt. Man macht sich Gedanken über LOC pro Funktion oder die Schachtelungstiefe von Kontrollanweisungen in Funktionen. Die Tiefe der Aufrufhierarchie von Funktionen zur Entwicklungszeit hingegen, scheint niemanden zu interessieren. Dabei ist sie es, die den logischen Zusammenhang von Code auseinanderreißt.

Aus unbegrenzter Aufrufschachtelung folgt, dass auch die Problemlösung beliebig über die Tiefe der Aufrufhierarchie verteilt werden kann. Im obigen Beispiel können ja auf jeder Ebene – Aufrufwurzel, H, F oder G – Anteile von “Geschäftslogik” stehen.

  1. Das bedeutet erstens, es bedarf zusätzlichen Aufwands, um zu entscheiden, auf welcher Ebene Geschäftslogik angesiedelt werden sollte. Das ist aber natürlich Aufwand, der nicht zur Lösung des Problems beiträgt. Also scheut man ihn, wo es geht. Das Ergebnis sind flache Hierarchien mit sehr langen Funktionen (s. Problem #1).
  2. Das bedeutet zweitens, Funktionen, die nicht Blätter in der Aufrufhierarchie sind, machen zusätzlichen Aufwand beim Testen. Es muss ja nicht nur ihr Beitrag zur Problemlösung überprüft werden, sondern es müssen auch noch die aufgerufenen Funktionen ersetzt werden (Attrappen).
  3. Und drittens sind Aufrufhierarchien ständig gefährdet, durch Veränderungswellen erschüttert zu werden. Veränderungen breiten sich entlang von Abhängigkeiten aus:
    Wenn C wie Client von S wie Service abhängig ist, dann kann es bei Änderungen an C nötig sein, S nachzuführen. Und falls sich S ändert, kann es nötig sein, C nachzuführen. Je breiter und tiefer Aufrufhierarchien sind, desto unüberschaubarer die Ausbreitung von Änderungen, die immer irgendwo nötig sind. Das ist umso schlimmer, da ja diese Hierarchien nur schwer zu übersehen sind. Sie existieren nicht textuell und auch kaum in anderer Ansicht in den IDEs.

Fazit

Funktionen (oder allgemeiner: Unterprogramme) sind aus meiner Sicht eine der Hauptursachen für die Undurchschaubarkeit von Code. Sie machen seine Ausdehnung in Breite und Tiefe grenzenlos.

Dagegen helfen dann auch keine Ermahnungen und Metriken. Denn die sind sehr geduldig. Wenn es eng wird, hört man nicht hin und setzt sie aus. Immer mit dem Verweis, dass gerade anderes wichtiger sei – und es ja auch ohne ginge.

So entstehen unwartbare Codekonvolute einen Funktionsaufruf nach dem anderen.

*Dagegen hilft nur, Funktionsaufrufe klaren Auges als Gefahr zu sehen und ihre Nutzung zu rigoros zu begrenzen.*

Wir müssen diese Altlast aus den Anfangstagen der Programmierung abwerfen (oder zumindest mit weniger Schädlichem integrieren). Funktionen sind ein Erbe der Nähe zur Mathematik. Das hat uns weit gebracht – aber nun haben wir eine Grenze erreicht, da der Schaden größer als der Nutzen ist.

Mittwoch, 4. Januar 2012

Bei häufigem Kontakt mit Gift refaktorisieren

images/refactoring-toxicity.pngGraham Brooks bricht eine Lanze dafür, Code nicht mit der Gießkanne zu refaktorisieren, sondern den Aufwand dort zu treiben, wo es gerade besonders nutzt. Das hört sich sinnig an.

Noch sinniger wird es, wenn Graham als Nützlichkeitsindikatoren Giftigkeit (Toxicity) und Unbeständigkeit (Volatility) angibt.

Giftig (als Steigerung von schmutzig) ist der Code dort, wo bestimmte Metriken besonders schlimme Werte annehmen. Unbeständig ist er dort, wo häufig an ihm gearbeitet wird.

In Kombination ergibt sich eine Giftigkeit x Unbeständigkeit Matrix, die klar sagt: Refaktorisiere, wo häufiger Veränderungskontakt mit hoher Giftigkeit besteht; aber lass Code stabilen (relativ) ungiftigen Code in Ruhe. Das hört sich sinnig an.

image
Quelle: Graham Brooks

Nun fragt Jens Schauder in einem Tweet aber zurecht: Und wie misst man nun Giftigkeit? (Die Unbeständigkeit ist kein Problem; sie lässt sich aus einem VCS Log herauslesen.)

Graham sagt nichts dazu. Er verweist vielmehr auf diesen Artikel von Erik Dörnenburg. Der zeigt dann ein konkreteres Giftigkeitsdiagramm und gibt konkrete Metriken an. Auch sehr sinnig.

Wenn Sie nun aber einmal diese Grafik genauer anschauen,…

image
Quelle: Erik Dörnenburg

…dann fällt Ihnen sicher auf, dass sich im Grunde nur bei vielleicht 10 (7,5%) von 132 Dateien die Giftigkeit aus mehr als Zyklomatischer Komplexität (ZK) und Methodenlänge (ML) ergibt.

Die Werte anderer Metriken spielen diesen beiden gegenüber kaum eine Rolle - oder sie korrelieren mit ihnen positiv: wo ZK+ML groß ist, da ist z.B. auch der Fan-Out (efferente Kopplung) groß.

Ich frage mich deshalb: Was soll dieser ganze komplizierte Metrikenkram? Wenn man sonst nichts zu tun hat, kann man sich damit einen schönen Tag machen und wichtig aussehen. Für die Entscheidung an der Praxisfront jedoch reichen einfachere Heuristiken. Und um nicht mehr als Heuristiken geht es ohnehin. Denn auch die exaktesten Berechnungen von Metriken sollten nicht darüber hinweg täuschen, dass Giftigkeit vor allem im Auge des Betrachters entsteht.

Ich mache es mir in der Codebeurteilungspraxis einfacher. Mein Fokus liegt auf nur zwei Metriken:

  • LOC: Je länger eine Codeeinheit, desto schmutziger und schließlich giftiger ist sie. (Oder genauer: Es geht eigentlich um Anweisungen und nicht um Lines. Aber LOC hat sich einfach eingebürgert als Begriff.)
    Wieviele LOC eine Codeeinheit hat, kann man sehen.
  • Zyklomatische Komplexität: Je höher die ZK, desto schmutziger und schließlich giftiger ist eine Codeeinheit.
    Wie die ZK einer Codeeinheit ist, kann man abschätzen nach der Zahl der Kontrollanweisungen (Fallunterscheidungen, Schleifen) und ihrer Schachtelungstiefe.

Auch hier gibt es natürlich eine gewisse positive Korrelation: bei wenigen LOC wird die ZK eher gering sein, bei vielen LOC wird sie tendenziell auch steigen.

Dennoch möchte ich auf keine der Metriken verzichten. Auch wenn sie korrelieren, repräsentieren sie unterschiedliche Aspekte des Codes. LOC steht für mich eher im Zusammenhang mit dem SRP. Und ZK steht für mich eher im Zusammenhang mit Testbarkeit.

Ab welcher Länge oder Komplexität wird Code nun aber nicht nur schmutzig oder giftig? Meine persönlichen Alarmglocken gehen an, wenn eine Methode länger als eine Bildschirmseite ist, also bei LOC > 20-30, und/oder mehr als 2-3 Kontrollanweisungen enthält und/oder die Schachtelungstiefe > 2 ist.

Letztlich geht es aber für das Refaktorisieren weniger um absolute Zahlen. Refaktorisiert werden sollte dort, wo die Giftigkeit “nur” größer ist als woanders. Das gilt für Methoden mit 10.000 LOC und ZK 20 vs 8.000 LOC mit ZK 15 genauso wie für Methoden mit 100 LOC und ZK 10 vs 80 LOC mit ZK 6.

Und dieses Verhältnis sollte dann auch noch im Lichte der Unbeständigkeit betrachtet werden. Da stimme ich Graham Brooks zu.

Darüber hinaus interessiert mich allerdings noch die afferente Kopplung (AK, Fan-In). Denn wenn Codeeinheiten von vielen anderen referenziert werden, haben sie eine große Strahlkraft. Änderungen/Fehler an/in ihnen wirken sich leicht auf weite Teile der Software aus. Das bedeutet, Codeeinheiten mit hoher afferenter Kopplung verdienen ein besonderes Augenmerk.

Wenn ich eine Liste mit Codeeinheiten und ihren Metriken wie die folgende hätte

Codeeinheit(Name, LOC, ZK, AK, Unbeständigkeit)

würde ich sie so sortieren:

  1. Zuerst Codeeinheiten mit hoher Unbeständigkeit...
  2. ...dann solche mit hoher afferenter Kopplung...
  3. ...dann solche mit hoher LOC...
  4. ...dann solche mit hoher ZK.

Komplizierter muss die Entscheidungsgrundlage für die Refaktorisierung nicht sein, finde ich. Es sind keine komplizierten Schwellenwerte nötig, keine weiteren Metriken. LOC und ZK kann man sogar mit dem unbewaffneten Auge recht gut beurteilen. Für Unbeständigkeit und AK ist Werkzeugunterstützung nützlich.

Mittwoch, 2. November 2011

Get real, Kanban!

Schöne Sache das Networked Kanban wie es Jurgen Appelo in seinem Blogbeitrag beschreibt - nur was ist denn daran wirklich neu? Jeder nicht triviale Produktionsprozess ist mehr als eine "Eimerkette". Und jeder Produktionsschritt arbeitet immer aus einer Warteschlange. Die ringelt sich auf dem Schreibtisch in Form einer Inbox, die besteht aus Post-It Notes am Monitor oder die zischelt in Excel oder einem anderen Tool im Intranet.

Die "Erfindung" von Kanban kann also nicht in Warteschlangen bestehen. Und wenn Kanban die Softwareproduktion als "Eimerkette" sieht, dann ist das eine sehr vereinfachende Sichtweise.

Aber zum Glück bietet Kanban mehr. Es geht darum, die ohnehin immer existierenden Warteschlangen zur Steuerung zu nutzen. Der bewusste Umgang mit Warteschlangen ist der Trick bei Kanban. Weg von der lokalen ad-hoc Optimierung hin zu einer globalen Optimierung.

Guter Ansatz - nur fehlt mir immer wieder dabei etwas. Daran rührt Jurgen Appelo, finde ich. Es fehlt die Frage, wie denn überhaupt der Produktionsprozess aussieht?

Die Kanban-Boards, die ich bisher gesehen habe, sind aus meiner Sicht so einfach, dass sie der Realität des Softwareproduktionsprozesses nicht wirklich gerecht werden, finde ich. Man freut sich, dass man ein Board aufhängt und nun Warteschlangen beobachtet. Das Vorhandensein eines Terrariums auf dem Gang wird als Meilenstein betrachtet.

Doch was ist es, das man dort beobachtet? Ist das wirklich genug? Ist das die Realität oder nur eine mühelose Abstraktion?

Jurgen Appelo  kommt der Wirklichkeit näher mit dem Netzwerk. So kann Kanban skalieren. (Ob Kanban dafür dann ein guter Name ist, lasse ich mal dahingestellt. Er ist zumindest eingebürgert.)

In Jurgens Beitrag kann man ahnen, dass Softwareproduktion erstens ein Netzwerk von Produktionsschritten ist und zweitens auch noch eine Holarchie: jeder Produktionsschritt auf einer Abstraktionsebene kann potenziell wieder ein Produktionsprozess bestehend aus mehreren Schritten sein. Ich fühle mich erinnert an Staged Event Driven Architecture, SEDA:

image

Die Masterfrage jedoch lässt auch Jurgen offen: Wie sieht denn überhaupt der Produktionsprozess im Ganzen aus? Wieviele Ebenen hat er, aus welchen Schritten besteht er?

Board-Beispiele wie dies

image

finde ich viel zu starr. Das Leben und die Produktion von Software finden nicht im Schwimmbad auf klaren Bahnen statt.

Indem Kanban aber immer wieder nur so beschrieben wird, macht man es Teams schwer, ihre Realität wirklich zu finden - und zu optimieren. Denn es kann ja nicht nur darum gehen, einen simplifizierten Prozess zu optimieren. Auch das führt nur zu Reibungen - über die man sich wundert, weil man doch lokal alles optimal gemacht zu haben meint.

Jurgens Beitrag sollte daher ein Appell sein, sich mehr Gedanken über den Produktionsprozess von Software zu machen. Wenn dabei dann erstmal soetwas herauskommt ist das auch ok:

image

So ist halt das Leben. Gut, dass man das weiß. Dann kann man an die Sache wirklich bewusst herangehen. (Im Bild ist zum Beispiel zu sehen, dass nicht die Softwareentwicklung das Problem ist. Die mit Kanban zu optimieren, brächte nichts. Das Problem steckt in den Schritten davor (rot).)

Kanban-Kärtchen schieben, ist auch wieder keine Silberkugel für die Softwareentwicklung. Genauso wenig wie jeden morgen 15 Minuten Standup-Meeting und alle 3 Wochen ein Release rausschieben nach Scrum.

Die Realität ist unordentlicher und schmutziger. Sie muss zuerst erkannt werden. Teams müssen sich das IST bewusst machen, bevor sie auf den einen oder anderen Zug aufspringen. Das, würde ich sagen, hat auch Jurgen in gewisser Weise gemerkt. Sein Beitrag hilft damit, realistischer in eine Veränderung des Vorgehens bei der Softwareentwicklung einzusteigen. Analyse und Nachdenken helfen halt doch.

Am Ende zählt nicht, ob man Scrum oder Kanban “nach dem Buch macht”. Es zählt, dass die Softwareproduktion besser ist als vorher.

Mittwoch, 12. Oktober 2011

Komponentenorientierung fürs Unternehmen

Kann Unternehmensorganisation von der Softwareentwicklung etwas lernen? Dass umgekehrt Softwareenwicklung von der Unternehmensorganisation lernen kann, zeigt ja in der letzten Zeit sehr schön Kanban. Das Vorgehensmodell überträgt erfolgreiche Organisationsmuster aus warenproduzierenden Unternehmen in die Programmierung

Aber auch umgekehrt ist Lernen möglich. Die Komponentenorientierung ist ein Beispiel, wo Unternehmen der Softwareentwicklung etwas abschauen können.

Komponenten in der Softwareentwicklung

In der Softwareentwicklung stehen Komponenten zunächst für Wiederverwendbarkeit, d.h. den unveränderten Einsatz von Code in verschiedenen Anwendungen. Die Motivation dahinter ist die Erhöhung des ROI: Wenn schon in Code investieren, dann am besten in einer Weise, dass der möglichst häufig zum Einsatz kommen kann.

Beispiel Rechtschreibprüfung: Wenn ein Softwareentwicklungsteam einen Blog-Editor entwickeln soll und dafür eine Rechtschreibprüfung braucht, dann tut es gut daran, diese Funktionalität so zu formen und zu verpacken, dass sie nicht nur im Blog-Editor zum Einsatz kommen kann, sondern auch in einem zukünftigen Twitter- oder Google+-Client oder auch noch in einem Chat-Client oder Email-Client.

Die Entwicklungskosten mögen für eine solch allgemeinere Rechtschreibprüfung etwas höher sein als für eine Blog-Editor-spezifische - doch das wird kompensiert durch zukünftige Ersparnisse. Bei den anderen Produkten fallen keine weiteren Kosten für die Rechtschreibprüfung mehr an.

Programmcode wiederverwendbar auszulegen, d.h. Komponenten zu entwickeln, spart also Geld. Noch mehr Geld spart jedoch, Programmcode gar nicht mehr zu entwickeln, sondern einzukaufen. Warum sollte das Softwarehaus überhaupt eine Rechtschreibprüfung entwickeln? Rechtschreibprüfung ist eine so grundlegende Aufgabe für viele Programme, dass sicherlich schon andere vor dem Softwarehaus dafür Lösungen entwickelt haben. Warum also nicht eine solche Lösung einkaufen? Das kostet zwar Geld, aber der enorme Aufwand, den Entwicklung und Pflege einer eigenen Rechtschreibprüfung bedeuten würden, steht gewöhnlich in keinem Verhältnis zum Kaufpreis einer Komponente oder gar eventueller Lizenzgebühren.

Wo die Aufgabe einer Software sich standardisieren lässt, sollte also nach einer existierenden Komponente Ausschau gehalten werden. Das ist eine Entscheidung für buy statt build, für den Einkauf statt den Selbstbau.

Die Entwicklung von allgemeinen Komponenten statt immer wieder spezieller Softwarebausteine spart also Kosten, wenn zu erwarten ist dass sie wiederholt zum Einsatz kommen.

Der Einkauf von Komponenten statt ihrer Entwicklung spart darüber hinaus Kosten, selbst wenn kein wiederholter Einsatz im eigenen Haus zu erwarten ist.

Software aus existierenden Komponenten zusammenzusetzen spart Geld; doch selbst wenn die Kosten genauso hoch wären wie die einer Eigenentwicklung oder vielleicht sogar etwas höher, wäre der Einkauf existierender Komponenten der Eigenentwicklung vorzuziehen.
Der Nutzen eingekaufter Komponenten liegt nämlich nicht nur in einer wahrscheinlichen Kostenersparnis, sondern in der Freisetzung von Ressourcen. Wo Funktionalität nicht selbst entwickelt werden muss, kann man seine Aufmerksamkeit etwas anderem widmen.

Der Einkauf von Komponenten, die für Standardfunktionalität existieren, konzentriert die Arbeitskraft auf das, was nicht standardisierbar ist. Und genau dort liegt die Aufgabe eines Teams.

Der Job eines Teams, das einen Blog-Editor entwickeln soll, ist es vor allem, sich auf das "Blog-Editor-hafte" zu konzentrieren. Was macht einen Blog-Editor aus? Wie unterscheidet er sich von einer allgemeinen Textverarbeitung, wie unterscheidet er sich von einem Twitter-Client? Alles, was ein Blog-Editor mit anderen Programmen gemein hat, sollte das Team nicht selbst programmieren müssen, sondern einkaufen.

Damit erreicht es, dass es mit dem, worum es ihm eigentlich geht, schnellstmöglich voran kommt.

Standardfunktionalität selbst zu entwickeln ist ein Luxus, den Teams sich leisten können, wenn sie durch das Mehr an Kontrolle über diesen Softwareteil, einen deutlichen Vorteil erlangen. [1] Das ist allemal in frühen Phasen eines Softwareprojektes eher selten der Fall. Projekte werden ja nicht aufgesetzt, um etwas Existierendes zu duplizieren, sondern einen Unterschied zu machen. Auf diesen Unterschied müssen deshalb alle Kräfte ausgerichtet sein.

Neben der durch Wiederverwendbarkeit ermöglichten Konzentration auf das Wesentlich bieten Komponenten aber noch einen weiteren Vorteil: Austauschbarkeit. Komponenten sind Black Boxes, deren Aufgabe durch einen separaten Kontrakt definiert ist. Wie welche Daten wann zwischen ihnen und der Umgebung fließen, das ist unabhängig davon beschrieben, wie konkret sie ihre Aufgabe erfüllen, d.h. wie sie implementiert sind. Kontrakte verbergen Details, mit denen sich Komponentennutzer nicht herumschlagen wollen und sollten.

Kontrakte für Standardfunktionalität sind Standardkontrakte. Das heißt, nicht nur eine bestimmte Komponente kann sie erfüllen, sondern viele verschiedene Varianten oder Modelle einer Komponente. Das Blog-Editor Team, das komponentenorientiert denkt und entwickelt, ist also nicht darauf angewiesen, die Rechtschreibkomponente von Hersteller X zu verwenden. Wenn es damit Erfahrung gesammelt hat und unzufrieden ist, kann es zur Rechtschreibkomponente von Hersteller Y wechseln. Das ist selbst dann der Fall, wenn es zunächst in eine eigene Rechtschreibkomponente investiert haben sollte (out-sourcing, buy) - oder umgekehrt, wenn es später von einer eingekauften Standardkomponente zu einer Eigenentwicklung wechseln will (in-sourcing, build).

Komponentenorientierung reduziert die Gefahr eines lock-in, d.h. der Festlegung auf eine ganz bestimmte Implementation von Funktionalität. Das ist günstig, falls die Implemenation in puncto funktionaler oder nicht-funktionaler Anforderungen nicht mehr genügt oder der Anbieter sich als unvorteilhaft erweist.

Es gibt noch weitere Vorteile der Komponentenorientierung für die Softwareentwicklung. Doch schon die aufgezeigten können der Unternehmensorganisation Impulse geben.

Die Vorteile von Komponenten nochmal auf den Punkt:

  • Komponenten setzen Ressourcen frei
  • Komponenten entkoppeln

Komponentenorientierung in der Unternehmensorganisation

Unternehmen haben über "Geld verdienen heute und in Zukunft" eine inhaltliche Mission. Sie bieten einen Haarschnitt, Möbel, Hausbau, Entrümpelung und Millionen andere Leistungen. Ziel jedes Unternehmers, insbesondere jedes Gründers, sollte daher sein, die besonders knappe Ressource Aufmerksamkeit auf seine Mission zu fokussieren.

Beispiel Ad-hoc Buchdruck: Die Mission des Unternehmens ist, Lesern von elektronischen Dokumenten - seien es PDFs oder Web-Seiten - die Möglichkeit zu geben, daraus Bücher zu machen. Beliebige Seiten beliebiger Dokumente sollen ad-hoc zusammengefasst werden können. Und diese Zusammenfassungen sollen dann on-demand in beliebiger Auflagenhöhe, d.h. beginnend mit Auflage = 1, als Buch gedruckt und innerhalb von 2 Tagen geliefert werden.
Was kann der Entrepreneur, der eine solche Leistung aufbauen möchte, nun von der Komponentenorientierung lernen?

1. Out-sourcing vs in-sourcing

Der Entrepreneur sollte sich fokussieren auf das, was seine Idee, sein Geschäft ausmacht. Für das Beispiel ist das die einfache individuelle Zusammenstellung von Inhalten zu Büchern, der Druck von Einzelexemplaren und die schnelle Lieferung.

Print-on-demand Dienste wie epubli oder lulu.com bieten das heute auch schon grundsätzlich - nur nicht in dieser Einfachheit und Geschwindigkeit. Die nicht-funktionalen Aspekte machen mithin die Geschäftsidee aus: individuelle Bücher aus existierenden Inhalten in Auflage 1 drucken und übermorgen liefern. Darauf muss sich der Entrepreneur fokussieren.

Das bedeutet im Umkehrschluss: Alles, was nicht im Fokus seiner Mission ist, sollte der Entrepreneur nicht selbst leisten. Stattdessen sollte er nach Komponenten Ausschau halten, die es für ihn leisten.

Sollte der Entrepreneur seine Buchhaltung selbst machen? Nein. Das gehört nicht zu seiner Mission.

Sollte er selbst drucken? Nein, jedenfalls nicht, wenn es sich vermeiden lässt. Drucken ist zwar Teil der Idee, doch deren Unterschied zu existierenden Angeboten besteht nicht in Funktionalität (Buchdruck), sondern in einer Qualität der Funktionalität (Geschwindigkeit). Die Aufgabe des Entrepreneurs ist es daher, Buchdruck nicht zu ermöglichen, sondern zu beschleunigen. Wenn er das kann, ohne selbst zu drucken, dann ist das der einzuschlagende Weg.

Sollte er selbst das Marketing machen? Nein. Das gehört nicht zu seiner Mission. Der Entrepreneur hat die Idee von einem Ganzen. Dieses Ganze aus Teilen zusammenzufügen, das ist seine vornehme Aufgabe. Er ist Integrator und Koordinator, nicht Executor.
Sollte der Entrepreneur selbst die Homepage seines Unternehmens erstellen? Nein. Das gehört nicht zu seiner Mission.

"Selbst machen" bezieht sich bei diesen Fragen nicht nur darauf, ob der Entrepreneur als Person, sondern ob sein Unternehmen eine Aufgabe selbst erledigt. In-sourcing vs out-sourcing ist die Frage.

Und die Antwort der Komponentenorientierung ist: out-sourcing soweit möglich, in-sourcing soweit nötig.

Dem out-sourcing kann natürlich das Budget entgegenstehen. Wo das Geld knapp/unsicher ist, tendieren Unternehmensgründer daher zum in-sourcen. Lieber die Buchhaltung im eigenen Haus, lieber die das Sekretariat im eigenen Haus, lieber Lager und Auslieferung im eigenen Haus usw.

Wo es vergleichbare Leistungen nicht am Markt einzukaufen gibt, stellt sich die Frage nach dem out-sourcing nicht. Immer mehr Leistungen lassen sich heute jedoch einkaufen. Der Gründer hat also die Alternative. Und er sollte sich im Zweifel für das out-sourcing entscheiden.
In-sourcing kostet auch Geld, manchmal vielleicht sogar weniger. In-sourcing hat aber mehrere Nachteile, die sich gerade ein Gründer bewusst machen sollte:

  • In-sourcing erfordert Führung. Out-sourcing erfordert nur Koordination der zugekauften Komponente mit anderen; aber in-sourcing braucht darüber auch noch Führung der "Implementation" einer Leistung. Sie ist keine Black Box. Der Entrepreneur muss sich um die einzelnen Mitarbeiter kümmern.
  • In-sourcing bindet. Ein Dienstleister kann ohne moralische Bedenken gewechselt werden; eigenen Mitarbeitern kann und/oder will man nicht so leicht kündigen.
  • In-sourcing erzeugt Fixkosten. Zugekaufte Leistungen werden meist nach Aufwand/Stück abgerechnet. Hat das Geschäft keinen Bedarf, fallen keine Kosten an. Interne Leistungen kosten jedoch immer Geld, auch wenn es nichts zu tun gibt. Gehälter und Mieten laufen weiter.
    Der Leitstern des Unternehmers sollte immer der Fokus sein. Bei jeder Entscheidung über in- vs out-sourcing sollte die Frage lauten: Wie kann ich mich mehr auf die eigentliche Mission konzentrieren?

Für Gründer kommt dann noch als zweite Frage hinzu: Wie kann ich flexibel bleiben, falls ich den Kurs des Unternehmens ändern muss?

2. Austauschbarkeit durch Kontrakte

imageEgal, ob eine Leistung selbst erbracht oder eingekauft wird, sie sollte klar kontouriert werden. Die Schnittstellen zwischen den Komponenten - ob interne oder externe - sind klar zu definieren. Kontrakte sollte so ausgelegt werden, dass die Kopplung zwischen Komponenten so lose wie möglich ist.

Wie eng Leistungserbringer - Abteilungen oder externe Dienstleister - gekoppelt sind, hängt davon ab, wie stark Änderungen auf der einen Seite des Kontraktes Änderungen auf der anderen Seite hervorrufen können.

Wenn der ad-hoc Buchdruck zum Beispiel den Versand selbst übernimmt, dann haben personelle Änderungen darin größere Auswirkungen, als bei Einbindung einer externen "Versandkomponente". Eine "Versandkomponente" ist für den Entrepreneur eine Black Box. Wie sie ihren Kontrakt erfüllt, ist ihm egal. Sollte es dort personelle Änderungen geben, darf das laut Vertrag keinen Einfluss auf die Leistungsqualität haben.

Intern lässt sich solche Entkopplung jedoch kaum herstellen. Und der Austausch einer internen "Versandkomponente" fällt auch nicht leicht.

Externe Komponenten erhöhen mithin die Flexibilität eines Unternehmens - zumindest ganz allgemein. Das ist besonders für Gründer wichtig. Die sind ja für ihre Mission noch auf der Suche nach einem Erfolgsweg. Da sollten sie in der Lage sein, Leistungen einfach zu verändern und auszutauschen. Bei internen Leistungen fällt das jedoch schwerer als bei externen. Mindestens die Kontrakte sind weniger explizit.

Deshalb sollte ein Unternehmen Wert darauf legen, nach Identifikation seiner Komponenten, deren Kontrakte möglichst ausdrücklich zu formulieren, ganz unabhängig davon, ob nun in- oder out-sourcing stattfindet.

Dazu gehört aber nicht nur die Form der Schnittstelle, also die Oberfläche, an der Komponenten miteinander (re)agieren. Es ist auch zu bedenken, was in einer Komponente an "Zustand" angehäuft wird. Der sollte sich bei Austausch nicht als hinderlich erweisen.

Ein Beispiel dafür sind Email-Dienstleister wie Google oder Hotmail. Wer sie benutzt, interagiert mit ihnen nicht nur über einen Email-Client (z.B. Web-Client oder Outlook), sondern akkumuliert auch "Zustand"; das sind all die Emails, die beim Dienstleister im Archiv liegen.
Wenn nun der Email-Dienstleister gewechselt werden soll, darf dieser "Zustand" dem nicht im Wege stehen. Er soll erhalten bleiben, wenn man z.B. von Hotmail nach Google oder Exchange umzieht. Darin liegen ja wertvolle Informationen aus Jahren der Arbeit. Wenn das nun nicht möglich ist, dann liegt ein lock-in vor. Der ist zu vermeiden. Darauf muss bei der Formulierung des Kontrakts geachtet werden.

Gerade wer auf externe Komponenten zurückgreifen will, tut gut daran, von vornherein den möglichen Fall des Wechsels anzusprechen. Im Kontrakt sollte stehen, was in dieser Hinsicht möglich ist, worauf zu achten ist. Bei externer Buchhaltung oder zugekauftem Marketing mag das relativ einfach sein, aber externe Softwareentwicklung macht da z.B. traditionell Probleme.
Das sollte aber nicht gegen out-sourcing sprechen, sondern nur für eine sorgfältige Kontraktgestaltung. Die Vorteile des out-sourcing gerade für Gründer sind so groß, dass sie nicht durch Unsicherheit in diesem Punkt verspielt werden dürfen. "Ach, dann machen wir das halt doch selbst, wenn es die Vereinbarungen so schwierig sind." wäre die falsche Reaktion.
Ein in-sourcing mag die Kontraktdefinition einfacher machen; sie fällt womöglich sogar ganz unter den Tisch, weil ja eben eine Leistung im Haus erbracht wird. Da muss man sich ja nicht um eine explizite Beschreibung kümmern, oder?

Der einfachere, weil fehlende Kontrakt zwischen Komponenten zieht dann aber später Probleme an. Ohne explizite Kontraktgrenzen wird das Unternehmen monolithisch, d.h. zu einem wenig flexiblen Block. Das verhindert zukünftige Entwicklung. Wissen ist dann eher implizit denn explizit; Leistungen werden irgendwie denn durch einen sauberen Kontrakt beschrieben erbracht. Wachstum fällt dann schwer.

Fazit

Unternehmensorganisation kann von der Softwareentwicklung lernen. Unternehmen wie Software sind komplexe Systeme, die davon profitieren, sauber strukturiert zu sein. Und beide profitieren davon, wenn sie nicht alles selbst leisten müssen, sondern sich auf ihren eigentlichen Zweck konzentrieren können.

Unternehmen sollten ihre Leistung daher als eine Kooperation von "Belangen" verstehen, die von Komponenten erbracht werden. Diese Komponenten sind Black Boxes definiert durch Kontrakte. Für jede Black Box kann dann entschieden werden, ob sie intern aufgebaut oder extern eingekauft werden soll.

Der externe Einkauf von Komponenten hat insbesondere dann vorteile, wenn das Unternehmen noch "in der Findungsphase ist", d.h. besonders flexibel agieren muss. Dann bedeutet Zukauf von externen Komponenten ein Plus an Flexibilität und Konzentration bei gleichzeitig geringeren Fixkosten.

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

Fußnoten

[1] Ein solcher Vorteil könnte in mehr Qualität bei nicht-funktionalen Anforderungen wie Performance, Sicherheit, Usability oder Internationalisierung liegen. Dass er bei der Funktionalität liegt, ist eher nicht zu erwarten, da es sich ja sonst nicht um Standardfunktionalität handeln würde.

Dieser Artikel ist ein Beitrag zur Blog-Parade des

image

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...