Follow my new blog

Sonntag, 7. Oktober 2012

Was andere schon richtig machen - Halden vermeiden


Das ist Wim Wenders. Er ist einer der bekanntesten zeitgenössischen deutschen Regisseure mit Weltruf. Von ihm können wir als Softwareentwickler etwas lernen.

Als Regisseur ist es seine Aufgabe, gewisse Ziele zu erreichen: Er hat ein inhaltliches/künstlerisches Ziel, er hat ein Terminziel und er hat natürlich auch ein Budgetziel. Gerade Termin und Geld zwingen dazu, die Produktion genau zu planen. Das beginnt beim Drehbuch, denn ohne Drehbuch ist ja ungewiss, was an Drehorten, Schauspielern, Bühnbild und schließlich Zeit und Geld überhaupt nötig wäre. Auf dessen Basis entsteht dann ein Drehplan. Der sagt, was wann wie wo genau zu drehen ist, so dass die kosten minimal sind. Was im Drehbuch wie in einer Theaterstück in einer für die Geschichte passenden Reihenfolge steht, wird dafür in Teile zerlegt und neu gemischt. Szenen am selben Ort zu unterschiedlichen Zeiten im Film, werden zusammengelegt, so dass der Drehort nur einmal besucht werden muss. Und was am Ende passiert, wird an den Anfang gelegt. Der Dreh ist mithin nicht chronologisch.

Das bedeutet, etwas das früh gedreht wird, aber erst spät im Film auftaucht, muss 100% so gedreht werden, dass es später passt. Der Regisseur muss also genau wissen und dem ganzen Team klar machen, was vorher passiert sein wird, damit die früh gedrehten späteren Szenen auch brauchbar sind. Sie liegen dann nach der Aufnahme auf Halde. Nicht nur steckt in ihnen jetzt Kapital, sie zwingen den Rest des Drehs auch in eine Form, der sie anschlussfähig macht.

Der nicht-chronologische Dreh ist damit eine lokale Optimierung im Sinne des Geld-/Zeitbudgets - und stellt ansonsten nur eine Belastung dar: Die Continuity steht vor einer großen Herausforderung, um visuelle Sprünge zwischen aneinander geschnittenen Einstellungen zu vermeiden, die mit großem Abstand gedreht wurden. Die Schauspieler stehen vor großen Herausforderungen, da sie Präsenz in ihren Rollen "auf Zuruf" ganz ohne Kontext herstellen müssen. Der Regisseur steht vor der großen Herausforderungen, das Ganze in seinem Zusammenhang und seiner Entfaltung allen Beteiligten immer wieder präsent zu machen, auch wenn jeden Tag nur beliebige Ausschnitte gedreht werden.

Als Resultat entsteht eine wachsende Halde von Takes, deren Qualität im Rahmen eines Ganzen erst im Schnitt deutlich wird. Nicht nur wird sich dann erst erweisen, ob alles so anschlussfähig ist, wie geplant.

Insofern kann denn auch das Drehbuch als Halde angesehen werden. Es ist ein Plan für die Herstellung einer Wirkung beim Zuschauer. Ob der aufgeht, zeigt sich ebenfalls erst im Schnitt.
Monatelang wird Geld in ein Drehbuch investiert, ohne dass man weiß, ob das Endresultat dem künstlerischen Anspruch gerecht wird. Wochenlang wird dann Geld in einen Dreh investiert, ohne dass man weiß, ob der in Summe das Drehbuch angemessen umsetzt und im Schnitt das gewünschte Ergebnis entstehen kann.

Während seiner ersten 10 Filme hatte Wim Wenders versucht, diese Unsicherheiten durch viel Planung zu kompensieren. Er hatte Drehbücher nicht nur umgesetzt, sondern auch geschrieben. Nicht umsonst war er Mitgründer des Filmverlags der Autoren. Damit vermied er Reibungsverluste zwischen Regisseur und Drehbuchautor. Seine Planung begann also schon bei der Idee.

Während des Drehs dann hatte er - wie er heute während eines Colloquiumgesprächs an der Hamburger Hochschule für Musik und Theater berichtete - alles genau vorausgeplant. Jede Einstellung, jeder Auftritt, jeder Abgang, alles hatte er spätestens am Vorabend eines Drehs genaustens ausgetüftelt. Damit sollten Reibungsverluste zwischen Schauspielern und ihm oder auch der Technik während des Drehs reduziert werden. Es galt ja, die knappe Drehzeit bestmöglichst zu nutzen.

So war das. Bis zum Film "Paris, Texas".

Da machte er es anders. Motiviert hatte ihn seine Erfahrung mit der Inszenierung eines Theaterstücks. Denn im Theater wird anders gearbeitet. Es gibt nur eine Einstellung, es gibt nur eine chronologische Darstellung. Und hat das Stück begonnen, muss der Regisseur auf die Schauspieler vertrauen. Während der ganzen Vorstellung kann er nicht mehr eingreifen.

"Paris, Texas" hat Wim Wenders dann anders gemacht.

Das Drehbuch war nur halb fertig bei Drehbeginn. (Eine zweite Hälfte existierte nur für die Geldgeber. Sie war nie für die Produktion bestimmt, sondern ein Fake.)

Und der Dreh verlief streng chronologisch.

Das Ergebnis? Ein Film der von der Kritik gelobt und mit der Goldenen Palme in Cannes ausgezeichnet. Das Ergebnis lässt sich also sehen - und das ganz ohne Aufhäufung von Halden. Für Wim Wenders ist klar, dass der Film seine Qualität nur erhalten konnte, weil eben nicht zwanghaft ein Ende vor Drehbeginn vorgedacht worden war und sich die Schauspieler mit ihren Figuren entwickeln konnten. Das Geheimnis für das gute Ergebnis, ja, das bessere Ergebnis als bei vorhergehenden Filmen besteht für ihn mithin in der Vermeidung von Halden. Er hat in nichts investiert, von dem er nicht wusste, ob es so später gebraucht werden würde. Er hat sich mit keiner Halde von Vorproduziertem die Möglichkeit zur Reaktion auf Entwicklungen während des Drehs eingeschränkt.

Mir scheint das eine kopierenswerte Haltung. Auch in der Softwareentwicklung sollten wir es vermeiden, ein ausgefeiltes Drehbuch von Anfang bis Schluss zu schreiben. Vor Beginn der Entwicklung zunächst ein Backlog satt zu füllen, versenkt Geld in Anforderungen, von denen nicht sicher ist, wann, wie und ob sie später umgesetzt werden sollten, ob sie einem Anwender wirklich den intendierten Nutzen bieten. Ein pralles Backlog schränkt durch die Investitionen auch die spätere Reaktionsmöglichkeit ein. Denn was im Backlog steht, muss ja umgesetzt werden. Immerhin steckt da ja einige Mühe drin. Die darf nicht umsonst sein. Wenn in Wochen oder Monaten Neues auftaucht, dann hat sich das hinten anzustellen. Mit einem ausführlichen Backlog gilt das Prinzip "First come, first serve".

Ebenso sollte die Softwareentwicklung vermeiden, Code auf Halde zu produzieren. Der nicht-chronologischen Drehreihenfolge beim Film entspricht eine Entwicklungsreihenfolge, die sich von Befindlichkeiten der Softwareentwicklung leiten lässt. Wenn die meint, es müsse zuerst eine Infrastruktur aufgesetzt werden, ein Persistenzframework geschrieben, ein Security-API entwickelt werden, bevor auch nur ein Anwender etwas in die Hand bekommt… dann produziert sie Code auf Halde. Von dem weiß auch niemand, wann, wie, ob er mal Nutzen entfalten wird.

Zum Glück vertritt die Agilitätsbewegung diese Sichtweise schon länger. Sie ermahnt, Code nicht auf Halde zu produzieren, sondern nur in nützlichen Inkrementen. Sie ermahnt, nicht zuviel in Vorabplanung von Anforderungen und Entwürfen zu investieren. In der Projektrealität ist das allerdings immer noch nicht überall angekommen. Deshalb finde ich es motivierend, aus ganz anderer Richtung davon zu hören, wie die Reduktion von Halden erfolgssteigernd wirken kann. Dass selbst die Filmproduktion davon profitiert… Wer hätte das gedacht?

Vielen Dank, Wim Wenders, für diese Plauderei aus dem Nähkästchen.


PS: Woody Allen hat übrigens auch erkannt, dass Drehbuchhalde und Takehalde sich abträglich auf das künstlerische Ziel auswirken können. Er stellt sie zwar noch komplett her - doch er erlaubt sich spätere Korrekturen. In den Verträgen seiner Schauspieler steht, dass sie - wenn ich es recht erinnere - bis zu einem halben Jahr nach Drehschluss noch für Nachdrehs zur Verfügung stehen müssen. Für Woody Allen kann sich einfach im Schnitt herausstellen, dass Haldenmaterial in seiner Qualität ungenügend ist. Dann bessert er aufgrund der gewonnenen Erkenntnisse einfach nach.
Das scheint mir auch in Linie mit der Agilität zu sein. Woody Allen hat erkannt, dass sich Ziel und/oder Weg mit der Zeit durch die Produktion verändern können. Das überrascht ihn nicht mehr, sondern er weiß es und trifft also vertragliche Vorkehrungen.

Donnerstag, 4. Oktober 2012

Gesucht: dreckige Realität

In einem Blogartikel hat Matthias Bohlen seinen Ansatz beschrieben, die Form eines Softwaresystems zu entwerfen. Leider hat mich die Lektüre unbefriedigt zurückgelassen.

Dass Matthias einen anderen Ansatz verfolgt als ich, erklärt das allerdings nicht. Warum sollte mich das auch stören? Der Entwurf hat mich ja nicht in frustriert-sprachloses Erstaunen versetzt. Also erwächst aus der Andersartigkeit seines Ansatzes eher energievolle Spannung. Ich werde seinem Entwurf auch einmal meinen gegenüberstellen. Dabei kann ich wieder für mich etwas lernen. Sie können für sich vergleichen. Und vielleicht ergibt sich daraus auch noch ein konstruktives Gespräch.

Nein, der Ansatz ist es nicht, der mich frustriert hat. Es war eher die Aufgabe. Aber auch wiederum nicht konkret die Bankendomäne, sondern die, hm, Präsentation und der Scope. Beides war so typisch. Nämlich typisch – sorry to say, Matthias – flach.

Ich habe es schon so oft gesehen in Büchern und Artikeln: Da wird eine Technologie oder Methode vorgestellt anhand eines Beispiels, bei dem es einfach funktionieren muss.

Bis zu einem gewissen Grad ist das auch legitim oder sogar nicht zu vermeiden. Auch ich habe schon so Technologien oder Methoden vorgestellt; manchmal liegt es an der Zeit, die zur Verfügung steht. Gerade deshalb bin ich da aber vielleicht auch sensibel. Ich möcht es besser machen für Sie und auch für mich.

Das Problem bei einer flachen Aufgabenstellung, bei Anforderungen, die eine Technologie oder Methode quasi zum Selbstgänger machen ist schlicht, dass man sich in die Tasche lügt. Das kann dem Präsentierenden selbst nicht gefallen. Und es ist natürlich unschön für die Leserschaft.

Ja, das ist es, das hat mich bei Matthias Lösung gestört. Das unselige, weil schon so oft zitierte Überweisungsszenario ist so trivial, dass sich damit ja fast jede Methode erfolgreich demonstrieren lässt. Ein “normal” objektorientierter Ansatz hätte dabei genauso sinnvoll dargestellt werden können wir ein prozeduraler oder ein funktionaler. Ich kann jedenfalls nicht erkennen, dass Matthias’ DCI-Ansatz einem anderen überlegen ist.

Damit will ich keine Aussage über seinen Ansatz machen. Bei der Größenordnung an Aufgabe macht er für mich schlicht keinen deutlichen Unterschied. Er funktioniert. Klar. So wie der “normale” objektorientierte Ansatz auch vorher funktioniert hat.

Was wir bei Matthias sehen ist eine Momentaufnahme eines kleinen Szenarios. Und wenn wir in ein Buch zur Objektorientierung oder zu DDD schauen, dann sehen wir dort auch Momentaufnahmen. Da hat jemand lange an Code gearbeitet, ihn fein ziseliert – und präsentiert das kunstvolle Ergebnis.

Nur ist so leider nicht die Realität. Die ist dreckig und hektisch und nicht so strukturiert in ihren Anforderungen. Außerdem sind die deutlich umfänglicher. Da gibt es Seiteneffekte. Da müssen nicht-funktionale Anforderungen berücksichtigt werden. Und da verändert sich so einiges über die Zeit.

Wir brauchen mehr Demonstrationen von Technologien und Methoden, die es damit aufnehmen. Auch und gerade, wenn es um Methoden geht. Denn ob eine Technologie funktioniert oder nicht, das lässt sich leicht selbst feststellen. Zur Laufzeit gibt es gewöhnlich blitzschnell Feedback. Doch ob eine Methode funktioniert bzw. ob man sie richtig anwendet, das ist viel schwieriger festzustellen. Womöglich stellt sich das erst nach Wochen oder Monaten heraus, wenn man sich damit in eine Ecke gepinselt hat.

Matthias’ Lösung kommt mir insofern “erschlichen” vor. Das Problem ist so abstrakt, isoliert dargestellt, dass sich quasi alles wie von selbst fügt. Erklärungen sind kaum nötig. Jeder nickt sofort, wenn da die Rollen aufgetischt werden… und jeder hält es dann für ganz natürlich, dass ein Konto das andere auffordert etwas zu tun.

Aber das kann doch nicht Matthias’ Ernst sein. In welcher Bankenwelt sollen denn Konten direkt miteinander sprechen? Auch wenn ich kein Banker bin, hat das für mich keinen Rückhalt in der Realität der Bankdomäne. Deshalb scheinen mir die Rollen Quellkonto und Zielkonto “erschlichen”. Wie bei einer petitio principii, wo das zu Beweisende vorausgesetzt wird. Oder so ähnlich wie die Antwort auf die Frage aus Ottos Quizparodie, wer denn der berühmte Maler in Rembrandts Haus sei.

Aber nochmal: Damit will ich Matthias´ Ansatz nicht per se kritisieren, sondern nur sagen, dass ich seinen Wert anhand des flachen Beispiels noch nicht erkennen kann. Wo die Anwendungsfälle so klein sind, dass Objekte und Rollen auf der Hand liegen, kann ich jede Methode rechtfertigen, die Objekte und Rollen favorisiert.

Um mich zu überzeugen braucht es also etwas Größeres und Dreckigeres. Etwas, das nicht schon für eine Methode analysegerecht kleingehackt ist.

Aus diesem Grund habe ich schon vor längerer Zeit in Anlehnung an die kleinen Code Katas größere Application Katas beschrieben. Einige finden sich hier: http://clean-code-advisors.com/ressourcen/application-katas. Technologisch sollten sie keine Herausforderungen darstellen. Aber methodisch kann man sich an ihnen abarbeiten. Deshalb liegen sie in mehreren Iterationen vor.

Wie passt darauf der Ansatz von Matthias? Wie flott segelt er durch diese Anforderungen? Wie leicht lässt sich eine DCI-Struktur über die Iterationen verstehen und verändern?

Diese Fragen gelten aber natürlich nicht nur für Matthias. Jeder, der eine Methode für die Softwareentwicklung im Ärmel hat, kann sich daran probieren. “Normale” Objektorientierung genauso wie Funktionale Programmierung oder der de facto Stil eines Teams. Ich selbst demonstriere Flow-Design immer wieder an solchen Beispielen im Rahmen von Artikeln oder im Seminar “Agile Architektur” der Clean Code Developer Akademie.

Doch die AppKatas sind weit weg von Matthias’ Aufgabenstellung. Da mag es schwer fallen zu sehen, was ich mit “mehr dreckige Realität” meine. Deshalb hier meine Version seines Überweisungsszenarios, als ganze Anwendung mit einem UI und persistenten Daten.

Application Kata - Banküberweisung

Auch das ist natürlich immer noch eine Spielzeugvariante dessen, was in Banken tatsächlich läuft. Aber es sind zumindest mehr Facetten darin enthalten, so dass es schwieriger wird, seinen Entwurfsweg zu finden. Die Entscheidungen sind nicht vorgekaut. Ein wie immer geartetes Modell – um das es ja beim Entwurf geht – liegt nicht auf der Hand.

Also, das ist eine Art von Herausforderung, wie ich sie häufiger formuliert und dann angegangen sehen möchte. Alles andere ist Spielkram.

 

PS: Wer sich beklagen möchte, in der Aufgabenstellung seien zu viele Dinge nicht ausformuliert und deshalb sei es schwierig zu entwerfen, der kann gern in den Kommentaren hier Fragen stellen. Dann konkretisiere ich als “Kunde” :-)

Mittwoch, 26. September 2012

Form Follows Feasibility

Code verändern, ist schwer. Wer kämpft sich schon gern durch einen Dschungel aus Legacy Code? Viel schöner ist die Entwicklung auf einer grünen Wiese. Code neu schreiben, ist demgegenüber leicht.

Wenn es in Projekten knirscht, weil die Last der Brownfield Codes erdrückend ist, ist Neuentwicklung allerdings nur selten eine Option. Der Aufwand wäre gewaltig; dafür gibt es kein Geld und keine Zeit.

Aus dem Morast scheint dann nur der trübe Weg langwieriger Refaktorisierungen zu führen. Wochen und Monate arbeitet sich das Team daran ab. Spaß macht das nicht. Motvierend für andere ist das auch nicht. Wenn die Codebasis an einem solchen Punkt ist, leidet das Produkt unter doppelt schlechter Qualität: nicht nur die des Codes lässt zu wünschen übrig, sehr wahrscheinlich ist auch die Qualität des Teams – oder zumindest seine Attraktivität – suboptimal.

Natürlich kann Software nicht dauernd neu geschrieben werden. Aber wenn es soviel leichter ist, Legacy Code neu zu schreiben, statt ihn zu ändern, warum gibt es dann kein Bemühen, der Leichtigkeit des Greenfield näher zu kommen?

Ich meine, darum sollte sich der Entwurf bemühen. Die Softwarearchitektur sollte die Machbarkeit der Neuentwicklung als nicht-funktionale Anforderung sehen, ohne die Nachhaltigkeit nicht zu erreichen ist.

Zunächst einmal wird Code natürlich zum Bug Fixing und für neue Anforderungen erweitert. Das geht auch eine ganze Zeit gut. Aber wie lange? Niemand weiß das genau. “Es kommt halt darauf an…” Das ist wahr – nur führt diese weise Haltung gewöhnlich eben zu dem monolithischen Code, der dann nicht mehr mit vertretbarem Aufwand veränderbar ist.

Deshalb glaube ich, dass die Entscheidung, wie lange an Code rumgeschraubt werden sollte, weniger weise, weniger emotional ausfallen sollte, sondern viel pragmatischer und regelhafter.

Hier mein Vorschlag:

  1. Eine Codebasis sollte in Komponenten von max. 10.000 LOC aufgeteilt werden.
  2. Eine Codebasis sollte in Services von max. 60.000 LOC aufgeteilt werden.

Unter Komponenten verstehe ich hier binäre Codeeinheiten mit separatem Kontrakt auf derselben Plattform. Komponenten machen also keinen Aufwand bei der Kommunikation untereinander.

Unter Services hingegen verstehe ich Gruppen von Komponenten mit einem separatem gemeinschaftlichen Kontrakt – allerdings auf u.U. unterschiedlichen Plattformen. Die Kommunikation zwischen Services ist also kein no-brainer mehr.

Die Aufteilung in Komponenten und Services dieser Größe schlage ich vor, weil sich für mich damit Grenzen machbarer Neuentwicklung ergeben.

Komponenten neu entwickeln

Eine Komponente von 10K LOC [1] kann ein Team in einem Monat [2] neu entwickeln, wenn es hart auf hart kommt. Das scheint mir ein überschaubarer Zeitraum aus Sicht des Managements. Eine Entscheidung dafür sollte das Geschäft nicht aufs Spiel setzen.

Eine Software wird von vornherein komponentenorientiert entwickelt. Das Team beobachtet das Wachstum der einzelnen Komponenten. Seien wir ehrlich: dabei kontinuierlich den Komponentencode sauber zu halten, ist eher eine Idealvorstellung. Kleinere Refaktorisierungen werden vorgenommen – aber wenn größere notwendig werden, ist dafür eher keine Zeit. Die werden vertagt… Also kommt der Tag, an dem eine Komponente 10K LOC umfasst und eigentlich so richtig refaktorisiert werden müsste.

An dem Punkt entscheidet sich das Team nun jedoch für eine komplette Neuentwicklung. Statt mühselig Code zu säubern, wird auf der grünen Wiese neu angefangen – im Rahmen des Komponentenkontrakts. Der resultierende Code ist dann nicht nur sauber, sondern rein ;-) Damit meine ich, dass Refaktorisierung nur zu Clean Code zweiter Wahl führt, weil sie sich im Rahmen des Existierende bewegt. Das schränkt die Kreativität ein, das behindert die Innovationsmöglichkeiten. Ein Beginn auf der grünen Wiese hingegen ist frei von solchen Altlasten. Da ist alles erlaubt und denkbar – solange der ursprüngliche Kontrakt eingehalten wird.

Services neu entwickeln

Einen Service von 60K LOC kann ein Team in 6 Monaten neu entwickeln, wenn es hart auf hart kommt. Das ist kein ganz kurzer Zeitraum, das entscheidet man nicht zwischen Tür und Angel – doch es ist immer noch viel überschaubarer als die Neuentwicklung einer kompletten Anwendung von 500.000+ LOC. 6 Monate sind absehbar. 6 Monate sind in vielen Teams ein Releasezyklus oder gar weniger.

Der Trick bei den Services ist nun, dass ihre Neuentwicklung noch mehr Freiheitsgrade bietet. Nicht nur kann die interne Struktur über Komponentengrenzen hinweg rein gemacht werden, nein, es kann sogar ein Plattformwechsel stattfinden. Damit ist die Bedingung für die Möglichkeit kontinuierlicher Innovation geschaffen. Denn die hängt nicht nur an den Fähigkeiten der Entwickler, sondern auch am technologischen Fortschritt.

Wer sich vor 5 Jahren einmal für Java entschieden hat, muss dann nicht bis zum Lebensende (der Software) alles in Java entwickeln. Für jeden Service kann vielmehr immer wieder neu entschieden werden, ob ein Plattformwechsel Vorteile bietet. Eine Entscheidung ist möglich, weil die Servicegrenze in der Architektur überhaupt gezogen wurden – und weil darauf geachtet wurde, die Größe im Rahmen machbarer Neuentwicklung zu halten.

Günstig mag ein Plattformwechsel sein, wenn eine andere Plattform bessere technische Möglichkeiten bietet. Aber der Wechsel kann auch durch Erhaltung der Attraktivität der Codebasis motiviert sein. Plattformen unterliegen Moden. Um dauerhaft Entwickler für die Mitarbeit zu begeistern, mag es angezeigt sein, Plattformmoden zu folgen. Wer will denn heute eine große Cobol-, Fortran-, PHP- oder VB6-Codebasis pflegen? Durchschnittlich attraktiv sind C# oder Java oder Ruby oder Python – dahinter lauern aber schon Scala, Groovy, Clojure, Erlang, F#, JS und andere.

Nur wer “Sollbruchstellen” im Code in Form von Servicegrenzen vorsieht, hat die Chance, technologisch kontinuierlich uptodate zu bleiben.

Heute wird vielen Softwaresystemen eine solche Grenze nachträglich durch den Wunsch nach mobile applications aufgezwungen. Da entsteht plötzlich ein zusätzlicher Service in Form einer App. Da tritt nach Jahren mal wieder – gezwungenermaßen – eine neue Plattform auf den Entwicklungsplan. Da entsteht plötzlich Attraktivität. Leider sind damit aber auch viele Teams überfordert, weil sie jahrelang keine Übung gehabt haben im Plattformwechsel oder gar auch nur im Neuanfang auf einer grünen Wiese.

Fazit

Refaktorisierungen zur Herstellung und Erhaltung evolvierbarer Stukturen sind nicht überflüssig – aber überbewertet, würde ich mal sagen. Viel öfter sollte Neuentwicklung statt Refaktorisierung gedacht werden. Dazu bedarf es aber klarer Grenzen, in denen Neuentwicklung auch machbar ist. Ich habe hier mal zwei solche Grenzen unterschiedlicher Granularität vorgeschlagen. Ob die bei diesen LOC und diesen Zeiträumen verlaufen sollten, sei dahingestellt. Da mag sich jedes Team seine eigenen Grenzen setzen. Dass es jedoch Komponenten- und Servicegrenzen geben sollte, da bin ich sicher. Wir brauchen diese beiden Horizonte.

Der Servicehorizont liegt mir besonders am Herzen. Denn einer der größten Übelstände der Softwareentwicklung scheint mir derzeit die Erstarrung durch Konsolidierung und Homogenisierung. Dem muss entgegen gewirkt werden. Vielfalt muss möglich sein. Denn Vielfalt bedeutet natürliche Lebendigkeit. Nur so ist Innovation kein Kraftakt alle Jubeljahre, sondern jederzeit möglich.

Die Herausforderung für die Softwarearchitektur besteht also darin, die Form der Software so zu entwerfen, dass auch echte Reinigungen machbar sind.

Fußnoten

[1] Die LOC habe ich mal über den Daumen für ein Team von 5 Entwicklern berechnet, das jeden Tag pro Entwickler im Schnitt 100 LOC Produktionscode herstellt. Bei 20 Arbeitstagen/Monat ergibt das 10.000 LOC.

[2] Den Monat Aufwand für eine machbare Neuentwicklung einer Komponente bzw. die 6 Monate für einen Service meine ich nicht wörtlich. Was ein Unternehmen für machbar bei Neuentwicklungen hält, soll es selbst entscheiden. Mir scheint ein Monat für eine Komponente jedoch nicht ganz unrealistisch. Und ein halbes Jahr für die Möglichkeit eines Plattformwechsels, hören sich auch nicht so schlecht an, oder? Wenn möglich, können Services aber natürlich auch kleiner gehalten werden. 3 Monate für eine Neuentwicklung wären besser als 6.

Dienstag, 25. September 2012

Vorsicht Co-Evolution!

In zwei Teams habe ich es in der letzten Woche gesehen: Dass die Entwicklung eines Teams behindert wird durch die Software. Und wenn ich an andere Teams denke, dann kann ich das selbe Muster sehen, glaube ich.

Ein Team beginnt die Entwicklung einer Software. Die Software bekommt dann eine Form, wie sie das Team denken kann und die Projektkultur zulässt. Da die Architektur- oder allgemeiner Entwurfskompetenz leider, leider im Allgemeinen recht schlecht ausgebildet ist, entsteht keine gute Struktur im Sinne von Evolvierbarkeit. Am Anfang ist das allerdings noch nicht schlimm.

Über die Zeit wird das natürlich nicht besser. Der Druck im Projekt steigt tendenziell, was gewöhnlich zu einer Erosion des wie auch immer ausgeprägten Anspruchs an Strukturqualität führt.

Wo die Strukturen eines Softwaresystems nun aber nicht ausgeprägt und auf Evolvierbarkeit angelegt sind, ist es schwer, sich darin zurecht zu finden. Je schlechter sich die Struktur entwickelt und je umfangreicher solche monolithische Codebasis wird, desto schwerer wird es, neue Teammitglieder einzuarbeiten. Immer mehr Domänen-Know-How ist nötig, immer mehr Erfahrung mit der Codebasis. In einem wachsenden, eng verwobenen Ganzen schrumpfen die Inseln der Unabhängigkeit, die sich separat verstehen ließen.

Team und Code stehen mithin in einer Co-Evolution. Das Team beeinflusst die Codestruktur. Und die Codestruktur beeinflusst das Team. Das ist mir jetzt klar geworden.

Dass das Team für die Codestruktur, für die Evolvierbarkeit verantwortlich ist, liegt auf der Hand. Es gibt Ansätze, mit denen sich die Evolvierbarkeit verbessern lässt. Die einzuführen kostet natürlich etwas Mühe. Das geht nicht von heute auf morgen. Aber es ist machbar. Es winkt eine längere Lebensdauer der Software bei größerer Lukrativität.

Und was, wenn man nichts für die Evolvierbarkeit tut? Na, dann wird es halt immer zäher, an ihr etwas zu verändern.

Soweit der übliche Gedankengang. Die Motivation für die Evolvierbarkeit kommt dabei aus der Codebasis. Ob und wie viel man für Evolvierbarkeit tut, hängt vom Schmerzempfinden ab. Tun Änderungen schon weh genug, dass sich die Mühe für mehr Evolvierbarkeit lohnt? In vielen Teams empfindet es das Team so – aber das Management spürt nicht das selbe.

Jetzt ist mir aber ein weiteres Symptom einer nur noch schwer evolvierbaren Codebasis klar geworden. Und dieses Symptom könnte geeignet sein, die Schmerzschwelle des Managements zu überschreiten. Meine Beobachtung ist, dass schwer evolvierbare Softwaresysteme zu schwer evolvierbaren Teams führen.

Ein Team erzeugt (unbewusst) immer Softwarestrukturen, die seinen Anspruch an die eigene Evolvierbarkeit widerspiegeln. Ich denke, auch das ist eine Ausprägung von Conway´s Law. Monolithische Software ist daher nicht nur eine Folge mangelnden Architekturverständnisses, sondern auch mangelnden Anspruchs an die Organisation.

Monolithisch gedachte Organisation führt zu monolithischer Software. Monolithische Software erhält umgekehrt die monolithische Organisation. Denn eine andere kann den Softwaremonolithen nicht weiterentwickeln.

Flexibel gedachte Organisation führt allerdings nicht automatisch zu evolvierbarer Software. Dazu muss die Kompetenz können, evolvierbare Softwarestrukturen herstellen zu können.

Und was ist eine monolithische Organisation? Eine, die auch Konstanz, auf Starre, Rigidität ausgerichtet ist. Dazu gehört das Äußere: Wie lange sind Mitarbeiter in einem Projekt? Wie fixiert ist die Organisationsstruktur? Wie groß sind die Puffer (Geld, Zeit, Motivtion)? Dazu gehört aber auch das Innere: Wie alt ist das Know-How der Mitarbeiter? Wie alt sind Tools und Technologien? Wie homogen ist die Systemlandschaft?

Als Ergebnis der Co-Evolution von Team und Software habe ich nun aktuell in zwei Fällen gesehen, dass das Team sich nicht weiterentwickeln kann. Es ist fixiert auf einen Technologiestack. Es ist fixiert auf eine große, nicht ersetzbare, sondern in ihrer chronischen Krankheit nur noch palliativ pflegbaren Codebasis. Es ist isoliert vom Markt der Softwareentwickler. Denn in so einem Team will niemand arbeiten. Wer bei Sinnen ist, lässt sich nicht auf eine große, monolithische Codebasis eingefroren in der technologischen Zeit von vor 10 Jahren ein. Ich wüsste jedenfalls nicht, wie groß die Anreize sein müssten, damit ich mich dafür erwärmen könnte. Und die faktische Schwierigkeit, Entwickler zu finden, deutet darauf hin, dass es andere genauso sehen. Das Gehalt ist ohnehin begrenzt – Schmerzensgeld gibt es also nicht genug. Und andere Nettigkeiten vom lockerem Umgangston über Konferenzbesuche bis zur Teamparty reizen offensichtlich auch nicht genug. Die fehlenden Anreize “coole Technologien” und “da lässt sich noch richtig was im Code bewegen” können dadurch nicht kompensiert werden.

Zähigkeit im Code drückt sich in Zähigkeit in der Teamentwicklung aus. Dem Management muss man also nichts von Cyclomatischer Komplexität o.ä. erzählen. Es reicht, wenn es sieht, dass die dringend benötigten Leute nicht an Bord kommen. Wenn Codequalität keinen Anreiz darstellt, etwas in der Softwareentwicklung zu verändern, dann hilft vielleicht die Aussicht, dass wenn nicht schon heute so doch über kurz oder lang das Team stillsteht. Denn dann ist weder Wachstum noch Innovation möglich.

Vorsicht also: Es gilt nicht nur, dass ein Team den Code produziert, der ihm entspricht. Darüber hinaus gilt vielmehr, dass Code zu einem Team führt, der ihm entspricht.

Wer eine Vorstellung davon hat, wie sein Team aussehen und sich entwickeln soll über 5, 10, 20 Jahre, der tut gut daran, von ihm entsprechenden Code herstellen zu lassen.

Samstag, 22. September 2012

Slack ist nicht alles

Slack sei das ultimative Tool für Kaizen – soll Arne Rook in einem Vortrag bei Immobilienscout24 in Berlin gesagt haben. Davon berichtet Stefan Haas in seinem Blog-Artikel “Slack is a Culture Shock”.

Dass Slack - also Spielraum, Freiraum, Autonomität - ein sehr wichtiger Aspekt jeder Arbeit ist, finde ich auch. Voll ausgelastete und allemal überlastete Systeme haben schlicht keine Puffer. Wenn es anders kommt als geplant, knirscht es sofort oder explodiert gar. Und ohne Slack gehen Motivation und Innovation zurück. Wer könnte auf die aber verzichten?

image

Als Lektüre zum Thema empfehle ich Spielräume von Tom DeMarco, Drive von Dank Pink und Why Work Sucks and How to Fix It von Ressler und Thompson.

So weit bin ich also ganz dabei. Slack ist wichtig, ja, unverzichtbar. Ohne Slack keine Selbstorganisation. Ohne Slack keine kreative kontinuierliche Verbesserung.

Aber Slack ist nicht alles.

In einem traditionellen Unternehmen über Slack zu sprechen, mag wie Ketzerei klingen: “Menschen sollen mit (mehr) Freiraum besser arbeiten? Nein, nein, das kann nicht sein.” Solcher Widerstand löst dann schnell den Missionarsreflex aus: “Ich bringe euch das Heil mit Slack; wenn ihr das nicht einsehen wollt, dann erst recht.”

Leider geht dabei zweierlei unter:

  • Menschen müssen es durchaus lernen, mit Freiräumen umzugehen.
  • Menschen brauchen ein Ziel.

Slack zunächst einmal zulassen, d.h. Spielraum vor allem in Bezug auf Zeit geben, aber auch beim Geld, bei Entscheidungen, bei der Arbeitsplatzausgestaltung oder –ortswahl, bei der Fortbildung usw. ist nur ein erster Schritt. Im zweiten muss dieser Spielraum auch genutzt werden. Da sehe ich aber immer wieder Zögern oder gar Unfähigkeit.

Zug zum Spielraum

In vielen Unternehmen klagen die Mitarbeiter über einen Mangel an Spielräumen. Das soll natürlich verbessert werden. Aber ich kenne auch eine ganze Reihe von Unternehmen, in denen es Spielräume gibt – die ungenutzt bleiben. Da wird Zeit gewährt für “Forschung” – nur nimmt sie sich niemand. Da steht Geld für Fachliteratur bereit – nur nutzt das niemand, um Bücher oder Zeitschriften Abos zu kaufen. Da wird sogar Fortbildung angeboten, womöglich um weitere offizielle Qualifikationen zu erlangen – aber keiner macht sich auf den Weg.

Paradiesische Zustände führen also nicht automatisch zu Entfaltung und Aufblühen. Warum? Weil Menschen es aus unterschiedlichen Gründen eben lernen müssen, sie zu nutzen. Was das für Gründe sein mögen, darüber will ich hier nicht spekulieren. Und ich will auch nicht in Zweifel ziehen, dass die Spielraumangebote ehrlich gemeint sind. Was nun? Ist doch schade um den schönen Slack, der da ungenutzt bleibt.

Mein Vorschlag: Die Nutzung von Slack sollte immer wieder nachgefragt werden. Man muss an den Menschen ziehen. Pull ist also nicht nur für die Softwareentwicklung ein wichtiges Prinzip. Auch die Mitarbeiterentwicklung braucht es. Immer wieder muss der Slack-Geber klar machen, dass er wünscht, dass der Spielraum genutzt wird.

Fragen wie “Warum hast du den Slack nicht genutzt?” sind da allerdings weniger hilfreich als “Was hast du in deinem Slack gemacht?” Erstere sind nämlich wieder mehr oder weniger subtil drohend/kontrollierend, Letztere hingegen interessiert und in sich wiederum freistellend.

Slack-Nutzung vorleben und Slack-Nutzung interessiert nachfragen, das scheint mir sehr wichtig, um Menschen anzuleiten, mit Freiräumen umzugehen.

  • Was hast du zuletzt “erforscht” in der Zeit, die dir das Unternehmen dafür bietet?
  • An welchem Fach-/Sachbuch liest du gerade, das du dir von dem Literaturbudget des Unternehmens gekauft hast?
  • Was hast du auf der letzten Fortbildung gelernt, die die das Unternehmen ermöglicht hat?
  • Wie hast du deinen Entscheidungsspielraum genutzt, den dir das Unternehmen bietet?

Das Medium, um Zug in dieser Weise auszuüben, ist für mich “die Runde”, also ein ungezwungenes, allerdings fokussiertes Treffen. Zeit für solche Runden ist im Wochenkalender für alle vorzusehen. Das ist Führungsaufgabe. Und da wird dann nachgefragt, ausgetauscht und Slack gelebt.

Zugziel

Zu glauben, dass durch Slack einfach alles besser würde, weil Menschen dann von selbst aufblühen, finde ich naiv. Ich habe ein optimistisches Menschenbild, doch dass “einfach so” nur durch Freiraum alles gut würde, glaube ich nicht. Er ist wichtig, nur eben nicht allein seligmachend.

Vor allem nützt er nichts, solange unklar ist, wofür er gut sein soll. Was tun mit dem ganzen Slack? Wohin die Energie, die dadurch frei wird, richten? Wohin blicken?

Ohne ein klares Ziel geht es nicht. Wer mag, kann auch Vision oder Mission dazu sagen. Oder auch Zweck. Stefan Haas hat das nur nebenbei angesprochen: “In an environment, where the purpose is clear…”, da werde Slack zur Wunderwaffe.

Leider treffe ich so selten auf Teams, Abteilungen, Unternehmen, die ein wirklich klares Ziel haben. Und ich meine 1 Ziel, 1 Mission, 1 Zweck. Wirklich nur 1. Für alle. Und auch noch sonnenklar.

Der Mangel an klarem Ziel scheint mir sogar noch ein größeres Problem zu sein als der Mangel an Slack. Ich sehe sogar die Gefahr, dass Slack als ein weiteres Mittel gesehen werden könnte, die Unklarheit oder Ambivalenz in Bezug auf ein Ziel zu kaschieren. Doch das kann nur nach hinten losgehen – und würde den Slack als Schuldigen anprangern.

Slack ist auch nur ein Mittel, um das 1 Ziel besser zu erreichen. Genauso wie Selbstorganisation oder Scrum oder ein Team Room oder Collective Code Ownership oder was sonst noch in der Softwareentwicklung.

Aber zu welchem Zweck soll dieses Mittel eingesetzt werden? Solange darüber unterschiedliche Meinungen herrschen, ist nicht zu erwarten, dass das Mittel sein Potenzial entfaltet. Dasselbe gilt für Kaizen. Wohin soll denn eine Organisation sich verbessern? Verbesserung ist kein Selbstzweck. Sie muss dem Organisationszweck dienen. Doch welcher ist das?

Ganz, ganz weit oben auf der Prioritätenliste steht für mich deshalb immer wieder die Zweckdefinition. Wofür gibt es das Unternehmen, die Abteiltung, das Team, das Projekt? Ohne glasklaren Zweck entsteht keine Kohärenz in dem, was die vielen Menschen in einer Organisation tun.

Und weil dabei Menschen eine Rolle spielen, kann diese Frage nicht beantwortet werden ohne zu klären, was diese Menschen eigentlich für sich selbst wollen. Der Zweck einer Organisation muss mithin immer im Einklang mit den Bedürfnissen der sie konstituierenden Menschen stehen.

Das, so scheint mir, ist noch ein größerer Kulturschock für viele Manager. Sich klar werden darüber, was man selbst will und was die Organisation will? Und dann auch das in Einklang bringen mit dem, was die Mitarbeiter wollen?

Ja, ich glaube, ohne geht es nicht mehr. Zumindest, wenn man ernsthaft besser werden will. Und wenn man ernsthaft CSR betreiben will. Die beginnt nämlich wie vieles andere auch bei sich selbst, bei den Mitarbeitern des eigenen Unternehmens.

Wenn ich es in einem Satz sagen sollte, dann vielleicht so: Purpose first, slack second.

Sonntag, 9. September 2012

Wieviel Entwurf ist genug Entwurf?

Endlich die Antwort auf die Frage, wie viel Entwurf einer Implementierung vorausgehen sollte.

Immer wieder wird darüber ja gerätselt. Seit agile Softwareentwicklung aufgekommen ist, herrscht Unsicherheit. Ein “Big Design Up-Front” (BDUF) ist zu vermeiden. Heißt das jedoch, dass gar nicht mehr entworfen werden soll? [1]

Wie ist dieses Dilemma aufzulösen?

Ich versuche es mal mit einer Analogie:

Mit dem Entwurf ist es wie mit der Gesunderhaltung: Wenn man nicht aufpasst, dann gibt es kein Halten.

Wann haben Sie genug für Ihre Gesundheit getan? Schon genug Sport getrieben? Schon genügend Vorsorgeuntersuchungen machen lassen? Schon genügend Blicke in den Körper werfen lassen mit Ultraschall, CT usw.? Schon genügend Blutanalysen machen lassen? Schon ausreichend auf die Ernährung geachtet? Und wie ist’s mit der spirituellen Seite der Gesundheit?

Es ist ein Fass ohne Boden, was man alles für die Gesundheit tun kann. (Nicht umsonst gibt es eine wachsende Gesundheitsindustrie.) Also müssen Sie Ihren Aufwand bewusst begrenzen. Sie können nicht den ganzen Tag darauf verwenden.

Aber es wäre auch falsch zu sagen, dass Sie nichts dafür tun müssten, weil sich alles von allein ergibt. Ich hoffe, da sind wir uns einig.

Das bedeutet, Sie tun am besten täglich ein bisschen. Vielleicht sind das nur 15-30 Minuten “Fitnesstraining” irgendeiner Art. Schon ein regelmäßiger forscher Spaziergang soll ja Wunder wirken. Dazu ein bisschen Obacht bei der Ernährung. Und dann noch alle Jahre wieder ein paar Vorsorgeuntersuchungen. Plus etwas Meditation zwischendurch.

Was bedeutet das für den Entwurf in der Softwareentwicklung?

  • Entwerfen Sie regelmäßig
  • Entwerfen Sie in einer Timebox

In der Regelmäßigkeit steckt die Wiederholung. Es gibt also keinen einmaligen Entwurf, der alles vorherbestimmt. Entwerfen ist vielmehr eine kontinuierliche Aufgabe wie Codieren. Bei neuen Erkenntnissen, muss auch neu entworfen bzw. der bisherige Entwurf überprüft und angepasst werden.

Und in der Timebox steckt die Begrenzung, damit Entwurf kein schwarzes Ressourcenloch wird. Es stimmt ja: Bubbles don´t crash. Und mit einem Entwurf ist noch kein Kunde glücklich gemacht worden. Deshalb muss das Entwerfen immer wieder ein Ende haben, um es in auslieferbaren und nützlichen Code zu überführen, zu dem der Kunde Feedback gibt – das wiederum Einfluss auf den Entwurf haben kann.

Ist Ihnen das genug Empfehlung zur Menge des Entwurfs bei der Softwareentwicklung?

Wenn nicht, dann hier konkrete Zahlen. Die werden Sie natürlich provozieren. Die meine ich auch nicht so, dass sie für alle Softwareprojekte heute und in Zukunft gelten. Natürlich nicht. Aber ich meine sie trotzdem ernst, sozusagen als Tendenz und Denkanstoß:

  • Entwerfen Sie am Anfang eines Projektes max. 2 Tage.
  • Entwerfen Sie anschließend jede Woche max. 4 Stunden.
  • Entwerfen Sie außerdem jeden Tag max. 2 Stunden.

Und das jeweils im Team.

Wie klingt das? Konkret, oder? Und nicht gerade BDUF. Sondern agil, weil kontinuierlich. Außerdem steckt da Kommunikation drin, weil das Team zusammen entwirft (Collective Design Ownership). Und es wird Architekturbewusstsein verkörpert.

Sie sehen, die Antwort auf die immer wieder gestellte Frage ist eigentlich ganz einfach. Wieviel Entwurf ist nötig? Im Schnitt sind es ca. 2-3 Stunden pro Tag. That´s it. Nicht die schiere Menge Entwurf ist wichtig, sondern die Regelmäßigkeit.

Fragt sich jetzt nur, was man während dieser Zeit tut? Aber das ist ein Thema für ein anderes Mal.

Fußnoten

[1] Was Entwurf nun genau ist im Vergleich zum Codieren, lasse ich mal dahingestellt. Soviel sollte aber klar sein, dass beim Entwurf eben kein Code geschrieben wird. Das heißt nicht nur, dass die Programmiersprache dabei keine Rolle spielt, sondern auch, dass das Abstraktionsniveau über der Programmiersprache liegt. Flow-Charts und Structogramme sind für mich daher eher keine Entwurfsmittel, sondern nur graphische Programmiersprachen.

Montag, 3. September 2012

Der Fortschritt in Datenflüssen

Datenflüsse sehen einfach aus, sind es auch in gewisser Weise – dennoch haben sie es in sich. Da habe ich neulich gemerkt, als ein Programm, das ich mit Flow-Design locker entworfen hatte, sich dann doch nicht so verhielt, wie erwartet. Es hat funktioniert, das war kein Problem. Aber sein Output war irgendwie, hm, strange.

Deshalb will ich hier einmal die Frage beleuchten, wie denn eigentlich die Verarbeitung in Datenflüssen fortschreitet. Oder besser: Welche unterschiedlichen Fortschrittsweisen kann es denn geben?

Ein Beispielszenario soll die unterschiedlichen Fortschrittsweisen vergleichbar machen. Es ist ganz einfach:

image

Nachrichten, die von einer Funktionseinheit verarbeitet werden, bezeichne ich mit deren Namen: a(x) kommt “von draußen” und wird von a() verarbeitet, b() erzeugt c(y), die von c() verarbeitet wird usw. Nachrichten tragen also das Ziel und nicht die Quelle im Namen. Für das Beispiel reicht das als Identifikation.

Ausgehen von einer Nachricht a() erzeugen die Funktionseinheiten nun diese Nachrichten:

  • a(1)
    • b(11)
      • c(111)
        • e(1111)
        • e(1112)
      • d(111)
      • c(112)
        • e(1121)
    • b(12)
      • d(121)
      • d(122)
      • c(121)
    • b(13)
      • c(131)
        • e(1311)
        • e(1312)
      • d(131)
      • c(132)
        • e(1321)
      • d(132)
      • d(133)
      • c(133)
        • e(1331)
        • e(1332)
      • d(134)

Dieser Baum beschreibt Input-Output-Zusammenhänge, z.B. Input b(12) an b() führt zum Output d(121), d(122) und c(121). Das bedeutet auch, das c(121) nach d(122) erzeugt wird.

Allerdings steckt keine Aussage darüber in dem Baum, wann die Nachrichten verarbeitet werden. Verarbeitet c() die Nachricht c(132) während b() noch weiteren Output generiert oder erst nachdem d(134) ausgegeben wurde?

Darauf geben die Fortschrittsweisen Antwort.

Depth-first

Die naheliegende Vorstellung vom Fortschritt der Verarbeitungsweise des Flows ist wohl, dass jede Nachricht sofort verarbeitet wird. Auf einen Zeitstrahl aufgetragen sieht das so aus:

image

Hier spiegelt sich der Baum direkt wider. So würde die Verarbeitung auch verlaufen, wenn der Datenfluss als Call Stack interpretiert würde, also die Funktionseinheiten sich geschachtelt aufriefen.

imageEine Schachtelung im Sinne einer Servicehierarchie soll ja aber gerade mit Flow-Design vermieden werden. Die Funktionseinheiten a()..e() sollen sich nicht kennen; c() soll nicht von e() abhängen, b() nicht von c() und d(), a() nicht von b().

Alternativ kann diese Fortschrittweise aber auch mit Event-Based Components (EBC) erzielt werden: Wenn a() mit b(11) einen Event feuert, dann arbeitet b() als Event-Handler den erst ab, bevor a() dazu kommt, b(12) auszugeben. Wenn b() währenddessen c(111) erzeugt, arbeitet c() die Nachricht erst ab, bevor b() d(111) erzeugt usw.

Alle Nachrichten werden also erstens sequenziell verarbeitet, d.h. nacheinander und auch noch streng in der Reihenfolge, in der sie erzeugt wurden. Und zweitens werden sie synchron verarbeitet, d.h. während der Verarbeitung wartet die erzeugende Funktionseinheit.

Die synchron-sequenzielle Verarbeitungsweise geht depth-first vor. Die Verarbeitung jeder Nachricht fließt sofort soweit nach rechts durch wie möglich.

Breadth-first

Ist der depth-first Fortschritt der richtige, der beste, der einzig wahre Fortschritt für Datenflüsse? Ich glaube nicht. Er mag der naheliegendste sein, doch das scheint mir für eine Bewertung zu wenig. Denn warum soll es richtiger sein als etwas anderes, Nachrichten sofort in der Tiefe zu verarbeiten und dabei Quellfunktionseinheiten darauf warten zu lassen?

Ich denke, zunächst einmal gleichberechtigt ist die breadth-first Verarbeitung von Nachrichten:

image

Die hervorgehobenen Nachrichten zeigen den Unterschied gegenüber dem depth-first Fortschritt. a() erzeugt die Nachrichten b(11), b(12) und b(13) und die werden zuerst komplett abgearbeitet, bevor deren Output dran kommt. Und auch der wird zuerst abgearbeitet, bevor sein Output dran kommt usw. Die am weitesten rechts stehende Funktionseinheit e() erhält also hier als letzte Arbeit, weil sie am tiefsten im Baum liegt als der der Fluss angesehen werden kann, wenn man ihn um 90°dreht.

Die Verarbeitung eilt damit nicht mehr bei jeder Nachricht zum Ende der Verarbeitung, sondern schreitet sequenziell über alle “Flussarme” hinweg fort. Ich stelle mir das als eine breite Welle vor.

image

Vorteil von depth-first ist, dass nach Anstoß eines Flows schnell erste Ergebnisse am Ende heraustropfen. Das bedeutet aber nicht, dass die Verarbeitung von weit vorangeschritten ist. Bei breadth-first hingegen können Sie sicher sein, dass Arbeitsschritte abgeschlossen sind, wenn ihre Ergebnisse verarbeitet werden.

Das fühlt sich für mich mehr nach Datenfluss an: Ein Input kommt bei einer Funktionseinheit “auf dem Tisch”, wird verarbeitet, dabei wird Output erzeugt – und wenn das alles fertig ist, dann geht es bei der nächsten Funktionseinheit weiter.

Zumindest empfinde ich das als “fluss-mäßiger”, wenn ich die Verarbeitung als synchron denke. Weder findet Verarbeitung auf mehreren Threads innerhalb einer Funktionseinheit statt, noch arbeiten mehrere Funktionseinheiten parallel. Wenn Funktionseinheiten ihre Arbeit abschließen können, bevor ihr Output verarbeitet wird, dann sind sie auch hübsch unabhängig von einander.

Synchrone Verarbeitung ist für mich der default beim Flow-Design. Sowohl Flow-Design Implementierungen mit EBC wie mit der Flow Runtime folgen dem auch – auch wenn sie sich in der synchronen Verarbeitung unterscheiden, wie Sie hier sehen.

Dennoch war ich damit nicht zufrieden. Denn dieser breadth-first Fortschritt hat sich in der eingangs erwähnten Anwendung als etwas merkwürdig angefühlt. Warum?

Solange am Anfang eines Flusses nur eine Nachricht steht, macht breadth-first kein Problem. Dann läuft die große Welle langsam in die Tiefe.

Falls auf a(1) jedoch noch a(2), a(3) usw. folgen und a(i) nicht komplett in der Tiefe verarbeitet ist, bevor a(i+1) eintrifft, kann es zum Stau kommen [1]. Es geht dann zwar alles ganz gerecht zu im Sinne sequenzieller Verarbeitung. Doch solche Gerechtigkeit ist nicht in allen Fällen wünschenswert. Manchmal wäre es gut, wenn Nachrichten einander überholen könnten – zumindest wenn sie in unterschiedlichen Flussarmen fließen. Warum muss ein d(2…) auf ein e(1…) zwangsläufig warten?

Round-robin

Angesichts des merkwürdigen Verhaltens der Anwendung habe ich einen Mittelweg zwischen depth-first und breath-first Verarbeitung gesucht. Eingefallen ist mir eine round-robin Verarbeitung von Nachrichten. Um das zu verstehen, hier die grundsätzliche Arbeitsweise der Flow Runtime:

image

Nachrichten kommen von außen zur Flow Runtime, die sie asynchron verarbeitet. Nachrichten, die bei der Verarbeitung entstehen, fließen entweder hinaus, weil sie Endergebnisse darstellen – oder sie fließen zurück in die Runtime, um von folgenden Funktionseinheiten verarbeitet zu werden. a(i) ist eine Nachricht, die von außen zur Runtime kommt. b(i) usw. sind Nachrichten, die die Runtime quasi an sich selbst schickt.

Jede Nachricht, die bei der Runtime eintrifft, wird auf deren einzigem Thread abgearbeitet; das symbolisiert der Kreis in der Funktionseinheit. Sie ist insofern autonom gegenüber ihrer Umwelt.

Immer wenn eine Funktionseinheit eine Nachricht verarbeitet hat, schaut die Runtime nach, ob weitere Nachrichten zur Verarbeitung anliegen. Die stehen in einer Queue, über die die Runtime in einer Schleife läuft. Der Inhalt dieser Queue sieht für das Beispiel über die Zeit so aus (von links wird angehängt, von rechts entnommen):

image

Das erklärt die breadth-first Verarbeitung: Jede Funktionseinheit wird für eine Nachricht abgearbeitet und stellt ihren Output ans Ende der Queue. Der kommt dann erst dran, wenn der Output vorheriger Funktionseinheiten verarbeitet wurde.

Dieses System habe ich nun aufgebrochen, indem nun jede Funktionseinheit eine eigene Queue besitzt:

image

Über diese vielen Queues läuft nun die Flow Runtime im round-robin Verfahren. Das bedeutet, für jede Nachricht geht sie eine Queue weiter. Es entsteht folgendes Muster:

image

Sie sehen, die Verarbeitung wird Nachricht für Nachricht gleichmäßig über die Funktionseinheiten verteilt. Die Verarbeitungsreihenfolge hat im Grunde nichts mehr mit der Tiefe einer Funktionseinheit im Fluss zu tun. Wo Output erzeugt wird, da wird er auch abgearbeitet.

Käme nun ein a(2) zwischendurch an, so würde es alsbald zur Verarbeitung gebracht, wenn seine Queue an der Reihe ist. Es müsste nicht warten, bis alles, was vorher schon aufgelaufen war, abgearbeitet ist.

Dieses Verfahren scheint mir noch gerechter als breadth-first. Es hat allerdings eine Besonderheit, derer man sich bewusst sein muss: Aufs Ganze betrachtet, erfolgt die Abarbeitung der Nachrichten nicht mehr notwendig streng in Erzeugungsreihenfolge. Nachrichten können einander überholen: e(1112) wird zum Beispiel vor b(13) verarbeitet.

Bei depth-first Fortschritt ist b(13) noch nicht erzeugt, wenn e(1112) abgearbeitet wird. Bei breadth-first wurde b(13) erzeugt und schon abgearbeitet lange vor e(1112). Bei round-robin jedoch steht b(13) noch unverarbeitet in der b()-Queue, während e(1112) schon in Arbeit ist.

Asynchron im Kreis

Auch bei round-robin findet innerhalb der Flow Runtime noch keine Parallelverarbeitung statt. Trotzdem geht es überall voran, sobald die Runtime Gelegenheit hat, eine Nachricht zu verarbeiten. Das ist kein pre-emptive Multitasking, weil ja jede Funktionseinheit so lange an einer Nachricht herumlaborieren darf, wie sie mag. Insgesamt auf den ganzen Fluss gesehen, fühlt es sich dennoch so an, als würde quasi parallel gearbeitet.

Richtig ernst wird das, wenn einzelne Funktionseinheiten asynchron arbeiten. Dann kann der Output während ihrer Laufzeit von der Runtime schon weiterverarbeitet werden.

imageBeispielhaft setze ich mal b() auf asynchrone Verarbeitung, damit Sie sehen, wie sich das Muster dann verändern könnte. Die Länge der Nachrichtenkästen soll nun die Verarbeitungsdauer andeuten.

 

image

Jetzt kommt es natürlich auch darauf an, wann b() Output erzeugt. c(111) wird parallel abgearbeitet und erzeugt e(1111). Währenddessen arbeitet b() weiter! Wann fließt dort aber d(111) heraus? Während c() am Werk ist und e(1111) generiert oder erst später? Denn danach richtet sich, ob auf c() unmittelbar e() folgt wie im Bild oder zuerst d().

Fazit

Die Verarbeitung in Datenflüssen ist anders als die in Servicehierarchien. Anders, doch deshalb nicht schlechter. Sie müssen sich umgewöhnen. Das mag schwer fallen, weil die “Stack-Denke” so tief in uns allen drin steckt. Doch ich meine immer noch, dass sich das lohnt.

Denn anders bedeutet hier chancenreich. So strange das Verhalten der eingangs erwähnten Anwendung war, es hat mich wieder beeindruckt, wie leicht die Flow-Operationen zu testen waren, weil sie unabhängig von einander sind. Und es war ganz leicht, individuell für jede zu entscheiden, ob sie synchron oder asynchron laufen soll.

Und letztlich finde ich es auch gut, überhaupt die Wahl zu haben zwischen Verarbeitungsweisen. Die könnte eine Runtime womöglich sogar zur Auswahl anbieten. Sogar den depth-first Fortschritt hatte ich schon einmal implementiert.

Fußnoten

[1] Dass weitere Nachrichten vor Abarbeitung beim Fluss eintreffen, setzt natürlich bei aller Synchronizität seiner Funktionseinheiten voraus, dass der Fluss als Ganzes gegenüber seiner Umwelt asynchron arbeitet. Das ist bei der Flow Runtime der Fall.