Follow my new blog

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.

Samstag, 1. Januar 2011

Kein Verständnis ohne Überprüfung – Warum Wasserfallentwicklung nicht funktionieren kann

imageGerade lese ich das Manuskript eines Buches, das demnächst im dpunkt.verlag erscheinen wird. Man hat mich nach meiner Meinung danach gefragt und ich finde es spannend. Die Autoren schauen durch die Brille der Psychologie unter fragen kritisch, inwiefern liebgewonnene oder gehypte Praktiken eigentlich auf einem wissenschaftlichen Fundament ruhen. Das finde ich gleichermaßen wichtig wie erhellend. Schön, dass in der letzten Zeit die softe Seite der Softwareentwicklung zunehmend Beachtung findet. Und der dpunkt.verlag tut sich da in Deutschland besonders hervor.

Aber mir geht es nicht um eine Buchrezension, vielmehr möchte ich Gedanken des Buches zum Anlass nehmen, mich selbst nochmal systematisch mit dem Vorgehen in der Softwareentwicklung zu beschäftigen. Was tun wir denn da eigentlich, wenn wir Anforderungen in Software gießen? Und wenn geklärt ist, was wir da tun, wie wird das am besten getan?

Meine These: Softwareentwicklung ist verlustbehaftete Kommunikation unvollständiger Daten. Deshalb ist Softwareentwicklung so besonders.

Datenübermittlung systematisch

Softwareentwicklung setzt Ideen im Kopf des Kunden um in Software, die der Kunde nutzt. Es geht also um eine Transformation von Ideen in ein Produkt. Naja, das ist noch nichts Besonderes; nichts anderes passiert in einem Restaurant, in dem ein Koch eine Idee für ein neues Gericht hat und am Ende liegt etwas auf dem Teller des Gastes. Auch da wird eine Idee in ein Produkt transformiert. Dennoch glaube ich, dass es nützlich ist, genau hinzuschauen und den Prozess der Softwareentwicklung einmal zu visualisieren.

Anfangen möchte allerdings mit simpler Datenübermittlung. Wenn einer “Daten im Kopf hat” die er einem anderen übermittelt, damit sie “in dessen Kopf sind”, dann sieht das so aus:

image

Der Sender, der mit den Daten im Kopf, kann seine Gedanken leider nicht per Telepathie dem Empfänger zukommen lassen. Also muss er sie in ein Medium verpacken (enkodieren). Dieses Medium interpretiert (dekodieren) der Empfänger; dadurch entsteht bei ihm eine eigene Vorstellung von dem, was der Sender im Kopf hat.

Wie das Bild zeigt, ist diese Datenübermittlung nicht problemlos. An mindestens zwei Punkten kann es zu Fehlern kommen:

  • Es kann dem Sender passieren, dass er seine Daten nicht 100% korrekt auf das Medium überträgt. Oder vielleicht ist der Sender schuldlos und das Medium schlicht nicht reichhaltig genug, um die Daten 1:1 zu repräsentieren. In jedem Fall kann es leicht passieren, dass das Medium am Ende nur eine verarmte/reduzierte Version der ursprünglichen Daten transportiert.
  • Und dann kann es natürlich dem Empfänger passieren, dass er die Daten im Medium inkorrekt interpretiert. Dann entsteht bei ihm eine andere Vorstellung als beim Sender. Die Daten im Kopf von Sender und Empfänger stimmen nicht überein. Allemal kann das nicht anders sein, wenn das Medium die ursprünglichen Daten nicht korrekt enthält – aus welchem Grund auch immer.

Übertragungsfehler – allemal bei menschlicher Kommunikation – sind mithin nicht zu vermeiden. Es stellt sich daher die Frage: Wie kann ein Sender eigentlich herausfinden, ob der Empfänger am Ende die beabsichtigten Daten “im Kopf hat”?

Wären Enkodieren und Decodieren ohne Probleme, das Medium reichhaltig genug, die Kommunikation also verlust und fehlerfrei… dann könnte der Sender immer sicher sein, dass beim Empfänger das vollständige Bild entsteht. Dann würde es am Ende genügen zu fragen, “Alles angekommen? Alles verstanden?” Und der Empfänger bräuchte nur mit “Ack” oder “Roger” oder “Verstanden” antworten.

Interessanterweise ist solch naive Nachfrage immer noch recht häufig zu hören. Achten Sie bei der nächsten Schulung einmal darauf oder wo immer ein Wissenstransfer stattfinden soll. Irgendwann wird da einer fragen, “Wie schaut es aus? Bis hierher alles klar?” und die kollektive Antwort der Wissensempfänger wird sein “Jo, alles klar. Es kann weiter gehen.”

Leider hat diese Antwort jedoch keinen Wert. Wissensempfänger können nicht wissen, ob sie das zu vermittelnde Wissen tatsächlich schon haben. Selbst in viel simpleren Situationen kann der Empfänger nicht einfach so wissen, ob er korrekt empfangen hat. Deshalb gibt es Prüfsummen.

image

Der Sender überträgt nicht nur seine Daten, sondern auch noch eine Prüfsumme, die sich aus den Daten errechnen lässt. Der Empfänger kann dann selbst feststellen, ob er korrekt empfangen hat, indem er mit demselben Algorithmus wie der Sender selbst die Prüfsumme berechnet und mit mitgelieferten vergleicht.

Stimmen die Prüfsummen überein, ist alles ok. Stimmen sie nicht überein, sind entweder die Daten oder die Prüfsumme inkorrekt übertragen worden. Der Empfänger muss den Sender also nur im (unwahrscheinlichen) Übertragungsfehlerfall belästigen. (Das gibt dem Sender zwar keine Sicherheit darüber, ob der Empfänger überhaupt Daten empfangen hat, aber den Fall lasse ich mal außen vor. In den Kommunikationen, um die es mir geht, ist das kein wirkliches Problem.)

Wenn für die zu übertragenden Daten eine Prüfsumme errechenbar ist, d.h. weitere Daten existieren, anhand derer der Empfänger prüfen kann, ob die eigentlichen Daten korrekt angekommen sind, dann ist Datenübertragung ohne weitere Kommunikation möglich. Dann kann der Datenstrom unidirektional vom Sender zum Empfänger fließen. Und nur im (seltenen) Fehlerfall ist ein Empfänger-Sender-Kommunikation nötig.

Solange jedoch keine Prüfsumme existiert, kann der Sender ohne eine “Spiegelung” durch den Empfänger nicht wissen, ob der die Daten korrekt empfangen hat.

image

Wenn der Empfänger während der “Spiegelung” zum Sender wird, kann es natürlich wieder zu einer verlust- und fehlerbehafteten Kommunikation kommen. Falls das Verständnis beim Empfänger inkorrekt ist, ist seine Darstellung dessen gegenüber dem ursprünglichen Sender ebenfalls notwendig inkorrekt. Darüber hinaus ist es aber wahrscheinlich, dass der Empfänger nicht einmal in der Lage ist, sein eigenes Verständnis präzise zu vermitteln. Zumindest ist das der Normalfall bei nicht trivialen Daten, also in allen menschlichen Lernsituationen.

Die Übertragung von Anforderungen aus dem Kopf eines Kunden in den Kopf eines Entwicklers kann nicht in einer Einbahnstraße geschehen. Anforderungen sind sehr komplizierte Daten, deren Enkodierung sicherlich stark verlust- und fehlerbehaftet ist. Dasselbe gilt für die Dekodierung. Das heißt, es sind viele Spiegelungen und Übermittlungswiederholungen nötig, um Anforderungen verlässlich zu übertragen. Zu glauben, es sei mit einem dicken Anforderungsdokument getan, das der Entwickler liest, versteht und in Software umsetzt, ohne großartig nachzufragen, ist naiv.

Anforderungen in Software transformieren

Am schönsten wäre es natürlich, wenn der Kunde selbst seine Software herstellen könnte. Mag sein – ist aber irreal. Soweit ist die Softwareentwicklung noch lange nicht, auch wenn Excel, Access und DSLs das manchmal suggerieren. Dennoch hier der Idealzustand im Bild:

image

Der Sender/Kunde enkodiert seine Anforderungen in eine Maschine (Software). Die transformiert dann Eingaben in Ausgaben, so wie der Kunde es sich wünscht.

Da das Encodieren auch hier natürlich fehler- und verlustbehaftet sein kann, stellt sich die Frage, wie der Maschinenbauer feststellt, ob die Maschine das tut, was sie tun soll? Ganz einfach: er muss sie mit Testeingaben füttern und schauen, ob sie daraus die zu erwartenden Ausgaben macht. Zur Definition einer Maschine gehören also nicht nur ihre Transformationsregeln (funktionale Anforderungen), sondern auch Beschreibungen zulässiger Eingaben und der daraus zu erzeugenden Ausgaben.

image

Wer eine Maschine bauen will, muss also sogar etwas mehr “im Köpfchen haben” als Regeln; er muss sehr konkret wissen, welche Eingaben erlaubt sind und in welche Ausgaben die durch eine korrekte Implementierung der Regeln in der Maschine transformiert werden sollen:

image

Damit stellt sich der reale Softwareentwicklungsprozess als Kette von Datenübermittlungen dar: der Kunde übermittel Anforderungen an einen Stellvertreter, der die Anforderungen dem Team vermittelt, das daraus eine Maschine baut, die der Kunde bzw. sein Stellvertreter prüfen müssen. Hier eine etwas verkürzte Darstellung der minimalen Entwicklungssequenz:

image

Eigentlich ganz einfach, oder? Es müssen die Anforderungen nur drei Mal verlust- und fehlerfrei enkodiert werden und zwei Mal verlust- und fehlerfrei dekodiert werden.

Entwicklung vs. Produktion

Selbst diese vereinfachte Darstellung der Softwareentwicklung sollte nun eines klar machen: Softwareentwicklung ist eben Entwicklung und nicht Produktion. Produktion ist nämlich, wenn eine existierende Maschine (oder Maschinerie) wiederholt Eingaben und Ausgaben transformiert. Die Eingaben mögen in einer gewissen, vergleichsweise geringen Bandbreite variieren und damit auch die Ausgaben. Aber letztlich läuft die Maschine(rie) ohne große Änderungen durch.

image

Das ist ja Sinn und Zweck einer Maschine(rie): Ermüdungsfrei immer wieder dasselbe tun. Ganz unkreativ – dafür verlust- und fehlerfrei.

Wenn eine Software erstmal entwickelt ist, dann ist sie selbstverständlich eine solche Maschine. Dann kann der Kunde mit ihr Ausgaben aus Eingaben produzieren. Immer und immer wieder.

Der Weg zur Software hin ist allerdings, der ist keine Produktion. Weder Kunde, noch Stellvertreter, noch Team sind Maschine(rie)n. Von ihnen kann bei aller möglicherweise vorhandenen Erfahrung nicht erwartet werden, dass sie verlust- und fehlerfrei Eingaben in Ausgaben transformieren. Enkodieren und Decodieren sind essenziell schwierig, sehr schwierig. Aus Anforderungen eine Software zu machen, ist mithin ein aufwändiger Kommunikations und Realisierungsprozess. Ansprüche, wie man sie an die Produktion stellt (z.B. gute Schätzbarkeit in puncto Geld- und Zeitaufwand), sind hier fehl am Platze. Softwareentwicklung ist vielmehr kreative Einzelstückherstellung, beginnt also immer wieder bei quasi Null.

Entwicklung als Lernprozess

Hier ein typischer Vermittlungsprozess ganz allgemeiner Art:

image

Die zu vermittelnden Daten sind kompliziert; es handelt sich um Wissen, also Fakten und Zusammenhänge. Enkodieren und dekodieren sind deshalb notwendig verlust- und fehlerbehaftet. Also ist eine Spiegelung des Empfangenen durch den Empfänger gegenüber dem Sender nötig. Und wenn der Empfänger “sich dumm anstellt” oder der Sender “sich nicht ausdrücken kann” oder schlicht die Natur der Daten auch bei kommunikationsfähigen und intelligenden Sendern und Empfängern keine Abkürzung erlaubt, dann ist eine “Datenübertragung” nur im Dialog möglich. Sender und Empfänger brauchen mehrere Iterationen, damit beim Empfänger die Daten in ausreichend hoher Korrektheit und Vollständigkeit ankommen.

Das nennt man dann Lernen.

Da es für nicht trivialen Lernstoff keine Prüfsummen gibt, braucht es einen Dialog, um Wissen vom Wissenden zum Unwissenden zu übertragen. Es geht zwar auch im Monolog, indem der Wissende sein Wissen einmalig enkodiert, z.B. in ein Buch oder ein Video, und der Unwissende dieses Medium wiederholt dekodiert und dann versucht, mit seinem Verständigs Eingaben in Ausgaben zu transformieren. Doch solches Autodidaktentum ist mindestens langwierig. Aber vor allem besteht keine Gewähr, dass der Unwissende je das Wissen des Wissenden erlangt. Denn bei aller Mühe, die sich der Wissende beim encodieren gibt, kann es zu Verlusten und Fehlern kommen. Wie aber soll dann der Unwissende aus einem löchrigen und/oder fehlerhaften Medium lückenloses und fehlerfreies Wissen dekodieren?

Wer an effektiver und effizier Wissensweitergabe interessiert ist, der stellt sich besser auf einen Dialog mit dem Wissensempfänger ein.

Jetzt konkreter für die Softwareentwicklung. Wie nun offensichtlich sein sollte, ist Softwareentwicklung Lernen. Der Stellvertreter lernt vom Kunden, der Softwareentwickler lernt vom Stellvertreter des Kunden, die Software “lernt” vom Entwickler. Mindestens drei Dialoge müssen also geführt werden, denn – darin sind sich wohl alle einige – Anforderungen sind keine trivialen Daten.

image

An den drei Dialogen gehts nichts vorbei. Die Frage ist allerdings, wie sie geführt werden sollten? Sequenziell…

image

…oder parallel/verschränkt:

image

Die Antwort sollte auf der Hand liegen: Sequenziell kommuniziert liegt eine erste Version der angeforderten Maschine nach 12 Kommunikationsschritten vor; mit verschränkten Dialogen steht eine erste Version jedoch schon nach 8 Kommunikationsschritten bereit.

Als mehrstufige Wissensvermittlung sollte Softwareentwicklung auf keiner Stufe glauben, bei endgültigem Verständnis angekommen zu sein. Deshalb ist ein Vorgehen, das erst eine Stufe abschließt, um dann die nächste zu beginnen, kontraproduktiv. Missverständnisse kann es in allen Kommunikationsphasen geben. Vielmehr ist ein zügiges Fortschreiten von der Idee zu ihrer Manifestation in einer Maschine anzustreben, um dem Ideal der Selbstentwicklung durch den Kunden (s.o.) möglichst nahe zu kommen.

Der Lernprozess ist nämlich noch unvollständig dargestellt. Die Herstellung der Maschine Software ist nicht schon abgeschlossen, wenn der Entwickler meint, den Dialog mit ihr beenden zu können. Softwareentwicklung ist vielmehr – da geht sie über das übliche Lernen hinaus – ein rekursiv gekoppelter Prozess.

image

Lernen ist bei allem Dialog nur als Einbahnstraße gedacht: Wissen wird vom Wissenden zum Unwissenden übertragen. Was der dann damit macht, ist dem Wissenden recht egal. Der Lehrer soll nur sicherstellen, dass die Übertragung verlust- und fehlerfrei ist.

Das ist anders bei der Softwareentwicklung. Da will der Wissende/Kunde hinterher mit dem Ergebnis der Übertragung, dem manifestierten Wissen, den Maschine gewordenen Anforderungen etwas anfangen. Deshalb kann der Kunde sich nicht darauf verlassen, dass die oben gezeigte Dialogkaskade – ob sequenziell oder verschränkt – bei genügend Mühe verlust- und fehlerfrei ist. Nein, der Kunde muss das Endergebnis auch noch selbst prüfen. Auch er muss mit der Maschine einen direkten Dialog führen.

image

Und dieser notwendige Dialog des Kunden mit der Software führt dann zu einer nächsten Lernkaskade. Beim üblichen Lernen steht der Lehrer nur am Anfang, seine Aufgabe ist das Senden (und die Überprüfung, ob der Empfang ausreichend ist). Bei der Softwareentwicklung steht der Lehrer/Kunde jedoch am Anfang und am Ende. Er ist gleichzeitig Sender und ultimativer Empfänger.

Das ist aber nicht insofern etwas Besonderes, als dass die Kommunikation grundsätzlich so schwierig ist vom Sender bis zum Produkt/zur Maschine. Das ist auch in anderen Situationen so. Wahrhaft besonders ist die Situation, weil die zu übermittelnden Daten so kompliziert sind.

Man mag es kaum glauben, aber es ist nicht zu leugnen:

  • Kunden kennen nicht einmal die funktionalen Regeln der Maschine vollständig, die sie in Auftrag geben.
  • Kunden kennen die Eingabemenge der Maschine nicht vollständig.
  • Kunden kennen die Ausgabemenge der Maschine nicht vollständig.

image

Das bedeutet für die Kette von Lernprozessen, aus denen die Softwareentwicklung besteht: Keine Stufe kann je sicher sein, dass sie das, was “eigentlich” gewollt ist, verstanden hat. Selbst nach einem Dialog mit seinem Sender hat der Empfänger kein wirklich, wirklich verlässliches Wissen. Kein Empfänger kann es haben, weil der ursprüngliche Sender, der Kunde, es nicht einmal hat.

Das liegt nicht an der Dummheit des Kunden, sondern an der Komplexität der Materie. Softwaremaschinen sind zum einen komplizierter als die meisten Hardwaremaschinene, zum anderen ist ihnen aber auch etwas anderes enkodiert. Softwaremaschinen sind geronnene Geschäftsprozesse und keine Materialtransformatoren. Und da Geschäftsprozesse auch ohne Computer viel schwerer greifbar sind als die Produktion von Wurst, Schuhen oder auch Häusern – schon weil Geschäftsprozesse die enthalten, also umfassender sind –, ist das Wissen des Kunden über sich notwendig lückenhaft.

Es geht also nicht ohne dass der Kunde selbst in Dialog tritt mit der Softwaremaschine, um erstens festzustellen, ob die Wissensvermittlung und Manifestation geklappt hat, und zweitens, um seine eigenen Vorstellungen in der Maschine reflektiert zu sehen. Erst durch den Dialog mit der Maschine kann der Kunde sich selbst und seine Wünsche verstehen. Software ist ein Spiegel; in ihm sieht der Kunde ein gnadenloses Abbild seines Selbstverständnisses bzw. seines Domänenverständnisses.

image

Softwareentwicklung arbeitet an der Grenze des explizit für den Kunden wissbaren. Die Wissensübertragung ist schwierig, weil Enkodieren und Dekodieren über mehrere Studen immer schwierig ist, aber insbesondere das ursprüngliche Wissen unvollständig ist. Deshalb muss die Softwareentwicklung dem Kunden schnell und häufig den Spiegel des aktuellen Softwarestandes vorhalten. Nur so kann der Kunde über sein eigenes vermeintliches Wissen reflektieren.

Prüfsummen für die Softwareentwicklung

Lehrer mögen es, wenn Schüler ihre Lernerfolge selbst kontrollieren können. Dann haben sie weniger Arbeit. Weniger Interaktionen im Dialog sind nötig. Und überhaupt mögen Sender Rückfragen nicht. Deshalb liegen Datenpaketen Prüfsummen bei.

Um in der Softwareentwicklung die notwendigen Dialoge zumindest so kurz wie möglich zu halten, wäre es also nützlich, auch hier Prüfsummen zu haben. Was sind aber Prüfsummen für die funktionalen Regelwerke, die Kunden in Software gegossen haben wollen?

Eingabe-Ausgabe-Paare sind solche Prüfsummen.

Die vornehmste Aufgabe jedes Empfängers von Anforderungen in der Kommunikationskette der Softwareentwicklung ist es daher, beispielhafte Ein- und Ausgaben zu sammeln. Sie sind wichtiger als das Regelwerk der Maschine, da dies ohnehin sehr wahrscheinlich Lückenhaft ist oder im Extremfall gar nicht vorliegt. Die letzte Wahrheit liegt daher immer bei gegebenen Eingaben mit zugehörigen Ausgaben, d.h. Akzeptanztestdaten. Ihnen muss die Aufmerksamkeit bei der Anforderungserhebung zuvörderst gelten.

Liegen Akzeptanztestdaten vor – je mehr desto besser –, kann ein Empfänger selbstständig prüfen, ob sein Verständnis des Maschinenregelwerks korrekt ist. Das senkt die Notwendigkeit für Interaktionen mit dem Sender.

Eine Maschinenbeschreibung sollte daher nicht nur ein Tripel sein – {Funktionale Regeln R, Eingabemengenbeschreibung E, Ausgabemengenbeschreibung A} –, sondern mindestens ein Quadrupel: {R, E, A, ea} – mit ea als Menge von Paaren von Elementen aus E mit zugehörigem Element aus A.

image

Spätestens der Entwickler sollte keine Anforderung entgegennehmen ohne Akzeptanztestdaten. Das spart Rückfragen bei vorgelagerten Sendern und somit Zeit wie Nerven.

Und auf ein Kunde sollte keine Entwicklung beauftragen, ohne für jede Funktionalität Akzeptanztestdaten zu spezifizieren. Solange die Entwicklung nicht kurz und knapp demonstrieren kann, dass die korrekt transformiert werden, muss dem Kunden die Maschine nicht vorgeführt werden.

Wie die korrekte Transformation demonstriert wird, damit der Kunde frühzeitig in “einen Spiegel blicken kann”, ist eine andere Frage. Jedes Mittel ist dabei recht; auf ein vollständige Software muss nicht gewartet werden. Ein spezieller Prüfstand kann auch ein probates Mittel sein – und sollte vom Kunden nicht abgelehnt werden mit dem Argument, da sei ja noch nicht alles fertig. Bei den Dialogen in der Kommunikationskaskade der Softwareentwicklung geht es um zügige Spiegelung aller möglichen Aspekte der Maschinendefinition im Kopf des Kunden. Komplette Benutzerschnittstellen sind dafür nicht unbedingt nötig.

Zusammenfassung

Softwareentwicklung ist keine Produktion im üblichen Sinn. Sie ist ein Lernprozess, unvollständiges und gar fehlerhaftes Wissen aus dem Kopf des Kunden in eine dennoch korrekte Maschine transferieren soll.

Da der Nürnberger Trichter bisher nicht erfunden wurde, ist Lernen ein mühsames Geschäft. Es bedeutet Dialog um Übertragungsverluste und –fehler bei der Wissensvermittlung zu kompensieren. Das gilt umso mehr, wenn das Lernen über mehrere Stationen erfolgen muss. Zentral für die Softwareentwicklung ist also die Kommunikations-, die Dialogkompetenz.

Eine besondere Herausforderung stellt dabei die mangelnde Qualität des expliziten Wissens im Kundenkopf dar. Spätestens sie zwingt den Prozess in eine ständige Rückkopplungsschleife mit dem Kunden. Je schneller der “anfassen kann”, was aus seinem Wissen gemacht wurde, desto eher kann er beurteilen, ob das, was er meinte zu wissen auch wirklich korrekt war. Unumgänglich ist also eine fortlaufende Prüfung von in Software manifestiertem Verständnis durch den Kunden.

Um sich selbst für die Prüfung zu entlasten, kann und soll der Kunde jedoch möglichst viele Akzeptanztestdaten beibringen. Das Entwicklerteam belästigt ihn mit einer neuen Version der Softwaremaschine nur, wenn es aufgrund dieser Testdaten sicher ist, die Anforderungen korrekt verstanden und umgesetzt zu haben. Daran sollte dem Kunden sehr gelegen sein, so dass er sich auch mit speziellen Prüfständen zufrieden gibt und nicht darauf besteht, Anforderungen nur durch die endgültige Benutzerschnittstelle zu prüfen.

Prüfstände haben den Vorteil, dass sie viel einfacher zu realisieren sind als eine Benutzerschnittstelle und auch für ansonsten “versteckte” Subfunktionalität aufgebaut werden können. Der Kunde kann sich dadurch viel zügiger “einen Spiegel vorhalten”, um sein Verständnis der Domäne zu überprüfen. Dass Kunden solchen “Blick in den Spiegel” scheuen mögen, steht auf einem anderen Blatt und vergrößert die Herausforderung Softwareentwicklung. Softwareentwicklung ist wie ein Therapeut, der mit seinen Fragen nicht an der Oberfläche bleibt, sondern sich auch ins Unterbewusste gräbt. Und wer mag das schon?

Verständnis und Einfühlungsvermögen gehören mithin auch zu den wünschenswerten Kompetenzen eines Softwareentwicklungsteams. Wer therapeutisch arbeitet, sollte sich nicht wie ein Elephant im Porzellanladen benehmen.

Und zuguterletzt: Was für Therapeuten angemessen ist, kann für Softwareteams nicht so falsch sein. Zur Softwareentwicklung gehört also auch die Reflexion, gar Supervision. Sonst ist nicht nur keine Nachhaltigkeit der unvermeidlichen eigenen Veränderung gewährleistet – die ist über Effizienz und Effektivität hinaus aber sehr wichtig für langfristigen Erfolg –, nein, Teams laufen sonst auch Gefahr, sich durch ihre Klienten nachteilig beeinflussen zu lassen. Ohne Reflexion/Supervision können auch “Softwaretherapeutenteams” in die Falle Gegenübertragung oder Neurose tappen. Aber davon ein andermal mehr…

An dieser Stelle sollte nur soviel klar geworden sein: Wasserfall oder nicht ist keine Frage von Weltanschauung, sondern eine Frage der Qualität des expliziten Wissens eines Kunden und der Kommunikationskompetenz aller Beteiligten im Softwareherstellungsprozess.

Da Menschen eine Menge wissen, aber oft sich selbst nicht kennen und dasselbe daher für Unternehmen, die aus Menschen bestehen, anzunehmen ist, sollte die Qualität des expliziten Wissens nicht allzu hoch eingestuft werden. Dasselbe gilt für die Kommunikationsfähigkeit vom Benutzer beim Kunden über einen ProductOwner bis zum Entwickler. Durchschnittlichkeit zu erwarten, scheint das Maximum. Und deshalb sind Wasserfall oder andere Vorgehensmodelle, die eher unidirektional sind und/oder viele Sender-Empfänger-Stufen bis zur Software haben, ungeeignet für die Softwareentwicklung.

Dienstag, 28. Dezember 2010

Ein Paradigma, viele Implementierungen

Event-Based Components (EBC) werden vor allem mit C#-Events in Zusammenhang gebracht. Nicht zu unrecht natürlich. Darum ging es ja auch am Anfang: Funktionseinheiten über Events verbinden. So können sie “im Außen” zusammengesteckt werden; “im Innen” haben sie keine Abhängigkeiten mehr.

Inzwischen hat sich die Welt aber gedreht. Aus den EBC sind die Komponenten verschwunden. Und nun verschwinden auch die Events. Oder besser: sie bekommen einen anderen Platz zugewiesen.

Flow Design

In den letzten Monaten habe ich mit vielen Entwicklern über EBC diskutiert und in vielen Zusammenhängen damit gearbeitet. “EBC-Denke” hat auch die Clean-Code-Developer-Trainings, die ich mit Stefan Lieser veranstalte, stark beeinflusst. Sie macht es schlicht soviel leichter, Anforderungen in Lösungen zu transformieren.

Bei all dieser Beschäftigung mit dem Konzept bleibt es nicht aus, dass es sich verändert. Der Blick darauf wird differenzierter. Und so ist mein aktueller Standpunkt, dass EBC nur eine bestimmte Ausprägung eines Paradigmas ist.

Objektorientierung ist ein Paradigma, bei dem es um polymorphe “Dinger” geht, die Daten und Funktionen vereinen und voneinander abhängig sind.

Das hinter EBC stehende Paradigma nenne ich mal “Flussorientierung”. Bei dem geht es um “Dinger”, die Eingaben in Ausgaben transformieren, dabei auf Ressourcen zurückgreifen – und zu Sequenzen verbunden werden. Solche Sequenzen können wieder als “Dinger” betrachtet werden, weil sie als Ganzes Eingaben in Ausgaben transformieren. Im Fluss durch die Sequenzen sind also Daten; die Ausgaben eines “Dings” sind die Eingaben anderer “Dinger”.

Diese Flussorientierung sehe ich inzwischen als so wichtig weil grundlegend an, dass ich sie von den EBC getrennt nutzen möchte. Sie ist für mich sogar so universell, dass ich sie nicht auf die Modellierung, d.h. den Entwurf funktionaler Strukturen beschränken möchte. Flussorientierung ist für mich vielmehr das Ausgangsparadigma für Architektur und Modell. Denn nur mit einer Flussorientierung können wir komplizierte Systeme agil evolvierbar entwerfen, d.h. in dünnen Längsschnitten. Das behaupte ich mal keck :-)

Meine Begründung: Flussorientierung ist der Objektorientierung im Entwurf in mehrerlei Hinsicht überlegen.

  1. Erstens entstehen durch Flussorientierung keine Systeme mit funktionalen Abhängigkeiten;
  2. zweitens sind flussorientierte Systeme intuitiv auf beliebig vielen Abstraktionsebenen denkbar und darstellbar;
  3. drittens geht es bei Anforderungen immer um Verarbeitungsflüsse, so dass flussorientierter Entwurf näher an den Anforderungen verläuft.1)

Aller Softwareentwurf sollte daher, so meine ich derzeit, mit Flussorientierung beginnen.

Flow Architecture

Die nicht-funktionalen Anforderungen sollten konsequent mit einer flussorientierten Architektur erfüllt werden.2) Was da in Flüssen steckt, ist architekturspezifisch. Daten fließen in der Architektur (zumindest) zwischen Bounded Contexts, Apps, Maschinenen und BS-Prozessen.

Der Architekt definiert und bindet physische Funktionseinheiten zu Sequenzen zusammen. Einen Betriebssystemprozess kann man “anfassen” (s. Taskmanager), eine App kann man “anfassen” (s. die BS-Prozesse auf den zugehörigen Maschinen) usw. Mit diesen Funktionseinheiten kann am Ende auch ein IT-Admin etwas anfangen oder eine Einkaufsabteilung, weil hard- oder softwaretechnische Server gekauft werden müssen.

Flow Model

Die funktionalen Anforderungen sollten vor dem Hintergrund der flussorientierten Architektur dann ebenfalls flussorientiert angegangen werden. Das geschieht im Rahmen der Modellierung, die logische Funktionseinheiten definiert und zu Flüssen verbindet. Wo Architektur immer aus einer begrenzten physischen Hierarchie besteht, sind Modelle freier. Ihre Strukturelemente haben deshalb allgemeinere Namen: Platine und Bauteil. Bauteile sind atomare Funktionseinheiten, Platinen zusammengesetzte bzw. zusammensetzende Funktionseinheiten. Die Hierarchien der Funktionseinheiten von Modellen können beliebig tief geschachtelt sein.

Zum Trost der Freunde der Objektorientierung: Die Bauteile des Modells wie auch die in ihm fließenden Daten sind die “Taschen”, in denen für mich Objektorientierung weiterhin ihren Wert hat. Sie sind überschaubar und klar abgegrenzt, so dass Objektorientierung die Harmonie des Großen Ganzen des Entwurfs nicht zerstören kann. “Taschen voller Matsch” sind zwar misslich, aber immer noch viel besser als ganze Anwendungen als Brownfield.

Es obliegt Architektur und Modell, diese “Taschen” so klein zu halten, dass die Nachteile der Objektorientierung ihre Vorteile nicht überwiegen. Das halte ich aber für keine allzu schwierige Aufgabe.

Flow Implementation

Flüsse fließen also überall im Entwurf, in der Architektur und im Modell. Das bedeutet, es kann nicht nur eine Übersetzung für sie geben. Das hatten EBC suggeriert. Aber das ist eine unnötig einschränkende Sichtweise.

Einen Fluss wie

image

in Interfaces mit Events zu übersetzen wie hier

interface IA
{
  event Action<string> Output;
}

interface IB
{
  void Process(string input);
}

ist nur eine Möglichkeit. Sie ist einfach und naheliegend – doch letztlich ist es eben nur eine Möglichkeit.

Genauso gut können die Funktionseinheiten in Methoden übersetzt werden:

string A() {…}

void B(string input) {…}

Das tun Stefan und ich zum Beispiel gern in TDD-Seminaren, wenn wir kleine Aufgaben modelliert haben. Für die meisten Code Katas wäre die Übersetzung in Interfaces/Events ein zu großes Geschütz. Mit der Übersetzung in Methoden muss man nun aber nicht auf die Segnungen der Modellierung auch bei kleinen Aufgaben verzichten.

Und sonst? Es gibt weitere Übersetzungen für Modelle. In einem Blogartikel habe ich zum Beispiel mal mit expliziten Kanälen gearbeitet. Oder hier eine Übersetzung für die Java-Welt – auch ohne Events.

Auf Architekturebene sieht die Übersetzung dann noch wieder anders aus. Wie könnte ein Datenfluss zwischen zwei Bounded Contexts übersetzt werden?

image

Aus dem schlichten Pfeil im Architekturmodell auf hoher Abstraktionsebene kann z.B. ein ETL-Betriebssystemprozess3) auf niedrigerer Ebene werden:

image

Tendenziell werden allerdings flussorientierte Modelle direkt in recht simple programmiersprachliche Konstrukte übersetzt und flussorientierte Architekturen mit flussorientierten Modellen verfeinert.

Aus jeder Entwurfsperspektive gilt jedoch: Für “Kästchen” (oder “Kuller”) und Fluss-Pfeile sind je geeignete Übersetzungen zu finden. Welche das sind, hängt vom Abstraktionsgrad, zur Verfügung stehenden Technologien und nicht-funktionalen Anforderungen ab.

Fazit

Was mit EBC begonnen hat ist nun viel größer geworden. Auf Flussorientierung kann und will ich nicht mehr verzichten. Funktionale und nicht-funktionale Anforderungen möchte ich mit dem Paradigma angehen. Große und kleine Probleme möchte ich damit lösen. Synchron und asynchron möchte ich dabei denken können.

Die Trennung von Paradigma und Implementation, die in EBC ursprünglich nicht so direkt zu sehen war, hilft mir dabei. Ich denke auch, dass sie Flussorientierung attraktiver macht, weil sie keine bestimmte Implementation aufzwingt. Jeder kann einen flussorientierten Entwurf in objektorientierten oder prozeduralen oder funktionalen Code gießen, wie er mag. Je systematischer das geschieht, desto größer der Gewinn. Interfaces und Events sind nur eine Variante von vielen.

In diesem Sinne: Happy Flow-Designing!

Anmerkungen

1) Dass die Objektorientierung meint, bei Anforderungen ginge es vor allem um Daten, halte ich inzwischen für eine sehr tragische Verirrung. Sie ist tragisch, weil Objektorientierung dazu gedacht war “alles besser zu machen”. Endlich Software so strukturieren, wie die Welt uns entgegentritt: als Gewebe von Dingen, die Zustände haben und tätig sind und unterschiedlichen Kategorien angehören.

Klingt ja auch total cool. Klingt sogar philosophisch-richtig, weil hier These (Platon als Proponent einer Ideenwelt unabhängig von den Dingen –> Klassen) und Antithese (Descartes als Stellvertreter des modernen diesseitigen, mechanistischen Denkens –> Objekte) synthetisch vereint scheinen.

Leider ging der Schuss nach hinten los. Ich sehe nicht, dass die Objektorientierung die Softwareentwicklung ihrem Anspruch entsprechend leichter gemacht hat. Unsere Softwaresysteme sind heute größer als früher. Aber nicht wegen der Objektorientierung, sondern weil Rechner heute mehr leisten. Selbst wenn wir heute nur mit C oder Modula arbeiten würden, wären unsere Softwaresysteme größer als die in den 1970ern.

Schuldig geblieben ist die Objektorientierung, dass Softwaresysteme wirklich deutlich leichter entworfen werden können. Und dass Softwaresysteme leichter evolvierbar gemacht und gehalten werden können als zu zeiten von C oder Modula.

Wie an anderer Stelle schon gesagt: Objektorientierung hat ihren Wert. Ich möchte sie nicht missen. Aber sie ist nicht der Heilsbringer, den die Branche in ihr gesehen hat und sieht. Und erkennen lässt sich das an der kontraproduktiven Gewichtung von Daten bei der Analyse von Anforderungen, um die dann herum Funktionalität gruppiert wird. Das Ergebnis sind oft/meist ideal-naive oder schlicht falsche Repräsentationen der Welt.

2) Flussorientierte Architektur ist kein neuer Gedanke. Event-Driven Architecture (EDA) ist ein etabliertes Architekturmuster in diesem Sinne. Ich möchte jedoch darüber hinaus gehen. EDA ist heute vor allem in “großen Anwendungen” in Gebrauch. Das empfinde ich als unnötige Beschränkung, die vor allem einem Denken entspringt, EDA hätte mit spezieller oder gar teurer Infrastruktur zu tun.

Jede Software kann davon profitieren, wenn ihre Architektur flussorientiert ist. Höhere Evolvierbarkeit sollte kein Privileg großer Systeme sein.

3) Ein ETL-Prozess nimmt in der Architektur insofern eine besondere Bedeutung ein, als dass er nicht nur einem Bounded Context angehört. Eine eigene Ebene in der Strukturhierarchie der Architektur verdient er deshalb jedoch nicht. Er kann als App oder als BS-Prozess angesehen werden.

Montag, 27. Dezember 2010

Ein Team das hat zwei Modi, zwei Modi hat ein Team

Schade, dass in diesem Beitrag von Bernd Oesterreich das Team so einseitig gezeichnet ist. Es ist fast ausschließlich im operativen Modus beschrieben. Ein Fußballteam während des Spiels.

Ungleich und nicht gleichberechtig: so sollen Teammitglieder vor allem sein? Wussten wir das aber nicht immer schon und haben der sozialromantischen Version demokratischer Kuschelteams misstraut? Nun wissen wir endlich, dass wir damit richtig lagen.

Oder?

Für meinen Geschmack wurde hier bei allem systemischen Ansatz leider das Kind mit dem Bade ausgeschüttet.

Klar, im operativen Modus, wenn das Spiel läuft, wenn Arbeit weggeschafft werden muss, wenn es ums Koordinieren von Handgriffen geht, dann ist "Ansagen" ein probates Mittel. Dann sind die Teammitglieder ungleich, weil immer einer etwas mehr Überblick hat als andere. Im Tagesgeschäft sind demokratische Entscheidungen oder gar Konsens hinderlich, weil ineffizient. Dann gehts um Ausführen, ums abarbeiten.

Der operative Modus ist aber nicht alles. Kann es nicht sein, weil es in einen Rahmen braucht. Auf den spielt Bernd Oesterreich zwar an

[…] erst schaut das Team, wer entscheiden soll und fallbezogen kompetent ist, dann entscheiden die ausgewählten Entscheider.

aber der ist für ihn Nebensache. Schade. Denn hier bestünde die Chance, dem Team eine wahrhafte Selbstorganisation einzuräumen, ohne die operative Effizienz in Sozialromantik zu ersticken.

Der Schlüssel liegt aus meiner Sicht in einer klaren Separation of Concerns (SoC). Warum sollte die auch nur im Softwareentwurf vorteilhaft sein? ;-)

Entscheidungsmethoden und –wege im operativen Geschäft sind klar zu trennen von Entscheidungsmethoden und –wegen der Rahmendefinition. Ein selbstorganisierendes Team tut nicht nur etwas im Sinne seines Zwecks, sondern organisiert sich eben auch, um ihn in bester Weise zu erfüllen.

Wie die Organisation im operativen Geschäft aussieht, ist dabei gar nicht so spannend. Darüber schreibt Bern Oesterreich aber vor allem. Wer geglaubt hat, das Konzept der Selbstorganisation mache darüber eine Aussage oder würde gar Gleichberechtigung und Demokratie “während des Spiels” empfehlen, hatte es schon immer missverstanden.

Selbstorganisation bezieht sich vielmehr auf die Rahmengebung, d.h. auf die Metaebene. In der Selbstorganisation entscheidet das Team, also alle Mitglieder kollektiv, darüber, wie es sich für das operative Geschäft organisiert. Wie diese Entscheidungen ausfallen, ist egal. Wie sie getroffen werden, ist spannend.

Da kommt man nämlich nicht an Gleichheit und Gleichberechtigung vorbei, wenn Selbstorganisation noch irgendwie einen Unterschied machen soll. Selbstorganisation bedeutet schlicht, dass die Teammitglieder in allen Rahmenentscheidungen gleich und gleichberechtigt sind. In Rahmenentscheidungen entscheidet kein Einzelner, sondern das Team als Ganzes. Alles andere würde das Team in Bezug auf Rahmenentscheidungen zu einer Gruppe degradieren, für die gedacht und entschieden wird.

Gerade in der Gemeinsamkeit der Entscheidung für den eigenen Rahmen zeigt sich das, was ein Team unterscheidet von einer Gruppe. Dass Menschen im operativen Geschäft als Team arbeiten, mag schwierig sein. Aber es ist umso schwieriger, je weniger diese Menschen sich gemeinsam einen Rahmen geben können. Denn die gemeinsamen durchlaufenen Prozesse zur Rahmenfindung und der von allen getragene Rahmen sind wesentliche Bestandteile der Kultur eines Teams.

Wer Selbstorganisation als sozialromantisch abtut und auf die Wahl von Führern stutzt, verschenkt großes Potenzial für die Motivation und bremst die “kollektive Intelligenz” des Teams aus.

Aber alles wird einfach, wenn man die zwei Modi eines Teams klar unterscheidet: operatives Geschäft und Rahmengebung. Dann kann Ungleichheit und Gleichheit, dann kann “Ansagen” und gemeinschaftliche Entscheidung nebeneinander existieren. Kein entweder-oder, sondern sowohl-als-auch.

Samstag, 25. Dezember 2010

In Abhängigkeit refaktorisieren

imageRefaktorisieren ist überbewertet – zu der Überzeugung gelange ich immer mehr. Refaktorisieren ist eine Symptomkur, die das System, das Paradigma, das zu dem Zustand geführt hat, den sie auflösen soll, nicht verlässt.

Vielleicht ist das auch der Refaktorisierung selbst nicht anzukreiden. Das mag sein. Refaktorisierung bedeutet dann nicht nur eine funktionserhaltende Umstrukturierung, sondern genauer eine paradigmatische funktionserhaltende Umstrukturierung.

Dann ist es denen anzukreiden, die die Refaktorisierung – speziell die objektorientierte Refaktorisierung - so vertreten, als sei sie der Schlüssel zum Glück. “Schaffe dir die Refaktorisierungsmuster drauf, schaffe dir ein Tool wie ReSharper an – und schon ist der Weg aus dem Brownfield abgesteckt.” Oder auch: “Folge treu dem Dreischritt red-green-refactor und ein Platz auf der ewig grünen Wiese ist dir gewiss.”

Bullshit!

Selbstverständlich hat Refaktorisierung einen Wert. Sie ist ein Tool, das man zur rechten Zeit einsetzen kann und soll. Mein Eindruck ist nur, dass die jedem Tool eigene Begrenzung beim Lob der Refaktorisierung vergessen wird. Wo hat je ein Artikel gesagt: “Stopp! Ab hier hilft Refaktorisierung nicht weiter, um Evolvierbarkeit zu erreichen.” oder “Refaktorisierung macht nur in diesem oder jenem Rahmen Sinn.”

Klar, es wird schonmal erwähnt, dass man vorsichtig sein solle und Refaktorisierung nicht zum Selbstzweck werden darf. Refaktorisierung solle vor allem helfen, einen anderen Zweck zu erfüllen; refaktorisiere dann, wenn eine Veränderung es gebietet und nicht “auf Vorrat”.

Damit ist aber keine Grenze des Werkzeugs Refaktorisierung aufgezeigt, sondern nur die Intensität seiner Anwendung.

Mir tritt nun aber immer deutlicher eine solche Grenze ins Auge. Die üblicherweise beschriebene Refaktorisierung, die ReSharper-Refaktorisierung, hat ihre Grenze in der Objektorientierung. Refaktorisierung kann nur mit den Mitteln der Objektorientierung arbeiten. Der Code nach der Refaktorisierung ist immer noch objektorientiert.

Wie gesagt: Der Refaktorisierung selbst kann man das nicht anlasten, aber ihren Proponenten, die diese Grenze nicht aufzeigen – und sei es aus den löblichsten Beweggründen.

Eigentlich müsste ich deshalb wohl sagen: Objektorientierung ist überbewertet. Entzündet haben sich meine folgenden Gedanken aber an der Refaktorisierung. Deshalb habe ich es eingangs anders formuliert.

Und was stört mich nun an der Refaktorisierung bzw. am OO-Paradigma?

Mich stören die Abhängigkeiten.

imageMan mag lesen, Objektorientierung sei die Summe aus Kapselung plus Polymorphie plus Vererbung. Das ist natürlich nicht falsch, selbst wenn man sich trefflich darüber streiten kann, ob denn nun Vererbung ein unverbrüchliches Element der Objektorientierung sei.

Das Problem solcher Beschreibung von Objektorientierung ist nicht, was sie als Eigenschaften nennt, sondern das, was sie auslässt. Sie lässt – ganz natürlicherweise, übrigens – aus, das was der Objektorientierung zugrunde liegt. Sie lässt ihr Erbe aus. Das liegt in der prozeduralen Programmierung. Und noch tiefer liegt es in der von-Neumann-Maschine.

Es gibt keine wirkliche Einheit von Daten und Funktionen in unseren Rechnern, wie Objekte sie vorgaukeln. Zwar liegen Daten und Funktionen im grundsätzlich selben Speicher (Prozessadressraum), doch darin sind sie sauber getrennt. Das ist heute immer noch so wie zu Zeiten von Modula, C, Fortran, Assembler.

Objekte sind daher nur moderne Formen von C-Strukturen oder Pascal-Records. Und Methoden rufen sich immer noch mit einer call-Maschinencodeanweisung auf. Ob die zu einer virtuellen oder realen Maschine gehört, ob der Sprung direkt oder über eine Dispatch-Tabelle geht… das ist einerlei.

Objektorientierung abstrahiert von der notwendigen Trennung von Daten und Funktionen im Speicher. Das ist eine gute Sache. Aber das ist eben nicht alles. Sie bleibt dem Abhängigkeitsdenken, dem Callstack-Denken verhaftet. Wie Methoden einander nutzen, unterscheidet sich nicht davon, wie weiland C-Funktionen sich genutzt haben.

Das halte ich für ein Problem. Hier sehe ich die Grenze für die Nützlichkeit der Objektorientierung (wie der prozeduralen Programmierung oder auch Anteile der Funktionalen Programmierung).

Man mag dagegen halten, dass so die Welt sei und doch auch soviel machbar sei und funktioniere. Klar. Das ist unzweifelhaft.

Bis zum Beginn des 19. Jahrhunderts war aber auch viel machbar und hat funktioniert. Stonehenge, Pyramiden, Petra, Chinesische Mauer, Petersdom… all das wurde vor allem mit “menschlicher und tierischer Kraft” geschaffen. Die Werkzeuge haben sich in 5000 Jahren natürlich verändert. Was den Erbauern von Stonehenge noch sehr schwer gefallen war und Jahrzehnte gedauert hat, war für die Baumeister des Kölner Doms kein wirkliches Thema mehr.

Doch was ist dieser Fortschritt angesichts dessen, was dann ab dem 19. Jahrhundert passiert ist? Dampfmaschine, Explosionsmotor und Elektromotor haben das Bauen revolutioniert - genauso wie die Fortschritte in den Materialien Stahl, Beton, Glas, Kunststoff.

Alle Verbesserungen innerhalb des Paradigmas (oder Denkhorizonts) “menschliche und tierische Kraft” (oder “Materialien aus der Natur”) konnten nicht vorausahnen lassen, was passierte, als man dieses Paradigma sprengte.

Innerhalb der Grenzen von “menschliche und tierische Kraft” war viel möglich, sehr viel – arrogant wäre jedoch der gewesen, der behauptet hätte, damit sei die Grenze des Machbaren erreicht, der Gipfel der Baukunst erklommen.

So sehe ich es mit der Objektorientierung. Mit ihr im Speziellen und im Rahmen des zugrundeliegenden “Abhängigkeitsparadigmas” für Funktionalitätsnutzung ist viel möglich, sehr viel. Arrogant wäre jedoch der, der behauptete, damit sei die Grenze des Machbaren (oder Wünschenswerten), der Gipfel der Programmierkunst erreicht. Man müsse nur lernen, Objektorientierung sauber zu betreiben und schon sei nur noch der Himmel die Grenze.

I beg to disagree.

Menschliche und tierische Kraft und natürliche Baumaterialien waren und sind genauso beschränkend wie das “Abhängigkeitsparadigma” der prozeduralen und objektorientierten Programmierung. Und beim hochgelobten Refaktorisieren zeigt sich das für mich sehr deutlich. Hier ein Beispiel mit einer Folge von Anweisungen in einer Methode in einer Klasse:

image

Was würde daraus durch Refaktorisierung? Mit “Extract Method” würden Anweisungsgruppen ausgelagert in eigene Methoden:

image

Und mit “Extract Class” würden Methoden dann nochmal in separate Klassen rausgezogen:

image

Für einen Freund der Objektorientierung und des Refactoring sieht das alles normal aus. Prinzipien wie SPR und SLA werden in Anschlag gebracht und treiben eine differenzierte Struktur aus. Statt alles in einer Methode abzufrühstücken, wird auf kleinere Funktionseinheiten verteilt. Die LOC pro Methode sinken, die LOC pro Klasse sinken, die Cyclomatische Komplexität sinkt. Alles scheint in Butter.

Ich sehe das anders. Denn ich sehe eine Vervielfältigung von Abhängigkeiten. Vor der Refaktorisierung war der Code schlecht zu verstehen, weil er Anweisungen auf verschiedenen Abstraktionsebenen enthielt und verschachtelt war; dafür stand er “auf einem Haufen”. Nach der Refaktorisierung ist der Code aber immer noch schlecht zu verstehen. Denn erstens sind die algorithmischen Verschachtelungen nicht aufgehoben, sondern nur verschleiert worden. Und zweitens sind zu ihnen Indirektionen in Form von Abhängigkeiten getreten. Der vorherige Haufen ist jetzt nur kein Haufen mehr, sondern in alle Winde zerstreut.

Der refaktorisierte Code ist selbstverständlich im OO-Paradigma geblieben. Die Abhängigkeiten sprießen. Und weil sie sprießen, muss nun auch noch über Praktiken wie IoC und Tools wie DI Container nachgedacht werden.

Etwas übertrieben gesagt: Die Objektorientierung ist eine so komplizierte Technologie, dass man sie nicht ohne eine Vielzahl von begrenzenden und stützenden Prinzipien, Praktiken und Tools einsetzen kann. Damit ist sie mehr ein Problem denn eine Lösung.

Mit so einem Statement mache ich mich natürlich unbeliebt. Das ist mir klar. Aber ich habe lange drüber nachgedacht und spreche es daher nicht leichtfertig aus. Die wahren Gewinne, die nur oder zumindest vor allem durch Objektorientierung erzielt wurden in der Softwareentwicklung, sind mir zu gering, als dass ich sagen wollte, sie sei eine einzige Erfolgsgeschichte. Und das sage ich, obwohl (oder weil?) ich seit 1990 objektorientiert programmiere.

Natürlich gibt es Gewinn durch Objektorientierung. Aber ich sage mal provokant: Wer hat je dagegen die Verluste durch Objektorientierung aufgerechnet? Ist die Bilanz wirklich positiv. Ich habe Zweifel.

Wie gesagt: Irgendwie funktioniert ja die OO-Software. Keine Frage. Aber wenn es heute unwartbare Systeme gibt, dann sind das Anwendungen auf OO-Basis. Man mag einwänden, das läge daran, dass ja vor allem mit OO-Sprachen entwickelt würde, deshalb müssten auch die unwartbaren System OO-Anwendungen sein. Haha. Ja, klar. Dagegen halte ich: Die Objektorientierung ist damit angetreten, genau das zu verhindern. Sie hatte versprochen, Softwareentwicklung “natürlich” zu machen – halt objektorientiert wie die Welt scheinbar ist und damit viel einfacher zu beherrschen als alles was vorher war – und damit eben auch evolvierbarer und korrekter. Und was ist rausgekommen?

Objektorientierung ist einfach kein Selbstgänger. Sie ist vielmehr so schwierig, dass sie deutlich in der Anwendung begrenzt werden muss, meine ich. Sie hat ihren Wert. Ich möchte sie nicht missen. Doch den sehe ich vor allem bei der “Programmierung im Kleinen”. Überschaubare Probleme, die in wenigen Hundert Zeilen Code zu lösen sind, die kann man objektorientiert angehen. Super! Volle Kraft voraus. Da entstehen dann Codegebilde, die man mit SOLID-Prinzipien und Refaktorisierung im Griff haben kann, deren Korrektheit man zeigen kann, die für sich evolvierbar sind.

Aber oberhalb dieser “Taschen der Objektorientierung”…? Was da? Helfen da Komponenten- oder Pakete wie sie die UML bietet? Nein. Die sind nur Objektorientierung in Verkleidung. Denn was Klassen/Objekten, Komponenten, Paketen gemeinsam ist, ist die Sicht auf die Welt durch eine Abhängigkeitsbrille. Die ist das Problem. Große Systeme lassen sich – so meine immer fester werdende Meinung – nicht wirklich agil und evolvierbar innerhalb eines fundamentalen Abhängigkeitsparadigmas realisieren.

Irgendwie geht das. Aber irgendwie geht irgendwie alles. Man kann auch mit Streichhölzern ein Formel 1 Auto bauen oder Kleidung aus Papier fertigen. Das sind hübsche Zeitvertreibe – nur wirklich praktischen Wert haben sie am Ende nicht. Oder wollte man sie praktisch einsetzen, wären große Probleme gleich zur Stelle.

Noch eine andere Analogie: Wir können auf der Basis Newtonscher Physik eine Menge Energie erzeugen mit Wind-, Kohle-, Wasserkraftwerken. Richtig, richtig viel Energie braucht dann aber ein anderes Paradigma: die relativistische Physik Einsteins und die Quantenphysik Heisenbergs. Nur damit sind Kernkraftwerke oder dermaleinst Fusionskraftwerke möglich.

Für mich bedeutet das zusammenfassend: Objektorientierung ist eine gute Sache. Ihr muss allerdings ein Platz und eine Reichweite zugewiesen werden, damit ihr Nutzen ihre Nachteile aufwiegt.

image

Im Bild sind das die “Taschen der Objektorientierung”. Sie sind überschaubar, innerhalb ihrer sind Abhängigkeiten in übliche OO-Manier erlaubt.

Außerhalb dieser “Taschen” jedoch… die “Taschen” selbst… da soll keine Objektorientierung im Spiel sein. Das Denken in Abhängigkeitsbeziehungen ist dort zu verrmeiden. Wie die “Taschen” nach außen beschaffen sind, woher sie kommen, das will ich an dieser Stelle offen lassen.

In der Natur finden sie keine Abhängigkeiten wie sie in objektorientierten Strukturen existieren. Die wahrhaft komplexen Systeme der Welt kommen ohne aus. Und auch die Teile menschengemachter Maschinen sind nicht abhängig voneinander wie es objektorientierte Funktionseinheiten sind.

Woher kommt da nur der Glaube, virtuelle, aber dennoch (oder gerade deshalb) riesige Softwaregebilde könnten geeignet auf einem Paradigma aufgebaut werden, in dessen Kern Abhängigkeiten stehen? Ich weiß nicht, ob ich diesen Glauben naiv oder arrogant nennen soll. In jedem Fall scheint es mir nun offensichtlich, dass er verfehlt ist.

Sobald ein Problem eine Größe hat, die sich mit OO-Mitteln in Hunderten LOC umsetzen lässt, finde ich Objektorientierung geeignet. Solange ein Problem jedoch umfangreicher ist, ist für Entwurf und Umsetzung die Objektorientierung und jedes andere Paradigma das auf “Abhängigkeitsdenken” basiert ungeeignet.

Hierarchie ist natürlich weiterhin ein wichtiges Strukturierungsmittel. Keine Frage. Doch das, was da in einer Hierarchie aufgespannt ist – die Funktionseinheiten, die zusammen die Lösung ergeben –, darf nicht über Abhängigkeiten verbunden sein. Das ist schlicht nicht überblickbar, nicht evolvierbar. Objektorierung bzw. OO-Denke verbietet sich also sogar doppelt: erstens, weil sie sich innerhalb des Abhängigkeitsparadigmas bewegt, zweitens weil sie kein Mittel zu natürlicher Hierarchisierung bietet.

Die üblichweise gemeinte Refaktorisierung ist mithin überbewertet, weil sie Objektorientierung perpetuiert. So wichtig sie in “Taschen” der OO nötig sein mag, so hilfreich dort Prinzipien wie SOLID oder Tools wie DI Container sein mögen… Ich bin der Meinung, dass man daraus nicht ableiten soll, Objektorientierung sei dazu geeignet, einfach/natürlich/intuitiv Größeres zu leisten.