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.

Kommentare:

mythicrider hat gesagt…

Sehr interessant!
Was ich nicht ganz verstehe: Warum ist "public void Ziehen(int spielfeldindex)" bei der Semantik angesiedelt, ich hätte die mehr bei den Aktionen erwartet bzw. sehe sie mehr als Aktion?

Ralf Westphal - One Man Think Tank hat gesagt…

@mythicrider: Ziehen() verändert gezielt den Zustand des Datenmodells. Das darf nur in bestimmter Weise geschehen. Deshalb ist es für mich Semantik.

Die Implementation ist dann in Form eines Prozesses :-) Da ist eine gewisse Überschneidung mit Aktion und Semantik. Aber ich würde eher sagen: Die Semantik stößt einen Prozess an, wenn sie komplizierter ist.

mythicrider hat gesagt…

Das heißt es gibt die Semantik "Ziehen" und den Prozess "Ziehen" und der Prozess ruf lediglich die Semantik?
Und wenn die Semantik einen Prozess anstoßen kann (quasi die entgegengesetzte Richtung) macht sie sich ja vom Prozess abhängig, oder ist dann hier wieder EBC im Spiel?

Ich kann mir das Zusammenspiel untereinander noch nicht ganz vorstellen

Ralf Westphal - One Man Think Tank hat gesagt…

@mythicrider: Ich kann verstehen, dass dir da etwas noch nicht klar ist. Ich suche auch noch nach einer etwas besseren Darstellung... einstweilen versuche ich es aber mit den bisherigen Begriffen.

Also: Mir geht es erstmal um eine grundsätzliche Kategorisierung von Code - und daraus folgend um eine physische Trennung oder gar eine spezielle physische Form für die Kategorien (Aspekte).

Nun sehe ich die Zustandsveränderung am Spielstand zuerst mal als Aspekte "Semantische Korrektheit". Ich denke, darüber können wir uns einig sein. Solange die überschaubar ist, ist alles in Butter. Einfach eine Methode auf der Klasse für die semantische Korrektheit (Entität) implementieren.

Nun aber: Wenn die semantische Korrektheit nicht in einfach zu überschauender Weise umgesetzt werden kann... dann ist dafür ein Prozess nötig. D.h. der Aspekt "semantische Korrektheit" wird implementiert mit Aspekten "Aktion" und "Koordination". Dafür sind weitere separate Funktionseinheiten aufzusetzen.

Am Anfang gilt somit: Semantische Korrektheit == Aktion. Später gilt: Semantische korrektheit -> Prozess (lies "A -> B" als A ist abhängig/nutzt B).

Mike Bild hat gesagt…

Leider bin ich erst heute zum lesen gekommen. Sehr cooler "totales" SoC und SRP Artikel! Sehr schön finde ich die Prinzipien der Trennung von syntaktisch/struktureller/schematischer und semantischer Konsistenz (wie Invarianten), sowie der Unterscheidung von Verarbeitung (Worker/Action) und Koordination/Controller (Prozess oder Fluss). Dein Artikel beschreibt sehr genau das Fachliche, die Business Domäne, bzw. Spiel Schema und Logik und lässt ein bisschen die technischen Aspekte wie Speichern, Log oder Remoting außen vor. Gut so, denn hier findet sich die technische Domäne wieder. Du schaust in Richtung CQ(R)S auch das finde ich passend. Diese darf mit dem fachlichem Kontext nur sehr gezielt zu tun haben und sollte sich sicher gleichen Prinzipien unterwerfen. Danke für diesen sehr spannenden Einblick. Cheers, Mike

Mike Bild hat gesagt…

Ich denke semantisch Korrektheit kann sich auf zwei Dinge beziehen - Zustand und Prozess bzw. Fluss. Ersteres lässt sich aus meiner Sicht gut und gern mit Invarianten eines Objektes, oder größer eines Systems, und/oder auch mit einer State Machine beschreiben. Letzteres als Workflow oder Pipeline. Beide müssen in eine bestimmt definierte, semantische korrekte, Ausführung gebracht werden. Ziehen() wäre für mich demnach auch eine durch ein Command ausgelöste Aktion die den Spielstand (Status) in einer bestimmten Semantik, evtl. durch Invariante gesichert, verändert. Nun könnte man vielleicht auch sagen, dass Spiel eine State Machine für Spielstand ist. Die Hoheit über die Transitionen hätte damit Spiel. Die Transitionen werden dann über das Command Ziehen ausgelöst. Cheers, Mike