Montag, 31. Dezember 2012

Prinzip der gegenseitigen Nichtbeachtung

Mehrschichtigkeit ist immer noch tief in den Köpfen verankert. Und wenn nicht in den Köpfen, dann im Code. Übel daran ist nicht nur, dass Mehrschichtigkeit viele Fragen weder beantwortet noch stellt, sondern dass damit Abhängigkeiten geschaffen werden. Code der einen Schicht hängt irgendwie von Code der anderen ab. Die eine Schicht braucht die andere.

Aber welche Schicht sollte welche brauchen? Darüber hat Jens Schauder in einem Blogartikel nachgedacht. Den habe ich gelesen – und dann habe ich gestutzt. Jens erkennt zurecht Abhängigkeiten als Problem – doch dann sieht er eine Lösung nur darin, dass er sie umdreht. Aus dem üblichen

image

wird bei ihm nur

image

Die Domäne soll Interfaces definieren, die UI/Persistence dann nutzen bzw. implementieren [1].

Das empfinde ich als keinen großen Schritt nach vorne. Klar, irgendwie wird jetzt die Domäne etwas mehr betont. Das ist nicht schlecht. Aber das grundsätzliche Problem der Abhängigkeiten wird nicht angegangen.

Was ist dieses grundsätzliche Problem der Abhängigkeiten? Naja, dass das Abhängige das Unabhängige braucht, um etwas leisten zu können. Wer vom Alkohol abhängig ist, der ist nicht arbeitsfähig ohne. Und wo ein UI abhängig ist von der Domain, da kann das UI nichts tun, solange es keine Domain-Schicht in der Hand hat.

Auch das hochgelobte Prinzip IoC macht da keinen großen Unterschied. Aus statischen Abhängigkeiten werden damit nur dynamische. Zur Laufzeit – also auch bei Tests – muss die Abhängigkeit erfüllt werden. Attrappen sind insofern weniger Lösung als Problem, dito DI-Container. Beide kaschieren die fundamental starre Verbindung, die Abhängigkeit selbst.

Ich sehe dafür auch keine Entsprechung in der sonstigen Technik. Wenn ich UI+Domain+Persistence mal zusammen als Application bezeichne, dann könnte die Analogie doch ein Motor sein, oder?

image

Eine Application besteht aus Teilen, ein Motor auch. Nur im Zusammenspiel aller Teile ergibt sich die gewünschte Funktion des Ganzen.

Jetzt aber der Unterschied, der entscheidende:

In der Application braucht und kennt UI die Domain, und bei Jens braucht und kennt Persistence ebenfalls Domain. UI wie Persistence sind unvollständig ohne Domain. Oder allgemeiner: das Abhängige ist unvollständig ohne das Unabhängige.

Im Motor hingegen… da weiß eine Zündkerze nichts vom Zylinder. Ein Zylinder weiß nichts von einer Zündkerze. Die Kurbelwelle weiß nichts von Kolben und umgekehrt.

In der materiellen Technik gibt es keine Abhängigkeiten wie in der Software.

In der materiellen Technik gibt es nur Teile, die für sich genommen eine Form und eine Funktionsweise haben. Sie folgen dem, was ich mal

Das Prinzip der gegenseitigen Nichtbeachtung
(principle of mutual oblivion (PoMO))

nennen will.

Eine Zündkerze beachtet kein anderes Teil. Ein Kolben, eine Kurbelwelle, ein Keilriemen auch nicht. Alle sind für sich geformt und ausgeprägt.

Dass man sie zusammenfassen kann zu etwas Größerem: klar. Dieses Größere definiert einen Kontext und fordert, dass die Teile zusammenpassen. Es ist die Existenzberechtigung für die Teile. Ohne das Größere müssten wir gar nicht drüber reden.

Aber das große Ganze kann eben in unterschiedlicher Weise seine Teile formen. Es kann das wie beim Motor tun. Das bedeutet, die Teile beachten einander nicht – sondern dienen dem Ganzen, das sie integriert.

Oder das Ganze kann es tun wie in der Software üblich. Das bedeutet, es macht die Teile abhängig von einander.

Konkret steckt diese Abhängigkeit in zwei unheiligen Verbindungen:

  • Es wird Input mit Output verbunden: Request/Response ist der vorherrschende Kommunikationsstil zwischen den Softwareteilen.
  • Es werden mehrere Funktionalitäten im selben Interfaces zusammengefasst.

Die erste unheilige Verbindung macht den Übergang von synchroner zu asynchroner und damit zu verteilter Kommunikation schwierig. Außerdem führt sie zu einem unbeschränktem Wachstum von Methoden.

Die zweite unheilige Verbindung macht die Umkonfiguration von Funktionalität schwierig – trotz allem ISP- und IoC-Aufwand, den man treiben mag.

Beim PoMO hingegen ist das alles kein Thema mehr. Denn beim PoMO gibt es keine Abhängigkeiten mehr zwischen “Kooperationspartnern”. Funktionseinheiten kennen einander nicht mehr. “Dienstleister” UI kennt keinen “Dienstleiste” Domain oder umgekehrt, dito “Dienstleister” Persistence.

Wenn die Anwendungsarchitektur dem PoMO folgt, dann sieht sie zunächst so aus:

image

Welch Wonne! Keine Abhängigkeiten, kein Testattrappendrama!

Aber natürlich sind die “Schichten” jetzt gänzlich unverbunden. Da entsteht noch keine Gesamtfunktionalität im Sinne der Application.

Dafür tut nun die Application etwas. Und das tut sie ganz offiziell. In der Abhängigkeitsarchitektur war das nicht so offensichtlich. Aber bei PoMO geht es nun nicht mehr anders. Insofern führt PoMO zu einer fundamentalen Separation of Concerns:

UI, Domain und Persistence gehören zum Concern (Aspekt) “Operation”. Und die Application vertritt den Aspekt “Integration”. Die Application verbindet die Teile so, dass sie zusammen etwas sinniges tun. Diese Verbindung könnte so aussehen:

image

Grün integriert, schwarz operiert. Teile wissen nichts davon, ob und wie sie verbunden sind. Und die integrierende Funktionalität weiß nichts davon, wie die Teile funktionieren. Sie ist ausschließlich von deren Form abhängig.

Und genau das macht den Unterschied aus zwischen einer PoMO-Architektur und dem Üblichen: Bei den üblichen Abhängigkeiten ist der Abhängige nicht nur von der Form des Unabhängigen abhängig, also dass zum Beispiel B eine Funktion mit einer Signatur wie Func<string,int> implementiert. Nein, A ist auch vom Namen der Funktion abhängig und damit von ihrer Semantik, dem, was sie leistet.

Das klingt ganz natürlich. Aber es ist eine Last. Das ist eine doppelte Abhängigkeit: eine von Form und Inhalt. Und dann obendrein noch die Abhängigkeit davon, dass diese Funktion auf einem bestimmten Interface implementiert ist und auch noch, dass sie ein Ergebnis zurückliefert. Puh… das sind ganz schön viele Abhängigkeiten, oder?

Bei PoMO jedoch, da kennt ein A kein B. Und es gibt keine Rückgabewerte; Daten fließen unidirektional. Und ob Methoden von der einen oder einer anderen Funktionseinheit angeboten werden, ist der integrierenden Funktionalität egal. Sie bringt “Angebote” einer Funktionseinheit mit “Nachfragen” anderer zusammen. Das ist alles.

Integration interessiert sich somit nur für Semantik. Das muss sie ja auch, denn sie definiert ein höheres Abstraktionsniveau.

PoMO löst damit mehrere Probleme von Abhängigkeiten. Manche dadurch, dass Abhängigkeiten abgeschafft werden. Manche dadurch, dass Abhängigkeiten fokussiert werden im Sinne von SoC und SRP.

Das scheint mir the way to go, um mal raus aus dem Mehrschichtigkeitssumpf zu kommen.

Fußnoten

[1] Am Anfang seines Artikels stellt Jens die Frage, was denn Teams überhaupt mit einem Pfeil “–>” in einem Diagramm meinen. Ob da immer Klarheit herrsche? Das kenne ich auch: Es wird bildlich entworfen, Kästchen, Kreise, Pfeile, Linien werden gemalt… Aber was bedeuten die? Zwei Entwickler, drei Meinungen ;-)

Für mich gibt es zwei zentrale Symbole für Beziehungen zwischen Funktionseinheiten:

  • Pfeil, A –> B
  • Lolli, A –* B

Der Lolli bezeichnet eine Abhängigkeitsbeziehung: A ist abhängig von B, d.h. A braucht B in irgendeiner Weise. A hat zur Laufzeit ein B in der Hand.

In den meisten UML-Diagrammen und auch bei Jens steht für diese Art der Beziehung allerdings der Pfeil. Der ist mir dafür aber verschwendet, weil bei ihm gar nicht recht klar wird, was die Pfeilspitze aussagt. Ein Pfeil ist anders als eine Linie asymmetrisch. Das ist schon passend zur Abhängigkeit. Abhängiges und Unabhängiges haben unterschiedliche Rollen. Doch die Pfeilspitze trägt für mich dabei zuviel Gewicht. Sie ist verschwendet bei einer simplen Asymmetrie.

Bei einem Datenfluss hingegen kommt die Pfeilspitze viel besser zum Einsatz. Da zeigt sie deutlich die Flussrichtung. Das entspricht ihrer üblichen Bedeutung von “Bewegungsrichtung”. In Diagramme stehen Pfeile für mich also dort, wo Daten fließen, z.B. von Funktionseinheit A zu Funktionseinheit B.

Solcher Datenfluss ist auch asymmetrisch; Datenproduzent und Datenkonsument haben unterschiedliche Rollen. Und Datenfluss ist unidirektional.

Freitag, 28. Dezember 2012

Der Feind des Besseren ist der heutige Erfolg

Eigentlich soll ja alles immer besser werden, oder? Die Prozessoren der nächsten Jahre sollen auf die eine oder andere Weise besser sein als die heutigen – so wie die heutigen besser sind als die der 1990er und die wiederum besser als die der 1980er usw. Auch unsere Autos sollen immer besser werden – so wie sie seit mehr 125 Jahren immer besser geworden sind. Das selbe gilt für Fernseher, Mixer und Solarzellen.

Aber nicht nur solch handfeste Dinge sollen besser werden. Auch bei Dienstleistungen mögen wir es, wenn die sich zum Besseren verändern. Wer möchte heute noch einen Gesundheitscheck wie vor 20 oder gar 100 Jahren? Wo wären wir, wenn die Flugabfertigung immer noch wie vor 60 Jahren abliefe? Selbst beim Friseur schätzen wir den Fortschritt.

Dito bei der Arbeitsweise unserer Unternehmen, oder? Und genauso bei der Software. Auch die soll besser werden, vom Betriebssystem bis zum Smartphone Game. Selbstverständlich nehmen wir die Software, die wir selbst entwickeln, da nicht aus.

Ja, das ist eine Welt, wie sie sein soll: alles wird immer besser und besser.

Was ist “besser”?

“Besser” ist dabei ein relativer Begriff in zweierlei Hinsicht. Zum einen steckt in “besser” ein Vergleich zum Bisherigen. Zum anderen gilt “besser” genauso wie das vorgelagerte “gut” nur in einem Kontext, d.h. in Bezug auf eine Umwelt – und darin in Bezug auf einen Wertmaßstab. Es geht mithin um einen Unterschied, der einen Unterschied macht.

Ein Fernseher mit Fernbedienung ist nicht einfach besser als einer ohne Fernbedienung. Aber die Fernbedienung ist schon mal ein Unterschied zwischen beiden.

Doch wenn dieser Unterschied #1 dazu führt, dass Käufer eine andere Entscheidung treffen (Unterschied #2), d.h. die Fernbedienung sie motiviert, den Fernseher mit Fernbedienung zu kaufen statt den ohne Fernbedienung… dann beurteilen wir den Fernseher mit Fernbedienung als besser im Vergleich zu dem ohne.

Der Wertmaßstab ist in diesem Fall zum Beispiel der Umsatz, der mit Fernsehern gemacht wird. Der Fernseher mit Fernbedienung ist besser in Bezug auf den Umsatz – aber vielleicht schlechter in Bezug auf den Kalorienverbrauch der Fernsehzuschauer (Wertmaßstab Gesundheit) oder den Anfall von Elektronikschrott (Wertmaßstab Nachhaltigkeit).

Wenn wir nun in die Welt schauen, dann – so glaube ich – stimmen wir überein, dass vieles über die Zeit tatsächlich besser und besser geworden ist. Der Spruch “Früher war alles besser.” stimmt also nicht pauschal, auch wenn uns der heutige Stand mancher Entwicklung frustriert [1].

Bedingungen für die Möglichkeit zur Verbesserung

Verbesserungen in allen Bereichen unseres Lebens scheinen so natürlich – aber wie kommt es eigentlich dazu? Wie funktioniert diese Evolution unserer Hilfsmittel und Dienstleistungen?

Mir scheint es zwei zentrale Bedingungen dafür zu geben:

  1. Es müssen überhaupt Ideen für Veränderungen produziert werden. Was könnte der Unterschied #1 zum Ist-Zustand sein? Dafür braucht es ein Milieu, das motiviert, solche Ideen zu produzieren. Sensibilität für Unzufriedenheit, Kreativität, Mut sind nötig.
  2. Die Ideen für Veränderungen müssen implementiert werden können, um zu sehen, ob es zu einem positiven Unterschied #2 in Bezug auf einen für Produzenten wie Konsumenten relevanten Wertmaßstab kommt.

Die Natur als Beispiel

Die Natur macht uns das sehr schön seit Milliarden von Jahren vor. Sie erfüllt beide Bedingungen: die erste durch genetische wie epigenetische Veränderungen, die zweite durch Reproduktion.

Krankheit und Tod zeigen auf, wo Lebewesen noch nicht zufriedenstellend an die Umwelt angepasst sind. Mutationen sind äußerst kreative Veränderungen – oft jenseits unseres Vorstellungsvermögens; wir hätten es nicht besser machen können. Und Mut hat die Natur genügend, da sie durch keine Glaubenssätze beschränkt ist.

So werden Lebewesen von Generation zu Generation besser und besser bzw. erhalten sich die Güte ihrer Überlebensfähigkeit.

Der Markt als Beispiel

Auch der Markt macht uns vor, wie die Bedingungen erfüllt werden können. Konkurrierende Anbieter generieren kontinuierlich Ideen für Veränderungen, die sie über neue Modelle an den Markt bringen.

Vor allem im Bereich des Materiellen funktioniert der Markt damit wie die Natur. Besser wird es durch Generationen von Modellen. Ein Fernseher hat eine Lebensdauer, ebenso ein Flugzeug und ein Röntgengerät. Spätestens nach dieser Lebensdauer kann es durch etwas Besseres ersetzt werden.

Tod als Herz der Evolution

Natur und Markt gleichen sich. In ihnen schlägt das selbe Herz der Evolution: der Tod. Das in der Vergangenheit Gute macht durch seinen Tod ganz automatisch Platz für das inzwischen besser gewordene.

Das Ganze - die Summe aller Lebewesen, d.h. das Ökosystem, bzw. die Summe aller Modelle, d.h. der Markt – erhält seine eigene übergeordnete Lebensfähigkeit also dadurch, dass seine Teile in einem ständigen Prozess von Werden und Sterben stehen.

Allemal gilt das für seine kleinsten Teile, aber durchaus für Subsysteme größerer Granularität. Die Lebensdauer bzw. die Dauer des Verbleibs im System wächst dabei tendenziell mit der Größe eines Subsystems.

Perverse Unsterblichkeit

Lebewesen sterben. Das gehört zu ihrer Definition. Misslich ist es für uns Menschen, dass wir von unserer Sterblichkeit wissen. Sie macht uns nur allzu oft Angst. Da helfen alle Maßnahmen der Gesundheitsvorsorge und Reparaturmedizin auch nicht. Nach rund 100 Jahren machen wir Platz für die Nachgeborenen.

Materielle Produkte haben auch nur eine überschaubare “Lebenserwartung”. Selbst bei bester Pflege fordern Physik (z.B. durch Reibung oder thermische Kräfte) oder Chemie (z.B. durch Oxidation) früher oder später ihren Tribut. Und wo das dem Hersteller zu lange dauert, da setzt er auf planned obsolescence.

Aber wie ist es bei Unternehmen? Wie ist es bei Software?

Bei immateriellen Hilfsmitteln [2] herrscht ein anderes Ideal: sie sollen unbegrenzt lange existieren. Niemand gründet ein Unternehmen mit einer Idee von dessen Lebenserwartung [3]. Und niemand schreibt Software mit einem Verfallsdatum [4].

Ist das nicht pervers, d.h. anormal, verkehrt, unnatürlich – für etwas, das ständig besser werden soll?

Markt und Natur, also die umfassenden Systeme, sollen nicht besser werden, sondern beherbergen das, was immer besser werden soll, um sich als Ganzes zu erhalten [5]. Die umfassenden Systeme kennen keinen auf gleicher Ebene existierenden Konsumenten. Es sind autopoietische Systeme, sie erhalten sich selbst. Wenn sie dienen, dann eher nach innen, ihren Konstituenten gegenüber.

Für mich sieht es so aus, als würden wir Unternehmen sowie Software heute in einem Widerspruch herstellen bzw. betreiben:

Wir strukturieren sie einerseits wie materielle Produktmodelle, d.h. vergleichsweise starr. Das haben wir ja auch über Jahrhunderte gelernt. Sie erfüllen zu einem Zeitpunkt einen Zweck, sind temporär also gut. Eine Verbesserung können wir erst mit einem neuen Modell erwarten.

Andererseits denken wir sie unsterblich, d.h. als übermaterielles System, das es durch ständige Veränderung im Inneren zu erhalten gilt.

Ja, was denn nun? Innen starres Hilfsmittel, das über Generationen evolviert – oder innen flexibles System mit unbegrenzter Lebenserwartung?

Da passt doch irgendetwas nicht, oder?

Hinderlicher Erfolg des Immateriellen

Die Quelle für diesen Widerspruch sehe ich in einer scheinbaren Wahl, die wir haben. Immaterielles wie ein Unternehmen oder Software unterliegt keinem natürlichen Verfall – also können wir die Lebenserwartung selbst bestimmen. Und da bestimmen wir mal, dass die unendlich sein soll. Geht doch. Und ist bestimmt unterm Strich billiger, als immer wieder etwas Neues von Grund auf herzustellen, oder?

Dass sowohl Unternehmen wie Software dann doch nicht ewig existieren, ist eine andere Sache. Shit happens. Potenziell könnten sie es jedoch. Darum gilt es, das anzustreben.

Der Erfolg zu einem bestimmten Zeitpunkt scheint in die Zukunft verlängerbar. Ein Auto funktioniert ja auch mit guter Wartung nicht nur heute, sondern auch noch in Jahrzehnten.

In Wirklichkeit steckt hier jedoch das Missverständnis. Erfolgreich heute – also in Bezug auf heutige Umwelt und Wertmaßstäbe gute Systeme – bedeutet nicht, erfolgreich morgen.

Umwelt und Wertmaßstäbe für Unternehmen und Software sind solchem Wandel unterworfen, dass in ihnen ständig umgebaut werden muss. Ein treffendes Bild scheint mir das einer stehenden Welle: die äußere Form bleibt trotz allem Fluss im Inneren erhalten.

Solange Erfolg mit einem Ist-Zustand an Strukturelementen und Beziehungen gleichgesetzt wird, funktioniert das aber nicht. Erfolg muss davon abgekoppelt werden. Welche Konstituenten ein System ausmachen, ist für die langfristige Existenz des Systems als Ganzem unwichtig. Es gibt nichts per se Erhaltenswertes. Es gibt immer nur einen Systemaufbau, der angesichts einer Umwelt und eines Wertmaßstabs gut genug ist – und sich leicht an Veränderungen anpassen lässt.

Anpassungsfähigkeit ist die einzige erhaltenswerte Eigenschaft. Alles andere steht ständig zur Disposition. Es gibt kein einfach so extrapolierbares Erfolgsrezept im Konkreten, sondern um im Abstrakten.

Praktische Konsequenz

Vielleicht sind unsterbliche Unternehmen und Softwareprodukte wünschenswert. Derzeit sehe ich jedoch nur Versuche zu solcher Unsterblichkeit unter zunehmenden Schmerzen. Wir wissen einfach nicht so richtig, wie wir Unsterbliches aufbauen müssen. Oder wenn wir es wissen, dann setzen wir dieses Wissen nicht systematisch um.

Was also tun? Wir müssen raus aus dem Widerspruch.

Die Blaue Pille nehmen würde bedeuten, bei den aktuellen Konstruktionsweisen zu bleiben. Dann sollten wir aber zumindest die Lebenserwartung von Unternehmen und Software von vornherein begrenzen. “Wir gründen unseren online Shop mit einer Lebenserwartung von 8-12 Jahren.”, “Wir legen die Software auf eine Nutzungsdauer von 3-5 Jahren aus.” – und danach ist Schluss. Ende. Tot. Unternehmen und Software werden dann ersetzt.

Da diese Endlichkeit allen von Anfang an klar ist, kann vorgesorgt werden. Ableger können rechtzeitig gezeugt werden, die neue Erkenntnisse umsetzen – wozu das schon länger Existierende nicht wirklich fähig ist. Die Ausgründung, die Neuentwicklung sind dann keine Überraschungen, sondern ganz natürlich.

Erfolg heute wäre dann kein Ballast mehr für das Bessere. Erfolg wäre nicht narzistisch, sondern bescheiden.

Die Rote Pille nehmen würde hingegen bedeuten, die aktuelle Konstruktionsweise umzukrempeln. Das scheint mir schwieriger – aber nicht unmöglich. Wir müssten lernen, Unternehmen und Software intern eher so zu strukturieren wie ein Ökosystem. Das würde bedeuten, ihre Konstituenten würden evolvieren wie der Automarkt oder die Arten auf einer einsamen Insel.

Hier würde Erfolg per definitionem nicht dem Besserem im Wege stehen. Anpassung wäre in die Systeme eingebaut.

Blaue Pille oder Rote Pille – oder ein Pillencocktail? Ich weiß nicht. Nur sollte es nicht so weitergehen wie bisher. “Aber wir haben doch die letzten 5 Jahren tolle Umsätze gemacht, wir hatten Erfolg… Warum sollten wir jetzt etwas verändern?” darf kein Argument mehr sein, mit dem Veränderungen geblockt werden.

Wir müssen die Bedingungen für die Evolution herstellen: Ideen müssen nicht nur her, sondern auch in bunter Vielfalt implementierbar sein. Evolution ist verschwenderisch. Ressourcenverbrauch auf momentane Bedürfnisse hin zu optimieren, steht längerfristigem Überleben im Wege.

Der wahre Feind der Verbesserung lauert also ständig schon im eigenen System: es ist der heutige Erfolg. Diese Situation müssen wir systematisch bekämpfen.

Fußnoten

[1] Warum wird der Spruch dennoch immer wieder geäußert. Je älter Menschen werden, desto eher scheinen sie ihn auszusprechen. Ich denke, das liegt daran, dass es mit dem Alter erstens überhaupt möglich wird, einen Vergleich zwischen früher und heute anzustellen. Und zweitens hat der Spruch weniger mit den Eigenschaften eines Gegenstandes oder einer Dienstleistung zu tun, als vielmehr mit dem Wertmaßstab.

Es kann sein, dass der vergleichsweise unverändert ist; er hat sich nicht auf die Co-Evolution von Angeboten und Umwelt eingelassen.

Oder es kann sein, dass er sich einfach nur in eine andere Richtung entwickelt hat. Mit zunehmendem Alter verschieben sich Bedürfnisse eben.

[2] Unternehmen subsummiere ich hier mal unter dem Begriff Hilfsmittel. Gegenüber seinen Kunden, ist ein Unternehmen natürlich kein Hilfsmittel, sondern produziert welche. Aber Eigentümer und Angestellte sehen Unternehmen natürlich als Hilfsmittel zur Existenzsicherung.

[3] Ausnahmen gibt es natürlich: Filmproduktionsfirmen haben u.U. eine bewusst auf die Herstellungsphase begrenzte Lebensdauer. Auch Fonds können so organisiert sein, dass ihre Firma nur für die Laufzeit existiert. Oder “Blasenfirmen”, die nur solange irgendwie lebensfähig und vor allem vielversprechend sein müssen, bis sie jemand kauft.

[4] Von Software, die durch absehbare Gesetzesänderungen nur begrenzt relavant ist, sehe ich einmal ab. Meist betreffen solche Termine der Umwelt ja auch nur Teile einer Software.

[5] Erhaltung kann natürlich auch als eine Form der Verbesserung angesehen werden – aber auf einer höheren Ebene. Wenn die Natur heute aber anders aussieht als vor 100 Millionen Jahren, für wen macht das einen Unterschied #2?

Freitag, 21. Dezember 2012

Verteilte Weihnacht mit der Flow Runtime

Für die Freunde des Flow-Designs hier zu Weihnachten eine kleine Überraschung: Flows verteilen mit der Flow Runtime einfach gemacht. Als Beispiel soll Bestellung und "Aufbau eines Weihnachtsbaums dienen.

Wer kennt das nicht: Vor lauter Weihnachtsfeier- und Jahresendprojektstress wird der Kauf eines Weihnachtsbaums bis zum letzten Tag verschoben. Und dann muss man sehen, was übrig bleibt. Das macht die Familie gar nicht glücklich.

Aber jetzt gibt es eine Weihnachtsbaumanwendung für den Windows Desktop :-) Mit der kann man sich einen Weihnachtsbaum in verschiedenen Größen bestellen. So siehts aus:

image

Mit dem Slider auf der rechten Seite stellt man die gewünschte Zahl der Äste ein – und schon wird der Weihnachtsbaum hergestellt und “aufgebaut”. Naja… nur auf dem Bildschirm. Aber immerhin. Bevor es gar keinen gibt… ;-)

Die Herstellung des Weihnachtsbaums übernimmt eine TreeFactory:

image

In ihr gibt es drei “Herstellungsstationen”, die ein Baum nacheinander durchläuft. Der Flow dafür sieht so aus:

image

Zuerst werden nur die Äste in Form von *-Reihen erzeugt, dann werden diese Reihen so eingerückt, dass das charakteristische Dreieck entsteht, und schließlich kommt darunter ein kleiner I-Stamm.

Lokale DIY-Herstellung

In der ersten Ausbaustufe wird der Weihnachtsbaum lokal hergestellt, sozusagen xmas tree fabbing. Die TreeFactory läuft in der Desktop-Anwendung im Rahmen eines allgemeineren “Weihnachtsfabrik”-Flusses. Weihnachtsbäume sind ja auch nur eine, was man zum Fest herstellen könnte:

image

Die Anwendung bestellt beim Start sogleich einen Baum mit 2 Ästen - und danach immer wieder, wenn der Schieberegler bewegt wird. Und das alles from the comfort or your living room :-)

image

Auslagerung der Produktion

Das Weihnachtsbaum fabbing ist hübsch modern. Aber leider kommt dabei nur ein Weihnachtsbaum aus “Plastik” heraus. Ein natürlicher Weihnachtsbaum wäre schöner, oder? Oder einer, bei dem man aus verschiedenen Materialien wählen kann? Jedenfalls scheint eine Auslagerung der Weihnachtsbaumproduktion Vorteile zu bieten.

Aus der lokalen Weihnachtsbaumproduktion soll deshalb eine entfernte werden. Allerdings ohne große “Off-Shoring-Schmerzen”. Wie kann das mit Flows gehen?

Aus der Desktop-Anwendung wird ein Desktop-Client und die Produktion wandert in einen Server.

Dazu wird der Flow xmasfactory in der Anwendung durch eine generische so genannte Stand-In Operation ersetzt:

image

Die sieht nach außen immer genauso aus wie die Funktionseinheit, die sie ersetzt – intern jedoch leitet sie Input über ein Kommunikationsmedium weiter an die entfernte Funktionseinheit und leitet deren Output zurück in den lokalen Flow. Im Beispiel geschieht das mit WCF als API und TCP als Medium.

Der Code ändert sich dadurch wie folgt:

image

Die WcfStandInOperation bekommt eine lokale Adresse, auf der sie entfernten Output empfängt, und eine entfernte Adresse für die “off-shore” xmasfactory, an die sie Input sendet.

In der Flow-Definition wird der bisherigen lokalen xmasfactory der Name der Stand-In Operation mit “#” voran gesetzt. Dadurch wird die Stand-In Operation adressiert, behält über die Instanzkennung [1] nach dem “#” aber die Information, welche Funktionseinheit sie ersetzt.

Die WcfStandInOperation kommt aus der Assembly npantarhei.distribution.wcf, die der Client nun referenzieren muss. Die ist nun Teil des NPantaRhei Repositorys.

image

Soweit der Client. Er wird im Grunde einfacher. Die Servicefunktionalität entfällt und wird ersetzt durch ein Stand-In.

Aber keine Sorge, der Server, in dem sie neu aufgehängt wird, ist leicht herzustellen. Der enthält die extrahierte Funktionseinheit nun allein auf oberster Ebene in einem Flow:

image

Daran ist keine Veränderung vorzunehmen; sie muss sogar so aussehen, wie zuvor, weil sie sonst nicht durch das Stand-In korrekt repräsentiert würde.

Allerdings wird die xmasfactory nun direkt über Flow-Ports angesteuert. Wie sollen die heißen, damit sie automatisch bestimmt werden können? Aus xmasfactory.Build_tree wird .Build_tree@xmasfactory.

Dieser kleine Trick ist nötig, weil in einer Portbeschreibung nur ein Punkt vor dem Portnamen stehen soll. Die Beschreibung .xmasfactory.Build_tree wäre daher ungültig. Also wird der Portname nach vorne gezogen und der Funktionseinheitsname mit “@” dahinter gesetzt. Das ist nicht besonders schön, funktioniert aber erstmal. So können aus der Instanzkennung und dem Portnamen vom Stand-In die serverseitigen Portbeschreibungen bestimmt werden und umgekehrt.

Damit Requests an den Server zu Input für den Flow werden und Output als Response zum Stand-In zurückfließen kann, wird die Flow Runtime in einem so genannten Operation Host gehalten:

image

Der Operation Host empfängt über einen transportmediumspezifischen Transceiver Daten als Input für Flows in der Runtime; und Output aus Wurzelports der Runtime leitet er über den Transceiver dorthin zurück, von wo der zugehörige Input gekommen ist. Den Bezug stellt er über eine out-of-band Correlation-Id her, die er dem Input bei Empfang aufprägt und die über den ganzen Flow erhalten bleibt.

Was sich vielleicht kompliziert anhört, ist in der Codepraxis aber simpel:

image

In der Flow-Definition taucht die xmasfactory wie bekannt aus der lokalen Anwendung 1:1 auf. Ansprechbar wird sie gemacht durch die globalen Port des Wurzel-Flows wie oben beschrieben.

Ansonsten kommt nur der WcfOperationHost dazu, der die Flow Runtime aufnimmt und eine Adresse erhält, auf der er angerufen werden kann.

image

Fertig!

image

Mehr ist nicht zu tun, um eine Funktionseinheit entfernt zu betreiben. Der Flow im Client merkt davon im Grunde nichts. Und die entfernte Funktionseinheit auch nicht. Sie müssen also nicht immer wieder neue Servicedefinitionen vornehmen wie bei WCF. Mit denselben Bausteinen – Stand-In Operation und Operation Host – können Sie jede Funktionseinheit verteilen. Nur die Daten, die über das Transportmedium als Input dorthin und als Output von dort zurück fließen, müssen [Serializable] sein.

Fazit

Die Verteilung von Flows sollte so einfach wie möglich sein. Ich glaube, das ist nun ganz grundsätzlich der Fall. Die WCF-Bausteine sind dabei nur erste einfache Adapter für ein Transportmedium als Beispiele dafür, wie es geht. Nähere Informationen dazu und auch zur Behandlung von Exceptions in verteilten Flows, in der dotnetpro 2/2013.

Ich hoffe, es ist rüber gekommen, dass Sie Flows jetzt lokal entwickeln und testen können – und anschließend einen beliebigen Teil einfach verpflanzen. Dass die Kommunikation dann asynchron wird, ist nicht so schlimm, weil Flows darauf ohnehinn viel besser vorbereitet sind.

Indem Sie vor der Verteilung zum Beispiel ein Operation im zu entfernenden Flow auf asynchrone Verarbeitung setzen oder den Verarbeitungsmodus der Flow Runtime auf asynchrone Breitenorientierung setzen, können Sie deklarativ ganz einfach simulieren, wie sich die Verarbeitung nach der Verteilung verhalten wird.

Oder sie betreiben die Flow Runtime für den Client und die Flow Runtime für den Server im selben Adressraum mit einem kleinen Bus verbunden statt über WCF. Dazu finden Sie eine Hilfestellung in der Assembly npantarhei.distribution, auf die npantarhei.distribution.wcf aufbaut.

Fußnoten

[1] Instanzkennungen sind schon lange ein Feature der Flow Runtime. Sie kommen immer dann zum Einsatz, wenn eine Funktionseinheit in einem Flow an mehreren Stellen mit dem selben Namen benutzt werden soll.

Das ist bei Stand-In Operationen auch der Fall. Die selbe Stand-In Operation soll für verschiedene entfernte Funktionseinheiten stehen können.

Dienstag, 18. Dezember 2012

Die TDD Single Responsibility

Gerade wird wieder eine TDD Demo über Twitter herumgereicht. Corey Haines hat sich an die Kata Roman Numerals gemacht.

image

Mal abgesehen davon, dass TDD anscheinend ein unerschöpfliches Thema ist und die Katas auf die Dauer langweilig werden… Mir gefällt die Darstellung aus einem anderen Grunde nicht so gut.

Corey gibt sich Mühe. Alles läuft seinen kanonischen TDD Weg. Es könnte ein schönes Video sein. Wäre da nicht die ständige Überraschung.

Ja, es hört sich so an, als würde Corey in die Lösung des Problems “Übersetzung arabischer Zahlen in römische” stolpern. Er zaubert Testfälle aus dem Hut und erstaunt sich dann immer wieder selbst mit der Lösung.

Und ich meine hier wirklich die Lösung und nicht den Code.

Da scheint mir ein Grundproblem im Umgang mit TDD zu liegen. Das habe ich auch neulich auf den XPdays in Coding Dojos gesehen. Das Muster ist so:

  1. Es wird ein Problem vorgestellt.
  2. Es wird mit dem Codieren à la TDD begonnen.

Das Ergebnis? Regelmäßig kein Code, der die Aufgabe vollständig erfüllt [1].

Mit dieser Realität sollten wir nicht streiten, denke ich. So ist es einfach. Man bemüht sich redlich um die rechte TDD-Schrittfolge. Das kommt mir vor wie beim Tanzen. Alle starren gebannt auf ihre Füße und hoffen, niemanden anzurempeln. Nur leider geht dabei das große Ganze verloren. Beim Tanzen der Spaß an der Bewegung und am Miteinander. Und bei der Softwareentwicklung lauffähiger Code. Vor lauter TDD-Rhythmus und versuchtem Design funktioniert es nicht mal.

Wie frustrierend, wie tragisch. Kein Wunder, dass auch 2012 immer noch TDD hoch und runter evangelisiert werden muss.

Dabei scheint mir die Rettung der Situation einfach: mehr Systematik.

Ja, tut mir leid, dass ich mit so einem lästigen Wort komme. Das klingt nach Einschränkung, nach viel Aufwand ohne schnellen Nutzen… doch das Gegenteil ist der Fall. Systematik macht frei. Aus Komplexem macht sie Kompliziertes.

Fehlende Systematik überfrachtet TDD. TDD soll plötzlich die ganze Softwareentwicklung retten. Endlich wird das mit der Korrektheit besser. Und dann auch noch bessere Dokumentation durch TDD. Und außerdem höhere Evolvierbarkeit durch besseres Design (lies: bessere Strukturen). Vor allem aber nicht zu vergessen: eine Lösung stellt sich auch wie von selbst ein.

Kommt das niemandem merkwürdig vor? One size fits all?

Ich bin ein großer Freund der Prinzipien Single Responsibility (SRP) und Separation of Concerns (SoC). Danach scheint mir ein bisschen viel Last auf den Schultern von TDD zu liegen.

Das ist es auch, was mich an Coreys Demonstration wieder stört. Er steht dabei nur als einer von vielen, die TDD zeigen. Die Vermischung von Lösung und Design stößt mir auf. Sie ist es nämlich, die zu den erwähnten Misserfolgen in den Dojos führt.

TDD bedeutete zunächst Test-Driven Development. Da ging es also um eine bestimmte Art zu codieren. Red-green-refactor. Das hat vor allem zu hoher Testabdeckung geführt.

Dann wurde aus dem Development das Design: Test-Driven Design. Die Betonung wurde damit auf den Refactoring-Schritt gelegt. Zur hohen Testabdeckung sollte dann auch noch eine “gut” Codestruktur kommen.

Und heute? Mir scheint, dass es gar nicht mehr um TDD geht, sondern um TDPS: Test-Driven Problem Solving. Nicht nur sollen Tests zu einem Design führen – denn über das Design soll man ja vorher nicht nachdenken, es soll entstehen, in minimaler Form. Nein, jetzt sollen am besten die Tests auch noch die Lösung herbeiführen.

Wenn Sie sich nun fragen, was denn da der Unterschied sei, dann rühren Sie genau an dem Problem, das ich meine: Es wird kein Unterschied zwischen Lösung und Code gesehen. Oder vielleicht sollte ich sagen, zwischen Lösungsansatz und Code? Ist es dann deutlicher?

Hier zwei Beispiele für den Unterschied.

Als erstes eine Lösung für das Problem des Sortierens eines Feldes. Text und Bild beschreiben einen Ansatz, sie beschreiben ein Vorgehen, sie sagen, wie man das Ziel ganz grundsätzlich erreichen kann:

image

Und jetzt ein Design in F# für diesen Lösungsansatz:

image

Im Design, im Code finden sich natürlich die Aspekte und Schritte des Lösungsansatzes wieder. Aber der Code ist nicht der Lösungsansatz. Er implementiert ihn in einer bestimmten Programmiersprache mit bestimmten Sprach- und Plattformmitteln.

Als zweites ein Lösungsansatz für ein Problem im Compilerbau, die Erkennung von Bezeichnern. Hier ein Syntaxdiagramm dafür:

image

oder alternativ ein Deterministischer Endlicher Automat:

image

Dass es überhaupt eine Phase zur Erkennung von Bezeichnern gibt (lexikalische Analyse), ist ebenfalls Teil eines Lösungsansatzes:

image

Das konkrete Code-Design, die Implementierung des Lösungsansatzes, könnte dann so aussehen:

image

Lösungsansatz – oder auch Modell – und Code in einer bestimmten Struktur – nach TDD auch Design genannt –, sind einfach verschiedene Aspekte. In den TDD-Vorführung wie bei Corey und den TDD-Selbstversuchen in den Dojos werden die jedoch nicht sauber getrennt. Immer wieder wird gehofft, durch Red-Green-Refactor nicht nur ein evolvierbares Design herzustellen, sondern auch eine Lösung zu bekommen.

Das (!) halte ich für falsch. Erstens ganz praktisch aus der Beobachtung heraus, dass so selten lauffähiger Code entsteht, der die Aufgabe erfüllt. Zweitens eher theoretisch aus dem Gedanken heraus, dass wir Menschen damit schlicht unterfordert sind. Das über Jahrtausende geschliffene Werkzeug “Denken” wird nicht genutzt. Man hofft vielmehr, durch Mustererkennung beim Code, irgendwie zu einer Lösung zu kommen.

Das funktioniert manchmal tatsächlich, wenn man genau hinschaut. Die Kata Roman Numerals könnte dafür ein Fall sein. Nur ist nicht zu erwarten, dass das immer so geht. Auf den Quicksort Lösungsansatz kommt man nicht durch TDD, davon muss man einfach eine Vorstellung entwickeln – eben einen Lösungsansatz. Im Kopf. Durch Nachdenken.

Und wie sollte es dann anders aussehen mit TDD?

Systematischeres Vorgehen

Systematisierung, Entzerrung, Entlastung, Fokus finde ich wichtig. Aus meiner Sicht sollte das Vorgehen diese Schritte beinhalten:

  1. Problem vorstellen
  2. Lösungsansatz entwickeln
  3. Testfälle ermitteln und priorisieren
  4. Lösungsansatz mit TDD implementieren

Schritte 2. und 3. können dabei mehrfach durchlaufen werden. Und wenn sich bei 4. noch neue Erkenntnisse zum Lösungsansatz ergeben sollten, dann ist das auch ok. Aber 4. ohne explizites 2. und 3. zu beginnen, halte ich für eine Überlastung.

Damit wären Lösungsansatz und Design getrennt. Damit würde – da bin ich ganz sicher – die Erfolgsquote jedes Coding Dojos steigen. Und wenn nicht, dann würde man genau sehen, woran es liegt: Liegt es an mangelnden TDD-Fähigkeiten oder liegt es an mangelndem Problemverständnis und dadurch fehlender Lösungsphantasie?

Die Single Responsibility von TDD liegt für mich bei der Testabdeckung und bei einer hohen Strukturqualität im Kleinen [2].

Was jedoch da überhaupt in Code gegossen und strukturiert werden soll… das ergibt sich nicht durch TDD, sondern durch Nachdenken. Den Lösungsansatz zu finden und die Testfälle zu priorisieren, das ist nicht Teil von TDD – muss aber gezeigt werden. Denn wird es nicht gezeigt bzw. wie bei Corey mit dem TDD-Vorgehen vermischt, entsteht entweder eine Überlastung, die die Aufgabenerfüllung behindert. Oder es entstehen “Wunderlösungen”, über die man nur staunen, sie aber eher nicht nachvollziehen kann. Wer “Wunderlösungen” goutiert, der wird es schwer haben, selbst Lösungen für andere Probleme zu finden.

Fußnoten

[1] Damit will ich nicht sagen, dass es keine Teams gibt, die die Aufgaben schaffen. Aber das passiert eben nicht regelmäßig und systematisch. Ich erinnere mich lebhaft an ein Dojo auf den XPdays und an eines auf der DDC im letzten Jahr. 5-10 Teams gab es jeweils. Und keines hat funktionierenden Code für die Kata Roman Numerals bzw. die Kata Zeckendorf (Übersetzung einer ganzen Zahl in eine Zeckendorf-Sequenz) abgeliefert.

[2] Wobei sogar diese Strukturqualität im Kleinen nicht einfach so entsteht, wie ich an anderer Stelle schon ausgeführt habe. Dafür braucht es Refactoring-Kompetenz und –Willen. Die kommen nicht aus TDD. Dafür braucht es aus meiner Sicht noch Hilfestellung, die Refactorings näher legt. Dadurch kann sich ein TDD 2.0 auszeichnen. Aber dazu ein andermal mehr…

Freitag, 14. Dezember 2012

Pervertierte Dienerschaft

Was kann uns der Deal zwischen den USA und der HSBC über die Softwareentwicklung sagen?

image

Dass der stellvertretende US Generalstaatsanwalt die Verantwortlichen für Milliarden-Geldwäscheaktionen ungeschoren davon kommen lässt, ist natürlich ein Skandal. Aber ist das das Hauptproblem? Nein.

Das Hauptproblem ist, dass überhaupt eine Begründung gegeben werden konnte, wie sie gegeben wurde:

Eine andere Strafe hätte nicht nur die HSBC destabilisiert, sondern das weltweite Finanzsystem. Weil die HSBC systemrelevant ist.

Dass es überhaupt 1 Firma, also eine Organisation außerhalb eines Staates geben kann, die eine Größe hat, die sie im Grunde außerhalb von Gesetz und Ordnung stellt… das (!) ist der wahre Skandal.

Ob die HSBC so eine Organisation ist, ist zweitrangig. Erstrangig ist, dass es denkbar, gar hoffähig ist, dass es solche Organisationen gibt.

Da gibt es ein Kartellrecht, das begrenzt, wie sich Organisationen entwickeln, um einen Markt nicht zu dominieren. Eine gute Sache!

Doch wo ist das Recht oder noch vorher das Prinzip oder der Wert, um Systemrelevanz zu vermeiden? Das finde ich viel wichtiger.

Wir hängen von vielen Systemen ab: Finanzsystem, Verkehrssystem, Kommunikationssystem, Energieversorgungssystem, Gesundheitssystem, ökonomisches System, politisches System usw. Dass es diese Systeme gibt, sei akzeptiert. Das nennen wir auch Zivilisation.

Doch dann sollten wir doch tunlichst darauf achten, dass wir diese Systeme funktionsfähig halten. Beim ökonomischen System und dem politischen System haben wir doch davon eine sehr präzise Vorstellung: Zur Funktionsfähigkeit, zur Flexibilität, zur Erhaltung von Freiheit gehört Vielfalt, also eben die Abwesenheit von Dominanz. Wir wollen keine Monopole in der Wirtschaft, wir wollen keine politischen Einseitigkeiten (Ein-Parteien-System, Diktatur).

Und dann lassen wir es zu, dass es im Finanzsystem so große “Player” gibt, die das System gefährden? Oder auch im Verkehrssystem: Die Deutsche Bahn hat dort systemrelevante Größe. Wir müssen uns der Bahn beugen. Für den Schienenfernverkehr ist sie alternativlos. Oder im Energiesystem: RWE & Co sind so groß, dass sie die Politik bestimmen.

Denn das ist es, was bei systemrelevanter Größe unweigerlich entsteht: eine “Inversion of Control”. Nicht das System (bzw. die Politik) kontrolliert die ”Player”, sondern ein “Player” kontrolliert das System. Systemrelevanz stellt den “Player” quasi außerhalb des Systems. Aus einem Diener wird ein Herr. Das ist pervers. Es widerspricht dem gesunden Menschenverstand, der von einer Bank, einem Verkehrsbetrieb, einem Energieproduzenten usw. erwartet, dass er den Menschen dient. Unternehmen sind Mittel und keine Zwecke.

Anders jedoch im Fall der HSBC: Hier hat sich das Mittel zum Zweck aufgeschwungen, den es zu verfolgen gilt. Die Erhaltung der HSBC ist wichtiger, als Recht in vollem Umfang zu sprechen.

Bezug zur Softwareentwicklung

Was für unsere zivilisatorischen Systeme gilt, gilt natürlich auch für unsere technischen. Wenn Systembestandteile systemrelevante Größe erreichen, beginnen sie ein Eigenleben. Sie stellen sich außerhalb sachlicher Entscheidungen. Sie tendieren dazu, sich selbst zu erhalten – unter allen Umständen.

Nicht mehr Werte und Prinzipien bestimmen dann den Mitteleinsatz. Sondern Mittel werden eingesetzt, weil sie bereits eingesetzt sind. Ihr Platz im System und/oder in den Köpfen ist so groß, dass ihr Austausch ein unabsehbares Risiko ist.

Dafür haben wir auch einen Begriff: lock-in. Der wird zwar gewöhnlich in Zusammenhang mit einem Hersteller gebraucht (vendor lock-in), doch der lock-in lauert überall. Letztlich ist lock-in auch immer selbstgemacht. Kein Hersteller kann uns fesseln und einsperren; die Entscheidung zur Aufgabe von Freiheiten, zum Eintritt in einen lock-in liegt immer bei dem, der einen Hersteller, ein Paradigma, eine Methode, ein Tool einlädt und zu systemrelevanter Größe heranwachsen lässt.

Am Anfang sieht das meist nicht so schlimm aus. Was später ein lock-in wird, beginnt als Vorteil irgendeiner Art. Und warum auch nicht? Niemand sollte per se etwas gegen SAP oder Oracle oder das relationale Datenmodell oder Entity Framework oder Sharepoint oder Java oder Ruby on Rails oder oder oder haben.

Vorteile bei Kosten, Performance, Skalierbarkeit, Entwicklerverfügbarkeit, Sicherheit oder sonst was sollen eingestrichen werden.

Ich denke jedoch, dass all diese “Vorteilsnahme” immer eine Begrenzung erfahren sollten, sobald ihr Mittel droht, systemrelevante Größe zu erreichen. Wenn SAP, Oracle, Entity Framework, Sharepoint, Java oder Ruby alternativlos werden, wenn man sich nicht mehr vorstellen kann, sie zu ersetzen… dann ist Gefahr im Verzug. Dann hat man alles auf eine Karte gesetzt und seine Seele verkauft.

Systemrelevante Technologien, Tools, Datenmodelle, Paradigmen sind Perversionen. Sie dienen nicht mehr dem System, sondern sind das System. Das ist falsch.

Beispiel: Das “System” “Nachhaltige Softwareentwicklung” kann sich entscheiden, Objektorientierung als Mittel zu wählen, um sein Ziel zu erreichen. Oder es kann sich entscheiden, auf die Sprache C++ zu setzen. Das ist völlig ok – solange klar erkannt wird, dass Objektorientierung und C++ eben nur Mittel sind – die austauschbar bleiben sollten.

Wer daran jedoch nicht denkt und 10 Jahre Millionen von Zeilen C++ produziert – der hat das ursprüngliche “System” “Nachhaltige Softwareentwicklung” untergraben. Es ist hohl geworden, weil Entscheidungen im Sinne der Nachhaltigkeit nicht mehr frei getroffen werden können. Immer sitzt die systemrelevante Größe OOP bzw. C++ mit am Tisch – und hat ein Interesse, sich zu erhalten.

Neue Trends – C# oder Funktionale Programmierung – haben dann kaum eine Chance. Sie zerschellen an der systemrelevanten Größe, die nicht nur die Artefakte dominiert, sondern auch raumfordernd in den Köpfen gewuchert ist.

So stirbt dann ein System. Es bricht nicht zusammen, sondern erstarrt durch Perversion eines oder weniger Konstituenten.

Systemrelevanz lauert überall. Plattformen oder Produkte sind dabei noch einfach auszumachen. Aber auch Tätigkeiten (z.B. bug fixing) oder Personen (so genannte Wissensinseln) oder Vorgehensmodelle oder Glaubenssätze (z.B. Präsenzarbeit) können Systemrelevanz annehmen, d.h. alternativlos und somit einschränkend sein.

Was mit der HSBC geschehen ist, oder besser: was eben nicht geschehen ist, das ist skandalös. Doch wir sollten nicht glauben, dass das nur eine Perversion ist, die irgendwo da draußen passiert. Pervertierte Dienerschaft existiert auch in unseren Systemen in vielerlei Gestalt.

Wer Nachhaltigkeit ernsthaft erreichen will, der muss jeden Tag darauf achten, das Wachstum der gewählten Mittel unterhalb systemrelevanter Größe zu halten. Nachhaltigkeit kann es nur in Vielfalt und Alternativenreichtum geben.

Mittwoch, 12. Dezember 2012

TDD skaliert nicht

Dieser Tage beschäftige ich mich intensiv mit TDD. Ich kann sogar sagen, dass ich ein rechter Freund von TDD geworden bin. TDD ist cool – oder genauer: TDD 2.0 :-) - das ist nämlich TDD as if you meant it (TDDaiymi).

Nicht trotz dieser Freundschaft, sondern wegen ihr schaue ich aber auch genauer hin. Und da sehe ich ganz klar eine Beschränkung in TDD. Und das ist die mangelnde Skalierbarkeit.

TDD ist und bleibt eine Methode für 1 Entwickler (oder auch 1 Entwicklerpaar). That´s it.

imageTDD setzt mit einem Test irgendwo an einer Schnittstelle an und treibt von dort eine Struktur aus. Die wird feiner und feiner. Erst eine Methode, dann mehrere; erst eine Klasse, dann mehrere. Growing Object-Oriented Software, Guided by Tests beschreibt das recht gut.

Bei der Lektüre wird die Begrenzung von TDD aber natürlich nicht so deutlich. Daran haben erstens die Autoren kein Interesse und zweitens ist das Buch selbst ein “Single User Medium”. Man liest es allein und kann deshalb dem TDD-Vorgehen gut folgen.

Doch was ist, wenn 2, 3, 5, 10 Leute im Team sind? Dann kann jeder nach TDD vorgehen. Super! Aber was geht jeder dann mit TDD an?

TDD skaliert, wie Holzhacken skaliert. Wenn ein großer Haufen Holz rumliegt, kann man an jeden Holzklotz einen Holzhacker setzen. Das funktioniert, weil das Gesamtergebnis so einfach ist: ein Haufen Holzscheite. Und jeder Holzklotz ist auch so überschaubar, dass man nur schwer 2 oder 3 Holzhacker an einen setzen wird.

Anders ist das bei einem Sägewerk. Da arbeiten auch mehrere Holzarbeiter – aber an ganz verschiedenen Positionen. Die machen eben nicht alle dasselbe. Die tun Verschiedenes – und koordinieren ihre Arbeit.

Genau diese Koordination fehlt TDD jedoch. TDD ist eine Axt, mit der einer Anforderungen zu Code zerkleinern kann. Und was die anderen tun… Egal. Die machen irgendwo anders mit ihren Äxten auch irgendwas.

TDD skaliert also insofern nicht, als dass damit Zusammenarbeit mehrere Entwickler nicht befördert wird. In TDD selbst steckt keine Idee von Teamwork. TDD ist nur eine Methode, die auf eine gegebene Schnittstelle angewendet werden kann, um dahinter Strukturen auszutreiben.

Aber wie kommt man zu dieser Schnittstelle? Wo ist die im Big Picture einer Software angesiedelt? Dazu hat TDD keine Ahnung.

Und deshalb verfallen Teams immer wieder auf die eine unvermeidbare Schnittstelle als Ausgangspunkt, die jede Software hat: die Benutzerschnittstelle. In der werden Features verortet – und dann je 1 Entwickler an 1 Feature gesetzt, um daran mit TDD zu rumzuhacken.

Klar, damit kann man 1, 2, 5 oder 10 Entwickler beschäftigen. Nur kommt dabei jedes Feature nicht maximal schnell voran. Das meine ich mit “TDD skaliert nicht”. In TDD steckt kein Ansatz, eine Anforderung beliebiger Granularität auf mehr als 1 Entwickler zu verteilen, um es in konzertierter TDD-Aktion schnellstmöglich umzusetzen.

Das meine ich gar nicht als Kritik an TDD. Eine Axt kann ich ja auch nicht dafür kritisieren, dass in ihr keine Idee davon steckt, wie man in konzertierter Aktion mit mehreren Holzarbeitern einen Baum fällt. Eine Axt zerteilt das Holz unter ihr. TDD strukturiert Code hinter einer Schnittstelle unter sich.

Wer die Softwareentwicklung skalieren will, wer schneller 1 Anforderung umsetzen will, um schneller Feedback zu bekommen, der sollte von TDD keine Hilfestellung erwarten. Dafür muss man sich woanders umschauen.

Eine Menge von Schnittstellen, die irgendwie zusammen an der Lösung von 1 Anforderung beteiligt sind und auf die mehrere Entwickler gleichzeitig mit TDD angesetzt werden können, muss sich durch eine andere Methode ergeben.

Deshalb sind TDD-Beispiele auch immer überschaubar. Man kann mit TDD allein nichts Größeres angehen. Code Katas – so nützlich sie sein mögen, um die Methode zu üben – sind winzig. Und wer dann nur mit Code Katas TDD übt, der lernt eben auch nicht mehr als TDD. Der lernt Programmierung in Bezug auf eine Schnittstelle. Das ist eine nützliche Kompetenz so wie das Feilen, Sägen, Schweißen usw. Doch nur weil 20 Leute da sind, die alle feilen, sägen, schweißen können, entsteht noch lange keine Dampfmaschine.

Über TDD hinaus müssen wir uns also mit der Arbeitsorganisation beschäftigen. Wir müssen Wege finden, wie wir Anforderungen in etwas transformieren können, das von mehreren Entwicklern gleichzeitig mit TDD umgesetzt werden kann. Was TDD fehlt, ist eine TDD-Vorbereitung. Sich Gedanken über Testfälle zu machen, ist allerdings zu wenig. Denn auch die beziehen sich ja nur auf eine Schnittstelle.

Also: Was tun vor TDD, damit TDD den größten Effekt hat, damit die Entwickler zum Besten eingesetzt werden, damit endlich aus einer Gruppe von Entwicklern ein Team wird?

Mittwoch, 21. November 2012

Spekulatives Coworking

Heute probiere ich mal einen Coworking Space aus. Places Hamburg heißt der. Ist noch so neu, dass die Handwerker im Hintergrund wurschteln und auch mal den Strom abklemmen. Naja, macht nix, dafür kostet das Coworking bis Ende November 2012 hier nichts. Dafür darf zwischendurch der Akku in meinem MacBook Air übernehmen.


Dass es an meinem Fensterplatz ein wenig zugig ist, weil die Heizung komplett unterhalb des massiven "Tischbrettes"ist, nehme ich auch in Kauf. (Manchmal ist es schon merkwürdig, wie Funktionalität für Design geopfert wird. Was Innenarchitekten sich so denken... oder eben auch nicht...)


Coworking-Atomphäre will ebenfalls noch nicht aufkommen. Ich bin der einzige der hier arbeitet und nicht dazu gehört. Ansonsten nur zwei ältere Damen im Café, das auch zu Places Hamburg gehört. Es ist eher lone working. Noch. Denn bestimmt werden sich über die nächsten Wochen weitere Coworking-Suchende einstellen.


Für heute stört mich das alles nicht. Ich komme mal raus aus meiner Home Office und sehe etwas anderes als mein übliches "Büro", das elbgold Café am heimischen Mühlenkamp nahe der Alster.

Doch ich nehme meine Erlebnis hier mal als Anlass darüber zu spekulieren, wie für mich eigentlich Coworking aussehen müsste. Was wäre für mich attraktiv als Alternative zu Home Office und normalem Café?

Zuhause arbeiten zu können, ist schon ein Geschenk. Alles, was ich brauche, ist zur Hand. Für Raum, Infrastruktur (WLAN, Drucker, Scanner usw.) und Verpflegung habe ich schon gezahlt. Sobald ich woanders arbeite - außer beim Kunden - können meine Kosten nur steigen. Wenn ich mich dafür entscheide, sollte also etwas dafür herausspringen. Ich will was bekommen für meine zusätzlichen Kosten.

Man könnte mich mit etwas locken, das ich zuhause habe (oder auch nicht) und mir der Coworking-Space auch bietet. Ganz oben auf der Liste der Coworking-Space Anbieter ist da ein Arbeitsplatz (Tisch, Stuhl), Strom und WLAN.

Hm... da bin ich allerdings nicht so beeindruckt. Tisch und Stuhl und sogar Strom bekomme ich in fast jedem Café oder einer Bibliothek auch. WLAN? Nein, tut mir leid, habe ich in ausreichender Geschwindigkeit in der Tasche. Mein iPhone mit 3G als Router reicht mir innerhalb von Großstädten an den meisten Plätzen.

Wofür also bei Places Hamburg 25 EUR pro Tag ausgeben? Oder beim betahaus 15+ EUR/Tag, im Werkheim 15 EUR/Tag, im @Lilienhof 45 EUR/Tag?

Wenn ich mehr Platz und vor allem "neutralen" Platz brauche, wo ich mich mal mit Kollegen/Kunden zusammensetzen kann... dann will ich einsehen, dafür etwas zahlen zu müssen. Das geht über das hinaus, was ich zuhause habe.

Oder wenn ich etwas geboten bekäme, was ich mir nicht leisten kann, z.B. ein SMART Board, dann würde ich das auch einsehen.

Aber für einen Preis ab 15 EUR bekomme ich... Strom, Tisch, Stuhl. WLAN. Das war´s?

Im elbgold, die mich gern auch einen ganzen Tag unbehelligt arbeiten lassen, bekomme ich für 15 EUR deutlich mehr. Nämlich zum Beispiel 5 x einen leckeren Chai oder 1 x Kuchen + 1 x Lachs Bagel + 2 x Chai.

Achja, da war noch etwas bei den Coworking Spaces: Es heißt ja Co-Working, also irgendwie geht es auch um Arbeit mit anderen gemeinsam. Man bietet eine Bude voller Leben, eine bunte Mischung aus Arbeitenden. Klar, das habe ich nicht zuhause - außer meine Tochter ist da und macht Hausaufgaben.

Ja, zuhause ist es manchmal etwas still und einsam. Deshalb gehe ich auch ins "Büro". Da treffe ich Menschen, die ich kenne. Wir plaudern dann. Oder ich sehe viele Menschen, die mir eine Art "rauschenden Hintergrund" für meine Arbeit bieten. Mir macht das nichts aus, wenn an allen Tischen geredet wird. Dabei kann ich mich wunderbar konzentrieren - und fühle mich irgendwie auch als Teil einer Gemeinschaft. Dass das keine ebenfalls arbeitenden Menschen sind, sondern solche, die ins Café zur Entspannung gehen, macht nichts. Soviel will ich gar nicht mit anderen über meine Arbeit reden.

Ein Coworking Space will mir nun das auch bieten: eine Gemeinschaft der Arbeitenden, der etwas Gesellschaft Suchenden. Und sogar noch mehr will der Space sein, nämlich einer, in dem sich auch Kontakte ergeben, in dem es zu überraschenden Netzwerkverknüpfungen und Zusammenarbeit kommt.

Tolle Sache. Dass sich im elbgold für mich schon ein halbes Dutzend schöner Zusammenarbeiten und Freundschaften entwickelt haben, will ich dagegen gar nicht anführen. Ich frage mich nur, warum ein Coworking Space nur dafür von mir 15 EUR oder mehr haben will?

Zugegeben, der Arbeitsplatz im Coworking Space ist eben ein Platz zum Arbeiten. Das ist manchmal ein bisschen bequemer als in einem Café. Wieviel möchte ich dann aber für solche Bequemlichkeit ausgeben?

Ich will auch einsehen, dass ein Café wahrscheinlich ungern alle seine Plätze von Leuten wie mir belegt sehen möchte. Ich verzehre auf meinem Stuhl über einen Tag weniger als würden Gäste darauf im Stundentakt wechseln.

Aber genug der Kritik. Ich schalte mal in den Spekulations- und Fabuliermodus. Was fände ich denn nun attraktiv? Tisch, Stuhl, Strom, WLAN führe ich nicht auf. Die sind selbstverständlich. Damit kann ein Coworking Space gegenüber einem Café nicht punkten.

Stattdessen würde mich reizen...
  • Ein Ort, an dem ich für mein Geld den Tag über versorgt bin. Ich möchte mich auf meine Arbeit konzentrieren und nicht auf "Nahrungssuche". Das bietet ein Café nur begrenzt. Mir geht es um einen gesunden Mittagstisch mit etwas Auswahl, gesunde Snacks und ein paar "Grundgetränke": das sollte im Preis enthalten sein.
  • Ein Ort, an dem ich meinen Modus wechseln kann. Stillarbeit am Tisch, Überlegen am Whiteboard, Diskutieren in kleiner Runde etwas separat. Das sollte nach Verfügbarkeit bzw. timeboxed im Preis enthalten sein.
  • Ein Ort, an dem ich Mittel nutzen kann, die ich sonst nicht habe. Ein SMART Board steht für mich da ganz oben auf der Liste. Heute noch einen Konferenzraum ohne auszustatten, finde ich anachronistisch. Beamer und Flipchart sind zu wenig. Andere Mittel wären Zugänge zu kostenpflichtigen online Angeboten, beispielsweise IEEE oder ACM. Die könnten auf Pad-Computern eingerichtet sein, die man sich ausleihen kann (solange der Vorrat reicht). Oder warum nicht eine Lego Mindstorm Ecke? Oder Raspberry Pi? Oder eine AR Drone? Halt geeky stuff, den ich sonst nicht habe. Daran könnte ein Coworking Space auch weitere Angebote hängen, z.B. Workshops zur Einführung in die Programmierung von solchen Technologien oder Wettbewerbe. Halt Community Events, die gern etwas kosten dürfen. [1]
  • Ein Ort, der skaliert, wo ich jenseits des Vorgenannten auch mehr Raum bekommen kann. Dafür bezahle ich dann gern mehr.
Muss das alles stylish sein? Darauf legt Places Hamburg nämlich wert. (Allerdings fühle ich mich hier eher wie im Einrichtungshaus. Die Nüchternheit ist mir ein bisschen zu modern, glaube ich. Das private Cinema im Keller jedoch... mit gemütlichen Sesseln... das finde ich cool.)

Nein, speziellen Style brauche ich nicht. Auf meinem Platz muss ich es einen Tag gut aushalten können ohne Rückenschmerzen. Das reicht. Biergartenbänke wäre da wohl zuwenig. Ansonsten jedoch kann es einfach, funktional sein, wenn das die Kosten überschaubar hält.

So, wie es aussieht, gibt es einen derartigen Coworking Space in Hamburg derzeit wohl nicht. Die Verpflegung muss extra bezahlt werden oder ist minimal. Der Moduswechsel ist nur begrenzt möglich ohne Aufpreis. Und besondere Technologie sehe ich weit und breit nicht.

In Summe sind Coworking Spaces für mich also noch nichts. Sie sind mir für ihr - sorry to say - hausbackenes Angebot zu teuer im Vergleich zu meinem Home Office oder einem Café. Nach dieser Spekulationsrunde weiß ich jedoch, warum ich mich bisher dafür nicht erwärmen konnte.

Wie sich ein Coworking Space nach meinem Geschmack finanzieren ließe, weiß ich grad auch nicht aus dem Stand. Muss ich aber auch nicht ;-) Ich wünsche mir einfach nur mal etwas. Jedenfalls wäre ich bereit, dafür einen monatlichen Grundbetrag auszugeben, z.B. 10-20 EUR. Und dann pro (halbem) Tag noch etwas dazu, z.B. 15-20 EUR - wenn ich dafür versorgt bin. Das ließe sich ja auch mit meinem heimischen Budget für Frühstück/Mittag oder so verrechnen.

Hm... vielleicht sollte ich jetzt mal auf die Suche gehen, ob es noch andere gibt, die sich Ähnliches wünschen. Dann machen wir den Geek Space Hamburg auf :-)

Fußnoten

[1] Einige dieser Mittel mögen speziell interessant für Softwareentwickler sein. Aber warum auch nicht. Wenn die Flash Designer oder Grafik Designer andere wollen, dann nur zu. Vielleicht bilden sich dann Coworking Spaces für Branchen statt ganz allgemein für Freiberufler.

Montag, 12. November 2012

Aspektvolles Programmieren

Neulich habe ich kritisiert, Lösungen für Code Katas seien oft schwer verständlich. Als Ursache dafür habe ich einen Mangel an herausgearbeiteten Aspekten identifiziert.

Mit Code Katas wird üblicherweise TDD geübt. Das hatte Dave Thomas zwar nicht speziell im Sinn, als er zu Code Katas ermunterte, doch es ist die de facto Praxis in Coding Dojos. Explizit hat Dave Thomas gesagt:

“Some involve programming, and can be coded in many different ways. Some are open ended, and involve thinking about the issues behind programming.” (meine Hervorhebung)

Dass über die Themen hinter der Programmierung nach-ge-dacht würde… Nein, tut mir leid, das habe ich in keinem Coding Dojo bisher erlebt, weder in einem offenen, noch in einem inhouse Dojo. Der Goldstandard ist vielmehr, dass schon Rechner und Beamer eingerichtet sind, kurz das Organisatorische besprochen wird, man sich flux über die Aufgabe austauscht – max. 15 Minuten – vor allem aber zügig das (Pair) Programming beginnt. Und damit endet dann ein eventueller “Nachdenkmodus” endgültig.

Klar, es gibt Diskussionen über die nächsten Schritte. Aber so richtig ins Nachdenken über “issues behind programming” kommt da niemand mehr. Ist ja auch nicht das ausgelobte Ziel. Das lautet vielmehr, die Lösung möglichst weit mit TDD voran zu bringen. [1]

Und so sehen dann die Lösungen aus. Man bekommt, was die Regeln vorgeben: grüne Tests. Das ist nämlich das einzige, was irgendwie “gemessen” wird. Darüber gibt es keinen Zweifel. Schon die Qualität der Tests ist nicht mehr leicht zu ermitteln. Sie ergibt sich eher implizit dadurch, ob der Fortschritt leicht oder schwierig ist. Denn sind die Tests schlecht geschnitten und ungünstig priorisiert, wird das Codieren zäh.

Aber wie denn anders?

Nun, es kommt erstmal aufs Ziel an. Was soll denn mit einer Code Kata erreicht werden? Wenn das ausschließlich lautet “TDD-Fingerfertigkeit entwickeln”, dann ist ja alles gut. Weitermachen mit Dojos wie bisher.

Nur finde ich das inzwischen erstens langweilig und zweitens am Hauptproblem vorbei. Langweilig, weil nach 5+ Jahren Coding Dojos auch mal was anderes dran sein dürfte. Oder findet das sonst niemand zum einschlafen, wenn erwachsene Männer womöglich Monat für Monat zusammenkommen, um diese Fingerfertigkeit zu entwickeln. Mit Verlaub: Das ist doch viel, viel simpler, als aus einer Karate Kata das Letzte herauszuholen.

Vor allem löst diese spezielle Variante automatisierter Tests nicht das Hauptproblem der Codeproduktion. Das ist mangelhafte Evolvierbarkeit von Code.

Nicht, dass es keine Teams gäbe, die nicht von automatisierten Tests profitieren könnten. Davon gibt es noch viele. Die sollen da auch möglichst schnell Fuß fassen. Nur ist deren Einführung erstens technisch kein Hexenwerk: NUnit runterladen, referenzieren, los geht´s. Und zweitens ist die konkrete Praxis automatisierter Tests relativ unbedeutend. Ob nur “irgendwie” test-first oder richtiggehendes TDD oder Tests nach der Implementation geschrieben… klar, die Ergebnisse sind unterschiedlich, doch entscheidend ist nicht dieser Unterschied durch die Stile, sondern der zur vorherigen Testlosigkeit.

Warum fällt es denn aber Teams auch heute noch schwer, überhaupt automatisierte Tests einzusetzen? Weil sie keine TDD-Fingerfertigkeit haben? Nein, weil sie keine Codebasis haben, die das (nachträgliche) Anbringen von Tests erlaubt. Und das kommt daher, dass sie nur wenig Vorstellung davon haben, wie eine saubere, testbare Codebasis überhaupt aussieht.

Damit sind wir beim blinden Fleck der TDD-Coding-Dojo-Praxis. Über dieses “issue behind programming” wird nicht nachgedacht. Das Refactoring im TDD-3-Schritt red-green-refactoring ist für das übliche Coding Dojo eine Black Box. Deren Deckel wird höchstens ein wenig imageangehoben. Besser aber man lässt sie geschlossen. Es könnte eine Büchse der Pandora sein. Wenn die erstmal geöffnet ist, käme man mit dem Codieren ja gar nicht mehr voran – oder würde gar nicht erst beginnen.

Das halte ich für sehr bedauernswert. Das möchte ich ändern.

TDD ist gut und schön. Wem das noch nicht langweilig ist, wer glaubt, dass alles besser wird, wenn er darin nur perfekt ist… der soll gern TDD hoch und runter üben. Mir käme das allerdings so vor, als würde jemand meinen, ein großer Kung Fu Meister zu werden, weil er den ganzen Tag am Mu ren zhuang arbeitet.

Zur Programmierkunst gehört aber doch mehr. Wenn nicht, wäre sie armselig und könnte schon bald von jedermann ausgeübt werden.

Was das ist, das mehr dazugehört? Damit meine ich nicht die unendliche Vielfalt an Technologien. Die ist sicherlich eine große Herausforderung. Doch letztlich ist das eher eine Frage der Masse. Das sind komplizierte Oberflächlichkeiten.

Viel, viel schwieriger sind jedoch die “issues behind programming”. Da geht es um Paradigmen. Da geht es um Methoden. Da geht es um eine Vielzahl von Qualitäten über die “simple” Korrektheit von Code hinaus.

TDD adressiert die vermeintlich. Aber in Wirklichkeit adressiert sie nicht TDD selbst, sondern das Refactoring, auf das sich TDD stützt. Nur wird eben genau das nicht geübt. [2]

Nun aber genug mit dem TDD bzw. Coding Dojo Bashing ;-) Ich will mich ja auch nicht zu sehr wiederholen.

Wie stelle ich es mir denn nun anders vor? Wie hätte ich mir ein Vorgehen zum Beispiel in Bezug auf die Kata Word Wrap gewünscht?

1. Domäne spezifizieren

Als erstes sollte gefragt werden, was denn wirklich, wirklich die Problemdomäne ist. Worum geht es? Und das sollte dann in einem Satz knackig formuliert werden.

Schon bei der Kata Word Wrap ist das nicht ganz einfach. Ich habe mich da auch erst verlaufen. Der Titel ist nämlich fehlleitend. Er passt nicht zur Aufgabenstellung.

Im Titel kommt Word Wrap vor. Damit scheinen Worte und ihr Umbruch im Kern der Ubiquitous Language zu stehen. Interessanterweise tauchen dann Worte in der Aufgabenbeschreibung eigentlich nicht mehr auf. Da steht zwar “You try to break lines at word boundaries.” – doch zentral ist nicht “word”, sondern “break lines”.

Es geht nicht um Word Wrap, sondern um Zeilen(um)bruch. Das mag zunächst im Gespräch ein feiner Unterschied sein – doch in der Implementation würde der sich ausweiten.

Ich habe mich auch verleiten lassen, eine Wortumbruchlösung zu suchen, statt einer für den Zeilenumbruch. Doch der geht an der Aufgabe vorbei. [3]

Mein Vorschlag für eine knackige Beschreibung der Problemdomäne wäre dann zum Beispiel diese:

Der Wrapper zerlegt einen einzeiligen Text in mehrere Zeilen einer Maximallänge zerlegen - wobei Worte nicht zerschnitten werden sollen, wenn es sich vermeiden lässt.

Diese Beschreibung betont die Zeilenorientierung. Worte kommen darin nur insofern vor, als dass sie nicht “beschädigt werden sollen”. Die Zerlegung soll zu keinen “Kollateralschäden” führen.

Ob diese Formulierung die beste ist, sei dahingestellt. Aber sie ist besser als keine. Die Kraft überhaupt einer knackigen Formulierung ist nicht zu unterschätzen. Sie richtet das weitere Denken aus, sie spannt einen Lösungsraum auf – und zieht damit gleichzeitig eine Grenze. So kann konstruktive Diskussion über einen Lösungsansatz entstehen. Denn die kann, um sich fokussiert zu halten, immer wieder auf diese knappe Spezifikation verweisen. Der Zweck ist also ähnlich der der Metapher im XP.

2. Lösung skizzieren

Mit der Beschreibung der Problemdomäne als Leitstern und der Anforderungsdefinition als Karte, sollte als nächstes ein Lösungsansatz erarbeitet werden. Das geschieht im Wesentlichen durch Nachdenken. [4]

Die Frage lautet: Wie würde ich das Problem lösen, wenn ich es von Hand tun müsste?

Bei der Beantwortung soll natürlich jede Art von Erfahrung eingebracht werden. Alles ist erlaubt. Das Ergebnis kann ebenfalls alles mögliche sein – nur kein Produktionscode.

Früher gab es für algorithmische Aufgabenstellungen die ehrenhaften Mittel Flowchart, Structogramm und vor allem Pseudocode. Heute scheint das nicht mehr hip zu sein. Überhaupt sehen Lösungsskizzen seltsam holprig aus, wo sie denn überhaupt externalisiert werden können. Meist existieren sie ja nur sehr diffus in den Köpfen der Entwickler – und niemand weiß so recht, ob alle denselben Lösungsansatz meinen.

Ich sehe da ein großes Defizit bei unserer Zunft. Die Kunst, Lösungen zu beschreiben, ohne sie gleich zu codieren, ist sehr schwach ausgeprägt.

Dabei verlange ich nichts Spezielles. Mir ist es im Grunde egal, wie man einen Lösungsansatz beschreibt. Er soll nur auf einem höheren Abstraktionsniveau als die spätere Implementierung liegen. Sonst hat man ja nichts gewonnen. Nur über “Skizzen” lässt sich effizient und effektiv diskutieren; nur sie lassen sich in einer Gruppe gemeinsam entwickeln. Code selbst ist dagegen zäh. Man verliert sich auch zu schnell in technischen Details und verliert das Big Picture der Lösung aus dem Blick.

Für die Kata Word Wrap könnte die Lösungsskizze zum Beispiel in einer Schrittfolge bestehen. Nix Grafik, nix Tool, nix großer Aufwand. Einfach nur mal hinschreiben, wie man schrittweise das Problem eines Zeilenumbruchs lösen könnte. Beispiel:

    1. Vom gegebenen Text bricht man eine Zeile von Maximallänge vorne ab. Es entstehen eine Zeile und ein Resttext.
    2. Dann schaut man, ob diese Zerlegung dazu geführt hat, dass ein Wort zerschnitten wurde. Falls ja, macht man die rückgängig: Es wird der abgeschnittene “Wortkopf” am Ende der Zeile zurück an den Anfang des Resttextes zum “Wortrumpf” versetzt.
    3. Die Zeile kann jetzt an den bisher schon erzeugten umgebrochenen Text angefügt werden.
    4. Sofern noch Resttext übrig ist, den als neuen gegebenen Text ansehen und damit genauso wie bis hierher verfahren.

Diese Lösungsschritte zu finden, ist doch kein Geniestreich, oder? Aber nun kann jeder im Team prüfen, ob er versteht, was die Aufgabe ist und wie eine Lösung aussehen könnte. Es kann Einigkeit im Team hergestellt werden. Ein Ziel kann anvisiert werden.

Und wer diese Lösung nicht gut genug findet, der kann jetzt aufstehen und eine Diskussion anzetteln. Gut so! Das ist nämlich auf dieser konzeptionellen Ebene viel einfacher, als wenn erst Berge an Code produziert sind.

Sobald die IDE angeworfen ist, sind alle im Codierungsbewusstseinszustand. Dann ist Diskussion über Grundsätzliches nicht erwünscht. Das finde ich auch total verständlich. Denn Lösungsfindung und Codierung der Lösung sind zwei ganz unterschiedliche Aspekte von Softwareentwicklung. [5]

Nicht nur bei Code ist es also klug, Aspekte zu trennen, sondern auch im Entwicklungsprozess.

3. Lösungsaspekte identifizieren

Aber erstmal zu den Aspekten der Lösung. Wie lauten die? Wie umfangreich sind sie? Wie stehen sie im Zusammenhang?

Im vorliegenden Beispiel sind die Aspekte:

  • Textzerlegung und
  • Textzusammenbau.

Zur Zerlegung gehört das Abschneiden einer Zeile vom gegebenen Text und die eventuell nötige Korrektur dieses Schnitts. Der Textzusammenbau besteht dann nur noch aus dem Anhängen der endgültigen Zeilen an einander.

Durch die Identifikation der Aspekte wird die Lösung nochmal auf ein höheres Abstraktionsniveau gehoben. Hier kann das Team wieder prüfen, ob sie sich stimmig anfühlt. Ja, es geht ums Gefühl, um Intuition, um Erfahrung. Denn bisher ist kein Code geschrieben.

Aber das macht nichts. Entwickler geben ja immer soviel auf ihre Erfahrung. Dann ist hier der richtige Zeitpunkt, sie mal einzusetzen.

Dass diese “Gedankenblasen” nicht crashen… Klar, das ist so. But it´s not a bug, it´s a feature. Würden sie crashen, wären sie Code. Und Code ist vergleichsweise starr – egal ob mit TDD oder sonstwie entwickelt.

Nachdenken, eine konzeptionelle Lösung finden, ist erstens eine Tätigkeit, die das Team zusammenführt. Und zweitens ist es eine qualitätssichernde Maßnahme. Es wird damit nämlich die Qualität des Input für den Engpass “Implementierung” im Softwareentwicklungsprozess angehoben.

Codieren ist kein Selbstzweck. Also sollten wir nicht möglichst viel Code, sondern möglichst wenig Code schreiben. Damit meine ich aber nicht möglichst wenige LOC, sondern möglichst wenige Überarbeitungen. Sozusagen “LOC in place” vs “LOC over time”.

Hier der Überblick über die Aspekte und ihre Zusammenhänge:

2012-11-10 08.35.34

So ein Bild kann man leicht kommunizieren. Es enthält die Essenz der verbalen Lösungsskizze. Und es zeigt deutlich die Aspekte. Wenn jetzt zum Beispiel ein neues Teammitglied an Bord käme, könnte es anhand dieses Bildes leicht in den Lösungsansatz eingeführt werden. Warum den also nicht dem Code beigeben? Oder versuchen, ihn im Code möglichst nachvollziehbar festzuhalten?

Ja, genau, dieser Entwurf sollte sich im Code widerspiegeln. Das ist nicht umsonst ein Clean Code Developer Prinzip. Denn wenn der Entwurf nur für den einmaligen Codierungsgebrauch gemacht würde und danach verschwände, müssten Entwickler in Zukunft wieder Codearchäologie betreiben, um herauszufinden, wie das denn alles gemeint war.

Zu sehen ist im Bild eine Aspekthierarchie. Zuoberst der umfassende Aspekt der Domäne, d.h. der Gesamtheit der Anforderungen.

Darunter die beiden aus der Lösungsskizze abstrahierten Aspekte.

Und auf der untersten Ebene deren Sub-Aspekte, die konkreten Operationen. Die Doppelpfeile deuten an, wie sie horizontal zusammenhängen, um die funktionalen Anforderungen zu erfüllen.

Das ist doch nicht schwer zu verstehen, oder? Da muss man kein UML-Studium absolviert haben. Man muss auch nicht OOP- oder FP-Jünger sein. Gesunder Menschenverstand reicht aus. Eine umfassende Aufgabe wurde hier nachvollziehbar zerlegt in kleinere Aufgaben. Das ist ein probater Ansatz bei einer so algorithmischen Aufgabenstellung.

4. Lösung codieren

Mit der Lösungsskizze in der Hand, kann man sich ans Codieren machen. Große Überraschungen sollten sich bei der geringen Komplexität nicht einstellen. Das Codieren kann geradlinig ablaufen. Ob mit TDD oder ohne? Ich sag mal: Zuerst einen oder mehrere Tests für eine Operation zu definieren, bevor man sie implementiert, ist schon eine gute Sache. Das kleinschrittige Vorgehen wie bei TDD jedoch, halte ich in diesem Fall nicht für zwingend. Also: Test-First, ja; TDD, nein.

Und mit welcher Funktionseinheit beginnen?

Das Schöne an der Lösungsskizze ist, dass man beliebig reingreifen kann. Man kann zum Beispiel mit dem Anfügen einer Zeile an den schon umgebrochenen Text beginnen. Hier die Tests zuerst:

image

Und hier die Implementation:

image

Das ist überschaubar. Das bringt schnell ein Erfolgserlebnis. Das lässt sich auch sofort in eine Wrap()-Funktion einsetzen…

image

um erste Integrationstests zu erfüllen:

image

Ist ja nicht so, dass bei diesem Vorgehen nicht in Durchstichen, d.h. in Nutzeninkrementen gedacht werden soll.

Aber warum nicht den Code direkt in Wrap() schreiben und später raus-refaktorisieren? Weil wir uns auch nicht dümmer stellen müssen, als wir sind. Wir wissen schon, dass das ein eigener Aspekt ist und andere hinzukommen werden. Und Aspekte kapselt man mindestens in eigene Methoden.

Der aspekteigene Test ist ein Unit Test. Wenn später mal etwas schiefgehen sollte, dann zeigen solche Unit Tests viel besser als Integrationstests, wo das Problem liegt. Der Test von Wrap() hingegen ist ein Integrationstest, weil er viele Aspekte auf einmal prüft.

Und so geht es dann weiter: Jeder Aspekt kann für sich codiert werden. Naja, jede Operation, also die Aspekte auf der untersten Ebene bei der obigen Zeichnung. Die darüber liegenden Aspekte integrieren diese, sie sind abhängig. Deshalb ist wohl tendenziell eine bottom-up Codierung angezeigt.

In jedem Fall entstehen Operationsmethoden, die jede für sich getestet sind. Wenn Sie die mit TDD implementieren wollen… ist das keine schlechte Idee. Ich bin ja nicht gegen TDD. Nur schmeckt mir der “Allmachtsanspruch” von TDD nicht. Im Rahmen eines durch Nachdenken entstandenen Lösungsansatzes kann TDD jedoch eine Hilfe bei der Implementierung dessen “Black Boxes” sein, d.h. der Operationen.

5. Ergebnissicherung

Das Ergebnis dieser Herangehensweise können Sie bei github einsehen. Tests und Produktionscode liegen in einem Gist. An dieser Stelle möchte ich daraus nur einen Teil zitieren, um zu zeigen, was ich unter Verständlichkeit verstehe:

image

Diesen Code kann man von oben nach unten lesen. Das Abstraktionsniveau sinkt von Methode zu Methode. Jede steht für einen Aspekt. Die Ebenen der obigen Zeichnung sind erhalten; der Code spiegelt damit den Entwurf. Oder umgekehrt: der Entwurf kann aus dem Code herausgelesen werden.

Wie funktioniert Word Wrap?

  1. In Wrap() hineinschauen und erkennen, dass dort einfach nur an eine Methode delegiert wird. Was sagt uns das? Hier ist Rekursion im Spiel. Das ist ein Pattern. Wrappen ist also eine Tätigkeit, die irgendwie mehrfach ausgeführt wird, um das Gesamtergebnis herzustellen.
  2. In Wrap_remaining_text() hineinschauen und dort die Schrittfolge sehen:
    1. Zeile extrahieren
    2. Zweile an neuen Text anhängen
    3. Das Ganze wiederholen mit dem restlichen Text [6]
  3. Und wie funktioniert das mit dem Extrahieren der Zeile? Einfach in der nächsten Methode nachschauen:
    1. Zeile einfach abschneiden vom Text
    2. Heilen eines eventuell dabei “verwundeten” Wortes

Und so weiter… Je nach Interessenlage des Lesers kann die Erkundung tiefer und tiefer vordringen. Jede einzelne Funktion ist überschaubar: der Name sagt den Zweck an, die wenigen Zeilen sind leicht zu entziffern.

Das ist grundsätzlich anders bei den im vorherigen Artikel zitierten Lösungen. Die sind monolithisch. Word Wrap findet dort irgendwie statt. Erfolgreich – jedoch wenig verständlich.

Reflexion

Ob ich “die beste” Lösung gefunden habe? Keine Ahnung. Ich habe eine ausreichend funktionierende gefunden; sie ist good enough. Und ist finde sie verständlich. Nicht nur, weil ich sie entworfen und implementiert habe, sondern weil die “Gedankenstrukturen” im Code sichtbar geblieben sind. Mehr ist mit heutigen Sprachen nicht zu wünschen, glaube ich.

Aber – so mögen Sie nun einwänden – die Lösung hat so viele LOC! Sie ist viel länger als Uncle Bobs.

Ja? Und? Warum ist das ein Problem? Macht das einen Unterschied bei der Compilation oder in der Laufzeit? Nein. Müssen wir auf Hauptspeicher oder Festplattenplatz achten? Nein.

Hat die Codierung deshalb länger gedauert? Das bezweifle ich. Aber selbst wenn, wäre mir das egal. Denn es kann nicht das Ziel der Softwareentwicklung sein, möglichst schnell Code zu schreiben, der funktioniert. Ziel muss es sein, Code zu produzieren, der lesbar und evolvierbar ist – ohne natürlich die funktionalen und nicht-funktionalen Anforderungen zu vernachlässigen.

Code wird viel öfter gelesen, als geschrieben. Deshalb sollte er fürs Lesen und nicht fürs Schreiben optimiert sein. Das habe ich getan, würde ich sagen. Das war zumindest meine Absicht.

Bonus: Nagelprobe Evolution

Ob Code clean genug ist oder nicht, kann man eigentlich nicht 100% entscheiden, indem man ihn nur liest. Seine Sauberkeit erweist sich erst, wenn man ihn verändern will. Wie geschmeidig ist er dann?

Das ist auch das Problem von 99% der Literatur. Dort Code präsentiert in eingefrorenem Zustand. Wie lange jemand dafür gebraucht hat, sieht man nicht. Wie gut die Struktur im Lichte der so wichtigen Evolvierbarkeit ist, sieht man nicht.

Also versuche ich es ein bisschen besser zu machen. Hier drei neue Anforderungen an den Code:

  1. Der umzubrechende Text besteht nicht aus einer, sondern aus mehreren Zeilen. Die können als Absätze verstanden werden.
  2. Zusammengesetzte Worte können bei Bindestrichen getrennt werden.
  3. Zeilen sollen im Blocksatz formatiert werden.

Wie genau diese Funktionalität aussieht, ist zunächst nicht wichtig. Sie zu implementieren, ist eine Fingerübung. Im Sinne der Evolvierbarkeit ist interessanter, wie leicht kann ich feststellen, wo überhaupt Veränderungen angebracht werden müssen? Werden bestehende Aspekte beeinflusst; welche? Sind neue Aspekte einzufügen; wo?

Für das alles, will ich natürlich nicht Berge an Code durchsehen müssen. Ich will auch höherer Abstraktionsebene erstmal darüber grübeln.

Blocksatz

Also: Wo müsste ich beim Blocksatz ansetzen? Muss eine Funktionseinheit, ich meine eine Methode, verändert werden? Sollte ich den Blocksatz irgendwo dazusetzen?

Blocksatz ist sicherlich ein ganz eigener Aspekt. Der hat nichts mit Texttrennung zu tun und auch nicht mit dem Zusammenbau des umgebrochenen Textes. Also darf keine bisherige Operation dafür verändert werden. Stattdessen sollte der Blocksatz zwischen den bisherigen Aspekten Textzerlegung und Textzusammenbau stattfinden. Weder muss die Textzerlegung davon wissen, noch der Textzusammenbau.

image

Der Blocksatz muss sich nur auf die gerade “abgebrochene” und “geheilte” Zeile konzentrieren. Das kann so im Code aussehen:

image

Wieder lasse ich bewusst Details weg. Weil ich es kann. Weil es überhaupt mehrere Abstraktionsebenen gibt in meinem Code. Wer sich für die genaue Implementation von Justify() interessiert, kann ja weiter “reinzoomen” im Gist.

Trennung bei Bindestrich

Wie ist es mit den Bindestrichen für die Trennung? Ist das ein neuer Aspekt wie der Blocksatz oder verändert diese Anforderung einen bestehenden?

Ich würde sagen, hier geht es um eine Variation der Operation “Schnitt korrigieren” im ersten Entwurfsbild bzw. Schritt 2 in der verbalen Beschreibung des Lösungsansatzes.

Falls ein Wort zerschnitten wurde, wird sein “Kopf” nicht einfach komplett wieder an den “Rumpf” im Resttext geheftet, sondern es wird nach einem Bindestrich im “Kopf” gesucht und nur der dahinter liegende “Teilkopf” wird angeheftet.

Hier dazu die neuen Testfälle für die “Heilung” der Zeilenabspaltung:

image

Dadurch ändert sich natürlich einiges am Arbeitspferd der “Heilung”. Dieser Aspekt bekommt Sub-Aspekte – eine Zeile kann entweder mit einem zerschnittenen Wort enden oder mit einer “Silbe”:

image

…die sich dann auch im Code niederschlagen müssen:

image

Und nochmal: Wer mehr Details sehen will, “zoomt rein”. Für ein Verständnis des Lösungsansatzes sind das nicht nötig.

Mehrzeilige Texte

Für die Behandlung mehrzeiliger Texte ist zunächst zu klären, wie der API aussehen soll. Liegen die Texte als ein string vor, in dem Zeilen durch \n abgetrennt sind? Oder liegen mehrere Texte z.B. in Form eines IEnumerable<string> vor?

Ich nehme mal an, dass es weiterhin nur ein string ist, in dem jetzt auch schon Zeilenumbrüche vorhanden sein können. Damit ändert sich der API nicht.

Diese Zeilenumbrüche sollen natürlich erhalten bleiben. Das ist in einem Testfall zu spezifizieren:

image

Der Lösungsansatz sieht dafür dann im Grunde aus wie der bisherige:

  1. Etwas von einem Rest abtrennen, nämlich den nächsten Text
  2. Das Abgetrennte verarbeiten, nämlich den bisherigen Word Wrap darauf ausführen
  3. Das Verarbeitungsergebnis an das bisherige Resultat anhängen

Graphisch wächst das Modell sozusagen nach oben. Es gibt eine neue Aspekt-Ebene oberhalb des bisherigen Word Wrap: das Word Wrap (multi) :-) Es zerfällt in das Word Wrap in drei Sub-Aspekte – von denen der letzte derselbe wie beim Word Wrap ist, der Textzusammenbau.

image

Bei soviel Ähnlichkeit liegt es nahe, die Struktur der Implementation zu kopieren. Eine Rekursion ist auch hier gleichermaßen elegant wie verständnisförderlich.

image

Wie funktioniert der Code? Wrap_multi_text() verrät es. Einfach wieder von oben nach unten lesen. Der Lösungsansatz steht dort 1:1 übersetzt.

Fazit

Tut mir leid, ich kann nicht anders als zu sagen: Das war alles ganz einfach mit ein bisschen nachdenken. Think a little, code a little.

Ich hoffe, Sie sehen das genauso: Der Code ist verständlich und evolvierbar. Dass er nicht der kürzestmögliche ist, sei zugestanden. Aber ist das wirklich sooo wichtig? Nein. An LOC zu sparen, stolz auf eine kurze-knappe Lösung zu sein, halte ich für eine premature optimization.

Nichts ist natürlich gegen Eleganz zu sagen, wenn Sie die Verständlichkeit und Evolvierbarkeit erhöht. Rekursion statt Schleife finde ich deshalb für diese Kata auch so gut wie Robert C. Martin. Aber an LOC sparen zu wollen, um vielleicht hier und da ein bisschen schneller mit dem Schreiben fertig zu werden, scheint mir wenig nachhaltig. Das ist nicht in die Zukunft gedacht, in der der Code noch viele Male gelesen und verstanden werden muss.

Zum Schluss auf den Punkt gebracht. Was habe ich getan? Ich habe ganz konsequent…

  • das SRP angewandt,
  • das Prinzip SLA angewandt,
  • möglichst die grundlegenden Semantikaspekte Integration und Operation getrennt,
  • Domänenaspekte für sich getestet.

Versuchen Sie das bei der nächsten Code Kata doch auch einmal. Aspektvolles Programmieren hilft.

Fußnoten

[1] Es täte mir leid, wenn ich damit zu pauschal urteilen würde. Wenn irgendwo die Dojo-Praxis davon deutlich abweicht, dann melde man sich. Ich werde mich dann bemühen, persönlich mal dabei zu sein und zu lernen, wie es auch anders gehen kann.

[2] Dass es inzwischen hier und da Coding Dojos oder Code Retreats gibt, die sich mit Legacy Code befassen, ist löblich. Die Frage ist nur, inwiefern dort konzeptionell an die Sache herangegangen wird. In welchem Rahmen wird dort über Pathogenese und auch Salutogenese von Software nachgedacht?

[3] Ob ein Wortumbruch statt eines Zeilenumbruchs sinnvoller wäre, lässt sich nicht entscheiden. Es gibt keinen Kontext für die Code Kata. Die Aufgabe fordert ihn jedenfalls nicht. Und wenn wir im Sinne von YAGNI auch lernen wollen, genau hinzusehen und nur zu implementieren, was wirklich gefordert ist, dann tun wir gut daran, uns an die Aufgabe zu halten – zumindest solange niemand da ist, mit dem wir über ihre Sinnhaftigkeit diskutieren können.

[4] Wer dabei auch mal etwas ausprobieren muss, soll das gern tun. Nachdenken darf durch Hilfsmittel jeder Art unterstützt werden. Bei den üblichen Code Katas sollte das jedoch eher nicht nötig sein. Die sind rein algorithmisch und sehr überschaubar. Ausprobieren im Sinne von Codieren sollte nicht nötig sein.

[5] Damit will ich nicht sagen, dass man die Lösungsentwicklung und Codierung personell trennen sollte. Auf keinen Fall! Aber wir sollten anerkennen, dass das eben zwei unterschiedliche Tätigkeiten sind, die auch unterschiedliche Kompetenzen brauchen. Deshalb sollten sie nicht pauschal vermischt werden, wie es bei TDD in der Literatur der Fall ist.

[6] Bitte beachten Sie den Gebrauch von dynamic hier. Dadurch wird der Code leserlich, ohne dass ich für den Zweck der Zusammenfassung einer Zeile mit dem verbleibenden Rest einen eigenen Typen einführen muss oder auf Tuple<> ausweiche.