Follow my new blog

Montag, 27. Mai 2013

Futter für das Coding Dojo

Wie soll man eigentlich Profi in der Anwendung der Prinzipien und Praktiken von Clean Code Developer werden? Durch Übung :-) Und zwar zunächst einmal durch Übung an Übungen, d.h. nicht am Produktionscode des Tagesgeschäftes.

Woher aber solche Übungen nehmen? Es gibt einige Übungsvorschläge, die sog. Code Katas. Doch die sind meist sehr, sehr überschaubar. Es sind vor allem Übungen im Entwickeln kleiner Algorithmen. Das ist nicht schlecht – aber am Ende zu wenig. Damit kann man ein paar Prinzipien und auch die Methode TDD üben… Doch realistisch sind solche Übungen nicht. Sie bereiten insofern nur begrenzt auf das reale Leben vor.

Deshalb haben Stefan Lieser und ich nun angefangen, Übungen zu sammeln, die ein breiteres Spektrum abdecken. Wir setzen sie schon seit mehreren Jahren in unseren Trainings der Clean Code Developer Akademie ein. Und jetzt “lassen wir sie frei”. Ein kleines Geschenk an die Community.

image

Im “Coding Dojo” der Clean Code Developer School bauen wir ein Verzeichnis von Code Katas und anderen auf. Wir teilen dabei die Übungen in verschiedene Kategorien je nach Umfang. Am unteren Ende stehen “Function Katas”, bei denen die Aufgabe darin besteht, nur eine Funktion zu entwickeln, um einige Anforderungen zu erfüllen. Darüber liegen “Class Katas” und “Library Katas”. Und weiter geht es mit “Application Katas” und “Architecture Katas”. Sie stehen für umfangreichere Aufgaben, zu deren Lösungen Benutzerschnittstellen und Ressourcenzugriffe oder gar Verteilung gehören. Es sollte also für jeden Geschmack und jedes Zeitbudget etwas dabei sein.

Auf der Seite des “Coding Dojo” sind die Aufgaben in diesen Kategorien gelistet; die zugehörigen Dokumente befinden sich hingegen bei scribd.com, wie oben im Bild zu sehen. Wir nutzen gern die Cloud als Speicehrort ;-) Hier ein Beispiel:


Das Verzeichnis der Übungsaufgaben soll natürlich ständig wachsen. Wir haben noch einige in petto, wir werden weitere bekannte und beliebte Aufgaben im Web “ernten” und aufarbeiten. Aber wir freuen uns auch, wenn Sie uns Ideen für Übungen mitteilen. Von einem umfangreichen und vielfältigen Verzeichnis an Katas können die Communities aller Plattformen nur profitieren.

Und nun: Auf zum Üben! Auf in ihr persönliches Coding Dojo!

Nehmen Sie sich (am besten regelmäßig) allein oder mit Kollegen ein bisschen Zeit dafür. Oder besuchen Sie uns in der Clean Code Developer School. Dort zeigen wir Ihnen in Ruhe, was wir unter sauberer und nachhaltiger Softwareentwicklung verstehen.

image

Donnerstag, 23. Mai 2013

Gleich zu Gleich für Clean Code

Letzten Dienstag habe ich im wöchentlichen Unterrichtsblock der Clean Code Developer School (ccd-school.de) “TDD as if you meant it” anhand der Kata WordWrap vorgestellt. Dann wollten die Teilnehmer es selbst probieren. Als Aufgabe habe ich die Kata ToDictionary vorgeschlagen.

Leider führte die dann nicht in so geradliniger Weise zu “Refactoring-Druck”, wie ich es mir erhofft hatte. Es wollten nicht die schönen Wiederholungsmuster wie bei WordWrap auftreten. Mist.

Jetzt habe ich sie selbst nochmal durchgeführt. Hier ein Zwischenstand:

image

Es gibt ein Muster:

  • Deutliche ist es zu sehen im zweiten Test, wo die Aufteilung einer Zuweisung (z.B. “a=1”) in Key und Value (z.B. “a” und “1”) mit anschließendem Eintrag in das Dictionary zweimal geschieht.
  • Weniger deutlich ist es im ersten Test, wo das auch passiert, aber in etwas anderer Form.

Aber was für ein Refactoring-Druck entsteht dadurch? Sollte ich Split()+Add() in eine eigene Methode rausziehen? Dann würde die Erzeugung des Dictionary zurückbleiben. Hm… das fühlt sich nicht gut an.

Ebenfalls unschön ist, dass die Erzeugung des Dictionary im zweiten Test “weit weg” von seiner Nutzung steht. Mir wäre dies lieber:

image

Wenn früher in Sprachen Deklarationen am Anfang eines Unterprogramms stattfinden mussten, dann hatte das weniger mit sauberem Code zu tun, als vielmehr mit der Notwendigkeit zu simpleren Parsern, würde ich sagen. Heute ist das kein Thema mehr. Also können Deklarationen stehen, wo es für das Verständnis sinnvoll ist. Und das, so scheint mir, ist nahe ihres Gebrauchsortes.

Außerdem sollten Deklarationen nur die kleinstmögliche Reichweite/Sichtbarkeit haben. Das beugt unnötiger/zufälliger Kopplung vor.

Dito sollten die Verwendungen von Variablen nahe beieinander stehen. Sie sind ja natürlich kohäsiv. Deshalb würde ich auch die zweite Eintragung an die erste rücken wollen:

image

Selbiges gilt für den Zugriff auf assignments. Also sollten die Aufteilungen der Zuweisungen in Key und Value beieinander stehen:

image

Und schon sieht die Welt viel musterhafter aus, oder? Hier sind Wiederholungen zu sehen, die nach Zusammenfassung schreien. Mit Linq ist das ganz einfach, ohne neue Methoden erzeugen zu müssen:

image

Das ist dann vielleicht noch nicht so gut zu lesen wie eine weitere Kapselung:

image

Doch mit oder ohne eigene Methoden ist die Zusammenfassungen besser zu lesen. Indem ich Gleich und Gleich sich habe gesellen lassen, statt dem ersten Impuls nach Auslagerung eines Musters zu folgen, ist die Sauberkeit noch größer geworden, würde ich sagen. Nun sind die wesentlichen Aspekte der Problemlösung deutlich zu sehen. Es ist klar, wie die Lösung voranschreitet. Insbesondere bei weiterer Kapselung der Aspekte in Methoden ist ToDictionary() sehr leicht zu lesen.

Dienstag, 14. Mai 2013

Gewinnen durch Vorläufigkeit

Warum soll im Business eigentlich immer alles perfekt sein? Oder, nein, nicht perfekt, aber "effizient". Ein Beispiel dafür ist mir gerade im Rahmen eines Hobbyprojekts untergekommen: der hey! publishing Verlag.

Eine befreundete Autorin hat dort einen Roman veröffentlicht, "Die Martinis":


Der ist als eBook erschienen. Und das war´s. Weiter ist nichts geschehen (Stand Anfang Mai 2013), obwohl zum Autorenvertrag auch eine Taschenbuchausgabe gehört.

Der Verlag hat über mehrere Monate hinweg noch keinen passenden Partner für die Taschenbuchausgabe gefunden, sagt er. Gesucht wird, so nehme ich an, eine Druckerei, die print-on-demand bietet. hey! publishing möchte sich keine Exemplare auf Halde hinlegen.

Das finde ich völlig verständlich. In der heutigen Zeit tut das für ein simples Belletristik-Taschenbuch nicht mehr Not. Print-on-demand funktioniert gut: die Qualität ist hoch, die Lieferung schnell. Und immer mehr Menschen lesen ohnehin ein eBook. Allemal einen Roman.

Aber warum findet der Verlag keine Druckerei? Das verstehe ich nicht. Print-on-demand ist keine Neuigkeit mehr. Meine Vermutung: Man möchte die Kosten minimieren. Man möchte die billigste Druckerei finden. Das kostet wiederum Zeit, weil man vergleichen muss. Das kostet auch Zeit, weil man erstmal ein Verlagsprogramm aufbauen muss, um nicht nur mit 5-6 Titeln auf die Suche zu gehen, sondern mit einem größeren Programm. Das drückt ja weiter den Preis.

Das finde ich auch irgendwie verständlich. So hat man es immer schon gemacht.

Aber sollte man es auch weiter so machen?

Ich glaube, nicht. Ich glaube, man sollte nicht so auf Effizienz von vornherein schielen. "Wir bringen das Taschenbuch erst heraus, wenn wir die perfekte Druckerei gefunden haben!" Das halte ich für eine hausbackene Einstellung, die Chancen auf Gewinne ausschlägt.

Zum Vergleich ein anderes Buch von Nika Sas, "Der kastrierte Schokobär":


Dieses Buch habe ich mit Nika Sas im letzten Jahr zuerst im Rahmen eines Blogs online veröffentlicht. Und nun, da ich etwas Erfahrung mit dem Selbstverlegen durch mein eigenes Buch "Systematisch produktiver und zufriedener" gesammelt hatte, habe ich ihr geholfen, den Roman ebenfalls über amazon zu veröffentlichen.

Vom fertigen Manuskript bis zur Verfügbarkeit im amazon Shop hat das insgesamt ca. 8 Stunden Aufwand gebraucht:
  • Konten und Titel bei createspace und Kindle Direct Publishing (KDP) anlegen
  • Manuskript in createspace Template überführen
  • Umschlag gestalten
  • Umsetzungen durch createspace und KDP überprüfen
  • Preise festsetzen
Dazu kamen noch ca. 2 Wochen Wartezeit auf Proofs, also Exemplare des Taschenbuchs, um die Qualität mit eigenen Händen zu prüfen.

Das war´s. Mit wenigen Stunden Aufwand ist Nika Sas mit ihrem Roman im Selbstverlag nun im Verkauf bei amazon sowohl als eBook wie als Taschenbuch. Das Geldverdienen kann beginnen. Und der Erkenntnisgewinn kann beginnen. Nika Sas kann durch ihre Präsenz Feedback generieren. Das halte ich für einen großen Vorteil.

Währenddessen sitzt hey! publishing noch an der Optimierung seines Titels. Man kann mit dem eBook etwas gewinnen. Aber mit dem Taschenbuch kann man nichts gewinnen. Es existiert nicht. Weder Durchsatz wird generiert noch Erkenntnisse.

Das halte ich für unzeitgemäß. Man optimiert einen Aspekt und verschenkt dabei Zeit und Gelegenheit. Wann der Aspekt optimiert ist, ist nicht absehbar. Autoren werden hingehalten, die Motivation sinkt. Wieviel die Optimierung im Vergleich zu einer suboptimalen vorläufigen Lösung mehr einbringt, ist auch nicht gewiss.

Was hätte der Verlag sich vergeben, wenn er zeitgleich mit dem eBook via createspace oder lulu.com oder epubli.de auch das Taschenbuch herausgebracht hätte? Klar, so hätte er weniger Marge gehabt. Aber wäre das so schlimm gewesen?

Wie gesagt: Mir geht es nicht darum, die Suche nach einer perfekten, nach einer effizienten Lösung aufzugeben. Wer meint, die finden zu können, der suche gern. Aber warum während der Suche nicht ein Zwischenergebnis veröffentlichen? Warum nicht mit Vorläufigkeit schon kleine Gewinne erzielen? Ein createspace Taschenbuch wäre für die Leserschaft ein Inkremente gewesen, das schon nutzt.

Ich denke, das ist ein Umdenken, das wir uns häufiger leisten sollten. Es geht um Sensibiliät für die mögliche Vorzeitigkeit von Optimierungen. Oder andersherum: Wir sollten den Mut haben, weniger als gewiss anzunehmen. Dann versuchen wir nämlich nicht, das vermeintlich Gewisse perfekt hinzukriegen, sondern nähern uns dem Ungewissen über vorläufige Schritte an. Das spart Geld, das stellt schneller Gewinn her. Mal "nur" Erkenntnisgewinn, mal aber auch schon ersten monetären Gewinn.

Dienstag, 7. Mai 2013

Mehr verdienen durch Experiment

Könnten Sie oder die Firma, für die Sie arbeiten, mehr verdienen? Ja? Wunderbar, dann los! Nein? Warum nicht? Woher weiß man das?

Auf diese Frage bin ich neulich bei einem Aufenthalt im Marriott Hotel in Zürich gestoßen. Dort fand ich nämlich dieses Angebot vor:


Zum Verkauf auf dem Zimmer standen solche 1 Liter Flaschen Wasser für 8,00 CHF (ca. 6,50 EUR) das Stück.

Ein stolzer Preis, oder? Vielleicht auch nicht, denn in der Schweiz ist ja so manches teuer. Vielleicht kostet Mineralwasser einfach so viel?

Ein Besuch im lokalen Migros Supermarkt hat mich dann allerdings informiert, dass eine Flasche vergleichbaren Mineralwassers dort nur 1,60 CHF (ca. 1,30 EUR) kostet.


Da habe ich mir dann eine gekauft und mehr als 5,00 EUR gespart. Davon kann ich mir dann zwei leckere Chai Tees in meinem Lieblingscafé in Hamburg leisten.

Ich will mich hier nicht über Mondpreise in Hotels empören. Das Marriott hat mich zu nichts gezwungen. Die 8,00 CHF für eine Flasche Wasser war wirklich nur ein Angebot - allerdings eines, das ich dankend und ohne nachzudenken ausgeschlagen habe.

Das Marriott hat keine 8,00 CHF Umsatz gemacht. Dem Marriott ist ein Durchsatz=Umsatz - variable Kosten von mindestens 7,50 CHR durch die Lappen gegangen.

Das erinnert mich an den Spruch der Börsianer: Nicht realisierte Gewinne, sind keine Gewinne.

Irgendwo beim Marriott hat jemand auf einen Durchsatz von 7,50 CHF spekuliert. "Lass uns das Wasser für 8,00 CHF pro Flasche verkaufen, dann können wir 7,50 CHF 'Gewinn' damit machen."

Leider ist dieser Fall nun wieder nicht eingetreten. Was hat die Chance auf den hohen Durchsatz also genutzt?

Viel gewinnen kann man auch beim Lottospiel. Aber der Erwartungswert ist sehr, sehr klein. Hat sich beim Marriott also mal irgendjemand Gedanken über den Erwartungswert gemacht? Ich bin im Zweifel.

Meine Vermutung: Das Marriott hat einen Anspruch. Man will Qualität bieten in der gehobenen Hotelklasse. Dafür kann man natürlich einiges an den Zimmer und beim Service tun. Und ich kann berichten, dass man sich da auch durchaus erfolgreich bemüht.

An der Qualität des Mineralwassers kann man jedoch nichts tun. Die ist auf dem Hotelzimmer dieselbe wie im Supermarkt.

Drückt dann aber vielleicht das Vorhandensein von Mineralwasser auf dem Hotelzimmer Qualität aus? Naja... diesen Service bieten auch 2-Sterne Hotels im Rotlichtviertel von Köln. (Ja, ja, da bin ich mal vor vielen Jahren gelandet, als die Hotelwahl über das Internet noch nicht so gut funktionierte.)

Eine Flasche Wasser auf dem Hotelzimmer ist also kein überraschender Service - insbesondere nicht, wenn dann so eine Flasche den fünffachen (!) Preis im Vergleich zum Supermarkt hat.

Dass eine Flasche Wasser oder Bier im Restaurant mehr kostet als im Laden, ist selbstverständlich. Über deren Verkauf muss sich das Restaurant finanzieren. Aber ein Marriott Hotel, das für eine Übernachtung im Einzelzimmer ohne weiteren Verzehr gern mal mehr als 400 EUR verlangt... das finanziert sich doch nicht über Mineralwasserverkauf.

Außergewöhnlicher Servicewille ist also nicht der Grund für Wasser zu 8,00 CHF auf dem Zimmer. Den hätte ich eher vermutet, wenn das Wasser kostenlos oder zum Ladenpreis angeboten würde.

Mir fällt daher leider, leider nur ein Grund für den hohen Preis ein: Gier.

Das klingt nicht schön, aber wie anders ist ein fünffacher Preis zu erklären? Ist die Leistung, das Wasser bereitzustellen und damit den armen Gästen den Weg zum fernen, fernen Supermarkt abzunehmen, 6,40 CHF wert?

Nein, tut mir leid, das ist für mich jenseits aller Angemessenheit. Das ist gierig und womöglich sogar respektlos gegenüber den Gästen. Man nutzt deren Unwissen über die lokalen Preise und ihre mangelnden Orts- und Sprachkenntnisse aus.

Aber wie gesagt, ich will mich gar nicht empören. Mich treibt vielmehr die Frage um: Wird die Gier befriedigt? Nützt es, gierig zu sein? Kann sich das Marriott die Hände reiben ob solch kluger Preisgestaltung?

Meine Antwort: Ich weiß es nicht. Und ich glaube, das Marriott weiß es auch nicht.

Womit ich beim Thema für diesen Artikel bin: Das Marriott weiß nicht, was es mit Wasser verdienen könnte, weil es nicht experimentiert.

Man mag mich eines Besseren belehren, doch bis dahin glaube ich, dass niemand im Marriott den Preis für maximalen Durchsatz "ausbalanciert" hat. Vielmehr hat irgendwer schlicht überlegt, "Welcher Preis passt zu unseren allgemein hohen Preisen? Und wo liegt gerade noch so eine Akzeptanzgrenze, dass man uns die Flasche nicht an der Rezeption um die Ohren haut?" Da klangen Preise über 10,00 CHF wahrscheinlich zu hoch und Preise unter 5,00 CHF zu billig. Und ist 8 nicht eine schöne runde Zahl?

Warum auch nicht den Preis so festsetzen? Irgendwo muss man ja mal anfangen.

Ich glaube nur, dass man da dann nicht stehenbleiben sollte.

So eine Festsetzung sollte nicht auf ewig sein, sondern als Experiment mit begrenzter Laufzeit betrachtet werden. Denn sonst kann man nicht herausfinden, ob ein anderer Preis mehr Erfolg bringt.

Auch wenn das Marriott mit 8,00 CHF pro Flasche im Jahr vielleicht 10.000,00 CHF Durchsatz macht, auch wenn man sich über diesen Durchsatz womöglich freut, heißt das nicht, dass 6,00 CHF oder auch 9,99 CHF pro Flasche pro Flasche nicht mehr Durchsatz produzieren könnten.

Ja, ich behaupte, das weiß dort keiner. Man orientiert sich an einem Anspruch und vergleicht vielleicht noch mit anderen Hotels derselben Klasse. Heraus kommt dann ein Preis, mit dem man sich wohl fühlt - aber keiner, von dem man weiß (!), dass er den Durchsatz maximiert [1].

Mein Vorschlag wäre, den Preis zum Beispiel für jeweils zwei Monate auf zum Beispiel 2,00 CHF, 4,00 CHF, 6,00 CHF und 10,00 CHF zu setzen. Wie oft das 8,00 CHF Angebot in zwei Monaten genutzt wird, weiß man ja heute. Aber wie oft würde Mineralwasser zu einem anderen Preis genutzt? Vielleicht verdoppelt sich der Verkauf für 6,00 CHF? Oder er bricht nur um 5% bei 10,00 CHF ein?

Ich habe da zwar eine Vermutung... doch das ist egal. Wenn das Marriott gierig ist, dann sollte es konsequent schauen, wie es die befriedigen kann. Das geht nur mit Experimenten.

Sie mögen nun denken, dass sich das doch für solch eine Nebensächlichkeit wie Mineralwasser nicht lohne. Da halte ich aber dagegen: Die Abwesenheit von Experimenten beschränkt sich nicht auf das Mineralwasser. Die ist vielmehr Symptom einer Haltung. Es wird nicht im Kleinen experimentiert, es wird nicht im großen experimentiert. Jedenfalls nicht, wenn man nicht muss.

Bei den Zimmerpreisen ist man quasi gezwungen zum Experiment. Da gibt es Wettbewerb, da ist die Auslastung schwankend nach Jahreszeit usw. Also variiert man die Zimmerpreise. Das tun alle und es kommt - so glaube ich - am Ende tatsächlich ein Preis heraus, der die Marge maximiert.

Aber wie ist es in anderen Bereichen? Gibt es Experimente im Restaurant des Marriott? Eher nicht.

Und wie ist es bei Ihnen im Unternehmen? Wer weiß (!), dass mit den aktuellen Preisen das Maximum an Marge erreicht ist?

Ich glaube, das weiß man nicht, sonder hat nur Vermutungen und handelt nach überkommenen Glaubenssätzen. "Das können wir nicht anders machen..." oder "Das war schon immer so..." halten Sie womöglich davon ab, mehr zu verdienen.

Warum also nicht ein paar Experimente wagen. Die müssen ja nicht verrückt sein. Sie müssen nicht alles aufs Spiel setzen. Aber ein bisschen "Mutation" in den "Preisgenen" kann nicht schaden, finde ich. Weniger auf den Wettbewerb schielen, mehr selbst herausfinden. Das wäre doch mal was, oder?


PS: Dass es auch anders geht, zeigt übrigens das schlossgut gross schwansee, welches ich am letzten Wochenende besucht habe. Dort hat man sich entschieden, das Mineralwasser als Service kostenlos anzubieten:


Meine Vermutung: Man hat realisiert, dass der Gewinn an Image größer ist als jeder monetäre Gewinn, den man durch den Verkauf zu welchem Preis auch immer erzielen könnte.

Endnoten

[1] Ich spreche ganz bewusst hier nur vom direkt mit dem Wasser umgesetzten Geld. Inwiefern sich ein Preis auf das Image niederschlägt, lass ich außen vor. Auch darüber könnte sich ja ein Marriott Gedanken machen:

Sind 8,00 CHF ein "würdiger" Preis, der das Image verbessert? Oder könnte es sein, dass dieser Preis dem Image schadet? Was würde ein Preis von 1,60 CHF für das Image tun? Was würde mit dem Image passieren, wenn man das Wasser - horribile dictu! - kostenlos anbieten würde?

Das sind alles Fragen, von denen ich glaube, dass sie nicht ernsthaft gestellt werden. Und wenn, dann sucht man nicht nach belastbaren Antworten.

Sonntag, 28. April 2013

Software fraktal - Funktionale Abhängigkeit entschärfen

Wir kommen nicht wirklich raus aus der funktionalen Abhängigkeit. Was aber können wir dann gegen ihre Probleme tun?

Neulich habe ich gezeigt, wie Sie funktionale Abhängigkeiten auflösen. Dabei bleibe ich auch. Funktionseinheiten - Methode oder Klasse - sollten für ihre Spezifikationen selbst verantwortlich sein. Soweit möglich. Doch damit wird das Problem der funktionalen Abhängigkeit nur eine Ebene höher geschoben.

Vorher:
int c() {
  ...  // Vorbereitung
  var y = s(x);
  ... // Nachbereitung
}

Nachher:
var x = c_vorbereitung();
var y = s(x);
return c_nachbereitung(y);

Wo c() vorher funktional von s() abhängig war, da ist das, was c() ausgemacht hat nachher auf c_vorbereitung() und c_nachbereitung() verteilt - und nicht mehr von s() abhängig.

In Bezug c() ist das gut. Aber nun ist der Code, der nachher die drei Methoden aufruft funktional von ihnen abhängig. Denn in Wirklichkeit sieht das ja so aus:

Vollständiges Nachher:
int i() {
  var x = c_vorbereitung();
  var y = s(x);
  return c_nachbereitung(y);
}

Die neue Methode i() hat den schwarzen Peter funktionale Abhängigkeit zugeschoben bekommen.

Oder?

Ich denke, wir sollten genau hinschauen, bevor wir die Situation als unverändert beurteilen.

Operationen

Ich möchte für die weitere Diskussion den Begriff Operation einführen. Eine Operation ist eine Funktion, die Logik enthält. Das bedeutet, sie enthält Kontrollstrukturen (if, for, while usw.) und/oder Ausdrücke (+, *, && usw.).

Die Funktion

int Add(int a, int b) { return a+b; }

ist nach dieser Definition eine Operation. Die obige Funktion i() hingegen nicht. Sie enthält keine Kontrollstrukturen und auch keine Ausdrücke; sie besteht lediglich aus Funktionsaufrufen und Zuweisungen [1].

Operationenhierarchien

Normalerweise findet die Verarbeitung in Operationenhierarchien statt. Es gibt tiefe funktionale Abhängigkeitsbäume und in jedem Knoten (Funktion) befindet sich Logik.

void s(int a) {
  var n = ... a ...;
  var l = t(n);
  foreach(var e in l) { ... }
}

List t(int n) {
  var l = new List();
  for(int i=0; i<n; i++)
    l.Add(u(i));
  return l;
}

int u(int i) {
  var x = ... + i * ...;
  var y = v(x);
  return ... - y / ...;
}

...

Die Logik auf jeder Ebene mag derselben Domäne angehören oder unterschiedlichen. Das ist egal. Es ist Logik. Und solche Logik gemischt mit funktionaler Abhängigkeit tendiert dazu, sich horizontal auszudehnen (vgl. Problem #3 in diesem Artikel). Das bedeutet, tendenziell sind die Operationen auf jeder Ebene umfangreich und somit nicht leicht zu verstehen [2].

Operationenhierarchien leiden deshalb unter einigen Problemen:

Problem #A: Grenzenloses Wachstum

Operationenhierarchien kennen keine Grenze beim Wachstum. In Breite und Tiefe können sie beliebig zunehmen. Neue Anforderungen oder auch gut gemeinte Refaktorisierungen führen zur Ausdehnung - und verschmieren damit Logik immer mehr. Logik kann ja auf jeder Ebene der Hierarchie in jedem Operationsknoten vorkommen.

Spüren Sie nicht, wie schwer es dadurch wird, Software überhaupt nur zu verstehen? Um nur irgendwie noch durchzublicken, müssen Sie Tools bemühen, die Ihnen Abhängigkeitsbäume darstellen. Nur so bekommen Sie ungefähr eine Idee davon, wo bestimmte Logik sitzen könnte.

Problem #B: Umfängliche Unit Tests auf jeder Ebene

Operationen sind die Orte, wo die Musik einer Software spielt. Also müssen sie solide getestet werden. Jeder Pfad durch sie sollte mit einem automatisierten Test abgedeckt sein. Deshalb ist ja auch TDD in aller Munde. Deshalb gibt es Tools, die die Code Coverage messen.

Wenn Code eine endlose Operationenhierarchie ist, müssen solch aufwändige Tests für jeden Knoten durchgeführt werden. Puh... Und dann enthalten diese Knoten auch noch funktionale Abhängigkeiten. Es gibt also noch ein weiteres Problem:

Problem #C: Attrappen auf jeder Ebene

Auf jeder Ebene einer Operationenhierarchie müssen funktionale Abhängigkeiten durch Attrappen ersetzt werden. Sonst wird nicht nur eine Unit, d.h. eine Operation getestet. Kein Wunder, dass es soviele Mock Frameworks gibt. Der Bedarf an Attrappen ist durch Operationenhierarchien riesig.

Integration

Operationen machen viel "Dreck". Den muss man mit viel Clean Code Development dann wieder wegräumen. Schauen Sie zum Vergleich nun aber noch einmal die obige Funktion i() an. Finden Sie die "dreckig"?

Ich nicht. Für mich ist i() ein Leuchtturm an Sauberkeit. Was i() wie leistet, ist ganz einfach zu erkennen. Zugegeben, das Beispiel ist abstrakt. Nehmen wir deshalb ein konkreteres:

string Erste_Seite_aufblättern() {
  var dateiname = Dateiname_von_Kommandozeile_holen();
  var alleDatenzeilen = Zeilen_lesen(dateiname);
  var ersteSeite = new Seite {
    Überschrift = Überschrift_extrahieren(alleDatenzeilen),
    Datenzeilen = Zeilen_der_ersten_Seite_selektieren(alleDatenzeilen)
  };
  return Seite_formatieren(ersteSeite);
}

Das ist ebenfalls keine Operation. Es passiert zwar einiges, doch Kontrollstrukturen und Ausdrücke sind abwesend. Alles, was passiert, steckt in Funktionen, die aufgerufen werden.

Die Funktion Erste_Seite_aufblättern() ist also stark funktional abhängig - dennoch ist sie leicht zu verstehen. Woran liegt das?

Hier wurde das SRP sauber beachtet. Diese Funktion hat wirklich nur eine Verantwortlichkeit: sie integriert. Sie tut nichts im Sinne einer Domänenlogik selbst, sondern delegiert ausschließlich. Ihre einzige Aufgabe ist es, Funktionen mit kleinerem Zweck zu einem größeren zusammenzustellen. Sie stellt eine Integration dar.

Operationen hingegen vermischen immer zwei Verantwortlichkeiten, wenn sie funktional abhängig sind. Ihr Hauptverantwortlichkeit ist die Ausführung von Logik. Neben der übernehmen sie jedoch auch noch integrierende Verantwortung, wenn sie zwischendurch andere Funktionen aufrufen. Das macht sie so schwer zu verstehen und zu testen.

Trennung von Integration und Operation

Für mich ergibt sich aus den Problemen der Operationshierarchien und der Probleme der funktionalen Abhängigkeit, dass wir etwas anders machen müssen, als bisher. Ein bisschen SOLID mit TDD reicht nicht. Wir müssen radikaler werden.

Ich glaube, besser wird es nur, wenn wir ganz klar Integrationen von Operationen trennen (Integration Operation Segregation Principle (IOSP)). SPR und das Prinzip Single Level of Abstraction (SLA) sind nicht genug. Sie führen nicht geradlinig genug zu verständlichen Strukturen.

Um Hierarchien von Funktionen kommen wir nicht herum. Ohne sie beherrschen wir nicht-triviale Logik nicht. Wir müssen Logikblöcke "wegklappen" und wiederverwendbar definieren können. Dafür sind Unterprogramme (Prozeduren und Funktionen) nötig.

Aber wir können uns darin beschränken, wie wir diese Hierarchien aufbauen. Wir können uns auferlegen, dass Operationen nur in ihren Blättern stehen dürfen - mit beliebig vielen Ebenen von Integrationen darüber.

Jede der beliebig vielen Integrationsfunktionen oberhalb von Operationen ist dann so gut zu lesen wie Erste_Seite_aufblättern().

Die Probleme funktionaler Abhängigkeit wären in den Integrationen zwar noch relevant; doch sie wären auf ein erträgliches Maß gedämpft:

Bereitstellungsaufwand wäre zu treiben (Problem #1) und es gäbe topologische Kopplung (Problem #2). Aber das grenzenlose Wachstum (Problem #3) sähe anders aus. Integrationsfunktionen mit vielen LOC wären viel, viel besser zu lesen als Operationen von gleicher Länge. Entstünden lange Integrationsfunktionen aber überhaupt? Nein. Die Möglichkeit zum grenzenlosen Wachstum gibt es zwar, doch da Integrationsfunktionen so übersichtlich sind, werden sie viel schneller refaktorisiert.

Wenn Sie in Erste_Seite_aufblättern() zwei Verantwortlichkeiten vermischt sehen (Widerspruch gegen SRP) oder meinen, da sei nicht alles auf dem selben Abstraktionsniveau (Widerspruch gegen SLA), dann klappen Sie einfach einen Teil weg in eine andere Integrationsfunktion:

string Erste_Seite_aufblättern() {
  var ersteSeite = Erste_Seite_laden();
  return Seite_formatieren(ersteSeite);
}

Seite Erste_Seite_laden() {
  var dateiname = Dateiname_von_Kommandozeile_holen(); 
  var alleDatenzeilen = Zeilen_lesen(dateiname);
  return new Seite {
    Überschrift = Überschrift_extrahieren(alleDatenzeilen),
    Datenzeilen = Zeilen_der_ersten_Seite_selektieren(alleDatenzeilen)
  };
}

Aus eins mach zwei. Das kann jedes Refactoring-Tool, das etwas auf sich hält.

Dadurch steigt zwar die Tiefe der Funktionshierarchie, doch das macht nichts. Jede Ebene für sich ist ja leicht verständlich. Sie brauchen nicht mal ein Werkzeug, das Ihnen Abhängigkeitsbäume zeichnet, da die Ihnen ja deutlich vor Augen stehen und nicht in "Logikrauschen" verborgen sind.

Auch die syntaktische (Problem #4) und semantische Kopplung (Problem #5) verlieren an Gewicht. Da Integrationen selbst keine Logik enthalten, haben Änderungen in Syntax oder Semantik keinen Einfluss auf die Integration selbst. Syntaktische Änderungen können durch Typinferenz womöglich verschluckt werden. Und wenn nicht, dann hilft wahrscheinlich ein kleiner Einschub zwischen den aufzurufenden Funktionen wie hier gezeigt.

Integrationen sind zwar grundsätzlich gerade an die Semantik der Funktionen gekoppelt, von denen sie abhängen. Sie sollen ja aus ganz konkreten Teilverantwortlichkeiten eine neue Summenverantwortlichkeit herstellen. Doch diese Kopplung ist loser, weil die nicht an eigener Logik hängt.

Soweit die Milderung der Probleme #1..#5 funktionaler Abhängigkeit durch das IOSP. Aber was ist mit den Problemen #A..#C der Operationenhierarchien?

ad Problem #A: Einzelne Integrationen können zwar wachsen, doch tendenziell tun sie das viel weniger als Operationen (s.o.).

Integrationshierarchien können auch wachsen und tun das durch Refaktorisierungen womöglich sogar mehr als Operationshierarchien. Doch das ist nicht schlimm. Jede Ebene ist einfach zu verstehen. Logik ist nicht vertikal und horizontal verschmiert. Sie steckt allein in den Blättern, den Operationen.

ad Problem #B: Integrationen haben von Hause aus eine sehr niedrige zyklomatische Komplexität. Es gibt nur wenige Pfade durch sie. Die kann man sehr einfach testen. Oft reicht ein einziger Test. Oder Sie lassen eine Integration auch mal ganz ohne Test.

Das meine ich ernst. Da Integrationen so leicht zu verstehen sind, kann ihre Korrektheit auch mal nur durch einen Code Review geprüft werden. Die Korrektheit besteht ja nur darin, dass eine Integration von den richtigen Funktionen abhängt und zweitens diese Funktionen in passender Weise für den Zweck der Integration "verdrahtet" sind. Ob das der Fall ist, ergibt sich oft durch Augenschein.

Allerdings sollten Sie automatisierte Tests der Integrationen auf oberster Ebene haben. Die prüfen den Gesamtzusammenhang. Solange der korrekt ist, sind auch darunter liegende Integrationen korrekt.

Und Sie decken natürlich Operationen mit automatisierten Tests ordentlich ab. Dort spielt die Musik der Logik, das ist nicht einfach zu verstehen, also müssen Tests helfen.

Mein Gefühl ist, dass das Testen von Funktionshierarchien, die dem IOSP folgen, deutlich einfacher ist. Allemal, da die Operationen ja nicht mehr voneinander abhängig sind. Sie sind durch das Principle of Mutual Oblivion (PoMO) entkoppelt.

ad Problem #C: Nicht zuletzt ist das Testen einfacher, weil der Bedarf an Einsätzen eines Mock Frameworks deutlich sinkt. Ich zum Beispiel benutze schon lange gar keinen mehr.

Attrappen sind hier und da noch nötig. Doch dann schnitze ich sie mir kurz für den konkreten Fall selbst. Wissen über einen Mock Framework vorzuhalten oder gar deren Entwicklung zu verfolgen, wäre viel umständlicher.

Warum sind Attrappen viel seltener nötig? Weil die vielen Tests von Operationen keine mehr brauchen. Sie sind ja in Bezug auf die Domäne ohne funktionale Abhängigkeit. Und weil Tests von Integrationen seltener sind.

Skalieren mit dem IOSP

Dass Sie Ihre Software nur mit einer Hierarchie von Integrationen realisieren können, an denen unten wie Weihnachtsbaumkugeln Operationen baumeln, glaube ich auch nicht. Aber ich denke, dies sollte die Grundstruktur sein:


Operationen sind die Black Boxes an der Basis funktionaler Abhängigkeitshierarchie. Nur dort sollte sich Logik befinden. Hier gilt es ausführlich zu testen. Diese "Büchsen" wollen Sie so selten wie möglich öffnen. Besser ist es, auf einer Integrationsebene darüber simple Veränderungen anzubringen und neue "Büchsen" unten hinzuzufügen.

Für die ganze Struktur gilt das IOSP, für jeden Knoten darin das PoMO.

Wenn diese Grundstruktur jedoch wachsen soll, dann reicht es nicht, an der Basis in die Breite zu gehen und bei der Integration in die Höhe. Ich denke, dann muss Schachtelung dazukommen.

Black Boxes sind nur aus einem bestimmten Blickwinkel oder aus einer gewissen Entfernung undurchsichtig und quasi von beliebiger Struktur. Wenn eine Operation wächst, dann sollten Sie das ebenfalls nach dem IOSP tun.


Software ist also von ähnlich fraktaler Struktur wie diese quadratischen Koch-Kurven:



Fazit

IOSP + PoMO sind für mich eine ganz pragmatisches Paar, um Software verständlich, testbar und evolvierbar zu halten.

Sie mögen einwänden, das ließe sich doch auch mir SRP und SLA und OCP (Open-Closed Principle) erreichen - und da haben Sie wohl recht. Nur passiert das nicht. SOLID allein reicht nicht. Deshalb finde ich es sinnvoll, daraus diese zwei Prinzipien zu destillieren. Oder diese simplen Regeln, wenn Sie wollen:
  • Teile Funktionen klar in Operationen und Integrationen:
    • Halte Funktionen, die Logik enthalten, frei von funktionalen Abhängigkeiten (Operationen).
    • Sammle funktionale Abhängigkeiten in Funktionen ohne Logik (Integration).
Wenn Sie Ihren Code so aufbauen, dann fühlt sich das zunächst natürlich ganz anders an als bisher. Sie werden Widerstand spüren. Und schon vorher werden Sie wahrscheinlich daran zweifeln, ob das alles überhaupt nötig sei.

Keine Sorge, das kenne ich alles. Ich bin selbst durch diese Zweifel gegangen. Ich kann Sie davon mit Argumenten auch nicht völlig befreien. Sie müssen das ausprobieren. Nehmen Sie sich eine Aufgabe vor, deren Code Sie einfach mal nach der Regel versuchen zu strukturieren. Eine Application Kata hat da gerade die richtige Größe [3].

Ebenfalls kann ich Ihr Schmerzempfinden nicht sensibilisieren. Wenn Sie heute darunter stöhnen, dass Ihre Codebasis schwer wartbar sei, aber keinen Schmerz empfinden, wenn Sie nur mit SOLID und TDD bewaffnet daran herumwerkeln, dann ist das halt so. Dann werden Sie in IOSP+PoMO als Extremformen von SPR, SLA und OCP keine Perspektive zur Besserung sehen. Aber vielleicht lesen Sie dann mal Watzlawicks "Vom Schlechten des Guten" :-)

Ich jedenfalls arbeite seit einigen Jahren zunehmend und heute nur noch so. Mein Leben ist dadurch glücklicher geworden :-) Ich kann meinen Code auch noch nach Monaten verstehen. Änderungen fallen mir leichter. Mit Mock Frameworks muss ich mich nicht mehr rumschlagen.

Versuchen Sie es doch einfach auch mal. Nur so als Experiment. Und dann sagen Sie mir, wie es Ihnen damit gegangen ist.

Endnoten

[1] Zuweisungen sind keine Ausdrücke, da nichts zu einem neuen Ergebnis kombiniert wird. Auf der linken Seite steht derselbe Wert wir auf der rechten Seite. Ohne weitere Modifikation durch Ausdrücke können Variablennutzungen durch Funktionsaufrufe ersetzt werden. i() könnte auch so aussehen:

int i() {
  return c_nachbereitung(s(c_vorbereitung()));
}

Die Variablen in i() sind nur eine Lesehilfe oder könnten bei mehrfacher Nutzung der Performance dienen.

[2] Ich weiß, das soll nicht sein. Und wenn man das Single Responsibility Principle (SRP) beachtet, dann werden einzelne Operationen auch nicht so groß.

Das stimmt. Aber erstens: Beachten Sie das SRP konsequent? Und zweitens: Sehen Sie, was kleine Operationen für eine Folge haben? Sie lassen die Operationenhierarchie weiter wachsen. Die einzelnen Operationen sind zwar kleiner, doch dafür ist die Hierarchie breiter und tiefer.

[3] Wenn Sie bei solcher Übung auf Schwierigkeiten stoßen, Fallunterscheidungen in Operationen auszulagern, ohne die funktional abhängig zu machen, dann ist das selbstverständlich. Aber vertrauen Sie mir: auch das ist ein lösbares technisches Problem.

An dieser Stelle will ich nicht näher darauf eingehen. Doch als Stichwort sei Continuation Passing Style (CPS) genannt.

Donnerstag, 25. April 2013

Selbstorganisation persönlich definiert

Was hat Agilität mit Selbstorganisation zu tun? Diese Frage stellte sich gerade in einem kleinen Twittergespräch mit Michel Löhr aka @1ohr.

Mein Standpunkt: Agilität und Selbstorganisation sind orthogonal.

Selbstorganisation hat etwas mit der Führung einer Organisation zu tun. Agilität hat etwas mit Art der Herstellung von etwas zu tun.

Agil ist die Herstellung für mich, sobald sie inkrementell ist, der Hersteller darauf bedacht ist, zu lernen (konkret und auf der Meta-Ebene), und die Kommunikation zwischen den Beteiligten eng ist. Das halte ich für eine pragmatische Definition von Agilität. Nicht zu eng, dass nur die 100%ige Einhaltung eines Vorgehensmodells adelt, aber auch nicht zu weit, als dass sie jeder als agil nennen könnte. Ausführlicher habe ich das in einem früheren Artikel erklärt.

Soweit die Agilität. Aber was ist mit der Selbstorganisation? Steckt die notwendig in der Agilität drin? Ich denke, nein. Weder inkrementelle Lieferung, noch das Lernen und auch nicht eine engere Kommunikation setzen Selbstorganisation voraus. Das alles mag mit Selbstorganisation noch besser gehen, doch sie ist für mich nicht notwendig und schon gar nicht hinreichend für Agilität [1].

Das sage ich natürlich mit einer Vorstellung von Selbstorganisation im Hinterkopf.

Soziale Systeme

Der Begriff "Selbstorganisation" beschwört natürlich einige Bilder herauf. Für manche ist dann die Anarchie nicht mehr fern. "Wenn jeder das machen kann, was er will, wo kommen wir denn dann hin?" Aber ist das Selbstorganisation, wenn jeder machen kann, was er will?

Natürlich nicht. Denn wenn jeder machen kann, was er will, dann gibt es keine Organisation im Sinne eines Zusammenschlusses von mehreren Menschen. Selbstorganisation ist nur ein Thema für soziale Systeme. Selbstorganisation setzt also voraus, dass mehrere Menschen zusammenkommen. Der Begriff bezieht sich immer auf einen Menschenansammlung.

Aber auch das ist noch zuwenig. Denn eine Menschenansammlung gibt es an jeder Bushaltestelle, wo Leute zusammen stehen. Die gehören aber nicht zusammen. Da verfolgt jeder seine eigenen Ziele, auch wenn sie im Sinne der Busbenutzung und des Wunsches, verlässlich und sicher befördert zu werden, eine Gemeinschaft sind.

Selbstorganisation ist nur ein Thema bei zusammengehörigen Menschen. Es braucht ein Ziel, dem die Menschen aktiv zustreben. Dadurch wird aus einer Gemeinschaft erst eine Gruppe.

Eine Gemeinschaft wird zwar durch ein gemeinsames Interesse charakterisiert, doch das wird von der Gemeinschaft noch nicht gemeinschaftlich aktiv verfolgt. Die Gemeinschaft überlässt das jemandem anderen (z.B. Busfahrer, Priester) oder jeder verfolgt selbst das Interesse nach seinem Gusto (z.B. Briefmarkensammler, Glaubensanhänger).

Eine Gruppe hingegen hat einen außen liegenden Zweck. Auf den hin wird sie geführt. Ebenfalls von außen. Deshalb ist die kleinste Einheit bei der Bundeswehr eine Gruppe mit einem Gruppenführer.

Aber eine Gruppe ist noch nicht selbstorganisiert. Sie ist geradezu das Gegenteil. Sie wird von jemandem anderen geführt, d.h. organisiert. Wer was wann und vielleicht auch wie tun soll, wird angesagt. Das ist auf einer Baustelle dasselbe, wenn der Polier die Maurer anweist. Das ist auch im Operationssaal so, wenn der Operateur "Schwester, Tupfer, bitte." sagt.

Gruppen entstehen immer dann, wenn Menschen koordiniert werden. Wenn einer die Richtung weist, die sie laufen sollen.

Das ist nicht per se gut oder schlecht. Das ist einfach nur ein Mittel, um ein Ergebnis mit mehreren Menschen zu erreichen.

Sind solche Gruppen denn nicht aber Teams?

Im landläufigen Sprachgebrauch ist das so. Aber für mich braucht es mehr, um aus einer Gruppe ein Team zu machen.

Ein Team ist eine Gruppe, bei das äußere Ziel zu einem gemeinschaftlichen inneren Ziel geworden ist. Und bei dem das Ziel in Selbstorganisation angestrebt wird.

Teams sind in Bezug auf ein Ergebnis intrinsisch motiviert. Das macht es ja so schwer, echte Teams herzustellen. Da ist nämlich Führungskunst auf höherer Ebene gefragt.

Und Teams entscheiden anschließend zumindest alles in Bezug auf das gemeinschaftliche Ergebnis Relevante selbst. Sie entscheiden über Mittel, Wege, Zeit - in einem gewissen vorgegebenen Rahmen. Teams sind insofern wie ein Marschflugkörper: Sie werden mit einer Aufgabe und einem Sack voll Ressourcen abgeschossen - und bewegen sich danach selbstständig ins Ziel. Dass sie dabei auf ihre Umwelt reagieren, tut dem keinen Abbruch. Denn sie entscheiden über ihre Reaktion.

Selbstorganisation

Jetzt habe ich gesagt, wo Selbstorganisation angesiedelt werden kann. Aber was ist denn nun diese Selbstorganisation?

Die Definition bei Wikipedia finde ich interessant - aber für den Hausgebrauch viel zu weitschweifig und abstrakt. Für mich ist Selbstorganisation viel einfacher immer dann vorhanden, wenn eine Gruppe eine Entscheidung über den einzuschlagenden Weg zu einem Ergebnis selbst treffen darf.
  • Soll mit Java oder .NET entwickelt werden? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf die Entwicklungsplattform selbstorganisiert.
  • Soll agil vorgegangen werden? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf das Vorgehensmodell selbstorganisiert.
  • Soll Peter der ScrumMaster sein und Klaus die CI einrichten? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf die Rollenbesetzung selbstorganisiert?
  • Soll das Daily Standup von 10 Minuten auf 15 Minuten ausgedehnt werden? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf das Lernen im Rahmen ihres Vorgehensmodells selbstorganisiert.
  • Soll jeden Dienstag von 15-19h eine gemeinschaftliche "Lernzeit" in der CCD School stattfinden, um die Weiterentwicklung in Bezug auf Clean Code und andere Aspekte nachhaltig zu betreiben? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf ihre Zeiteinteilung und das Budget selbstorganisiert.
Sie sehen: Selbstorganisation ist binär. Sie ist da oder nicht. In Bezug auf einen Aspekt der Arbeit. Insofern ist Selbstorganisation auch immer irgendwie vorhanden. Einfach nur deshalb, weil sie Gruppenführer nicht um jeden Mist kümmern können und wollen. Immer wird der geführten Gruppe irgendetwas zur eigenen Entscheidung überlassen.

Gerät die Führung jedoch unter Druck, reagiert sie oft mit Einschränkung der Selbstorganisation. Dann werden Entscheidungsfreiheiten beschnitten. Es wird enger geführt. Micro-Management ist das Gegenteil von Selbstorganisation.

Von "der Selbstorganisation" wird allerdings erst gesprochen, wenn die de facto Selbstorganisation einen gewissen Schwellenwert überschritten hat. Solange eine Gruppe nur über Triviales selbst entscheiden kann, wird sie gewöhnlich und zurecht nicht als selbstorganisiert bezeichnet. Dann ist sie kein Team.

Ich denke, von erwähnenswerter (und dadurch auch kontroverser) Selbstorganisation kann erst gesprochen werden, wenn eine Gruppe sich selbst führt und nicht nur koordiniert. Das bedeutet, sie hat die Hoheit über Grundsatzentscheidungen jenseits der streng fachlichen. Dazu gehören für mich:
  • Arbeitsinhalt
  • Arbeitsort
  • Arbeitszeit
  • Arbeitsmittel
  • Rollenverteilung
Nein, durch die Freiheit zur Entscheidung in all diesen Belangen, entsteht keine Anarchie. Das hieße, Menschen als grob naiv und fahrlässig und desinteressiert anzusehen.

Außerdem bedeutet Entscheidungsfreiheit ja nicht Unbegrenztheit. Selbst ein Kurierfahrer fühlt sich auf der Straße ja frei, obwohl er erstens einen Auftrag hat und zweitens die StVO einhalten muss und drittens anderen Verkehrsteilnehmern mutwillig keinen Schaden zufügen will.

Selbstorganisation ist möglich in Grenzen. Nein, ich würde sogar sagen, sie braucht Grenzen. Die setzen nämlich Kreativität frei. Das ist wie beim Künstler, der seine Meisterschaft in den Grenzen von Motiv und Mittelwahl beweist.

Selbstverständliche Grenzen sind das gemeinschaftliche Ziel und die Professionalität der Teammitglieder [3].

Aber solange ein Unternehmen noch an Budgets glaubt, darf es einem Team auch ein Budget vorgeben. Dessen Einhaltung ist dann ein Ziel wie die Auslieferung von Software, die bestimmte Anforderungen erfüllt.

Genauso muss sich ein Team verantworten. Es arbeitet nicht im luftleeren Raum.

Doch die Grenzen sollten locker sein. Und die, die sie vorgeben, sollten sich ansonsten im Hintergrund halten. Sie sollten sich für Ergebnisse interessieren und nicht für den Weg, den das Team in Selbstorganisation wählt.

Fazit

Ob Sie in einem Team arbeiten, können Sie nun selbst prüfen. Hat ihre Entwicklergruppe Entscheidungsfreiheit in den angeführten Belangen? Dann arbeiten Sie leider noch nicht wirklich in einem Team. Da helfen auch aller guter Wille und Grillabende nichts.

Und wie steht es mit der Agilität und Selbstorganisation? Ich hoffe, Sie stimmen mit mir überein, dass Agilität "die Selbstorganisation" nicht voraussetzt. Agil können auch Gruppen arbeiten. Dafür braucht es nicht zwingend Teams [2]. Aber mit Teams geht Agilität wie vieles andere auch in der Softwareentwicklung natürlich besser.

Agilität mag ohne Selbstorganisation nicht wünschenswert sein, aber sie ist möglich.

Endnoten

[1] Wenn ich hier "Selbstorganisation" so pauschal gebrauche, dann adressiere ich damit auch eine ebenso pauschale (wie diffuse) Vorstellung davon. Umfassende Selbstorganisation halte ich für nicht nötig für die Agilität.

Andererseits lässt sich Selbstorganisation ja aber nicht vermeiden, wie Sie bei der weiteren Lektüre feststellen werden. Insofern ist Selbstorganisation selbstverständlich immer Teil von Agilität oder gar notwendig. Das halte ich jedoch für trivial.

[2] Ich denke, das beweist auch die Praxis, in der sich viele Projekte agil nennen, aber nur von Gruppen durchgeführt werden. Selbstorganisation, ich meine echte Selbstorganisation, ist einfach nicht weit verbreitet. Wäre doch schade, wenn sich deshalb die Agilität nicht ausbreiten könnte.

[3] Deshalb ist es so wichtig, beim Übergang zu mehr Selbstorganisation an der Professionalität zu arbeiten. Die ist nämlich durch die lange Gängelung unter einer Gruppenführung atrophiert. Nicht jeder fühlt sich deshalb auch gleich wohl, wenn er mehr Entscheidungsrecht und -pflicht bekommt.

Mittwoch, 24. April 2013

Raus aus der funktionalen Abhängigkeit

Wenn funktionale Abhängigkeiten so problematisch sind, wie können wir sie denn vermeiden? Ich denke, die Antwort ist ganz einfach:

Wir sollten einfach keine funktionalen Abhängigkeiten mehr eingehen.

Funktion c() wie Client sollte sich nicht mehr an eine Funktion s() wie Service binden. Denn wenn sie das tut, wenn sie nicht mehr delegiert, dann kann sie sich endlich selbstbestimmt auf ihre einzige Domänenverantwortlichkeit im Sinne des Single Responsibility Principle (SRP) konzentrieren.

Technisch bieten Funktionen dafür die besten Voraussetzungen. Die Funktion

int Add(int a, int b) { return a+b; }

macht ja schon nur ihr Ding. Sie definiert sich komplett selbst. Ihre syntaktische Spezifikation besteht nur aus einer Signatur:

Func<int,int,int>

Die könnte man mit einer Beschreibung der gewünschten Semantik ("Die Funktion soll zwei Zahlen addieren.") zur Implementation in ein fernes Land geben. Die Entwickler dort müssten nichts weiter berücksichtigen.

Anders ist das bei einer Funktion wie dieser:

string ReportGenerieren(string dateiname) {
  string report = "...";
  ...
  int[] daten = LadeDaten(dateiname);
  foreach(int d in daten) {
    ...
    report += ...;
    ...
  }
  ...
  return report;
}

Deren Spezifikation besteht nicht nur aus:
  • Func<string,string>
  • "Funktion, die den Report wie folgt generiert: ..."
sondern auch noch aus der Signatur der Funktion zum Laden der Daten und deren Semantik:
  • Func<string,int[]>
  • "Funktion, die die soundso Daten in folgender Weise als int-Array liefert: ..."
Entwickler in einem fernen Land müssten nun diese Dienstleistungsfunktion entweder ebenfalls entwickeln oder gestellt bekommen oder zumindest als Attrappe bauen. Damit ist die Funktion ReportGenerieren() nicht mehr selbstbestimmt.

Allgemein ausgedrückt: Bei funktionaler Abhängigkeit besteht die Spezifikation einer Funktion aus 1 + n Signaturen (mit zugehörigen Semantiken). 1 Signatur für die eigentlich zu betrachtende Funktion und n Signaturen für Funktionen, von denen sie funktional abhängig ist.

Das ist doch kein Zustand, finde ich. So kann es nicht weitergehen, wenn wir zu besser evolvierbarer Software kommen wollen.

Aufbrechen funktionaler Abhängigkeiten

Also, wie kommen wir raus aus funktionaler Abhängigkeit? Wir müssen zunächst genauer hinschauen. Funktionale Abhängigkeit besteht aus drei Phasen:
  1. Vorbereitung
  2. Aufruf
  3. Nachbereitung

int c(int a) {
  // vorbereitung
  int x = ... a ...;
  // aufruf
  int y = s(x);
  // nachbereitung
  int z = ... y ...;
  return ... z ...;
}

Jede abhängige Funktion kann man mindestens einmal in diese Phasen zerlegen. Bei mehreren funktionalen Abhängigkeiten können sich Phasen natürlich auch überlagern. Dann mag eine Nachbereitung die Vorbereitung eines weiteren Aufrufs sein.

Sind diese Phasen aber erkannt, kann man jede in eine eigene Funktion verpacken:

int c_vorbereitung(int a) {
  return ... a ...;
}

int c_nachbereitung(int y) {
  int z = ... y ...;
  return ... z ...;
}

Die bisherige abhängige Funktion wird dann zu einer Folge von unabhängigen Funktionen. Die können so zum bisherigen Ganzen zusammengeschaltet werden:

return c_nachbereitung(s(c_vorbereitung(42)));

Das ist jedoch schlecht lesbar, wenn auch vielleicht schön knapp formuliert. Besser finde ich es daher wie folgt:

int x = c_vorbereitung(42);
int y = s(x);
return c_nachbereitung(y);

Oder moderner könnte es in F# auch so aussehen:

42 |> c_vorbereitung |> s |> c_nachbereitung

Da ist doch sonnenklar, wie die Verantwortlichkeit, für die das ursprüngliche c() stand, erfüllt wird, oder? Das kann man lesen wie einen Text: von links nach rechts und von oben nach unten.

Bitte lassen Sie es sich auf der Zunge zergehen: Keine der Funktionen hat nun noch eine funktionale Abhängigkeit mehr. [1]

c_vorbereitung() ruft keine Funktion auf und weiß nichts von s() und c_nachbereitung(). Dito für s() und c_nachbereitung(). Keine Funktion weiß von einer anderen. Das nenne ich "gegenseitige Nichtbeachtung" (engl. mutual oblivion). Und wenn man sich so bemüht zu codieren, dann folgt man dem Principle of Mutual Oblivion (PoMO).

Funktionen sind ein Mittel, um Funktionseinheiten so zu beschreiben, dass sich nicht um Vorher oder Nachher scheren. Es gibt aber auch andere Mittel. Methoden können mit Continuations versehen werden oder Klassen können mit Events ausgestattet werden.

Egal wie das Mittel aber aussieht, die Funktionseinheit beschreibt immer nur sich selbst. Ihr Interface/ihre Signatur ist rein selbstbestimmt. Bei Funktionen fällt das gar nicht so sehr auf. Deren Ergebnisse sind sozusagen anonym. Anders sieht das aus, wenn Continuations ins Spiel kommen:

void c_vorbereitung(int a, Func<int,int> ???) {
  ???(... a ...);
}

Wie sollte die Continuation ??? heißen? Sollte sie vielleicht s genannt werden? Nein! Dann würde c_vorbereitung() wieder eine semantische Abhängigkeit eingehen. Die Prozedur würde eine bestimmte Dienstleistung erwarten, die ihr übergeben wird.

??? muss vielmehr rein auf die Prozedur bezogen sein. Die Continuation könnte neutral result oder continueWith betitelt sein. Oder sie könnte sich auf den produzierten Wert beziehen:

void c_vorbereitung(int a, Func<int,int> on_x) {
  on_x(... a ...);
}

Einen Vorteil hat eine Continuation gegenüber einem Funktionsergebnis sogar: damit können beliebig viele Ergebnisse zurückgeliefert werden, ohne eine Datenstruktur aufbauen zu müssen.

Vorteile der Vermeidung funktionaler Abhängigkeiten

Inwiefern dient das PoMO aber nun der Vermeidung der durch funktionale Abhängigkeit aufgeworfenen Probleme?

ad Problem #1: Es gibt keine Abhängigkeit, also muss auch keine Bereitstellung erfolgen. Die Frage nach statischer oder dynamischer Abhängigkeit stellt sich nicht einmal. Um c_vorbereitung() oder c_nachbereitung() zu testen, sind keine Attrappen nötig. Von Inversion of Control (IoC) oder Dependency Injection (DI) ganz zu schweigen.

ad Problem #2: Es gibt keine Abhängigkeit, also gibt es auch keine topologische Kopplung. Ob s() auf dem einen oder auf dem anderen Interface definiert ist, interessiert c_vorbereitung() und c_nachbereitung() nicht.

ad Problem #3: Das Wachstum von Funktionen wird ganz natürlich begrenzt. Wann immer eine Funktion eine Dienstleistung benötigt, muss sie beendet werden. Sie bereitet sozusagen nur vor. Für die Nachbereitung ist eine weitere Funktion zuständig. Überlegen Sie, wie lang Funktionen dann noch werden, wenn sie nur noch von einer Delegation bis zur nächsten reichen dürfen. Ich würde sagen, dass es kaum mehr als 10-20 Zeilen sein können, die Sie guten Herzens ohne Verletzung eines anderen Prinzips anhäufen können.

ad Problem #4: Ohne funktionale Abhängigkeit gibt es zumindest keine direkte syntaktische Abhängigkeit. Wenn sich die Form des Output von s() ändert, muss c_nachbereitung() nicht zwangsläufig nachgeführt werden. Es könnte auch eine Transformation zwischen s() und c_nachbereitung() eingeschoben werden:

...
Tuple t = s(x);
int y1 = t.Item1;
return c_nachbereitung(y1);

Das mag nicht immer möglich sein, doch zumindest sinkt das Risiko, dass sich Änderungen an s() ausbreiten.

Dito für eine Änderung der Form der Parameter von s(). Auch hier könnte eine Transformation zwischen c_vorbereitung() und s() die Veränderung abpuffern.

ad Problem #5: Semantische Änderungen, also logische Kopplungen, haben am ehesten weiterhin eine Auswirkung auf Vorbereitung bzw. Nachbereitung. Doch auch hier ist Pufferung möglich, z.B.

Vorher:
int[] sortierteDaten = s(...);
return c_nachbereitung(sortierteDaten);

Nachher:
int[] unsortierteDaten = s(...);
int[] sortierteDaten = unsortierteDaten.Sort();
return c_nachbereitung(sortierteDaten);

Fazit

Die Auflösung funktionaler Abhängigkeit ist immer möglich. Das ist kein technisches Hexenwerk.

Das mag hier und da (zunächst) mühsam sein - doch der Gewinn ist groß. Und wenn man ein bisschen Übung darin hat, dann kann man quasi nicht mehr anders denken. Glauben Sie mir :-) Ich mache das jetzt seit vier Jahren so und es geht inzwischen mühelos.

Sollten funktionale Abhängigkeiten immer aufgelöst werden? Nein. Das wäre Quatsch. Man würde auch in einen infiniten Regress hineinlaufen. Die Frage ist also, wann auflösen und wann nicht? Doch davon ein anderes Mal.

Für heute ist mir nur wichtig, eine Lösung der aufgeworfenen Probleme aufgezeigt zu haben. Die sind aber natürlich gegen andere Werte abzuwägen. Evolvierbarkeit als eine Form von Flexibilität hat ja immer den Gegenspieler Effizienz.

Endnoten

[1] Falls Sie einwenden wollen, dass doch nun irgendwie auf einer höheren Ebene eine Funktion von drei anderen abhängig geworden sei, dann halten Sie den Einwand bitte noch ein bisschen zurück. Sie haben ja Recht. Doch darauf möchte ich in einem späteren Blogartikel eingehen.

Nehmen Sie für heute einfach hin, dass das, was c() mit seiner funktionalen Abhängigkeit war, aufgelöst wurde in drei unabhängige Funktionen.