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.