Follow my new blog

Sonntag, 27. Februar 2011

Beziehungsratgeber für Softwareentwickler

Komplexität vergällt uns das Programmieren. Unwartbarkeit ist ein Komplexitätsproblem. Aber was macht die Komplexität in Software aus?

Für mich ist etwas komplex, wenn ich es durch Nachdenken nicht verstehen kann.

Solange etwas kompliziert ist, hilft Nachdenken. Beispiel: Der Boyer-Moore Algorithmus zur Zeichenkettensuche ist kompliziert (zumindest für mich). Ich verstehe ihn nicht sogleich, aber wenn ich die Beschreibung ein paar Mal lese und darüber nachdenke, dann blicke ich durch und kann ihn implementieren.

Insofern sollte Software eigentlich gar nicht komplex sein. Programme sind doch nur Ansammlungen von Algorithmen, die jeder für sich höchstens kompliziert sind.

Das wäre wohl korrekt, wenn da nicht Beziehungen bestünden. Die einzelnen “nur” komplizierten Teile einer Software sind miteinander verbunden. Sie stehen in Abhängigkeitsverhältnissen. Und das macht ihre Summe mehr als kompliziert, nämlich komplex.

imageDenn wo viele, gar unüberschaubar viele Beziehungen bestehen, da ist unklar, wie sich Veränderungen an einem Teil über die Beziehungen auf andere Teile auswirken. Mit unendlich viel Zeit, wäre das zwar herauszubekommen – doch wer hat die schon. Nachdenken würde also zwar grundsätzlich helfen, nur nicht in der verfügbaren Zeit. Somit ist Software nicht nur eine komplizierte Summe aus mehr oder weniger komplizierten Teilen, sondern ein komplexes Ganzes.

Also: Beziehungen zwischen Funktionseinheiten machen Software komplex. Je mehr es sind, je undurchsichtiger sie sind, desto komplexer das Ganze. Das bedeutet: Softwareentwicklung muss sehr bewusst mit Beziehungen zwischen Funktionseinheiten umgehen, um die Komplexität nicht ungewollt wachsen zu lassen.

Hier deshalb ein paar Beziehungsratschläge:

Ratschlag #1: Beziehungen vermeiden

Wenn Beziehungen aus Kompliziertem Komplexes machen, dann scheint es angeraten, auf sie zu verzichten, wo es nur geht. Denn ohne Beziehung keine (ungewollte) Ausbreitung von Änderungsnotwendigkeiten. Wenn Funktionseinheit A und B keine Beziehung haben, dann kann eine Veränderung an A keinen Effekt auf B haben.

image

Wo hingegen eine Beziehung besteht, da können sich Änderungen ausbreiten:

image

Hier ein Beispiel: Methode A() ruft Methode B() auf, um Daten zu beschaffen; identifiziert werden die Daten durch einen Schlüssel. A() ist also abhängig von B():

void A()
{
  var daten = B(“123”);
  …
}

void B(string schlüssel) {…}

Wenn nun B() seine Signatur ändert und statt einer Zeichenkette nur noch ganze Zahlen als Schlüssel akzeptiert, dann hat das Auswirkungen auf A().

Das ist eine recht simple Abhängigkeit und der Compiler erkennt, wo sie nicht erfüllt ist. Was aber, wenn die Abhängigkeit nicht syntaktischer Natur ist?

Nehmen wir an, B() liefert eine Zeichenkette der Form “a=1;b=2” zurück, aus der A() relevante Werte extrahieren soll. Dann zerlegt A() diese Zeichenkette sicherlich zunächst beim Trennzeichen “;”:

void A()
{
  var daten = B(“123”);
  foreach(string zuweisung in daten.Split(‘;’))
  {…}
}

Wenn jetzt B() die Wertzuweisungen in der Zeichenkette aber nicht mehr durch “;” trennt, sondern durch “,” oder beide Trennzeichen zulässt, dann hat das ebenfalls Auswirkungen auf A() – allerdings kann der Compiler nicht helfen, diese zu aufzustöbern. Es liegt eine tückische semantische oder logische Abhängigkeit vor.

Fazit: Sobald Beziehungen ins Spiel kommen, wird es bei der Softwareentwicklung ekelig. Vermeiden Sie daher Beziehungen oder minimieren Sie sie soweit es eben geht. Vorsicht vor allem vor logischen Beziehungen!

Ratschlag #2: Beziehungen syntaktisch machen

Nicht zu vermeidende Beziehungen sollten, wenn es irgend geht, syntaktisch sein und nicht logisch. Logische Abhängigkeiten machen das Ganze vor allem komplex, weil Werkzeuge nicht helfen, sie zu überblicken. Der Mensch muss sie “im Kopf haben”. Und wenn er das nicht hat, dann merk er erst zur Laufzeit, ob durch Änderungen an einer Funktionseinheit etwas bei anderen “verrutscht ist”.

Bezogen auf das obige Beispiel bedeutet das, die Rückgabe von Name-Wert-Paaren in Form eines String aus B() sollte ersetzt werden z.B. durch ein Dictionary. Das Wissen um Trennzeichen ist dann konzentriert auf B(); sollten sie sich ändern, hat das keine Auswirkungen mehr auf abhängige Funktionseinheiten wie A().

Strenge Typisierung ist also der erste Schritt zu einem anti-komplexen Beziehungsmanagement. Ein spezifischer Typ ist besser als ein allgemeiner. string reduziert logische Abhängigkeiten gegenüber object. Und HashTable reduziert sie noch weiter. Und Dictionary<string,int> reduziert sich noch weiter.

Abstrakte Datentypen (ADT) und Kapselung sind der nächste Schritt. Wieder in Bezug auf das Beispiel oben: Ein Dictionary<> ist zwar spezifischer als ein string, doch es lässt zum Beispiel zu, dass Werte überschrieben werden können. Vielleicht soll das jedoch nicht erlaubt sein für das, was zu Beginn als Zeichenkette kodiert war. Solange ein Dictionary<> “durch die Gegend geschoben wird”, existiert also noch eine subtile logische Abhängigkeit: die Funktionseinheiten, die mit den Daten umgehen, müssen wissen, dass sie Werte im Dictionary<> nicht überschreiben dürfen. Eine formale Prüfung, ob sie das auch beherzigen, gibt es aber nicht. Erst zur Laufzeit wird das sichtbar.

Besser ist es da, statt eines Standard-Dictionary einen spezielleren Datentyp zu implementieren, z.B. NonOverridableDictionary<TKey, TValue>. Die Beziehung zwischen A() und den Daten wird damit weiter syntaktisch formalisiert.

Fazit: Beziehungen, deren Gültigkeit der Compiler überprüfen kann, sind anderen vorzuziehen.

Ratschlag #3: Süchtige vereinfachen

Der Drache der Komplexität ist noch nicht erschlagen, wenn die Beziehungen so gering wie nötig und so syntaktisch wie möglich sind. Auch dann kann es noch Funktionseinheiten geben, die von vielen anderen abhängig sind. Es sind die unvermeidbar “Süchtigen”; sie brauchen viele andere Funktionseinheiten zu ihrem Glück.

image

Wenn Sie auf solch ein Beziehungsmuster stoßen, dann… ja, dann hilft nur eines: Halten Sie die süchtige Funktionseinheit so einfach wie möglich in Bezug auf die Funktionalität bzw. Domäne ihrer “Suchtmittel”.

Der Grund ist einfach: Wenn eine Funktionseinheit von vielen anderen abhängt, dann ist die Wahrscheinlichkeit sehr hoch, dass sich an einer Funktionseinheiten etwas ändert, die relevant für die Süchtige ist. “Irgendwas ist immer” könnte man sagen.

Solche Änderungen können sich aber nur fortsetzen entlang der Beziehungen (vom Unabhängigken zum Abhängigen), wenn der Abhängige kompliziert ist, d.h. große Angriffsfläche bietet. Denn Kompliziertheit geht gewöhnlich einher mit vielen logischen Abhängigkeiten.

Da logische Abhängigkeiten aber so schwer zu sehen sind, lautet der Rat, eher auf die Kompliziertheit der süchtigen Funktionseinheit zu achten, d.h. auf deren Umfang und den internen Aufbau. Der Umfang sollte klein, der interne Aufbau simpel sein.

Wenn sich nun an den komplizierten Funktionseinheiten etwas ändert, dann ist die Wahrscheinlichkeit, dass das eine Auswirkung auf das einfache Abhängige hat, gering.

image

Typisches Beispiel für ein solches Abhängigkeitsmuster sind DI Container. Bei ihnen werden viele Typen registriert, ihre Aufgabe ist sozusagen, maximal abhängig zu sein. Aber das macht nichts, weil DI Container sehr einfach sind in Bezug auf das, wozu sie Beziehungen haben. Sie wissen von den Domänen der Funktionseinheiten, die bei ihnen registriert werden, nichts.

Fazit: Je einfacher eine Funktionseinheit, desto größer kann die Zahl ihrer Abhängigkeiten sein.

Ratschlag #4: Promiskuitive vereinfachen

Das Gegenteil von süchtigen Funktionseinheiten, d.h. solchen, die von vielen anderen abhängen, sind solche, zu denen viele andere Abhängigkeitsbeziehungen haben. Es sind Promiskuitive mit großem Netzwerk. Fallen an ihnen Änderungen an, dann haben die potenziell ein großes Ausbreitungsgebiet.

Was können Sie dagegen tun? Machen Sie prosmikuitive Funktionseinheiten so simpel wie möglich in Bezug auf die Funktionalität der von ihnen abhängenden.

image

Das bedeutet, Änderungen setzen sich nicht so leicht entlang der Beziehungen fort. Vor allem aber: Es fallen Änderungen an der promiskuitiven Funktionseinheit gar nicht so häufig an. Denn was einfach ist, das sollte sich nicht so oft ändern.

Typisches Anti-Pattern für Promiskuitive sind “fette” Domänendatenmodelle. Von ihnen hängen viele andere Funktionseinheiten ab – aber die Domänendatenklassen selbst sind funktionsreich (d.h. kompliziert) und haben auch noch eine große Oberfläche durch ihre Properties.

Fazit: Je weiter bekannt eine Funktionseinheit, desto simpler sollte sie sein.

Ratschlag #5: Funktionseinheiten einzäunen

Funktionseinheiten sind nicht nur direkt, sondern auch indirekt gekoppelt. Änderungen an einer können sich deshalb u.U. weitreichend fortsetzen. Veränderungen an einem Teil führen dann zu Veränderungen an einem anderen, die wiederum zu Veränderungen an einem dritten Teil führen usw. Der Ausbreitungsprozess ist rekursiv entlang von Abhängigkeitsbäumen.

image

Um solchen Ausbreitungswellen Einhalt zu gebieten, ist es ratsam, Beziehungsnetzwerke in “Kästchen” zu isolieren. Selbst wenn die bisherigen Ratschläge beherzigt sind, sollten Beziehungen nicht beliebig verlaufen können.

image

Vielmehr sollte Kohäsives, d.h. Funktionseinheiten die enger in Beziehung stehen, von anderem deutlich getrennt werden. Das enger Zusammengehörige aus dem vorigen Bild ist im nächsten zu diesem Zweck “umzäunt” und die Beziehungen zwischen den “Nachbarn” verlaufen über eine “Kontaktperson”, einen Vermittler.

image

Änderungen hinter der Fassade des Vermittlers haben dann eine geringere Chance auf Abhängige durchzuschlagen.

image

Die sich durch die Trennung ergebenden Zusammenfassungen können dann als Funktionseinheiten auf höherer Abstraktionsebene angesehen werden. Auf sie sind dort dann natürlich dieselben Ratschläge zur Komplexitätsreduktion durch bewusste Beziehungspflege anzuwenden.

image

Fazit: Bewusst eingezogene Mauern zwischen Gruppen von Funktionseinheiten, die die Beziehungsaufnahme einschränken, reduzieren die Komplexität.

Zwischenstopp

Softwareentwicklung ist Beziehungsmanagement. Denn Software ist immer ein Geflecht aus Funktionseinheiten, die von einander abhängig sind. Fragt sich nur, wie. Ohne sorgfältige Planung von Beziehungen und der gegenseitigen Kompliziertheit der Bezogenen, läuft Software in eine Komplexitätsfalle.

Unabhängig von Plattform und Programmiersprache gibt es jedoch Ratschläge, denen Sie folgen können, um nicht Opfer wuchernder Beziehungen zu werden. Wo Beziehungen nötig sind, lässt sich ihr Beitrag zur Komplexität minimieren.

Bewegen Sie die Ratschläge #1 bis #5 im Herzen und schauen Sie dann auf Ihren Code oder die Empfehlungen der Literatur zu ihrer Plattform. Sie werden erkennen, dass hinter Technologiediskussionen und neuen Rezepten für bessere Software oft auch nur der Wunsch steht, Komplexität durch Beziehungsmanagement in den Griff zu bekommen. Exemplarisch seien im Folgenden einige bekannte Prinzipien aus diesem Blickwinkel betrachtet, die sich zum größten Teil auch als Bausteine bei der Clean Code Developer Initiative wiederfinden.

Beziehungsmäßige Prinzipien

Ratschlag #1 ist so fundamental, dass er im Grunde die Mutter aller Softwareprinzipien darstellt. Er findet sich im alten Prinzip Lose Kopplung wieder. Lose Kopplung bedeutet, Beziehungen zu vermeiden bzw. zu minimieren.

Ratschlag #2 ist immer wieder im Gespräch, wenn es um die Typisierung von Programmiersprachen geht. Typsicherheit, Typinferenz, dynamische Typen, Variants… das alles sind Aspekte expliziter syntaktischer Beziehungen.

Während lange Zeit galt, dass strenge Typsierung fundamental für die Reduktion der Fehlerzahl in Software sei, hat sich das Bild in den letzten Jahren jedoch verändert. Testautomatisierung wird von einigen Diskutanten als Alternative angeführt. Wo Tests automatisiert und schnell nach einem Compilerlauf ausgeführt werden können, ist Feedback zur Beziehungskonformität fast ohne Verzug auch zur Laufzeit zu erreichen. Das relativiert Ratschlag #2 etwas, dennoch gilt weiterhin: Einschränkungen der Beziehungsmöglichkeiten sind immer noch der Entdeckung von Beziehungsfehlern vorzuziehen. Gut, wenn Sie schnell erkennen können, wo etwas falsch läuft; noch besser, wenn Sie manche Fehler erst gar nicht machen können. Das Thema Abstrakter Datentyp (ADT) ist mit Testautomatisierung allemal nicht vom Tisch.

Womit wir bei den Prinzipien Information Hiding und Law of Demeter wären. Beide plädieren für eine Verringerung der Oberfläche von Funktionseinheiten, so dass weniger Beziehungen zu ihnen aufgebaut werden können. Auf höherer Abstraktionsebene tut das auch die Komponentenorientierung. Es sind Prinzipien und Praktiken im Sinne von Ratschlag #5.

Das gilt auch für das Single Responsibility Principle, das allerdings auch Ratschlag #3 und #4 zum Thema hat. Denn der Fokus auf nur eine Verantwortlichkeit pro Funktionseinheit reduziert deren Kompliziertheit. Dasselbe will natürlich auch Keep it simple, stupid (KISS).

Konkreter im Sinne von Ratschlag #3 ist dann Single Level of Abstraction (SLA), dessen Anwendung dazu führt, Kompliziertheit aus einer Funktionseinheit hinauswandert in neu geschaffene auf niedrigerem Abstraktionsniveau. SLA macht aus normalen Funktionseinheiten Süchtige - die allerdings simpler sind als vorher.

Das Open Close Principle (OCP) wendet sich auf der anderen Seite eher an promiskuitive Funktionseinheiten im Sinne von Ratschlag #4. Seine Anwendung macht sie weniger änderungsanfällig.

Das Inversion of Control Prinzip (IoC) schließlich adressiert abhängige Funktionseinheiten jeder Art. Es will ihre Beziehungen lockern durch Austausch statischer gegen dynamische Abhängigkeiten. Das reduziert zwar nicht die Zahl der Abhängigkeiten, macht sie aber flexibler, so dass Textautomatisierung besser ansetzen kann.

Fazit

Das Thema Beziehungsmanagement ist für die Softwareentwicklung also alt. Viele Prinzipien, die heute weithin anerkannt sind, um korrekten und evolvierbaren Code zu schreiben, drehen sich um Beziehungen – ohne das immer explizit zu machen. Damit ist die Zentralität des Beziehungsthemas leider leicht verschleiert. Statt eines Namens hat es viele. Das verhindert eine Diskussion darüber, wie es an der Wurzel gepackt werden kann.

Denn Prinzipien sagen zwar, was getan werden kann, um das fundamentale Beziehungsproblem zu mildern, doch sie drängen sich nicht auf. Jeder Entwickler hat die Freiheit, sie anzuwenden oder auch nicht. Im Zweifelsfall entscheidet er sich – natürlich nur für den Moment und immer aus guten Gründen – deshalb dagegen. Und so wächst die Komplexität in Software weiter, obwohl wir eigentlich genau wissen, was ihre Ursache ist und was wir dagegen tun können.

Prinzipien geben allerhöchstens Handreichungen, definieren aber keine Methode und kein Rahmenwerk. Prinzipien wie auch die hier gegebenen Ratschläge allein sind daher schwache Waffen gegen die Komplexität.

Ich glaube, wir müssen stärkere Waffen in Anschlag bringen. Wenn der Spruch gilt, “Sag mir, wie du mich misst, und ich sage dir, wie ich mich verhalten werde”, dann müssen wir mehr am Start haben als fluffige Prinzipien. Die sind nämlich keine genügend starken Wände, an denen man sich die Stirn einhauen kann, bis man von selbst auf dem Pfad der Tugend bleibt. Wenn Beziehungsmanagement so zentral für die Softwareentwicklung ist, dann müssen wir Sprachen oder allemal Denkwerkzeuge haben, die das Beziehungsmanagement in den Mittelpunkt stellen. Beziehungen müssen sich weitgehend von allein und intuitiv den Ratschlägen entsprechend bilden. Sonst ist nicht zu hoffen, dass die wenigen Ratschläge oder die vielen Prinzipien zur Reduktion von Komplexität führen.

Mittwoch, 16. Februar 2011

Traktor für die Softwareentwicklung

Wer ist eigentlich am wichtigsten in einem Softwareentwicklungsprojekt? Sind es die Entwickler, ohne die es zu keiner Software kommt? Oder ist es der Requirements Engineer, ohne den die Entwickler nicht wüssten, was sie tun sollen? Oder ist es der Verkäufer, ohne den es gar kein Projekt gäbe, für das Anforderungen erhoben werden müssen? Oder ist es der Kunde, der schließlich das Geld gibt? Oder ist es – sofern vorhanden – die Qualitätssicherung, ohne die man das Entwicklungsprodukt nicht auf den Anwender loslassen kann?

Natürlich sind all diese Rollen wichtig. Erst wenn alle zusammenspielen, kommt gute Software heraus. Und dennoch… ich denke, es gibt eine erste Rolle und den vielen gleichen, den primus inter pares. Ohne sie geht nichts. Und ohne ein größeres Bewusstsein ihr gegenüber wird es nicht besser mit der Softwareentwicklung. Merkwürdigerweise hat diese Rolle keinen eigenen Namen, glaube ich. Ich kenne zumindest keinen.

Diese Rolle, die ich meine, die wichtigste, das ist… die Abnahme.

Der Abnehmer, d.h. der, der prüft, ob eine Software den Anforderungen entspricht, ist die wichtigste in der Produktionskette vom Kunden über den Verkauf bis zum Anwender.

Scrum betraut den ProduktOwner (PO) mit der Verantwortung. Da ist die Abnahme Teil des Prozesses. Mich stört dabei allerdings die Personalunion von Anforderer und Abnehmer. Der PO schiebt vorne Anforderungen rein und nimmt hinten ihre Erfüllung ab. Klingt plausibel, denn weiß er als Anforderer nicht am besten, ob seine Wünsche erfüllt sind? Angesichts der Wichtigkeit der Abnahme finde ich die Vereinigung jedoch kontraproduktiv.

Und warum ist die Abnahme so wichtig? Weil an ihr das liebe Geld hängt. Erst wenn Software abgenommen ist, fließt das Geld vom Kunden zur Softwareentwicklung.

Alles, was im Backlog steht, alles, was das Team so mühevoll produziert, alles, was die Qualitätssicherung durchwinkt, all das ist eitler Tand, solange er nicht abgenommen ist. Backlog, entwickelte Features und qualitätsgesicherter Code inkl. aller Dokumentation ist Umlaufbestand und damit gebundenes Kapital.

Wenn ein Team von 5 Entwicklern einen Sprint von 14 Tagen hinlegt, dann ist dieser Umlaufbestand am Ende des Sprints sehr hoch. Sein Wert beträgt nach landläufiger Rechnung mindestens 5 * 10 Arbeitstage * Deckungskosten/Entwickler, also z.B. 50 * 300 EUR = 15.000 EUR. Wenn dann der PO nicht zügig die produzierten Features abnimmt, ist das ein Problem. Dann fließt kein Geld. Das heißt, das Softwareentwicklungsunternehmen tritt in Vorleistung und kann ein Liquiditätsproblem bekommen.

Soweit aber nur die landläufige Rechnung in einer idealen Welt, in der der PO quasi blitzartig abnimmt.

Jetzt ein Blick durch die Brille der Durchsatzrechnung (throughput accounting) der Theory of Constraints (TOC). Dazu hänge ich mal die Rollen zu einer Produktionskette zusammen:

Backlog füllen (Bf) –> Features implementieren (Fi) –> Qualität sichern (Qs) –> Features abnehmen (Fa)

Jeder Produktionsschritt kann theoretisch 8 Std pro Tag leisten. Allerdings haben sie unterschiedliche Kapazität und stehen in der Praxis eben nicht 8 Std pro Tag zur Verfügung.

Die Leistungsfähigkeit der Schritte misst sich am Backlog. Bf ist also das Maß aller Dinge. Wenn Bf 8 Std das Backlog füllt, wie lange braucht dann Fi, um diese Menge an Features umzusetzen? Wie lange braucht Qs, um die Qualität zu prüfen? Wie lange braucht Fa, um die Features abzunehmen?

Ganz grob sehen die Verhältnisse vielleicht so aus:

Bf:Fi = 1:10 bis 1:50 – Fi braucht 10 bis 50 Mal so lange wie Bf für eine Menge an Features.

Bf:Qs = 1:0,1 bis 1:5 – Die Qualitätssicherung kann manchmal sehr schnell sein, manchmal braucht sie allerdings auch einige Zeit; in jedem Fall sollte Qs aber schneller als die Entwicklung sein.

Bf:Fa = 1:0,1 bis 1:2 – Die Abnahme sollte nicht so lange dauern wie die Qualitätssicherung; aber das ist sicherlich von der Güte der Anforderungen abhängig. Je besser die Definition of Done, je umfangreicher die Akzeptanztests, desto schneller die Abnahme.

Die langsamste Produktionseinheit ist Fi; sie ist der Engpass der Produktion. So scheint es und deshalb wird dort auch am meisten herumgeschraubt, um die Effizienz zu erhöhen.

Meiner Erfahrung nach geschieht das durchaus zurecht. Die Effizienz der Softwareentwicklung hat, um es vorsichtig auszudrücken, oft noch einiges ungehobenes Potenzial. Im Sinne der TOC lohnt sich die Effizienzsteigerung dort jedoch nicht, solange sie nicht wirklich der Engpass ist. Und das scheint mir der Fall in vielen Projekten.

Oberflächlich betrachtet, scheint die Fi immer der Engpass – aber nur solange die Produktionskette unvollständig ist und ohne die Abnahme gesehen wird.

Agile Softwareentwicklung kennt eine Definition of Done. Die empfinde ich oft jedoch noch als sehr technisch. Ihr fehlt der Reflex, den Abnehmer einzubeziehen. Alle Akzeptanztests sind eitel, solange nicht ein vom Kunden autorisierter Abnehmer sie durchführt und abzeichnet. Denn erst dann kann Geld fließen.

imageWo ist aber dieser Abnehmer? In vielen, nein, in den meisten Projekten, die ich als Berater im Rahmen meiner Arbeit als clean-code-advisor sehe, gibt es einen solchen Abnehmer nicht. Es gibt erstens niemanden, der verlässlich abnimmt, und zweitens sogar einen eigenen Willen hat, abzunehmen und deshalb immer wieder an den Kettengliedern vor sich zieht.

Die allermeisten Projekte, die ich sehe, haben keinen Traktor. Das heißt, auch wenn die Kapazität von Fa im Vergleich zu Fi oder Bf gering sein muss, gibt es sie im Grunde nicht.

Nehmen wir an, Bf hat 3 Tage lang am Anfang des Monats das Backlog gefüllt; nehmen wir an, Fi und Qs haben es geschafft, einen Großteil dieser Füllung bis Monatsende abzuarbeiten. 30.000+ EUR liegen damit auf Halde. Sie können erst nach offizieller Abnahme abgerechnet werden. Aber wo ist der Abnehmer?

Der bräuchte nach obigen Kapazitätsverhältnissen ja vielleicht nur 1-2 Tage, um die Halde durchzuarbeiten. Doch diesen Abnehmer gibt es in den meisten Fällen nicht. Ja, genau, es gibt niemanden, der konsequent jeden Monat 1,2 oder 3 Tage nur dafür zuständig ist, gelieferte Features abzunehmen. Geschweige denn, dass es jemanden gäbe, der das nicht nur alle Monate tut, damit gar nicht so eine große Halde entsteht, sondern jede Woche oder gar jeden Tag.

In den meisten Projekten ist also gegen die Intuition nicht Fi oder Qs der Engpass, sondern Fa. Fa steht nicht verlässlich mit vielleicht 1 Tag pro Woche (oder 4 Tagen pro Monat) zur Verfügung, sondern vielleicht nur mit 1 Tag pro Monat. Fa fällt damit sozusagen 3 von 4 Tagen, die nötig wären, aus.

Jetzt die Frage, wie teuer ist dieser Ausfall, dieser Mangel an Kapazität. Der Kunde mag rechnen, dass der Abnehmer pro Tag vielleicht 500 EUR kostet. Deshalb will er ihn auch nur so wenig wie möglich bei der lästigen Abnahme einsetzen, die ja irgendwie nichts bringt. Er spart also 1.500 EUR, wenn er ihn nur 1 Tag/Monat statt 4 Tage dem Projekt zuteilt.

Aus Sicht der Softwareentwicklung sieht das Bild jedoch anders aus. Denn wenn der Abnehmer nur 1 Tag statt 4 verfügbar ist, d.h. auch nur 25% der Halde abnehmen kann, dann wird die Halde immer größer. Am Ende des ersten Monats liegen 30.000+ EUR auf Halde, der Abnehmer prüft im Rahmen seines Tages 25% und es werden 7.500 EUR zur Bezahlung freigegeben.

Am Ende des zweiten Monats liegen dann schon 52.250+ EUR auf Halde. Davon werden wieder nur 7.500 EUR bezahlt, weil der Abnehmer nur einen Tag Zeit hat. Und so weiter, und so fort.

Natürlich ist das eine vereinfachende Rechnung – aber vielleicht gar nicht so stark, wie es scheint. Denn was an Abnahmefrequenz und –kapazität in manchen Projekten mehr vorhanden ist, wird durch Abnahmeverweigerungen womöglich wett gemacht. Abnahmen laufen ja nicht problemlos einfach durch, sondern führen sofort oder im späteren Betrieb auch zu Abweisungen.

Ich bleibe deshalb dabei: Der erste Engpass der meisten Projekte ist die Abnahme. Sie ist nicht verlässlich und mit zu wenig Kapazität ausgestattet. Und vor allem, sie wird ihrer Rolle nicht gerecht, weil sie nicht wirklich an der Kette vor sich zieht. Sie steht nicht jeden Tag auf der Matte und fragt, “Wo sind die nächsten Produkte, die ich abnehmen kann?”

Und das hat Auswirkungen. Weitreichende. Die erste und für Manager eigentlich unmittelbar spürbare: die Liquidität ist geringer als sie sein könnte. Immer wieder tritt die Entwicklung in Vorleistung, weil sie auf Halde produziert hat. Da ist man stolz, dass die Anforderungen munter ins Backlog fließen; man freut sich an einer schnurrenden Implementierung und einer sauberen Qualitätskontrolle – nur am Ende wird nicht abgenommen. Die ganze schöne hohe Qualität liegt dann auf Halde. Und letztlich weiß keiner, ob die hohe Qualität überhaupt den Kundenwunsch trifft. Denn das stellt sich erst heraus, wenn sie auch abgenommen wurde.

Die zweite, eher subtile Auswirkung: Solange der Engpass Abnahme nicht beseitigt ist, lohnen alle anderen Verbesserungsmaßnahmen an Produktionsschritten davor nicht. Das ist eine ganz klare Aussage der TOC.

Es lohnt nicht, den Verkauf anzukurbeln, wenn hinten keine Abnahme stattfindet. Insofern könnte es sogar eine Maßnahme sein, den Verkauf in die Abnahme einzubeziehen. Vielleicht sollte er seine Provision erst bekommen, wenn auch abgenommen ist?

Es lohnt nicht, Aufwand in Continuous Integration zu stecken, solange es keine “Continuous Acceptance” gibt. Dasselbe gilt für höhere Effizienz bei der Implementation oder mehr Kapazität in der Qualitätssicherung. Das alles führt nur zu einer schnelleren Füllung der Halde. Produktionskosten werden schneller in Bestände überführt; der Durchsatz steigt deshalb aber nicht.

Deshalb lautet in letzter Zeit meine erste Frage an Projektverantwortliche, wenn sie um Rat suchen: Wer ist für die Abnahme zuständig, wie aktiv zieht derjenige am Team, wie hoch ist die Abnahmefrequenz?

Bevor nicht diese Verantwortlichkeit ganz klar zugewiesen ist, bevor nicht die diese Rolle von sich aus und ganz verlässlich immer wieder bei Fi und Qs auf der Matte steht, bevor nicht mindestens jede Woche, besser noch jeden Tag!, abgenommen wird, solange lohnt kaum eine andere Maßnahme.

Das mag bitter klingen, weil Softwareteams, die sich an Problemen aller Art abarbeiten, auf die Abnahme wenig Einfluss haben, aber es hilft nichts. So ist die Natur der Dinge. Und die Praxis Prozessoptimierung in vielen Branchen und zahllosen Unternehmen zeigt, dass es nur so geht: Verbesserung muss am Engpass ansetzen, sonst verpufft sie und/oder führt zu Frust. Der Engpass in der Softwareentwicklung, deren Zweck das Geld verdienen ist, ist nun aber zunächst derjenige, der das Geld freigibt.

Bringen Sie also die Abnahme als Traktor für den ganzen Entwicklungsprozess in Stellung, dann wird alles andere leichter oder gar erst möglich.

Sonntag, 6. Februar 2011

Spielend programmieren

imageBesser wird es nicht durch Klagen. Besser wird es nur, wenn man sich überhaupt vorstellen kann, wie es besser sein könnte. Dafür muss man sich manchmal frei machen von dem, was ist. Einfach alle Begrenzungen hinter sich lassen. Mal frei fabulieren, wie die Welt aussehen sollte, und beherzt eine Antwort finden auf die Frage: “Ja, wie hätte ich es denn gern, wenn ich mir etwas wünschen dürfte von einer Fee?”

Heute habe ich mir gegönnt, diese Frage für die Programmierung mal für mich zu beantworten. Geplant hatte ich das nicht. Eher bin ich ohne zu fragen über meine Antwort gestolpert.

Wie wünsche ich mir also die Programmierung?

Ich wünsche mir die Programmierung spielerisch(er). Ich wünsche mir, dass Programmieren so funktioniert wie TinkerBox von Autodesk.

TinkerBox ist ein Spiel, in dem man “Maschinen” baut bzw. vervollständigt, um eine Aufgabe zu lösen. Zugegeben, das sind sehr, sehr, sehr einfache Aufgaben im Vergleich zu einer Warenwirtschaft oder einem Compiler oder einer Stellwerkssteuerung.

Aber auch Druckpressen, Autos, Fahrstühle sind sehr viel komplizierter als die TinkerBox-Maschinen und doch funktionieren sie letztlich nach denselben Gesetzen.

Hier ein paar Impressionen von TinkerBox:

Als ich mit TinkerBox angefangen habe auf meinem iPad zu spielen, habe ich einfach das Gefühl gehabt: “Wow, so sollte auch die Programmierung laufen!” Ich möchte Programme visuell zusammensetzen. Ich möchte sie sofort probeweise laufen lassen. Dabei möchte ich zusehen, wie die Teile zusammenspielen.

Natürlich kann das nicht ganz so simpel sein wie bei TinkerBox. Aber warum muss es denn sooooo anders aussehen? Warum muss es aussehen wie heute, wo ich eigentlich nur wie vor 30 Jahren Text in einer imperativen Sprache in einen Editor klopfe? Das kann doch nicht das Ende der Fahnenstange sein. Wir können doch nicht ernsthaft der Meinung sein, mit einer textuellen IDE (und ein bisschen Visualisierung drumherum) die Spitze des Möglichen in der Softwareentwicklung erklommen zu haben.

Nein, ich möchte, dass das anders aussieht. Ich will Software aus Bausteinen aufbauen. Ich will sie zusammenstecken. Ich will sehen, wie sie funktioniert – zum einen, indem ich am Code die Funktionalität ablese, zum anderen, indem ich den Code dabei beobachte.

Manche der Bausteine sind dabei Standardbausteine, andere sind Bausteine, die ich noch “zuhauen” muss, wieder andere setze ich aus anderen zusammen.

image

Bei TinkerBox gibt es nur Standardbausteine. Die Kunst besteht darin, sie in zielführender Weise zu kombinieren. Für die Softwareentwicklung reicht das natürlich nicht. Wir brauchen Bausteine, die wir “parametrisieren” können. In die gießen wir mehr oder weniger normalen Quellcode. Das finde ich ok. Denn die Menge des Quellcodes ist dann überschaubar.

TinkerBox hat in mir den Gedanken verstärkt, dass wir in der Softwareentwicklung Konstruktion und “Kreation” strikt trennen müssen. Wir brauchen beides, aber es sind grundsätzlich verschiedene Tätigkeiten.

Bei der Konstruktion nehme ich Bausteine und setze aus ihnen etwas Größeres zusammen.

Bei der Kreation denke ich mir neue Bausteine aus (oder “parametrisiere” Standardbausteine). (Ob “Kreation” der beste Begriff dafür ist, lasse ich mal dahingestellt. An dieser Stelle wollte ich aber nicht Implementation schreiben.)

Bausteine zusammenstecken, sie zu einem funktionierenden Ganzen fügen, das ist etwas ganz anderes, als Bausteine zu entwickeln, zu kreieren.

In der Softwareentwicklung trennen wir aber bisher nicht sauber, sondern sind im Grunde ständig mit der Kreation beschäftigt. Die jedoch ist viel schwieriger als die Konstruktion. Der einfache “Beweis”: Selbst Kinder können mit Legobausteinen tollste Dinge konstruieren – kreiert haben aber nicht Kinder die Legobausteine, sondern Erwachsene.

Genauso ist es mit Excel. Millionen von Poweruser können aus den Standardbausteinen in Excel tollste “Rechengebilde” konstruieren. Kreiert haben diese Standardbausteine jedoch Programmierer.

So ganz neu ist die Vorstellung, die ich hier äußere, natürlich nicht. Von Komponenten, die per glue code nur noch verbunden werden müssen, träumte man schon in der 1990ern oder gar davor. Realisiert ist diese Vision aber nur sehr begrenzt.

Mir geht es auch nicht darum, Laien zu Softwareentwicklern zu machen. Ich möchte den Softwareentwicklern nur das Leben erleichtern. Sie sollen sich mehr auf das Wesentliche konzentrieren. Das – so stelle ich mir in einem kühnen Traum vor – können sie aber besser, wenn Programme visueller machen. Die Anhaftung an Text als primärem Ausdrucksmittel für Software, ist anachronistisch und kontraproduktiv. Ich kenne keine andere Branche, in der Designdokumente primär textueller Art sind; nur die Softwareentwicklung beharrt darauf. Denn Programmierung ist Design.

Also: Befreien wir uns von der Last der Texte! Machen wir die Softwareentwicklung haptischer. Entlasten wir uns durch Trennung von Konstruktion und Kreation. Ich glaube, dann wird vieles besser in der Programmierung. Denn dann können wir besser über Software reden und wir können dann besser gemeinsam an Software arbeiten.

Dienstag, 1. Februar 2011

Mehr vom Selben hilft nicht mehr

Martin Fowlers Vortrag hat mich leider enttäuscht. Heute abend hätte er über “Software Design in the 21st Century” sprechen sollen. Gesprochen hat er aber stattdessen in drei “Minivorträgen” über DSLs – anlässlich seines gerade erschienenen Buches zum Thema –, REST und die Agilitätsbewegung am Ende der ersten 10 Jahre. Das war alles wenig inspirierend und wenig inspiriert, fand ich.

Nun frage ich mich aber, warum hat er nicht das ursprüngliche Thema behandelt? Sicher war nicht nur ich im ausverkauften Saal auf seine Sichtweise gespannt. Meine Vermutung – mal so ganz keck geäußert: Er hat dazu auch keine rechte Idee.

Die Branche mokelt halt so vor sich hin. SOA ist entweder tot oder hat sich still ins Pragmatische zurückgezogen. Funktionale Programmierung rettet im Verbund mit Multi-Core Prozessoren auch nicht kurzfristig die Softwarewelt. Um XP ist es sehr still geworden; funktioniert also entweder einfach oder wird so lange adaptiert, bis davon nichts mehr übrig ist. Scrum verliert sich in Lagerfehden. Und sonst so?

Tja… das große Problem wartbarer Software, d.h. flexibler, evolvierbarer, langlebiger und auch noch möglichst fehlerfreier Software, die im Rahmen gegebener Ressourcen entwickelt wird, das Problem ist schlicht ungelöst. Und eine Lösung ist nicht in Sicht.

Nicht, dass ich eine Silberkugel im Samtkästchen von Martin Fowler erwartet hätte. Aber mit DSLs, REST und ein bisschen Geschichten erzählen unter dem Titel “Software Design in the 21st Century" aufzutreten, finde ich schwach.

So einfach vergessen kann ich den Abend aber nicht. Denn da ist etwas, das Martin Fowler eher nur am Rande gesagt hat, das mich wurmt. Er hat nämlich gesagt, wir stünden bei der Agilität noch am Anfang. Warum? Weil wir selbst bei der Objektorientierung nach 40 Jahren noch nicht am Ziel seien. Die Agilität solle sich also besser darauf einstellen, auch noch ein paar Jahre zu brauchen.

Einerseits ist da sicherlich wahr: Entwicklungen brauchen ihre Zeit, selbst wenn sie in die richtige Richtung gehen. Und da die Agilität nicht nur mit Technologie zu tun hat, sind ihre Auswirkungen weitreichender als bei der Objektorientierung. Mehr Menschen müssen sich anpassen. Also dauert es auch lange oder gar länger.

Aber andererseits… Wir reden hier ja nicht über Naturgesetze oder den menschlichen Körper. Da mag es Jahrhunderte dauern, bis wir die wahren Zusammenhänge erkennen. Wir reden über menschengemachte Konzepte und Werkzeuge. Und da erlaube ich mir die Frage: Wenn etwas Menschengemachtes nach Jahrzehnten immer noch Schwierigkeiten bereitet, ist das dann ein Problem der Leute, die damit immer noch und immer wieder Schwierigkeiten haben? Oder ist das ein Problem von Konzept bzw. Werkzeug?

Die Natur lässt sich nicht ändern, ob wir sie verstehen oder nicht. Bei der Softwareentwicklung habe ich da aber einen anderen Anspruch.

Fowler, Beck, Martin et al. werden nicht müde hervorzuheben, dass die allermeisten Entwickler die Objektorientierung nicht wirklich verstanden hätten. Es würde eher mit OO-Sprachen immer noch prozedural programmiert.

Das mag sein – aber wessen Problem ist das denn? Warum erhebt sich nicht laut und breit die Frage, ob denn Objektorientierung in der scheinbar nur sehr, sehr, sehr schwer zu vermittelnden “richtigen” Weise überhaupt erstrebenswert ist? Wenn Millionen von Entwicklern über Jahrzehnte es nicht hinkriegen, dann können wir – so glaube ich – ausschließen, dass die Millionen zu doof sind. Oder wenn, dann ist das eben so. Auch egal. In jedem Fall muss dann die Methode, das Paradigma überprüft werden.

In der Finanzwelt gilt: “Nicht realisierte Gewinne sind keine Gewinne.” Das sollten wir auf die Softwareentwicklung übertragen: “Erfolgversprechende Ansätze, die jahrzehntelang hinter den Erwartungen zurückbleiben, sind überbewertet.”

Wenn die Objektorientierung nach 40 Jahren die Softwareentwicklung nicht deutlich herausgerissen hat aus der Unzuverlässigkeit, dann ist sie schlicht das falsche Mittel. Dann müssen wir ein anderes suchen.

Und genauso die Agilität: Wenn die nach 10 Jahren noch am Anfang steht bei dem großen Hype, den sie erfahren hat, dann müssen wir uns etwas anderes suchen, um die Probleme, die immer noch so weit verbreitet sind, zu lösen. Dann ist irgendetwas noch mindestens knapp daneben bei der Agilität.

Was mich also enttäuscht hat bei Martin Fowlers Vortrag war die ruhige “Sturheit”, mit der er “mehr vom Selben” wenn schon nicht gefordert, dann doch aber suggeriert hat. Mehr “richtige” Objektorientierung hilft, mehr “richtige” Agilität hilft. “Leute, haut einfach rein; ihr habt es noch nicht doll genug versucht.” Klingt wie aus einem Watzlawik-Buch. Nur da wird solches Verhalten leider als pathologisch entlarvt.

Das Ergebnis des Abends für mich: Martin Fowler hat für mich live repräsentiert, was ich nach Lektüre seines und anderer Blogs schon geahnt hatte. Die Vordenker von einst sind auch nur Menschen und können nicht dauerhaft vordenken. Auch sie sind Gefangene eines Systems, selbst wenn sie es selbst (mit)geschaffen haben. Oder gerade deshalb?

Wirklich Vordenkerisches werden wir kaum noch von Fowler oder Martin oder Beck zu hören bekommen. Alistair Cockburn scheint mir allerdings noch etwas wacher. Ansonsten schauen wir besser an den Rand des Mainstream. Dort steigen die Nebel dessen auf, das sich dermaleinst zum Software Design des 21. Jahrhunderts verfestigen wird. NoSql, Erlang, DCI, Kanban… das sind für mich Begriffe, die Impulse in die Branche senden.

Keine Ahnung, wie das aber alles mal am Ende aussehen wird. OO und Agilität und DSLs und REST werden dadurch natürlich auch nicht von der Bildfläche spurlos verschwinden, sondern vielmehr assimiliert. Aber die Softwarentwicklungswelt wird anders aussehen, da bin ich mir sicher. Denn wenn sie nicht anders aussieht, dann habe ich wenig Hoffnung, dass wir uns aus dem wachsenden Brownfield befreien.

Samstag, 29. Januar 2011

Abhängige Flüsse

Abhängigkeiten sind ein zentrales Problem in der Softwareentwicklung. Deshalb haben Flow Designs den Anspruch, mit ihnen konsequent aufzuräumen. Wie sich nun herausstellt, ist das zwar möglich – aber manchmal umständlich. Abhängigkeiten auch in Flow Designs integrieren zu können, erscheint deshalb sinnvoll. Hier mein Vorschlag, wie das geschehen könnte.

Problem #1: Akteure und Request/Response-Kommunikation

Am Anfang des Entwurfs mit Flow Designs (bzw. Event-Based Components) stand die Verbindung von Akteuren als Funktionseinheiten. Beispiel: Ein Frontend fordert Daten von einem Repository. Das konnte entweder mit zwei Drähten ausgedrückt werden…

image

…oder mit einem Draht, dessen Anfragen einen “ad hoc Antwortpin” mit zum Repository schicken:

image

Solche Reques/Response-Kommunikation kann zwar auch mit Flüssen modelliert und implementiert werden, doch das ist umständlich. Frage und Antwort in Input- und Output-Pin zu trennen, fühlt sich nicht intuitiv an.

Das führte dann dazu, von Akteuren weitgehend auf Aktionen umzustellen. Aktionen kennen per definitionem keine Rückgabewerte. Also stellt sich das Problem einer Request/Response-Modellierung nicht:

image

Das funktioniert gut. So lässt sich schneller ein Fluss entwerfen, weil nicht erst aus Anforderungen umständlich ein Akteur ermittelt werden muss; der liegt nämlich nicht so häufig auf der Hand, wie z.B. die Objektorientierung es gern hätte.

Allerdings wird damit der Bezug zu einer Ressource, wie sie hinter einem Repository steht, auf potenziell viele Funktionseinheiten verteilt:

image

Nicht, dass das sehr schlimm wäre. Damit lässt sich leben. Doch “reibungsfrei” fühlt sich das nicht an.

Und wo ist das Respository im Entwurf? Die drei Funktionseinheiten könnten auf eine Platine gelegt werden. Damit wäre aber wieder ein Akteur im Spiel, der nun zwar nicht das Request/Response-Problem zeigen würde, aber dessen Antworten sich nicht unbedingt Flüssen zuordnen ließen.

image

Das ließe sich durch explizite Lebenszeitverwaltung (Singleton/Multiton) beheben – aber irgendwie fühlt es sich immer noch nicht so richtig gut an. Warum soll ich zwingend die Aktionen in eine Akteur-Platine stecken, um ihren Zusammenhang im Modell zu verdeutlichen? Damit verliere ich einen Verständlichkeitsvorteil von Aktionen.

Problem #2: Aktionsübergreifender Zustand

Auch wenn ich keinen Drang verspüre, Aktionen zu einem Akteur zusammenzufassen, möchte ich manchmal, dass sie etwas gemeinsam haben: Zustand. Wie drücke ich in einem Flow Design aus, dass Funktionseinheiten, die zu ganz unterschiedlichen Concerns gehören, Zugriff auf denselben Zustand haben? Ich hatte mir dazu schon länger diese Notation ausgedacht:

image

Das war ok für mich, fühlte sich jedoch noch nicht rund an. Die Tonnen an den Funktionseinheiten brachen irgendwie aus der Notation aus. Und die Gemeinsamkeit von Zustand war nur über den Namen an den Tonnen ablesbar.

Abhängigkeiten als Lösung

Ich hätte es nicht gedacht, aber Akteurproblem und der gemeinsame Zustand lassen sich mit demselben Mittel lösen: mit Abhängigkeiten. Und das in ganz einfacher Weise, wie ich finde.

Funktionale Abhängigkeiten drücke ich in Flow Designs nun so aus:

image

In der UML werden Abhängigkeiten auch mit Pfeilen ausgedrückt. Das vermeide ich hier. Pfeile stehen für mich nur noch für Flüsse in Pfeilrichtung. Trotzdem muss eine Abhängigkeitsverbindung asymmetrisch sein. Abhängige und unabhängige Funktionseinheit müssen klar zu erkennen sein. Deshalb der Punkt am Ende der Abhängigkeitslinie.

Abhängigkeiten sind damit erstens überhaupt möglich und zweitens nicht im Weg, weil sie orthogonal zum Fluss verlaufen. Mit ihnen vorsichtig umzugehen, versteht sich von selbst. Abhängigkeiten bleiben “böse”. Aber bevor sich Flow Design in den Fuß schießt, um sie ganz zu vermeiden, ist es besser, Abhängigkeiten ausdrücken zu können.

Die beiden geschilderten Probleme lösen sich mit Abhängigkeitsmodellierung in Wohlgefallen auf, finde ich. In Problem #1 bleiben die Aktionen sichtbar und trotzdem ist zusammengehörige Funktionalität an einem Ort versammelt, in einer Repository-Funktionseinheit. Ob die Aktionen Singletons oder Multitons sind, ist nun nicht mehr wichtig. Das Repository kann aber ganz einfach ein Singleton sein.

image

In so einem Diagramm würde ich natürlich Repository nicht um weitere Abhängigkeiten anreichern. Ich sehe die unabhängige Funktionseinheit als Black Box. Wie es da drin aussieht, ob da wieder Flüsse definiert sind oder alles “traditionell” nach OOP funktioniert, das ist mir erstmal egal. Der Vorteil von Flow Designs, aus Abhängigkeitsverhauen zu befreien, darf nicht leichtfertig aufgegeben werden. Also vorsichtig mit solchen Abhängigkeiten.

Für mich ist auch immer Ziel, solche unabhängigen Funktionseinheiten intern dann soweit wie möglich mit Flows zu modellieren. Die Abhängigkeit würde dann dazu dienen, Flüsse zu entkoppeln.

image

Problem #2 löst sich mit Abhängigkeiten auch ganz einfach. Alle Aktionen hängen vom selben Zustand ab.

image

Die “Zustandstonne” steht dabei für eine generische Funktionseinheit zum Halten von Zustand, z.B. SharedState<T>. Auf so einer Funktionseinheit lassen sich auch sehr schön Funktionen anbringen, die nützlich in UI-Szenarien (MVVM etc.) und bei asynchronen Modellen sind.

Abhängigkeiten implementieren

Die Übersetzung von Flow-Design-Funktionseinheiten in Interfaces hat sich bewährt. Wie passt dazu die Einführung von Abhängigkeiten. Zunächst schien es, als sollten Abhängigkeiten dazu führen, dass für abhängige Funktionseinheiten keine Interfaces, sondern abstrakte Basisklassen mit Ctor-Injection generiert werden. Das kann man auch machen, wenn man mag – aber ich denke, es ist universeller, Abgängigkeiten über ein Interface auszudrücken. Beispiel zum vorangehenden Bild:

interface Do_sth_in_concern_A : IDependsOn<StateA>
{…}

interface Do_sth_in_concern_B : IDependsOn<StateA>
{…}

Der Start von Flow-Design-Code bekommt damit eine weitere Phase:

  1. Build
  2. Bind
  3. Inject – Abhängigkeiten via Interface injizieren
  4. Config
  5. Run

Build und Bind laufen eigentlich gleichzeitig ab, wenn Platinen die Verdrahtung im Ctor vornehmen, weil sie die Funktionseinheiten, von denen sie immer schon abhängig waren, per Ctor-Injection bekommen.

Danach werden die nun modellierbaren funktionalen Abhängigkeiten injiziert. Es müssen lediglich alle Funktionseinheiten darauf geprüft werden, ob sie das Interface implementieren. Dann bekommen sie eine Instanz der unabhängigen Funktionseinheit. Das kann z.B. so geschehen:

IDependsOn<StateA> do_sth_in_concern_a = …;
do_sth_in_concern_a.Inject(diContainer.Create<StateA>());

Die Konfigurations- und Run-Phase basieren dann auch auf Interfaces. Aber das ist nichts neues.

Ich finde, so ergibt sich ein sauberes Bild.

Und wie sieht so eine unabhängige Funktionseinheit aus? Ganz normal. Sie kann eine traditionelle OO-“Oberfläche” haben, einen ganz üblichen API. Die Flow-Funktionseinheiten, die davon abhängen kapseln den gegenüber dem restlichen Flow.

Zusammenfassung

Für mich löst sich mit der Möglichkeit, Abhängigkeiten auszudrücken, die derzeit letzte “Holprigkeit” in Flow Designs auf. Erste Erfahrungen mit der Modellierung mit und Übersetzung von Abhängigkeiten fühlen sich gut an. Und auch die Erklärung von Flow Design ist nun noch einfacher. Denn wer eine Hürde sieht, einen traditionellen API in Aktionen umzuformulieren, kann ihn (erstmal) behalten. Das erhöht die Attraktivität von Flow Design für Brownfield-Projekte, würde ich sagen.

Donnerstag, 13. Januar 2011

Verantwortlichkeiten abstecken

Zwei der wichtigsten Clean Code Developer (CCD) Prinzipien sind Single Responsibility Principle (SRP) und Separation of Concerns (SoC), die beide Spezialfälle des noch grundlegenderen Don´t Repeat Yourself (DRY) sind.

So wichtig diese Prinzipien aber auch sind, ich glaube, wir nehmen sie noch nicht wichtig genug. Oder anders: Wir durchdringen sie noch nicht genug. Wir schauen bei der Responsibility und den Concerns noch nicht genau genug hin.

Das möchte ich hier nachholen. Ich will noch genauer hinschauen. Wo versuchen wir heute noch nicht genügend zu trennen? Was denken wir als zusammengehörig, das eigentlich verschieden ist?

Concerns/Belange

Genau hinschauen beginnt oft mit einer Präzisierung von Begriffen. Welche Begriffe schweben denn im Raum der obigen Prinzipien. Da ist zuerst Concern/Belang. Was ist ein Concern?

Die Literatur unterscheidet Core Concerns und Cross-cutting Concerns, also Kernfunktionalität und Querschnittfunktionalität. Hilft das? Hm… Mir fehlt da ein Begriff, der der Domäne.

Concerns sind für mich Funktionalitätsdomänen. Wobei ich Domäne mal mit “fundamentaler Zweck” übersetze. Davon kann es viele geben in einer Software. Die wesentliche Domäne ist natürlich die Geschäftsdomäne des Auftraggebers. Aus ihr heraus entsteht der Wunsch nach einer Software. Für jemanden, der ein Tic Tac Toe Spiel bestellt, ist Tic Tac Toe die Geschäftsdomäne. Für jemanden, der einen online Shop für den Schuhverkauf bestellt, ist es der Schuhverkauf.

Neben der Geschäftsdomäne gibt es dann weitere Domänen. Ob die nun querschnittlich oder längschnittlich sind, find ich nicht so wichtig. Entscheidender ist zu erkennen, dass sie eben für andere Zwecke stehen. Funktionalität zum Füllen eines Warenkorbs in einem online Shop dient einem anderen Zweck als Funktionalität zur Absicherung des Zugriffs auf den Warenkorb oder Funktionalität für eine verzugsfreie Anzeige von Produktempfehlungen.

Vielleicht könnte man formulieren: Der Domain-Concern wird durch ein Hauptwort oder ein Verb beschrieben – und weitere Concerns können dem als Adjektive/Adverben beigeordnet sein, z.B.

  • sicherer Warenkorb
  • verschlüsselt bestellen
  • übersichtliche Lagerbestandsentwicklung
  • belastbare Produktabfrage
  • persistente Bestellung

Das gilt dann natürlich für jeden Concern. Eben standen Substantive/Verben immer für den Domain-Concern. Das muss aber nicht so sein. Auch andere Concerns lassen sich mit Beiordnungen qualifizieren:

  • schnelle Verschlüsselung
  • beobachtbar cachen
  • belastbare Persistenz

Insofern geht es eigentlich immer um einen Fokus-Concern (Substantiv/Verb) und qualifizierende Concerns (Adjektive/Adverbien). Vielleicht wäre es deshalb am besten, den Business-Concern eher mit Money-Concern zu betiteln? Denn das Geld, das der Kunde für ihn zahlt, ist das einzige, was ihn von anderen Concerns wirklich abhebt, würde ich sagen. Aus technischer Sicht sehe ich keinen wirklichen Unterschied zu anderen Concerns. Denn wer an Funktionalität im angeblichen Cross-cutting-Concern “Sicherheit” sitzt, der merkt nichts davon, dass der Cross-cutting ist. Er ist für ihn während der Zeit am wichtigsten.

Der Money-Concern steht natürlich am Anfang. Wegen ihm wird eine Software in Auftrag gegeben. Deshalb kann man ihn als Wurzel eines Concern-Baums betrachten:

image

Doch einmal in den Baum eingestiegen, gibt es für keinen Concern einen Vorrang mehr. Ein Baum ist selbstähnlich. Er sieht strukturell überall gleich aus; der qualifizierende Concern für einen anderen ist gleichzeitig Fokus-Concern weiterer.

Aspects/Aspekte

Cross-cutting Concerns werden auch Aspekt genannt. Das finde ich wenig hilfreich oder gar behindernd. Denn damit ist ein Begriff verbraucht, der anderweitig nützlich sein könnte. Ich würde mit Aspekt nämlich gern innerhalb von Concerns Unterscheidungen treffen.

Concerns “färben” Funktionalität im Sinne von Zwecken ein. Die können funktionalen oder nicht-funktionalen Anforderungen entsprechen.

Innerhalb von Concern-Funktionalität finden wir dann aber auch noch Unterschiedliches, dem Code dienen kann. Das würde ich gern mit Aspekt bezeichnen.

Zwei offensichtliche Aspekte scheinen Daten und Verhalten repräsentiert durch Felder und Methoden von Klassen in OO-Sprachen. Mir scheint jedoch, dass es lohnt, noch näher hinzuschauen. Dann meine ich diese Aspekte zu erkennen:

  • Code, der die Struktur von Daten definiert bzw. erhält; er dient der strukturellen, syntaktischen Konsistenz
  • Code, der die semantische Konsistenz von Daten sicherstellt
  • Code, der Daten verarbeitet
  • Code, der die Verarbeitung von Daten koordiniert

Beispiele für diese Aspekte sind leicht gebracht:

Syntaktische Konsistenz

Eine Klasse strukturiert Daten zunächst syntaktisch, d.h. sie fasst zusammen und setzt in Beziehung, was überhaupt grundsätzlich zusammen gehört, z.B.

class Kunde
{
  public string Name;
  public List<Adresse> Adressen;
}

Kunde ist ein Datentyp, der aus den Datentypen string und List<Adresse> zusammengesetzt ist. Dadurch bekommt er Grundfunktionalität, die eine Struktur definiert. Instanzen des Datentyps können dann jede Form im Rahmen der konstituierenden Typen annehmen. Ein Kunde-Objekt kann einen Namen mit 0 Buchstaben oder 1000 Buchstaben haben, er kann keine Adresse enthalten oder 1 Million Adressen.

Semantische Konsistenz

Alles was mit einem Datentyp syntaktisch möglich ist, ist aber nicht unbedingt innerhalb des definierenden Concern gewünscht. Es muss also eingeschränkt werden auf das, was ein Datentyp darstellen soll, auf seine Bedeutung. Semantische Regeln sind nötig. Die könnten für einen Kunden zum Beispiel sein, dass kein Kunde-Objekt ohne Name existieren darf oder dass es keine zwei Adressen in derselben Straße im selben Ort geben darf.

Für die obige Klasse läge es dann z.B. nahe, den Zwang zum Namen mithilfe eines Konstruktors und einer Validation in einer Property auszudrücken:

class Kunde
{
  public Kunde(string name)
  {
    this.Name = name;
  }

  private string _name;
  public string Name
  {
    get { return _name; }
    set {
          if (string.IsNullOrEmpty(value)
            throw SemanticError("Kunden müssen einen Namen haben!");
          _name = value; 
         }
  }

  …
}

Syntaktische und semantische Korrektheit sind nach außen durch den Kontrakt eines Datenmodells definiert. Sie drücken sich daher “an der Oberfläche” von Daten aus, in dem API den man benutzt, um mit ihnen umzugehen.

Und nach innen beziehen sich syntaktische wie semantische Konsistenz nur auf die zu einer Datenstruktur gehörenden Daten. Für die syntaktische ist das klar. Aber für die semantische mag das nicht so einfach zu schlucken sein. Doch ich meine es ernst: Semantische Konsistenz bezieht sich für mich nur auf die Daten innerhalb einer Datenstruktur; Beziehungen zu anderen, außerhalb liegenden Daten, sind dafür irrelevant. Semantische Korrektheit bedeutet, dass Veränderungen an Daten innerhalb eines gewissen Rahmens stattfinden müssen, der jenseits des struktuellen liegt – aber deshalb nicht grenzenlos ist. Zwei Beispiele:

  • Aufgabe der semantischen Konsistensicherstellung kann es sein, einen Kunden auf maximal drei Adressen zu begrenzen oder keine Dubletten bei den Adressen zuzulassen.
  • Keine Aufgabe der semantischen Konsistenzsicherstellung ist es jedoch, zu verhindern, dass einem Kunden eine Adresse zugewiesen wird, die schon bei einem anderen Kunden hinterlegt ist. Das würde über die zu einer Datenstruktur Kunde wie oben skizziert gehörenden Daten hinausgehen.
Datenverarbeitung

Wenn Daten syntaktisch und semantisch so sind, wie sie sein sollen, dann kann man mit ihnen etwas anfangen. Dann kann die Datenverarbeitung beginnen. “Und Action bitte!” könnte man sagen ;-)

Es könnten zum Beispiel Kunden mit einem gewissen Umsatz in einem gewissen Zeitraum einen Gutscheincode mit Verfallsdatum per Email geschickt bekommen. Dazu müssten verschiedene Daten verschiedener Typen durchsucht und aggregiert werden, andere Daten würden erzeugt und es würde ein Seiteneffekt (Email) generiert. Vielleicht so:

void Gutschein_verteilen(int mindestumsatz,
                         Zeitraum umsatzZeitraum,
                         double gutscheinbetrag,
                         string emailtext)
{
  foreach(var kunde in DB.LadeAlleKunden())
  {
    var zeitraumUmsatz = DB.LadeRechnungenEinesKunden(kunde.Kundennr)
                        .Where(rg => umsatzZeitraum.Enthält(rg.Datum))
                        .Sum(rg => rg.Nettoumsatz);
    if (zeitraumUmsatz >= mindestumsatz)
    {
      var gutschein = new Gutschein(kunde, gutscheinbetrag);
      DB.SpeichereGutschein(gutschein);
      Email.SendenAnKunde(kunde, emailtext);
    }
  }
}

Datenverarbeitung nutzt also Daten über deren Oberfläche. Sie selektiert, transformiert, generiert, verändert sie. An Details der Syntax oder Semantik der einzelnen Daten ist Verarbeitung nicht interessiert. Verarbeitung geht mit Black Boxes im Sinne eines gewissen Zwecks um.
Koordination

Datenverarbeitung findet in Schritten statt. Egal wie groß die sind, es sind immer mehrere Schritte. Am einfachsten zu erkennen ist das, wenn mehrere Methoden daran beteiligt sind.

Das ist selbstverständlich – aber genau darin liegt das Problem. Da es ohne Schritte in einer bestimmten Reihenfolge nicht geht, nehmen wir die Herstellung der Reihenfolge nicht als einen eigenen Aspekt wahr.

Aber es ist etwas anderes, einen Umsatz zu berechnen oder eine Email zu verschicken oder einen Gutschein zu erzeugen, als diese Funktionalitäten in eine bestimmte Reihenfolge zu bringen.

Die obige Methode Gutschein_verteilen() hat vor allem die Aufgabe, Arbeitsschritte im Sinne der Aufgabe “Gutschein verteilen” zu koordinieren. Erst muss dies geschehen, dann dann jenes, schließlich noch etwas anderes. Sie stellt einen Schritt-für-Schritt-Verarbeitungsfluss her.

Sie tut allerdings noch etwas anderes. Sie bringt Schritte nicht nur in eine Sequenz, sondern trifft auch noch Entscheidungen. Das heißt, sie verarbeitet auch Daten.

image

Ist das nicht normal? Doch, das ist ganz normal. Die Methode ist typisch. Das ist ja mein Punkt: Deshalb ist es so schwer zu sehen, dass es den Koordinationsaspekt überhaupt gibt.

Responsibilities/Verantwortlichkeiten

Mit der Zerlegung in Concerns und Aspekte sind Funktionseinheiten noch nicht fein genug definiert, würde ich sagen. Die Verarbeitung von Daten (Aspekt) in einem Cache (Concern Performance) ist sicherlich nicht mit einer Funktionseinheit erledigt. Also gilt es, auch Funktionalität noch zu zerlegen in überschaubare Verantwortlichkeiten.

Das führt für mich schließlich zu einem konsequent prinzipienbasierten Codebaum:

image

Zu einer Anforderung ist zunächst festzustellen, welche Concerns an ihr beteiligt sind. Innerhalb der Concerns geht es dann um die Identifikation der Aspekte. Und pro Aspekt ist dann die Funktionalität in überschaubare Verantwortlichkeiten zu gliedern.

Vielleicht hört sich das sogar noch gar nicht so merkwürdig an. Clean Code Developer bemühen sich darum in irgendeiner Weise sicher jeden Tag. Aber ich glaube, im Ergebnis ist eine so systematische Trennung selten. Sie passt nämlich nicht so recht ins übliche OO-Denken.

Praktische Konsequenzen für Entwurf und Implementierung

Nun zur Disziplin des Prinzipienreitens. Ich nehme das entwickelte Schema mal ernst. Das bedeutet: Jede Funktionseinheit dient nur einem Aspekt. Code, der Datensyntax definiert, tut dann nichts anderes. Code, der semantische Konsistenz herstellt, tut dann auch nichts anderes. Ebenso Aktionen und Koordinationen: sie tun nichts anderes. Aktionen koordinieren nicht, Koordinationen tun nichts.

Wie sieht dann Code aus? Anders als vorher? Ja, ich denke. Als Beispiel mag diesmal ein Tic Tac Toe Spiel dienen.

Syntax

Ein Tic Tac Toe Programm hat sicherlich einen Zustand, es hält also Daten. Das könnte ein Spielstand sein, der Auskunft gibt, welcher Spieler dran ist, ob das Spiel noch läuft (oder durch Patt oder Gewinn beendet ist) und wie die Spielsteine auf dem Spielbrett liegen.

enum Spieler
{
  Keiner, Kreuz, Kreis
}

enum Spielmodus
{
  Läuft, KreuzHatGewonnen, KreisHatGewonnen, Patt
}

class Spielstand
{
  public Spieler SpielerAmZug = Spieler.Kreuz;
  public Spielmodus Modus;
  public Spieler[,] Spielbrett = new Spieler[3,3];
}

Dieser Code definiert eine Datenstruktur, ein Schema. Er steht für den Aspekt “Syntaktische Konsistenz” des Concern “Money”, also der Businessdomäne. Die syntaktische Konsistenz stellt der Code ohne weiteres Zutun her.

Semantik

Jetzt die semantische Konsistenz. Zu ihr gehört, dass Spielfelder nur einmal von Keiner auf Kreuz oder Kreis gesetzt werden können oder dass, wenn das Spiel beendet ist, kein Spieler mehr dran ist und auch keine Veränderungen mehr am Spielbrett vorgenommen werden können. Wohin mit dieser Funktionalität?

Ich behaupte mal keck: Wenn “Semantische Konsistenz” ein anderer Aspekt ist als “Syntaktische Konsistenz”, dann gehört seine Implementation nicht in die Funktionseinheiten des Aspekts “Syntaktische Konsistenz”. Also brauche ich eine weitere Funktionseinheit, z.B. in dieser Weise:

class Spiel
{
  private Spielstand _spielstand;
  …     
}

Die Aufgabe von Spiel ist, die Datenstruktur vor semantisch inkonsistenten Veränderungen zu schützen. Das bedeutet, sie darf den Spielstand nicht nach außen zeigen, denn sonst könnte ja jeder nach Belieben darauf schreibend zugreifen. Stattdessen muss sie ihn hinter einer Reihe von domänenrelevanten Operationen verstecken. Das ist sogar Objektorientierung par excellence, denn das nennt sich Kapselung.

Naheliegend scheint für Tic Tac Toe die Operation Ziehen(). Mit ihr wird ein Stein für den aktuellen Spieler auf ein Spielfeld gesetzt; die Umgebung soll sich nicht darum bekümmern, ob das ein Kreis oder ein Kreuz ist. Beim Ziehen verändern sich Daten, deshalb ist es ein Kommando (Command) im Sinne von CQS.

Wer anschließend mit dem neuen Spielstand arbeiten will, darf allerdings nicht darauf hoffen, dass Spiel ihn so einfach liefert wie Spielstand. Spiel mag Daten haben, aber Spiel selbst ist keine Datenstruktur. Spiel ist nur für Semantik, nicht für Syntax zuständig. Spiel hat deshalb keine Properties. Im Sinne von DDD würde ich Spiel daher eine Entität nennen.

Irgendwie muss ja aber z.B. die Anzeige des Programms mindestens das Spielbrett in die Hand bekommen, um es anzuzeigen. Dazu kann Spiel eine Abfragemöglichkeit (Query) anbieten, z.B. AktuellerSpielstand().

class Spiel
{
  private Spielstand _spielstand

  public void Ziehen(int spielfeldindex) { … }

  public Spielstand AktuellerSpielstand() { … }
}

Wie der Spielstand auf die Query zurückgegeben wird, ist natürlich die Frage. Da sehe ich mehrere Abstufungen.

  1. Der Spielstand kommt in einer speziellen Datenstruktur zurück, die nur dafür da ist, gelesen zu werden, z.B. SpielstandView.
  2. Der interne Spielstand ist so definiert, dass nur Spiel ihn überhaupt verändern kann. Ihn rauszureichen, bringt ihn also nicht in Gefahr.
  3. Der interne Spielstand wird kopiert in einen neuen Spielstand. Der könnte zwar von der Umwelt von Spiel verändert werden, doch das würde den internen Spielstand nicht in Gefahr der Inkonsistenz bringen.
  4. Der interne Spielstand wird ohne weiteres komplett rausgereicht in dem Vertrauen, dass niemand in der Umwelt auf ihn schreibend zugreift.

Ja nach Anwendung, Entität, Teamorganisation kann eine andere Option angemessen sein, um Zustand aus einer Entität rauszureichen. Am saubersten wäre Option 1 – aber auch am aufwändigsten. Am einfachsten wäre Option 4 – aber auch am risikoreichsten.

Ich persönlich hätte in einer kleinen Tic Tac Toe Anwendung kein Problem, Option 4 zu wählen. Der Zustand ist trivial und es gibt auch nicht mehrere Sichten darauf. Für andere Szenarien würde ich mich aber sicher auch mal anders entscheiden.

Alles in allem ist das für diesen Artikel jedoch eine Nebensächlichkeit. Wesentlich finde ich die deutliche Trennung von syntaktischer und semantischer Konsistenzverantwortlichkeit.

Beim Datenmodell diese beiden Aspekte auseinander zu halten, scheint mir zunehmend wichtig. Es sind zwei Seiten einer Medaille.

Aktionen

Genauso sehe ich es inzwischen bei den Aspekten Verarbeitung und Koordination. Sie sind für mich nämlich die zwei Seiten der Medaille Prozess. Oder vielleicht sollte ich Aktion statt Verarbeitung sagen? Dann bestünde der Prozess aus Aktion und Koordination. Geht besser über die Zunge, oder? ;-)

Ja, das fügt sich jetzt noch ein bisschen schöner. Da sind Daten und Prozess und zu beiden gehören zwei Aspekte:

  • Datenmodell
    • Syntax
    • Semantik
  • Prozess
    • Aktion
    • Koordination

Beim Datenmodell geht es, nun, um Daten. Da ist alles ein bisschen steifer ;-) Daten sind halt Daten sind halt Daten. Die stehen nur in der Gegend herum – bis jemand mit ihnen etwas tut. Das ist dann ein Prozess. Der hat Eingaben und Ausgaben und liest aus Ressourcen und verändert Ressourcen. Das alles sind Daten, also ist ein Prozess der Datenverarbeiter schlechthin.

Der tut seine Sache meistens in mehreren Schritten, in einzelnen Aktionen. Jede Aktion trägt zum Prozessergebnis ein Stück bei. Jede Aktion sollte dafür in einer eigenen Funktionseinheit stehen. Beispiel Tic Tac Toe: Nach einem Zug muss der neue Modus ermittelt werden. Liegt ein Gewinn vor oder Patt oder ist nur der andere Spieler dran?

Das hat nichts mit der syntaktischen oder semantischen Konsistenz der Daten zu tun. Hier geht es um einen Prozess, der auf einem Spielstand arbeitet, ihn auch verändert – aber nicht im Sinne einer Datenkonsistenz. Nachdem Ziehen() ausgeführt wurde, sind die Daten konsistent. Wenn nun aus ihnen ermittelt wird, ob ein Gewinn vorliegt, dann trägt das nichts zur Konsistenz bei. Falls jedoch nach der Prüfung der Modus verändert werden soll, dann selbstverständlich in konsistenter Weise.

Wenn ich die Prinzipien, Concerns und Aspekte weiter ernst nehme, dann bedeutet das, die Funktionseinheit zur Modusermittlung muss wiederum separat sein von der Datenstruktur und der Entität. Weder Spielstand noch Spiel können zuständig dafür sein festzustellen, ob ein Gewinn vorliegt. Der Prozess dafür muss woanders aufgehängt sein. Wie wäre es mit eniem Schiedsrichter, der nach jedem Zug das Spielergebnis prüft?

class Schiedsrichter
{
  public void SpielergebnisBeurteilen(Spiel spiel)
  {
    var spielstand = spiel.AktuellerSpielstand();
    Spielmodus gewinnerSpielmodus;
    if (GewinnFeststellen(spielstand.Spielbrett,
                          out gewinnerSpielmodus))
      spiel.SpielBeenden(gewinnerSpielmodus);
    else if (PattFeststellen(spielstand.Spielbrett))
      spiel.SpielBeenden(Spielmodus.Patt);

  }

  private bool GewinnFeststellen(Spieler[,] spielbrett) {…}

  private bool PattFeststellen(Spieler[,] spielbrett) {…}
}

Der Schiedsrichter schaut sich das Spielbrett des Spielstands des Spiels an, beurteilt ob ein Gewinn vorliegt usw. und teilt ggf. dem Spiel den neuen Modus mit. Das kann dann in semantisch konsistenter Weise tun, was immer es tun muss, um seinen Zustand zu ändern.

Zu sehen sind verschiedene Aktionen mit fokussierter Verantwortlichkeit: GewinnFeststellen() und PattFeststellen(). Dazu kommt ein weiteres Kommando auf den Daten: SpielBeenden().

Aktionen sollen nur eines sein: atomare Funktionalität im Sinne eines Concerns. Von GewinnFeststellen() und PattFeststellen() nehme ich das einmal an. Doch diese Forderung gilt auf jeder Ebene. Eine Funktionseinheit soll sich entscheiden, welchen Aspekt sie implementiert. Und wenn sie sich für Aktion entscheidet, dann soll sie sich darauf konzentrieren. Wie steht es in dieser Hinsicht mit SpielergebnisBeurteilen()?

Koordination

SpielergebnisBeurteilen() implementiert keine atomare Funktionalität im Sinne eines Concerns. Sie ist zusammengesetzt, da sie ja Aktionen und Kommandos aufruft. SpielergebnisBeurteilen() kann daher nicht dem Aspekt Aktion/Datenverarbeitung angehören. Auch geht es nicht um Datenkonsistenz.

Es bleibt nur der Schluss, dass SpielergebnisBeurteilen() zum Aspekt Koordination gehört. Das bedeutet für mich, die Methode muss sich darauf beschränken, den Verarbeitungsfluss zu regeln. Ihre Aufgabe ist ausschließlich zu koordinieren, welche Aktion auf welche folgt.

Das tut SpielergebnisBeurteilen() vor allem mit Sequenzen, z.B. folgt auf spiel.AktuellerSpielstand() immer GewinnFeststellen(). Aber auch Fallunterscheidungen kommen zum Einsatz.

Die finde ich knifflig bei der Koordination. Sie sollten vermieden werden, weil sie Domänenfunktionalität darstellen. In einer if-Bedingung steckt immer ein wenig Logik des Concerns. Das kontaminiert den Aspekt-Code.

Deutlicher ist das oben in Gutschein_verteilen(). Dort steht ein Ausdruck in der Bedingung, der morgen anders sein könnte, weil sich eine Geschäftsregel geändert hat. Wird auf solch ein Detail nicht geachtet, verteilt sich Domänenlogik schnell nicht nur horizontal auf derselben Abstraktionsebene, sondern auch vertikal. Damit ist dann keine saubere Hierarchisierung zur Abstraktion und damit zur Bewältigung von Kompliziertheit/Komplexität mehr möglich.

Für den vorliegenden Fall würde ich die if-Anweisungen akzeptieren, weil der Code überschaubar ist und die konkreten Bedingungen in einer Funktion versteckt sind. Ganz allgemein würde ich aber empfehlen, Koordinationscode anders aussehen zu lassen als Aktionscode, um nicht Gefahr zu laufen, dass “action creep” einsetzt. Wenn nicht sonnenklar ist, dass Code nur koordinieren soll, kommt irgendwer früher oder später auf den Gedanken, ihn mit anderen Aspekten “zu verschmutzen”.

Wie könnte so ein anderes Aussehen aussehen? Mir fallen dazu natürlich Event-Based Components ein ;-) Wer hätte das gedacht. Das könnte ungefähr so gehen:

class SpielergebnisBeurteilen
{
  public SpielergebnisBeurteilen(GewinnFeststellen gewinnFeststelen,
                                 PattFeststellen pattFeststellen,
                                 Spiel spiel)
  {
    _process = gewinnFeststellen.Process;
    gewinnFeststellen.Gewonnen += spiel.Gewonnen;
    gewinnFeststellen.NichtGewonnen += pattFeststellen.Process;
    pattFeststellen.Patt += spiel.Patt;
  }

  private Action<Spieler[,]> _process;
  public void Process(Spieler[,] spielbrett)
  {
    _process(spielbrett);
  }
}

Die Klasse SpielergebnisBeurteilen ist jetzt ein reiner Koordinator. Im Konstruktor wird der Datenfluss zusammengesteckt, d.h. die Zusammenarbeit der Aktionen koordiniert. Das sieht etwas kryptisch aus – an dieser Stelle sage ich aber mal, das ist ein Feature. Denn weil das so aussieht, kommt man kaum auf die Idee, den Code anzufassen, um mal eben noch ein bisschen Domänenlogik reinzufummeln.

Verstehen soll man den Code auch nicht. Der kann nämlich generiert werden. Verstehen soll man das Modell des Codes, sozusagen den Koordinationsplan:

image

Zusammenfassung

Ich glaube, wir sollten die Prinzipien SRP und SoC noch ernster nehmen, wenn wir wahrhaft sauber programmieren wollen. Sie sollten uns leiten – und nicht ein irgendwie geartetes Werkzeug wie eine Programmiersprache.

C# & Co bieten eine Hand voll Ausdruckmittel. Die sind an sich neutral. Dass man Daten und Methoden in einem Konstrukt zusammenfassen kann, bedeutet nicht, dass man es in einer gewissen Weise tun muss. Wenn es eine Klasse Kunde gibt, dann muss die nicht deshalb eine Property Name und auch noch eine Methode BestimmeOffenePost() haben.

Statt einer naiven oder gar falschen Abbildung der Welt aufzusitzen sollten wir uns von Prinzipien leiten lassen. Das bedeutet für mich, dass Code, der unterschiedlichen Concerns und unterschiedlichen Aspekten angehört, sauber getrennt wird.

Code, der Struktur definiert, sollte nur das tun. Code, der semantische Strukturkonsistenz sicherstellt, sollte nur das tun. Code, der Daten verarbeitet, sollte nur das tun. Code, Datenverarbeitung koordiniert, sollte nur das tun.

Damit uns diese Separation leicht fällt, sollten wir für sie klare Ausdruckweisen mit den Mitteln unserer Programmiersprachen suchen. Die könnten zum Beispiel sein:

  • Datenstrukturen (Syntax) werden über Klassen/Strukturen definiert.
  • Die semantische Konsistenz von Datenstrukturen stellt ein Klasse sicher, die sie als Zustand enthält und ihre konkrete Form gegenüber der Außenwelt verbirgt.
  • Datenverarbeitung, d.h. Domänenlogik, findet in Aktionsklassen statt (EBC-Bauteile). Diese Klassen sollen sehr feingranular sein, um sie wiederverwenden zu können und sie nicht mit Koordination aufzuladen.
  • Koordination der Datenverarbeitung findet in speziellen Koodinationsklassen statt (EBC-Platinen). Ihre Aufgabe ist ausschließlich, Aktionen zu Prozessen zusammen verbinden.

Objektorientierung behält damit ihren Platz. Das ist gar keine Frage. Aber ihr Bedeutung verändert sich. Objektorientierung ist nicht mehr “für alles zuständig”, ist nicht mehr die alleinige Brille, durch die Software gesehen werden sollte. Stattdessen sollten ihre Mittel, wie sie eine Sprache wie C# implementiert, differenziert im Sinne der beiden großen Aspektgruppen Datenmodell und Prozess eingesetzt werden:

image

Interessant ist dabei die semantische Konsistenz. Dort begegnen sich traditionelle OOP und “POP” (Prinzipienorientierte Programmierung). Denn: Semantische Konsistenz kann nur über Prozesse hergestellt werden.

image

In diesem Sinne: Alles fließt – aber wir sollten gut darauf achten, wo. Lassen wir uns das Denken nicht durch ein Werkzeug vernebeln, sondern denken wir in Prinzipien.

Montag, 3. Januar 2011

Software-Prüfstände: Das fehlende Glied in der agilen Softwareentwicklung

Software soll in dünnen Längsschnitten jeden Tag geliefert werden. Dass das mit Software geht, weil sie nicht aus Materie besteht, habe ich hier beschrieben. Und warum das sinnvoll im Sinne eines schnellen Lern- und Klärungsprozesses für den Kunden ist, habe ich hier erklärt. Schließlich habe ich noch hier ein Beispiele dafür gebracht, wie das konkret aussehen könnte.

Dieses Denken macht für mich den Kern von agilem Vorgehen aus. Wenn ich Agilität auf einen Begriff eindampfen sollte, dann wäre das “Längsschnitt”. (Und wenn ich Lean auf einen Begriff eindampfen sollte, dann wäre das “Pull”. (Den Wert von “Pull” hatte ich schonmal in einem Traum angedeutet; aber ein andermal mehr davon. Heute geht es mir nur um Agilität.))

Immer wieder höre ich dann jedoch den Einwand, dass sich nicht immer Längsschnitte durch eine Software legen ließen, die in einem Tag realisiert werden können. Dem widerspreche ich erstmal und behaupte, dass mit etwas Systematik, Bewusstheit und Kreativität das immer möglich sein sollte. Aber ich will anerkennen, es mag nicht immer einfach sein. Deshalb hier ein Vorschlag, der tägliche Lieferung und damit tägliche Zufriedenheit für das Team erreichbarer macht.

Wat is ne Feature?

Im Zentrum der Diskussion um Längsschnitte steht für mich ein Begriff: Feature. Der Kunde beschreibt seine Wunschsoftware in Use Cases oder User Stories. Die sind aber zu groß, als dass man sie zügig umsetzen könnte. Also zerlegt das Team sie in Features.

Und was ist ein Feature? Ein Feature beschreibt eine überschaubare Funktionalität, d.h. es stellt Kundennutzen dar. Seine Überschaubarkeit gegenüber einer User Story resultiert aus einer gewissen Formalisierung. Zu einem Feature gehören für mich:

  1. Eine Interaktion der Umwelt mit der Software, die den zum Feature gehörigen Code – “Featureprozess” - anstößt. Das kann ein Button-Klick sein, eine Mausbewegung, ein Tastendruck oder ein Webserviceaufruf.
    Ein “Featureprozess” wird durch eine Interaktion zur Zeit in Gang gesetzt. Es können ihm aber auch verschiedene Interaktionen zugeordnet sein. Oder es kann eine Interaktion mehrere Features anstoßen.
  2. Eingabedaten, die der “Featureprozess” verarbeitet. Eine leere Eingabe ist auch möglich.
  3. Ausgabedaten, d.h. das Ergebnis, das der “Featureprozess” produziert. Es kann auch sein, dass keine Ausgaben produziert werden. Eingabe und Ausgabe beziehen sich auf die Umwelt, die mit der Software interagiert. Die Eingaben kommen aus der Umwelt von einer Rolle, die den “Featureprozess” aufruft; die Ausgaben gehen zurück an diese Rolle.
  4. Ressourcen, die der “Featureprozess” nutzt. Das kann eine Datenbank sein oder ein Drucker, der Zugriff kann lesend und/oder schreibend sein. Wo er schreibend ist, erzeugt der “Featureprozess” einen Seiteneffekt. Ressourcen stellen Abhängigkeiten für den “Featureprozess” dar.

Ein Feature kann also mit einem Quadrupel beschrieben werden, das aus den Mengen I (für die auslösenden Interaktionen), E (für die erwarteten und erlaubten Eingaben), A (für die erwarteten Ausgaben) und R (für die Ressourcen) besteht: Feature = (I, E, A, R).

Als Flow Design Diagramm könnte ein Feature dann so aussehen:

image

Ein Feature einer Tic Tac Toe Software ist sicherlich, ein neues Spiel zu starten (F1):

  1. Interaktion: Klick des Menüpunktes Game|New, aber es könnte auf dem iPhone auch eine Schüttelgeste sein.
  2. Eingabe: keine
  3. Ausgabe: der Spielstand eines neuen Spiels
  4. Ressourcen: keine

Für das Feature “Neues Spiel” gilt also 1 Interaktion = 1 Feature.

Anders bei beim Klick auf ein Spielfeld. An dieser Interaktion hängen mehrere Features:

image

Das erste Feature führt den Zug aus (F2), das zweite Feature prüft, ob durch den Zug das Spiel gewonnen wurde (F3). Zug ausführen kann unabhängig von einer Gewinnermittlung gedacht werden. Deshalb ist es allemal sinnvoll von zwei Features zu sprechen.

Als Quadrupel notiert sehen die Features z.B. so aus:

  • F1 = ({Game|New, Schütteln, Programmstart}, {}, {Spielstand}, {})
  • F2 = ({Klick auf Spielfeld}, {Zug}, {Spielstand}, {})
  • F3 = ({Klick auf Spielfeld}, {Spielstand}, {Spielstand}, {})

Damit lässt sich doch schon was anfangen, oder? Jedes Feature stellt Kundennutzen dar. Jeden Tag ein Feature realisieren, würde das Team zufrieden stimmen und dem Kunden zügiges Feedback ermöglichen.

Feature Slicing

Was nun, wenn die Features noch zu groß für die Umsetzung innerhalb eines Tages sind? Dann heißt es klein schneiden. Stefan Lieser und ich nennen das Feature Slicing. Jeden Tag nur ein Feature Slice realisieren, ist auch eine gute Tat ;-)

Natürlich will der Kunde eigentlich nur Features als Ganzes – so wie er auch die Software eigentlich nur komplett fix und fertig als Ganzes will. Tja… wir würden ihm die auch gern so liefern, doch so funktioniert Softwareentwicklung nicht. Darauf zu hoffen, dass Entwickler aus einem dicken Anforderungskatalog ohne Murren und Nachfragen im stillen Kämmerlein eine korrekte und vollständige Software zimmern, ist naiv.

Alle Parteien sollten vielmehr daran interessiert sein, schnell und häufig Feedback zu bekommen und zu geben. Ja, das mag gerade für den Kunden nervig sein… aber Zugfahren ist in Zeiten von Schneechaos auch nerviger als Teleportation. Da die jedoch noch nicht erfunden ist, muss man in den sauren Apfel des Zugfahrens beißen. (Oder Auto fahren oder fliegen: einerlei, alles ist nerviger als Teleportation, da sind wir uns doch einig, oder? ;-) Dito für das häufige Feedback: nervig, jedoch unausweichlich, eine sine qua non.

Also: jeden Tag ein Feature Slice liefern. So kommt Software verlässlich voran: “A feature slice a day keeps the lawyer away”, wie die Engländer sagen – oder so ähnlich ;-)

Wie sähe das für die Tic Tac Toe Features aus, wenn wir mal annehmen, dass die zu groß für eine Realisierung in einem Tag sind?

  1. F1.1: Programm startet und zeigt ein nicht leeres Spielfeld an
  2. F1.2: Anwender kann ein neues Spiel beginnen
  3. F2.1: Anwender kann ins Spielfeld klicken und es wird immer ein Kreuz gesetzt
  4. F2.2: Anwender kann ins Spielfeld klicken und es wird ein Kreus bzw. ein Kreis gesetzt, jenachdem welcher Spieler am Zug ist; Spielfeldinhalte werden dabei überschrieben
  5. F2.3: Züge auf schon  besetzte Felder werden nicht ausgeführt
  6. F3.1: Ein Zug in das erste Spielfeld wird als Gewinn erkannt
  7. F3.2: Drei Spielsteine desselben Spielers in einer Spalte werden als Gewinn erkannt
  8. F3.3: Drei Spielsteine desselben Spielers in einer Zeile werden als Gewinn erkannt
  9. F3.4: Drei diagonale Spielsteine desselben Spielers werden als Gewinn erkannt

Die Feature Slices können in dieser Reihenfolge realisiert werden oder aber auch in der Reihenfolge F1.1, F2.1, F2.2, F3.1, F2.3, F1.2, F3.2, F3.3, F3.4. Und so sähe dabei die Entwicklung des Modells über die ersten sieben Tage aus:

image

Jeden Tag bekommt der Kunde ein bisschen mehr zu sehen. Jeden Tag kann er ein bisschen Feedback geben und damit der Entwicklung zeigen, ob sie noch auf Kurs ist. Oder er kann sogar das Projekt für beendet erklären. Vielleicht ist er erstmal zufrieden, wenn F2.3 realisiert ist? Er verzichtet auf eine automatische Gewinnerkennung und ist zufrieden mit dem Programm als “elektronisches Papier”, auf dem gespielt werden kann. Oder ihm ist plötzlich wichtiger, dass ein menschlicher Spieler gegen einen “TicTacToeBot” spielen kann. Oder er möchte, dass man verteilt spielen kann, d.h. ein Spieler an diesem PC, ein anderer an einem anderen.

Wenn Feature Slices tagesdünn sind, kann der Kunde jeden Tag sein Verständnis der Problemdomäne aktualisieren und darauf aufbauend den Kurs ändern. Selbst Scrum erlaubt das eigentlich nur zu Beginn eines Sprints, also alle 2-3 Wochen. Im Durchschnitt läuft also die Entwicklung von 1-1,5 Wochen in eine potenziell verkehrte Richtung. Man möge das mit dem Tagessatz des Teams multiplizieren, um einen Eindruck vom Schaden zu bekommen, der durch infrequentes Feedback, d.h. einer großen Menge von “work in progress” (WIP) entstehen kann.

Mehr Flexibilität mit Prüfständen

Mit Feature Slicing, d.h. ganz dünnen Längsschnitten durch die Software, kommt man dem Ideal täglicher Lieferung schon sehr nahe, würde ich sagen. Ein Rest Unsicherheit bleibt jedoch: Können Feature Slices als ganze Längsschnitte einen Tag dünn geschnitten werden? Was, wenn gerade die Oberfläche, durch die der Kunde testen soll, ein Engpass ist?

Dazu kommt, dass die Entwicklung in Features Slices in Form von Durchstichen Abhängigkeiten berücksichtigen muss. Das Feature “Gewinn prüfen” kann nicht eher als an Tag 4 angegangen werden - zumindest, solange die Überprüfung durch den Kunden von einer Interaktion mit der Benutzeroberfläche abhängig ist.

Selbst bei einem so einfachen Szenario wie Tic Tac Toe gibt es Abhängigkeiten, die wie ein Korsett wirken:

image

Feature Slice F1.2 ist abhängig von F1.1, ebenso F2.1. F2.2 ist abhängig von F2.1 usw. Die Pfeile im Abhängigkeitsdiagramm zeigen vom Abhängigen zum Unabhängigen. Sobald Unabhängiges realisiert ist, kann das Abhängige angegangen werden. Wenn F1.1 und F2.1 und F2.2 vorliegen, steht der Weg frei für F1.2 oder F2.2 oder F3.1, aber noch nicht F3.2.

Das ist misslich, wenn Abhängiges risikobehaftet ist. Im Beispiel Tic Tac Toe könnte das die Gewinnermittlung sein, oder – falls der Kunde den Kurs geändert haben sollte – ein TicTacToeBot. Dann wäre es doch schön, Code für ein solches Features möglichst früh angehen zu können, um zu sehen, ob es realisierbar ist und welches Feedback der Kunde zu solch kritischem Anforderungsaspekt liefern kann.

Dem widerspricht bisher, dass die Gewinnermittlung ein UI und einige Feature Slices braucht, um vom Kunden getestet werden zu können. Was also tun?

Die Antwort kommt aus Maschinenbau und Elektrotechnik und heißt: Prüfstand.

Hier ein Prüfstand für Motoren:

image

Hier einer für Stoßdämpfer:

image

Und hier einer für eine Computerplatine:

image

Alle tun es. Nur die Softwareentwicklung nicht. Alle nutzen Prüfstände, um Subsysteme in Isolation zu testen.

Die Softwareentwicklung kennt das zwar auch beispielsweise in Form von Unit Test Frameworks. Doch ein Unit Test Framework ist kein Werkzeug, das man einem Kunden in die Hand geben kann.

Wenn ich Prüfstand sage, dann meine ich ein eigenständiges Programm, das den Zweck hat, eine Funktionseinheit, die an beliebiger Stelle eines “Featureprozesses” stehen kann, dem Kunden (!) für den Test zugänglich zu machen. Wie das geschieht, ist einerlei. Wenn es reicht, dafür einem FIT (Framework for Integrated Tests) zu nehmen, dann gern. Aber wenn das nicht reicht, dann sollte man sich nicht scheuen, einen Prüfstand selbst zu bauen. Das muss nicht schwierig sein, da es dabei nicht um Benutzerfreundlichkeit und Robustheit geht. Es ist nur ein (temporärer) Testrahmen für ein Subsystem. Der selbst darf ruhig etwas fragil sein; es geht darum, was darin aufgehängt ist. Dessen Korrektheit und Robustheit zu testbar zu machen, ist sein Zweck.

Wie könnte das für Tic Tac Toe aussehen? Wenn die Gewinnerkennung ein Feature wäre, das von einem frühzeitigen Feedback profitieren könnte, wie könnte dann ein Prüfstand aussehen? F3.1 hat eher mit der Gewinnanzeige im UI zu tun. Es ginge also um F3.2, F3.3 und F3.4.

Wie wäre es damit?

image

Ein supersimples UI. Auf Knopfdruck wird der Text, der ein Spielbrett repräsentiert, in einen Spielstand (GameState) übertragen, an die Gewinnermittlung übergeben und das Ergebnis angezeigt.

image

Damit könnte die Entwicklung beginnen, um vom Kunden Feedback nur zu diesem kritischen Aspekt der Anforderungen zu bekommen. Wenn die Lieferung der Feature Slices zu F3 dann positives Feedback bekommen, kann die Entwicklung die anderen Features angehen. Die Funktionseinheiten von F3 passen in deren Code, weil entweder schon vorher ein umfassenderes Modell entworfen wurde oder die Einpassung durch Flow Design einfach ist.

Zusammenfassung

imageDas Objektspektrum 1/2011 hat auch schon über Prüfstände berichtet. Wir können von der Industrie lernen. Prüfstände helfen, die Entwicklungsreihenfolge zu flexibilisieren und das Ideal täglicher Nutzenlieferung zu erreichen.

Prüfstände machen Akzeptanztests von “tieferliegenden” Features möglich. Es sind sozusagen Unit Tests, die der Benutzer durchführt. Sie setzen nicht an der Oberfläche einer Software an – dann wären es Integrationstests –, sondern an beliebiger Stelle darunter. Solange die prüfbare Funktionalität für den Kunden verständlich ist, also einen gewissen Nutzen hat, kann ein Prüfstand lohnend sein. Dann kann der Kunde motiviert werden, sie damit vorläufig zufrieden zu geben, um das Projekt in optimaler Weise voran zu bringen.

Wichtig dabei zu betonen: Prüfstände sind keine Prototypen. Ein Prüfstand macht Produktivcode zugänglich. Was im Prüfstand hängt, wird nicht weggeschmissen. Das ist ja der Trick. Der Kunde prüft “realen Code” – allerdings in Isolation.

Aus meiner Sicht sind Prüfstände ein “missing link” in der Kette aus Tests. Sie fehlen im Bewusstsein der meisten Teams. Da wird entweder in manuellen Tests durch die Oberfläche hindurch gedacht. Oder es werden mit Unit Test Frameworks Unit Tests und Integrationstests für den Anwender unsichtbare gefahren. Ersteres kann der Kunde verstehen, letzteres nicht. Also kann er erst einbezogen werden, wenn ausreichend Oberfläche zur Verfügung steht und Feature-Code darüber zugänglich wird.

Mit Prüfständen jedoch kann der Kunde viel früher befragt werden. Das flexibilisiert die Entwicklungsreihenfolge und kann damit Kurs und Finanzierung eines Projektes beeinfluss. Insgesamt bekommt der Kunde mit Prüfständen schneller sichtbar Code für sein Geld. Und zwar Code, der bleibt. Darüber sollte er froh sein. Und darüber sollte das Team froh sein.