Follow my new blog

Donnerstag, 26. August 2010

Präzisierung eines Rahmens für TDD

TDD ist nur ein Tool. Deshalb kann man TDD nicht nur besser oder schlechter bedienen, sondern auch angemessen oder unangemessen benutzen. Gestern beim 4. Coding Dojo in Hamburg gab es für beides Beispiele. Das war schön, denn so konnten wir etwas lernen.

Über die “Bedienung” des TDD-Werkzeugs ist anderswo schon einiges geschrieben worden. Dazu gehört die Einhaltung des Red-Green[-Refactor]-Rhythmus oder die Bennung von Tests oder die Zahl der Asserts pro Test usw. Deshalb an dieser Stelle heute keine Bemerkungen dazu.

Die Angemessenheit findet allerdings aus meiner Sicht noch keine angemessene Beachtung. Wann ist TDD angemessen? Wann nutzen und wann nicht? In einem früheren Posting hatte ich mir dazu schonmal Gedanken gemacht. Von dem dortigen Rahmen für TDD bin ich weiterhin überzeugt:

  1. Bevor Sie in die Tasten greifen und einen Test schreiben, sollten Sie sicherstellen, dass Sie das Problem durchdrungen haben.
  2. Auch nach Durchdringung des Problems sollten Sie nich sofort loscodieren, sondern erst einen Lösungsansatz entwerfen.

Im 4. Coding Dojo in Hamburg wurde nun gestern eine Kata fortgeführt (KataPokerHands), die beim 3. Dojo angefangen worden war. Bei dem bin ich nicht dabei gewesen, insofern kann ich nicht genau sagen, inwiefern sich die Gruppe in diesem Rahmen bewegt hat. Sicherlich hat man aber versucht, die Anforderungen zu verstehen. Und im Code waren auch Effekte von Nachdenken zu sehen. Dennoch bewahrheitete sich recht schnell die schon am Ende des 3. Dojos geäußerte Vermutung, dass die Lösung suboptimal sei.

Meine Erkenntnis nach dem 4. Dojo ist, dass der bisherige Rahmen noch zu unpräzise ist. Er gibt am Ende der Implementation keine Richtung. Was tun mit dem Verständnis des Problems und dem Lösungsansatz? Wo und wie beginnen mit der Umsetzung des Lösungsansatzes?

TDD beginnt beim API

Ergebnis des Lösungsentwurfs ist eine Menge von Funktionseinheiten, die zusammen die Anforderungen umsetzen. Je nach Ausdauer beim Nachdenken über den Lösungsansatz sehen Lösungsansätze natürlich unterschiedlich aus. Das 3. Dojo ist für die KataPokerHands auf diese Funktionseinheiten gekommen:

image

Mich haben 30 Sekunden Nachdenken hingegen zu einer größeren Menge Funktionseinheiten gebracht:

image

Ich will nicht sagen, dass der eine oder andere Lösungsansatz besser ist. Hier gehts nur darum, dass die Funktionseinheiten des Lösungsansatzes auf unterschiedlicher Ebene liegen können. Manche sind “von außen” sichtbar – Hand oder Pokerspiel entscheiden –, manche nicht.

Mit “von außen” meine ich, dass ein Nutzer der Lösung sie sieht, mit ihnen umgehen muss. Für Blatt bewerten in meinem Ansatz gilt das z.B. nicht; es ist eine interne Funktionseinheit, mit der ich mir die Implementation leichter machen will. Ich muss nicht erst durch TDD auf sie stoßen und durch Refaktorisierung freistellen. So mag ich es – andere ziehen einen anderen Weg vor.

Nun hat also eine Entwurfsphase einen Lösungsansatz mit einigen Funktionseinheiten geliefert. Wie sollte TDD darauf nun ansetzen? Immer beim API.

TDD sollte sich initial immer nur um die Member kümmern, die zum API einer Funktionseinheit gehören. Das sind die Member, die für die Nutzer einer Funktionseinheit relevant sind.

Hier hatte das 3. Dojo aus meiner Sicht einen Fehler begangen bei allem guten Willen zu TDD. Damit hat man eine Chance verschenkt, durch TDD ein gutes Design “austreiben” zu lassen.

Damit Sie verstehen, was ich beobachtet habe, hier die Anforderung der Kata in Kürzestform:

Vergleiche zwei Pokerblätter und bestimme das Gewinnerblatt

Das 3. Dojo hatte sich dafür entschieden, ein Pokerblatt durch eine Instanz der Klasse Hand zu repräsentieren:

class Hand
{
…       
}

Und dann? Dann hatte man angefangen, Tests zu schreiben für Properties, die anzeigen, ob ein Blatt der einen oder anderen Poker-Figur (Pair, Full House usw.) entspricht. Die Klasse wurde schnell mit getestetem Code gefüllt.

class Hand
{
    public bool IsPair { get {...} }
    public bool IsThreeOfAKind { get {...} }
    public bool IsFourOfAKind { get {...} }

}

Die Tests waren schön grün. Aber war das Design auch gut? Nein, nicht wirklich. Das hatte die Gruppe dann auch schon am Ende des 3. Dojos erkannt und dieser Eindruck wurde beim 4. Dojo, das den Code weiterentwickeln sollte, bestätigt.


Ich glaube nun, der Grund dafür, dass trotz TDD kein gute Ergebnis herausgekommen war, liegt daran, dass die Tests nicht beim API, sondern bei imaginierten Interna angesetzt haben. Aus der Aufgabenstellung geht einfach nicht hervor, ob eine IsPair Property o.ä. benötigt wird. IsPair gehört nicht zum API, weil es den Nutzer der Klasse Hand nicht interessiert. Der will nur wissen, welches von zwei Hand-Objekten der Gewinner ist.


Beim 4. Dojo kam man gleich zu Beginn auf diese Frage und entschied sich, bei allen Zweifeln am Design doch den bisherigen objektorientierten Ansatz weiter zu verfolgen. Das Ergebnis sah dann so aus:

class Hand : IComparable<Hand>
{
    public int CompareTo(Hand other)
    {
        …
    }
}
Hand h1, h2;

if (h1.CompareTo(h2) > 1)
    Console.WriteLine("h1 gewinnt");

Ein Nutzer stellt Blätter in Hand-Objekten zusammen und vergleicht sie über die Standardschnittstelle IComparable<T>.


Das ist doch ein wunderbar objektorientierter API, oder?


Leider hatte das 3. Dojo nicht darüber nachgedacht oder zumindest nichts daraus gemacht. Denn hätte es seinen ersten Test gegen diesen API geschrieben, wäre die weitere Entwicklung sicherlich anders verlaufen.


Beim Lösungsansatz des 3. Dojo gab es nur eine wesentliche Funktionseinheit und die deshalb auch dem Nutzer der Lösung sichtbar war. Ihr API fiel zusammen mit der Sicht des Nutzers.


Mein Lösungsansatz hat hingegen nicht nur für den Nutzer sichtbare Funktionseinheiten. Was ist denn da der API z.B. für Blatt bewerten? Das finden Sie heraus, wenn Sie sich fragen, wer die Nutzer der Funktionseinheit sind. Das könnte z.B. die umschließende Funktionseinheit Pokerspiel bewerten sein. Was könnte die von Blatt bewerten wollen? Ich denke, in diesem Fall ist es so einfach wie: “Ich will Blatt bewerten mit einem Blatt aufrufen und eine Blattbewertung als Ergebnis bekommen.”


Ich würde mich daher für eine Übersetzung von Blatt bewerten in eine Funktion entscheiden. Dasselbe gilt für die Funktionseinheit Gewinner ermitteln.

class Blatt
{
   
}
   

public class PokerspielBewerten
{
    internal class Blattbewertung : IComparable<Blattbewertung>
    {
       
        public int CompareTo(Blattbewertung other)
        {
           
        }
    }


    public enum Gewinner
    {
        Schwarz,
        Weiß,
        Patt
    }


    internal Blattbewertung BlattBewerten(Blatt blatt)
    {
       
    }


    public Gewinner GewinnerErmitteln(Blatt schwarz, Blatt weiß)
    {
        Blattbewertung bewertungSchwarz = BlattBewerten(schwarz);
        Blattbewertung bewertungWeiß = BlattBewerten(weiß);

        switch(bewertungSchwarz.CompareTo(bewertungWeiß))
        {
            case -1: return Gewinner.Weiß;
            case 1: return Gewinner.Schwarz;
            default: return Gewinner.Patt;
        }
    }
}

In diesem Fall sind die APIs der nicht öffentlichen Funktionseinheiten simpel. In anderen Fällen müssen Sie etwas länger nachdenken. Immer jedoch geht es um die minimale Zahl an Members einer Funktionseinheit, die für ihre unmittelbare Umgebung, ihre Nutzer sichtbar sein müssen. Ausschließlich darauf sollte TDD ansetzen.


Und wie komme ich bei meinem Lösungsansatz darauf, dass es überhaupt eine Funktionseinheit Blatt bewerten geben sollte? Nachdenken hilft ;-) Aber natürlich sollte das nicht zu spekulativ sein. Festzustellen, ab wann es spekulativ wird, ist einerseits Erfahrungssache, andererseits Geschmackssache. Und selbstverständlich sind Sie hinterher immer schlauer; sie können sich also auch vertun und zuwenig oder zuviel nachdenken. Das passiert. Es deshalb jedoch zu lassen, verschenkt Produktivitätspotenzial.


Ohne Nutzen nützt TDD nichts


Mit TDD konsequent nur auf dem API von Funktionseinheiten zu beginnen, ist ein Erfolgsfaktor. Bis Sie damit Erfolg haben, kann es jedoch dauern. Deshalb sollten Sie ebenfalls beachten: Produzieren Sie so schnell wie möglich Nutzen.


Auch hier hatte das 3. Dojo “gesündigt”. Nach mehreren Stunden der Arbeit konnte der Code zwar eine Menge und alle Tests waren grün – nur wäre keine Auslieferung des Codes möglich gewesen. Es gab keinen API, es gab nichts für einen Nutzer Nützliches. Die so entscheidende Vergleichsfunktionalität für Hand-Objekte gab es nicht.


Hätte das 3. Dojo mit Tests durch den API begonnen, wäre das Problem nicht vorhanden oder zumindest nicht so groß gewesen. Doch auch dann wäre die Marschrichtung noch nicht sonnenklar gewesen. Wie hätte man entwickeln sollen? Zuerst alle Figuren erkennen und vergleichen können? Oder nur wenige Figuren, dafür aber die auch noch nach ihrem Kartenwert unterscheiden können (ein 2-Pair ist weniger wert als ein König-Pair)?


Diese Frage stellt sich noch deutlicher, wenn Ihr Lösungsansatz mehrere Funktionseinheiten enthält. Sollte ich zuerst Blatt bewerten komplett implementieren, bevor ich mich an Gewinner ermitteln machen?


Suchen Sie die Antwort immer beim Kundennutzen. Stellen Sie sich vor, dass der Kunde nach dem nächsten grünen Test die Software ausprobieren möchte. Kann er das? Haben Sie mit dem letzten red-green-Zyklus etwas geschaffen, das für den Kunden einen Unterschied macht?


Implementieren Sie also immer in Durchstichen. Bei mehreren Funktionseinheiten sollten Sie sich also nicht an internen festbeißen. Übertragen auf meinen Lösungsansatz hatte das 3. Dojo seine ganze Energie in Blatt bewerten investiert. Es hätte nicht ausliefern können. Ohne Gewinner ermitteln wäre für den Kunden/Nutzer kein Wert erkennbar gewesen.


Darüber hinaus hatte dsa 3. Dojo aber auch unabhängig von den Funktionseinheiten den Fokus auf der Implementation der Figurenerkennung. Die ist ber nur für einen Teil der Gewinnermittlung relevant. Bei gleicher Figur geben die Kartenwerte den Ausschlag. Und bei gleichen Kartenwerten die Bewertung der nicht zur Figur gehörenden Karten.


Diese weiteren Bewertungen waren nicht angegangen worden. Man hatte also keinen Eindruck davon, ob sie einfach oder kompliziert durchzuführen wären. Vielleicht wären sie ja sogar entwurfsrelevant gewesen?


Teilen Sie Anforderungen in Äquivalenzklassen und realisieren aus jeder zunächst nur ein Feature. Die Äquivalenzklassen für das Pokerspiel sind aus meiner Sicht:



  • Karten haben eine Farbe
  • Karten haben einen Wert
  • Gewinnerkennung aufgrund Figur
  • Gewinnerkennung bei gleicher Figur nach Kartenwert
  • Gewinnerkennung bei gleicher Figur und gleichem Kartenwert nach Restkartenwert

Es hätte dann genügt, Karten mit zwei Farben und Werten zu erkennen. Und es hätte vor allem genügt, nur Zwillinge und eine kleine Straße als Figuren zu erkennen, aber auch deren Kartenwert und Restkartenwert zu bestimmen.


Ein imaginierter Kunde hätte dann schon ganz verschiedene Aspekte eines Pokerspiels ausprobieren können. Das hätte geholfen herauszufinden, ob die Anforderungen verstanden wurden. Und das hätte geholfen, das Design möglichst früh auszutreiben bzw. zu verifizieren.


Fazit


TDD is the way to go beim Unit Testing. Aber nicht grüne Tests allein oder der Rhythmus red-green[-refactor] sind ausreichend. Softwareentwicklung und Testen sind kein Selbstzweck. Denken Sie daher immer an den Nutzer des System under Test. Beginnen Sie Ihre Tests beim API der entworfenen Funktionseinheiten; starten Sie also mit Black Box Tests Ihrer Systems under Test. (Was nicht heißt, dass TDD bedeutet, sie sollten nur Black Box Tests schreiben; Funktionseinheiten, die TDD austreibt, können sehr wohl unsichtbar für einen SUT-Nutzer sein. D.h. auch White Box Tests sind völlig ok mit TDD, nein, sie sind am Ende sogar eigentlich immer nötig.)


Produzieren Sie dann so schnell und so vielfältig Kundennutzen in möglichst vielen Bereichen.


Selbst wenn Sie nicht viel von großartiger Architektur halten, tun Sie Ihrer TDD Praxis einen gefallen, wenn Sie immer den Kunden im Blick haben. Auch nicht zu verachten ist, dass kleine Nutzeninkremente und quasi ständige Auslieferfähigkeit Ihnen ein gutes Gefühl machen.

Montag, 23. August 2010

Das Domänobjektmodell anders gedacht [OOP 2010]

Mit der vorherrschenden Sichtweise, was ein Domänenobjektmodell sein soll, bin ich nicht einverstanden. Meine Kritik habe ich in diesem Posting geäußert. Wie soll es denn aber sonst gehen?

Um meine Vorstellung von einem Domänenobjektmodell zu beschreiben, versuche ich am besten, Jimmy Nissons Beispiel auf meine Weise umzusetzen. In “Applying Domain-Driven Design and Patterns: With Examples in C# and .NET” hat er auf Seite 118f dieses Szenario beschrieben…

image

…und im Anschluss in einem solchen Domänenobjektmodell umgesetzt:

image

Mein Stein des Anstoßes sind die Operationen auf Customer und Order, die über die Konsistenzwahrung des Datenmodells hinausgehen. Customer.HasOkCreditLimit(), Order.IsOkAccordingToSize() und Order.IsOkAccordingToCredit() sind Funktionen, die nicht nur auf den Daten im Domänenobjektmodell operieren, sondern weitere Services benötigen. Das halte ich für falsch verstandene Kapselung.

Aber wie würde ich es nun besser machen?

Bessere Anforderungen

Am Anfang einer besseren Lösung stehen bessere Anforderungen. Ich glaube inzwischen, dass die Vorstellungen vom Domänenobjektmodell so sind wie sie sind, weil seine Proponenten es als Framework sehen. Sie leiden damit unter denselben Symptomen wie z.B. übliche Datenzugriffschichten: ihre Entwürfe sind sehr allgemein, weil sie keine konkrete Vorstellung davon haben, wie wo wann von wem der Code genutzt wird.

Ich glaube, dass die Domänenmodelle, die uns in der Literatur vorgestellt werden, Ergebnisse von bottom-up Design im Sinne eines Schichtenmodells sind. Sie stellen Versuche dar, die Domäne ganz allgemein abzubilden, sozusagen universell und für alle möglichen Anforderungen in der Zukunft.

Löblich, dass sie damit nicht bei der Datenzugriffsschicht beginnen. Aber der Einstieg in die Modellierung bei der Geschäftslogikschicht ist am Ende nicht besser. Gerade wo die Domäne unbekannt ist, muss das auf Irrwege führen. Der Bezug zu dem, was ein Anwender ganz konkret will, fehlt. Und zwar solange wie Eigenheiten von Klassen nicht ganz klar einem Ursache-Wirkungszusammenhang zugeordnet werden können, der für den Benutzer Wert hat.

Jimmy hat nun im Buch zwar Anforderungen genannt – aber ohne rechten Zusammenhang. Das sind keine User Stories, sondern viel allgemeinere Formulierungen. Schon der Satz “We define the limit when the customer is added initially […]” klingt nach technologischer, d.h. User-Story-unabhängiger Sicht. Dito z.B. “[…] the solution needs to decide on the versioning  unit for customers and for orders.” Anforderungen und Lösung sind einfach nicht klar getrennt. Und das – so scheint mir – führt dann schnell zu Lösungen, die entkoppelt sind – allerdings entkoppelt von der Welt der Anwender. Damit ist die Entkoppelung schlecht, weil sie sich einschleicht (YAGNI und KISS werden da schnell vergessen). Niemand weiß, ob sie überhaupt gebraucht wird.

Mein Ansatz ist da anders. Für mich muss sich jedes Artefakt, jede Eigenart aus dem Bezug nicht nur zu einem Feature ergeben, sondern zu einer Interaktion der Anwendung mit einem Anwender (einem Menschen oder einer anderen Anwendung). Was sich nicht auf solche “Trigger” zurückführen lässt, steht unter Verdacht, überflüssig oder zumindest suboptimal geschnitten zu sein. Wer YAGNI und KISS und damit den Geldbeutel des Kunden ernst nimmt, kann nicht anders entwerfen. Alles Entwerfen muss vom Frontend ausgehen. Und genau das kann ich bei den Domänenobjektmodellen der Literatur nicht erkennen.

Aber darauf näher einzugehen, mag Thema für einen weiteren Artikel werden. Hier möchte ich es einfach mal tun, um zu zeigen, zu welch anderem Modell man damit kommt. Um Jimmys Anforderungen spinne ich daher mal ein paar User Stories, von denen ich dann ausgehen. Die decken nicht alles ab, was Jimmy erreichen will, aber zumindest das, was zu den kritisierten Funktionen führt.

Eine User Story für Jimmys Szenario

Als Vertriebler möchte ich Aufträge erfassen. Am Telefon spreche ich mit meinen Kunden – Bestandskunden und neuen – über ihre Bedürfnisse und trage ihre Bestellungen gleich in einen Auftrag ein.

Bei Bestandskunden überprüfe ich im Gespräch die Stammdaten, z.B. seine Adresse. Bei Neukunden erfasse ich diese Daten erstmalig.

Sind die Kundendaten abgeglichen, geht es an den eigentlichen Verkauf. Was kann ich für den Kunden tun? Es kann sein, dass er schon auf meinen Anruf gewartet hat und gleich mit einer Bestellung loslegt. Es kann aber auch sein, dass es sich im Laufe des Gespächs erst herausstellt, dass er das eine oder andere Produkt bestellen will oder sollte. Ich kenne ja sein Business und kann ihn da beraten.

Am Ende des Gesprächs gehe ich dann nochmal alle Bestellpositionen mit dem Kunden durch. Wenn alles korrekt aufgenommen ist, schließe ich die Bestellung ab; der Kunde bekommt sie dann per Email zugeschickt und gleichzeitig läuft sie weiter zum Fulfillment.

Wenn es ein “normaler” Kunde ist, läuft das so. Dann gibt es keine Probleme. Bei manchen Kunden müssen wir jedoch aufpassen. Die haben eine schlechte Zahlungsmoral. Oder wir kennen sie noch nicht gut. Dann passen wir ihr Kreditlimit an. Jeder Kunde hat so ein Limit, das angibt, wie hoch der Gesamtbetrag aller noch nicht komplett bezahlten Bestellungen sein darf.

Hat ein Kunde z.B. ein Kreditlimit von 5000 EUR, dann darf die Bestellung, die ich mit ihm zusammenstelle, keinen höheren Warenwert haben. Oder der muss sogar noch geringer sein, wenn der Kunde frühere Bestellungen noch nicht vollständig bezahlt hat. Sind noch Bestellungen im Wert von z.B. 2000 EUR offen, dann darf die neue Bestellung Wert von 5000-2000=3000 EUR nicht überschreiten.

Zusätzlich hat haben wir im Unternehmen ein allgemeines Limit für den Bestellungswarenwert. Unter dem müssen alle Bestellungen liegen unabhängig vom Kreditlimit des Kunden. Das können z.B. 10000 EUR sein. Ein Kunde mit dem Kreditlimit von 15000 EUR dürfte also auch pro Bestellung für maximal 10000 EUR ordern.

Während des Gesprächs mit einem Kunden sollte dessen Kreditlimit für mich klar zu sehen sein; dann kann ich ihn z.B. darauf ansprechen, dass wir über eine Neubestellung reden, obwohl andere noch nicht vollständig bezahlt sind.

Aber ich möchte das Limit nicht ständig selbst im Blick haben müssen. Das Programm soll mich automatisch warnen, wenn die Bestellung durch Veränderung an Bestellpositionen an Grenzen stößt (Kreditlimit oder Bestellwarenwertlimit).

Ah, ein Kontext. Hieraus lässt sich eine Ubiquitous Language ableiten, die Begriffe wirklich in Beziehung setzt. Und hieraus lässt sich auch erkennen, wann was warum an Domänenlogik gebraucht wird. Natürlich muss man dafür nachfragen beim Kunden. Aber so eine User Story schafft viel besser ein Bild im Kopf als die Punkte in Jimmys Aufzählung.

Featureliste

Die Diskussion über die User Story lasse ich hier mal aus und spule schnell vor. Für mich ergeben sich folgende für die Domänenmodelldiskussion relevanten Features:

  1. Die User Story beginnt mit einem Dialogfenster, in dem die Kundenstammdaten zu sehen sind. Das sind Kundennummer, Name, Kreditlimit, Adresse.
    Gegenüber dem Benutzer wird an dieser Stelle nicht mehr zwischen einem Bestandskunden und einem Neukunden unterschieden.
    Wie der Anwender dahin kommt, ist nicht Teil der User Story. Er könnte z.B. den Kunden aus einer Liste ausgewählt haben.
  2. Im Dialogfenster kann der Anwender die Stammdaten bearbeiten – und speichern.
  3. Beim Speichern wird geprüft, ob das Kreditlimit problematisch ist. Das ist der Fall, wenn es verringert wurde und nun kleiner als die Summe unbezahlter Aufträge ist. Gespeichert werden die Stammdaten trotzdem – allerdings bekommt der Anwender einen Hinweis angezeigt.
  4. Wenn das Kreditlimit des Kunden ausgeschöpft ist, ist eine Auftragserfassung nicht möglich. Das gilt nach dem Speichern, falls durch Änderung des Kreditlimits dieser Zustand eintritt, oder auch sofort nach Öffnen des Dialogfensters.
  5. Nach Überprüfung der Kundenstammdaten kann ein neuer Auftrag erfasst werden.
  6. Angezeigt werden während der Auftragserfassung Kundenname, Kreditlimit, Auftragsnummer, Auftragsdatum, Auftragssumme und Auftragsstatus.
  7. Die Auftragsnummer wird automatisch vergeben und muss nur über alle Aufträge hinweg streng monoton aufsteigend sein.
  8. Der Anwender fügt dem Auftrag Auftragspositionen hinzu. Dazu wählt er ein Produkt aus einer Liste und gibt die Menge ein, in der der Kunde es haben möchte.
  9. Nach jeder Auftragspositionserfassung wird die Auftragssumme aktualisiert.
  10. Am Ende des Kundengesprächs wird der Auftrag storniert, wenn der Kunde nichts bestellen will.
  11. Am Ende des Kundengesprächs wird der Auftrag platziert, wenn der Kunde zufrieden ist.
  12. Beim Platzieren des Auftrags wird geprüft, ob er im Rahmen des Kreditlimits des Kunden liegt. Falls nicht, wird der Auftrag nicht platziert, sondern der Anwender informiert.
  13. Beim Platzieren des Auftrags wird geprüft, ob er im Rahmen des Bestellungswarenwertlimits liegt. Falls nicht, wird der Auftrag nicht platziert, sondern der Anwender informiert.

Natürlich ist diese Liste nicht vollständig. Aus der User Story kann man im Gespräch noch viel mehr rausholen. Doch für den hiesigen Zweck sollte das reichen.

Datenmodell

Jimmys Domänenobjektmodell liegt ein Datenmodell zugrunde. Dass er das nicht explizit gezeigt hat, halte ich auch für ein Problem. Zwar ist sein Klassendiagramm dem sehr ähnlich – doch es führt ganz selbstverständlich darüber hinaus. So ist das mit Klassendiagrammen. Das sollen sie – aber ist das auch gut?

Selbst wenn am Ende alles mit Klassen implementiert wird, finde ich es wichtig, sich im Sinne des Single Responsibility Principle zunächst zu konzentrieren. Deshalb habe ich für mein Datenmodell die Crow’s Foot Notation benutzt:

image

Nicht alles, was im “Datenanteil” von Jimmys Klassendiagramm enthalten ist, findet sich allerdings darin wieder. Für die Diskussion der Art der Modellierung der Domänenfunktionalität ist es aber genug.

Zwei Hinweise jedoch:

  • Die Auftragssumme taucht im Datenmodell nicht auf, weil es eben ein abstraktes Datenmodell ist und kein Klassendiagramm. In ein Klassendiagramm würde ich die Auftragssumme als Property aufnehmen. Seine Summe zu berechnen halte ich für ganz legitime Funktionalität eines Auftrag-Domänenobjektes. Dafür ist nur auf die Daten im Domänendatenobjektmodell zuzugreifen.
  • Die Hinweise auf die Services zur Prüfung der Limits fehlen selbstverständlich, weil dies ausschließlich ein Datenmodell ist. Egal, wie die Limits geprüft werden, das kann nicht sichtbar sein in diesem Modell.

Das Datenmodell ist das Ergebnis einer Analyse. Es beschreibt den Ist-Zustand. Diese Daten gibt es heute und so hängen sie zusammen.

GUI-Modell mit Triggern

Aus Anforderungen lässt sich gewöhnlich recht einfach ein Datenmodell ableiten. Daten und ihre Beziehungen sind auch vergleichsweise stabil. Damit haben Sie zumindest schonmal eine Grundlage für das weitere Nachdenken.

Das bedeutet nicht, dass es für immer so bleiben wird. Im Laufe des Entwurf und der Implementierung können sich immer Erkenntnisse ergeben, die eine Veränderung nahelegen. Aber irgendwo müssen Sie ja anfangen. Da ist ein Datenmodell kein schlechter Start. Vermeiden Sie jedoch den Drang zur Perfektion.

Ein weiterer Startpunkt ist das Frontend. Wie Anwender mit der Software umgehen wollen, lässt sich auch recht gut aus den Anforderungen ableiten. Vermeiden Sie dabei ebenfalls den Drang zur Perfektion. Meine Frontend-Entwürfe mache ich deshalb entweder am Whiteboard oder mit Balsamiq Mockups. Da kommt der Kunde nicht auf die Idee, die Software sei fertig, weil er ja schon irgendwo draufklicken kann.

Hier ein Frontend für meine Interpretation von Jimmys Szenario:

image image

Ein Dialog mit zwei Tabs. Auf dem ersten werden die Kundenstammdaten überprüft. Auf dem zweiten erfasst der Anwender den Auftrag. Die Pfeile bezeichnen “Trigger”, die Prozesse anstoßen, die ein “Feature produzieren”. Die Trigger sind also Ursachen, die zu nutzerrelevanten Wirkungen führen.

Es gibt natürlich noch mehr Trigger wie z.B. Scrollen im Grid oder Auswahl eines Landes aus der Combobox. Doch die zeichne ich nicht ein, weil sie in einer dünnen Schicht GUI-Code automatisch behandelt werden.

Darstellungswürdige Trigger sind für mich nur die, bei denen Code jenseits des GUI gefordert ist. Bei gegebenen Features besteht die Herausforderung nun darin, aus ihnen diese Trigger herauszuarbeiten.

Hier die Matrix mit der Zuordnung von Features zu Triggern:

Trigger/Feature

1

2

3

4

5

6

7

8

9

10

11

12

13

Öffnen

X

 

 

X

 

 

 

 

 

 

 

 

 

Schließen

 

 

 

 

 

 

 

 

 

X

 

 

 

Zum Auftrag

 

 

 

X

X

 

 

 

 

 

 

 

 

Speichern

 

X

X

X

 

 

 

 

 

 

 

 

 

Zu Kundenstamm

 

 

 

 

 

 

 

 

 

 

 

 

 

Produkt wählen

 

 

 

 

 

 

 

X

X

 

 

 

 

Menge eingeben

 

 

 

 

 

 

 

X

X

 

 

 

 

Platzieren

 

 

 

 

 

 

 

 

 

 

X

X

X

Interessanterweise benötigt kein Feature den Trigger “Zu Kundenstamm wechseln”. Nichts muss getan werden, wenn der Anwender den Reiter “Kundenstamm” anklickt. Bei Klick auf den Reiter “Auftrag” hingegen muss geprüft werden, ob die Kundenstammdaten verändert wurden und das Kreditlimit es gestattet, den Auftrag zu bearbeiten.

Noch interessanter ist jedoch, dass die Features 6 und 7 keinen Trigger haben. Wie kann das sein? Feature 6 wird schon durch das Layout des GUI befriedigt. Und Feature 7 fällt in die Zuständigkeit des Codes, der den Dialog anstößt. Der versorgt ihn mit Kundenstammdaten und einem leeren Auftrag inkl. Auftragsnummer.

Modellierung

Mit dem Datenmodell und den Frontend-Triggern in der Hand kann es jetzt endlich losgehen mit der Modellierung einer Lösung. Sie spannen einen Kontext auf, in den ein Domänenobjektmodell eingepasst werden kann.

Domänenprozesse

Irgendwie muss ich von den Anforderungen zu einem Modell für die Anwendung kommen. Der Begriff des Modells ist mir dabei sehr wichtig. Modell bedeutet nämlich nicht Code, sondern eben “nur” Modell, d.h. eine Abstraktion von Code. Erst in einem späteren Schritt wird das Modell in Code übersetzt.

Klassen, Komponenten oder Pakete (Namensräume) sind für mich allerdings keine Modellelemente. Die UML definiert für mich insofern kein Metamodell. Klassen, Komponenten und Pakete sind Artefakte einer Implementationsplattform. Deshalb taugen sie zur Modellierung nicht.

Ich versuche also nicht, aus Anforderungen Klassen “herauszulesen”. OOAD und auch Jimmys Variante von DDD sieht das anders. Für sie sind Klassen Modellierungsmittel. Und das funktioniert auch am Anfang gerade bei Literaturbeispielen oft ganz passabel. Zum einen, weil diese Beispiele klein sind. Zum anderen, weil der Leser nicht sieht, wie lang der Autor darüber nachgedacht hat.

Zwar kann ich Ihnen auch nicht wirklich zeigen, wie lang ich über das folgende Modell nachgedacht habe. Aber mein Anspruch ist, diese Zeit zu minieren. Mein Ansatz dafür: Ich beginne mit dem Wichtigsten und dem Deutlichsten, was die Anforderungen enthalten.

Das sind nämlich nicht (!) die Daten. Es sind die Prozesse, die auf den Daten ablaufen. Daten sind nur statische Bits, wenn sie nicht von Prozessen verarbeitet werden. (Umgekehrt gilt natürlich, dass die schönsten Prozesse nichts nützen ohne Daten. Dennoch haben die Prozesse für mich Priorität. Und sei es, weil sie dazu tendieren, volatiler zu sein als Datenstrukturen.)

Wenn wir also über Domain Driven Design sprechen, dann sollten wir mit den Domänenprozessen beginnen. Welche sind das? Ganz einfach: Jeder Trigger steht für einen Domänenprozess. Deshalb bin ich ja der Meinung, dass sie so einfach aus den Anforderungen abzulesen sind:

  1. Sie verstehen die Anforderungen.
  2. Sie leiten aus den Anforderungen ein Frontend ab.
  3. Sie definieren zusammen mit dem Kunden die zu den Features gehörenden Trigger im Frontend.
  4. Für jedes Trigger modellieren Sie den Domänenprozess.

Oben sehen Sie Anforderungen/Features und Frontend mit Triggern. Die Schritte 1 bis 3 bin ich also schon durchlaufen. Nachstehend nun die Domänenprozess für die wesentlichen Trigger. (Schließen und Zum Auftrag habe ich ausgelassen, da sie im Frontend abgehandelt werden können.)

Domänenobjektmodell01

Domänenobjektmodell02

Als Metamodell habe ich Event-Based Components (EBC) benutzt, d.h. jeder Kasten steht für eine Aktivität/Aktion und die Pfeile für Daten, die von Aktion zu Aktion fließen.

Mit Ausnahme vom Domänenprozess Öffnen beginnen alle Prozesse beim Frontend und enden auch dort wieder. Das Frontend ist Quelle und Senke für Prozessparameter bzw. Ergebnisse. Ohne Initiation durch das Frontend kein Prozessablauf.

Beim Öffnen habe ich den Initiator offen gelassen. Er liegt außerhalb von Jimmys Szenario.

An dieser Stelle möchte ich nicht näher auf EBC eingehen. Zweierlei verdient jedoch eine nähere Betrachtung:

Ubiquitous Language: Ihnen mögen beim Datenfluss Begriff wie Bonitätsabfrage oder Warenlimitstatus aufgefallen sein. Die finden sich nicht direkt in den Anforderungen. Aus meiner Sicht gehören Sie jedoch zur Problemdomäne, weil sie domänenspezifische Kommunikation beschreiben. Selbst wenn das Problem durch einen Prozess bestehend aus Menschen gelöst würde, wäre z.B. zu Fragen, aufgrund welcher Daten derjenige, der eine Bonitätsprüfung vornimmt, arbeitet. Wie heißt das “Dings”, was er entgegennimmt, um die Prüfung durchzuführen? Wie heißt das “Dings”, das er als Ergebnis produziert? Ich habe ersteres Bonitätsabfrage genannt und letzteres Bonitätsstatus. Mit diesen Begriffen kann ich nun präzise über die Problemdomäne mit Domänenexperten sprechen. Sie gehören aus meiner Sicht zur Ubiquitous (UL) Language von DDD. Und deshalb müssen Sie quasi auch im Modell auftauchen.

Enthält das Modell Begriffe, die nicht Teil der UL sind, dann ist die UL unterspezifiziert – oder das Modell technisch.

Enthält die UL Begriffe, die nicht im Modell auftauchen, dann läuft sie Gefahr, überspezifiziert zu sein – oder das Modell passt noch nicht zur Domäne.

Kontextobjektmodelle: Im Modell sehen Sie Daten, deren Namen auf *D, *VM und *Cmd enden, z.B. AuftragD, AuftragVM, AuftragCmd. Die Suffixe stehen dabei für unterschiedliche Kontexte, in denen dieselben Daten benutzt werden.

Der D-Kontext ist der Domänenkontext. AuftragD ist das Objektmodell, auf dem die Domänenlogik arbeitet. Es entspricht am ehesten dem Datemodell.

Der VM-Kontext ist das Frontend. Anders als bei Jimmy arbeitet es für mich nicht (!) auf dem Objektmodell des Domänenkontext. Ein Frontend hat ganz andere Bedürfnisse als die Domäne. Während z.B. für die Domäne das Objektmodell nur eine Form hat, ist das Frontned an immer wieder anderen Objektmodellen interessiert. Das Frontend bietet Sichten auf die Daten – und die will es nicht selbst produzieren, sondern geliefert bekommen. Zum Frontend fließen daher View Models (VM) des Objektmodells der Domäne. Während AuftragD recht tief geschachtelt ist, wie das Datenmodell nahelegt, muss das z.B. für AuftragVM nicht gelten. In AuftragVM könnten z.B. Auftragspositionen und Produkte in einem Objekt zusammengefasst werden.

Für den VM-Kontext kann sogar das DataSet zu neuen Ehren kommen. Ich bin da schmerzfrei. Warum sollte nicht AuftragVM als DataSet mit 2 Tabellen realisiert werden? Ein DataSet bietet viel, was ein Frontend braucht, z.B. Bindbarkeit, Changetracking.

Der Cmd-Kontext schließlich bezieht sich auf die Speicherung von Änderungen. Wie Änderungen am D-Objektmodell gespeichert werden, sollte nämlich unabhängig von der Domäne sein. Im Extremfall bedeutet das, es fließen von der Domäne keine Datenobjekte wie es bei einer Persistenz mit O/R Mapping der Fall wäre, sondern Kommandoobjekte. Sie beschreiben, welche Änderungen an den persistenten Daten vorgenommen werden müssen.

Wer hier an CQRS denkt, der ist auf der richtigen Fährte. Eine nähere Beschäftigung damit möchte ich jedoch vertagen. An dieser Stelle ist mir nur wichtig zu bemerken, dass sich CQRS gut mit dem EBC-Metamodell verträgt.

Domänendaten

Die Domänenprozess stehen am Anfang meiner Modellierung. Nur eine Nasenlänge dahinter folgen jedoch die Daten. Für sie entwerfe ich ein Domänendatenmodell. Das Datenmodell (s.o.) ist dafür ein guter Anfang.

Hinzu kommen jedoch Datentypen, die während der Prozessmodellierung aufgetreten sind wie Bonitätsabfrage, Warenlimitstatus oder AuftragCmd.

Insgesamt ist die Domänendatenobjektmodellierung jedoch vergleichsweise einfach. Ich muss mir ja keine Gedanken um komplizierte Domänenlogik machen. Die steckt in den Domänenprozessen.

Datenbezogene Operationen hingegen darf und soll das Domänendatenobjektmodell haben. Ein Beispiel dafür wäre eine Funktion/Property Auftragssumme() auf Auftrag. Eine weitere wäre eine Methode AddProduct(ProductD product, int qty), die nicht nur eine Auftragsposition erzeugen würde, sondern ggf. eine bestehende für das Produkt um die Menge erweiterte, falls nicht mehrere Auftragspositionen sich auf dasselbe Produkt beziehen sollten.

Insgesamt ist das Domänendatenmodell relativ unspannend, finde ich. Daher detailliere ich es hier nicht. Technische Herausforderungen enthält es zwar, aber die sind insb. von der Ausprägung der Kontexte Frontend und Persistenz abhängig. Sie haben nichts mit DDD im engeren Sinn zu tun.

Domänenprozessobjektmodell

Und wo sind nun die Klassen? Ohne Klassen, könnte ich ja nicht mit der Implementierung loslegen. Die Domänendaten sind einfach in Klassen und andere Typen zu übersetzen. Was aber ist mit den Domänenprozessen? Wie werden aus Aktivitäten/Aktionen-Kästchen Komponenten, Klassen, Methoden?

Klassen gehören zur Implementierung, Prozesse mit ihren Aktionen sind das Modell. Dass muss nun in Code übersetzt werden. Aus Kästchen müssen Methoden, Klassen, Komponenten werden. Wie das?

Ich den folgenden Bildern habe ich zunächstmal inhaltlich Zusammengehöriges farblich markiert:

Domänenobjektmodell01b

Domänenobjektmodell02b 

Die Prozessschritte scheinen drei Bereichen anzugehören:

  • Da ist zum einen die “Verwaltung” des Datenobjektmodells mit Auftrag, Positionen, Kunde usw.
  • Dann gibt es die Prüfung der Bonität und des Warenwertlimits.
  • Und schließlich müssen die Daten geladen und gespeichert werden.

Diese Bereiche fasse ich nun zu Komponenten zusammen. Das sind für mich die Akteure zu den Aktionen. Sie sind die Verantwortlichen, die Substantive, für die bisher nur ihre Taten modelliert waren als Verben (oder Verbphrasen).

Statt meinen Entwurf mit der Suche nach solchen Akteuren zu beginnen – was ich für schwer halte –, habe ich die naheliegenden Aktionen aus den Anforderungen abgeleitet. Das war eine Analysephase, eine Zerlegung. Jetzt folgt darauf eine Synthesephase, in der ich Muster erkenne und zu Akteuren zusammenfasse. Das halte ich für leichter.

Hier nun meine Komponenten:

Domänenobjektmodell03

Zwei Komponenten für die Businesslogik, eine für die Persistenz. Die Bonitätsprüfung ist im Grunde nur ein (zustandsloser?) Service, das Repository ebenfalls.

Die Kernfunktionalität steckt im Aggregat. Es ist zustandsbehaftet und verwaltet einen Objektgraphen des Domänendatenobjektmodells. Als UML-Skizze sähe das so aus:

Domänenobjektmodell04

Dazu kämen noch weitere Klassen wie AuftragVM oder KundeCmd, aber die lasse ich mal außen vor. Das sind eher Nachrichtenklassen.

Die Arbeitspferde sind die hier gezeigten: Aggregat, Repository und Prüfer sind EBC-Komponenten in Form von Akteuren. AuftragD, KundeD usw. sind Datenmodellklassen, die soweit Funktionalität enthalten, wie sie für die Sicherung eines konsistenzen Umgangs mit den Daten sinnvoll erscheint.

Selbstverständlich hat die Umgebung des Aggregats keinen Zugriff auf seinen Zustand. Das ist der Trick dieser Modellierung. Damit werden weitreichende Abhängigkeiten vom Datenmodell vermieden. Entkopplung ist das Zauberwort.

Andererseits enthält das Aggregat diese Daten und kennt sie genau. Es ist abhängig von der Hierarchie. Aber das macht nichts, weil Auftrag Aggregat und AuftragD usw. eng zusammengehören. Sie sind die zwei Seiten der Domänene: Daten und Operationen. Sauber getrennt und doch vereint durch Composition. Kohäsion ist das Zauberwort.

Im vorletzten Bild ist übrigens sehr schön die Gewichtung der Komponenten zu erkennen. Zentral ist das Aggregat mit seinen vielen EBC-Pins. Dort spielt die Musik. Da steckt die Domänenlogik drin. Da ist´s dann auch erlaubt, dass es viele Verbindungen gibt. Das Repository ist eher ein Anhängsel, weil Persistenz halt sein muss. Also hat es wenige Pins. Und der Prüfer ist simpel in seinen Interaktionen, auch wenn er zur Domäne gehört, weil er ein Dienst mit sehr spezieller Aufgabe ist.

Zusammenfassung

Puh, jetzt ist dieses Posting doch länger geworden als gedacht. Und sicher ist es in einigen Punkten zu skizzenhaft, um den Ansatz gleich nachzuprogrammieren. Dennoch war es mir wichtig, es erstmal so “rauszulassen”, um dem Thema Domänenobjektmodell eine schließende Klammer zu geben.

Was ist der Unterschied zwischen Jimmys Ansatz und meinem? Zentral ist die Trennung von Operationen und Daten. Die ist gerade im letzten Bild mit den Klassen deutlich.

Mein Ansatz geht darüber jedoch hinaus, als dass er das Prinzip der Kapselung noch rigoroser anwendet. Das Aggregat verbirgt wahre Struktur der Daten. Es reicht zwar Daten raus, doch das sind nur Sichten auf die das Domänendatenobjektmodell. Wie das aussieht, ist niemandem bekannt. Und das ist gut so. Denn wenn es sich ändert, dann schlagen diese Änderungen nicht notwendig durch auf andere Bereiche der Anwendung.

Vielleicht entscheidet jemand, die Adresse in den Kunden mit hineinzunehmen. Dann kann das ViewModel für den Auftrag immer noch so aussehen wie zuvor. Und auch das KundenCmd muss sich nicht zwangsläufig verändern.

Bei Jimmy ist das Domänenobjektmodell so “aufgeladen”, dass es in weiten Teilen der Anwendung benutzt werden kann und soll. All diese Anwendungsteile können sich dann an Operationen, Daten und Beziehungen binden. Ändert sich einer dieser Aspekte… dann beginnt eine Kette von Dominosteinen zu kippen.

Vom Standpunkt der Prinzipien SRP, SoC, LoD, Lose Kopplung/Hohe Kohäsion, Information Hiding halte ich daher meinen Ansatz für sauberer – wenn auch vielleicht nicht so objektorientiert, wie OOAD-Hardliner es mögen. Doch das ist mir egal. Nicht OOAD ist ein verfolgenswertes Ziel, sondern Evolvierbarkeit. Und die beruht auf Prinzipien.

Sonntag, 22. August 2010

Nähe oder Distanz, das ist hier die Frage

Sollen Softwareteams in einem Raum arbeiten? Diese Frage haben die Agilisten ganz klar mit Ja beantwortet. Wohin man schaut die Empfehlung, Entwickler möglichst nah zusammen zu setzen. Vor kurzem hat Martin Fowler sich daher bemüßigt gesehen, Empfehlungen für die Gestaltungsfreiräume des Team Room zu geben.

Damit könnte doch das Thema ein für alle Mal zur Ruhe gebettet werden, oder? Setzt die Leute in einen Raum und der Rest wird sich finden. Die breitbandige und informelle Kommunikation in so einem Raum, wo die Entwickler “colocated” sind, kann die Software nur besser machen. Oder?

Ich habe selbst lange Jahre in solch einer Umgebung Software entwickelt und fand das gut. Und neulich beim Clean Code Developer Praktikum haben wir es auch so gehalten: eine Woche lang zu siebt in einem recht kleinen Raum lernen, planen, implementieren. Da entsteh eine sehr produktive und dichte Atmosphäre. Ich bin ganz dafür.

Aber… aber ist das alles? Golo Roden hat Zweifel angemeldet. In zwei Blog Postings hier und hier hat er keck behauptet, solche räumliche Nähe sei überbewertet. Solcher Nonkonformismus hat natürlich Widerspruch hervorgerufen in den Kommentaren und im Blog von Ilker Cetinkaya hier.

Der eine findet, räumliche Nähe wird überschätzt, der andere ist sich sicher, sie werde unterschätzt. Ja, was denn nun?

Ich neige Golos Position zu und habe das in Kommentaren bei ihm auch schon Kund getan. Aus meiner Sicht ist Golo auch nicht apodiktisch. Er sagt nicht, man müssen Entwickler dringend verteilt arbeiten lassen. Anders die Gegenposition der Agilisten, die eher darauf bestehen, dass es ohne räumliche Nähe nicht wirklich gehe.

Um nun die Diskussion zu “depolarisieren”, versuche ich einmal, einen Blick aus größerer Flughöhe.

Der historische Kontext

Ilker et al. berufen sich auf die Agilisten von Cockburn über Fowler bis Beck, die mahnen, man solle dringend Entwickler in einen Raum setzen. Das ist ein argumentum ad verecundiam, also ein Trick der Eristischen Dialektik, um den Gegner einzuschüchtern. Wer will sich schon mit seiner persönlichen, unmaßgeblichen Meinung gegen die Autoritäten wenden? Im Sinne eines rationalen oder gar wissenschaftlichen Dialogs, ist das aber natürlich zuwenig.

Damit will ich nicht sagen, dass Fowler et al. per se unrecht haben. Doch ich möchte ermuntern, auch sie als fehlbar bzw. in ihre Kontexte eingebunden zu sehen. Die Frage muss also erlaubt sein, warum Fowler et al. so sehr die Colocation befürworten?

Ich denke, die Antwort liefert der historische Kontext. Die Agilitätsbewegung ist als Gegenentwurf entstanden. Sie “lehnt sich auf” gegen eine Art der Softwareentwicklung, die auf formale Prozesse, lange Planung, strikte Rollentrennung und starre Kommunikationsmuster gesetzt hat. Früher – und auch heute noch, man soll es ja nicht glauben – wurde Softwareentwicklung als Tätigkeit angesehen, die man wie Bauprojekte generalstabsmäßig planen musste.

Das ist dann umso häufiger schief gegangne, je komplexer Software geworden ist. Und irgendwann haben die Agilisten dann gesagt: Schluss! Es kann nur besser werden, wenn sich etwas grundlegend ändert. Unter anderem war das die Kommunikation im Softwareteam.

Eine Ursache für aus dem Ruder gelaufene Softwareprojekte war für sie die inflexible, reglementierte Kommunikation zwischen Teammitgliedern (und auch mit dem Kunden). Informationsverlust, Wissensinseln, Verzögerungen und mehr können darauf zurückgeführt werden.

Deshalb  haben die Agilisten dem starren Reglement die Colocation entgegen gesetzt. Ein kompliziertes Regelwerk haben sie mit dem “Chaos” der Abwesenheit von Regeln bei breitbandiger ad hoc Kommunikation gekontert.

Eine gute Idee. Und so preisgünstig ;-)

Das Agile Manifest

image Anders als die Befürworter räumlicher Nähe in der aktuellen Diskussion nun meinen, steht die Colocation jedoch nicht im agilen Manifest. Bezug zu einem dessen vier Werte - Individuals and interactions over processes and tools - besteht zwar – doch die Colocation lässt sich da nicht wirklich herauslesen. Sie mag als Interpretation dieses Wertes angesehen werden, doch nicht als Wert selbst.

Die Menschen und ihre (ungeplanten) Interaktionen über Protokolle, Regeln, Werkzeuge zu stellen, ist sogar so allgemein in der Formulierung, dass sowohl räumliche Nähe wie räumliche Distanz von Teammitgliedern dadurch gestützt werden. Denn wenn räumliche Distanz den Menschen mehr dienen sollte als räumliche Nähe, dann wäre das ganz im Sinne dieses Wertes.

Das Offshoring

In den Kommentaren zu Golo findet sich immer wieder als Beweis gegen seine Behauptung, dass Offshoring Projekte so oft fehlgeschlagen seien, dass der Traum von der verteilten Entwicklung als ausgeträumt gelten sollte.

Dagegen ist einzuwenden, dass Golo gar nicht behauptet hat, dass Offshoring “so einfach” funktionieren würde. Er hat auf ein solches Szenario nicht mal angespielt. Offshoring bedeutet zwar verteilte Entwicklung; aber nicht jede verteilte Entwicklung muss mit den besonderen Problemen des Offshoring kämpfen (z.B. Zeitverschiebung, Kulturunterschiede, Problemdomänenferne).

Also: Offshoring-Niederlagen sind kein Argument gegen Golos These.

Die Allaussage

Golos These ist eine Existenzaussage. Er sagt: “Es gibt mindestens ein Projekt, dass verteilt gut bewältigt wird.” Um diese Aussage zu beweisen, muss er nur ein positives Beispiel bringen. Das tut er. Vom Standpunkt der Logik aus ist es unerheblich, dass das ein Beispiel ist, dass nicht für viele Teams gilt. Damit kann eine Diskussion darüber beginnen, wie es sein kann, dass überhaupt so ein Positivbeispiel existiert. Diese Frage aufzuwerfen und nach den Gründen zu suchen, das wäre eine spannende Aufgabe für die Community.

imageDie beharrt hingegen auf der Allaussage der Agilisten: “Alle Projekte, die gut bewältigt werden sollen, müssen colocated organisiert sein.” Leider hilft das aber nichts, denn eine solche Allaussage kann mit einem Gegenbeispiel wie von Golo vorgetragen entkräftet werden. Und wieder sollte das nicht Anlass zu Wehklagen sein, sondern die Frage nach den Gründen aufwerfen.

Wie kann es sein, dass Golo positive Erfahrungen mit verteilten Teams gemacht hat? Wie kann es sein, dass andere Projekte ebenfalls kein Problem haben mit verteilten Teams? Denn dass das so ist, sollte unzweifelhaft sein, auch wenn die Literatur davon nicht überquillt. Open Source Projekte im Allgemeinen und db4o im Speziellen fallen mir da ein. Ich weiß von Carl Rosenberger, dem Chefentwickler von db4o, dass er sehr zufrieden war und ist mit seinem verteilten Team. Schon vor Jahren schwärmte er von den verteilten Pair Programming Sitzungen und pries die Möglichkeit, Entwickler nach Kompetenz rekrutieren zu können, ohne auf ihren Wohnort achten zu müssen.

Die Empirie

Aber die Frage nach den Gründen für Projekterfolge mit Verteilung wird nicht wirklich gestellt. Stattdessen drohen die Colocation-Befürworter mit der Messungskeule. “Hast du denn überhaupt einen Vergleich”, fragt Ilker Golo. Wieso behauptet Golo, dass die verteilte Arbeit mit Peter gut sei, wenn er es in räumlicher Nähe Peter nie probiert habe? Das kann doch wohl nicht sein.

Doch, das kann sein, denn Golo hat nie behauptet, die Verteilung sei besser als die räumliche Nähe. Er hat nur gesagt, sie sei (auch) gut. Er empfinde kein Problem durch die räumliche Distanz. Golo hat keinen Leidensdruck und tritt damit keck als Gegenbeispiel zur Allaussage der Agilisten auf. So simpel ist das. Ich halte das für absolut legitim.

Ilker hingegen suggeriert, dass er gemessen habe, welchen Unterschied es mache. Oder wenn er schon nicht selber gemessen hat, dann doch “die Autoritäten”: “Diese Behauptung ist kein Wunschtraum, sondern messbar & belegbar.”

Ich bezweifle zwar aus mehreren Gründen, dass es solche Messungen im wissenschaftlichen Sinne gibt – aber sei es drum. (Man möge mich bitte auf Quellen, die solche Messungen anstellen hinweisen, falls ich mich irre.)

Selbst wenn es tatsächlich effektiver und effizienter wäre, ein Team in einem Raum unterzubringen als es zu verteilen… – was eigentlich weder Golo noch ich bestreiten – sollte es man es denn deshalb auch immer ganz dringend tun?

Ilker behauptet, ja, das sei dringend wichtig, denn: “Golo überspringt in seiner Betrachtung meines Erachtens die Interdisziplinarität agiler Teams.”

Nähe-Distanz-Wertesystem

Was soll das eigentlich alles mit der Colocation? Bessere Kommunikation sei das Ziel. Ich behaupte aber, das ist nur ein Proxy. Bessere Kommunikation ist kein Selbstzweck. Warum also bessere Kommunikation? Damit die Entwicklung effektiver und effizienter ist. Es sollen bessere Entscheidungen getroffen werden (Effektivität) und die Arbeit soll zügig vonstatten gehen (Effizienz). Der Kunde soll das, was er wirklich haben will, möglichst schnell bekommen. Hört sich auch gut an. Ist jedoch immer noch nicht der ultimative Zweck.

Colocation muss sich – wie alles andere auch – daran messen, wie es Einfluss auf den Profit eines Unternehmens nimmt.

So einfach ist das. Und so komplex.

Denn da ist die Frage angebracht, ob denn Colocation die einzige Möglichkeit ist, den Profit zu steigern. Woraus setzt der sich eigentlich zusammen? Colocation ist der Meinung, der Profit steige, wenn die Software funktional auf den Punkt, evolvierbar, korrekt und produktionseffizient entwickelt wird. Und mehr Effektivität und Effizienz können da nur hilfreich sein, oder?

Hört sich plausibel an. Finde ich aber zu kurz gesprungen. Profit ist die Differenz zwischen Umsatz und Kosten. Woran dreht denn da aber Colocation? Scheinbar sinken durch Colocation nur die Kosten und damit steigt der Profit, weil ja der Umsatz gleich bleibt.

Kurzfristig mag das so sein. Langfristig jedoch wird der Umsatz durch Colocation aber sinken, weil die Effektivitäts- und Effizienzvorteile (zum Teil) an den Kunden weitergegeben werden. Das ist natürlich kein spezieller Nachteil der Colocation. Das passiert vielmehr immer, wenn die Produktivität steigt. Aber Colocation sollte sich eben auch nichts in die Tasche lügen.

Sinken denn aber durch Colocation die Kosten immer? Ich glaube, das ist nicht der Fall. Solange Colocation als Proxy hochgehalten wird, ist das nur nicht zu erkennen. Denn dann ist die Tendenz stark, Probleme durch Colocation zu lösen, statt nach der wahren Ursache zu suchen.

Ich nenne zwei Probleme in bewusst etwas drastischer Sprache, die Colocation verschärft, statt sie zu lösen:

  • Motivation: Colocated Teams sind Zwangsveranstaltungen. Colocation bedeutet gleicher Raum und gleiche Zeit für alle. Das mag irgendwie der Kommunikation förderlich sein – aber fördert das auch die Motivation? Ich habe in Büros mit mehreren Entwicklern gearbeitet und es gehasst. Ein Team Room wie bei Martin Fowler kann die Hölle sein. Alle sind am Telefon, wenn einer es ist. Alle hören die Gespräche der anderen. Alle werden gestört, wenn der Chef reinplatzt. Es sei denn, man schützt sich durch einen Kopfhörer und Musik.
    Ich habe solche Zwangsgemeinschaften immer als Belastung empfunden. Sie haben meine Motivation gedämpft und damit meine Leistung verschlechtert.
    Geschieht das über längere Zeit, steigt die Fluktuation im Team. Und das (!) ist richtig teuer.
  • Kompetenz: Colocated Teams müssen mit den Kompetenzen leben, die sich an einem Ort sammeln lassen. Sind das aber wirklich die besten für das Projekt? Ist ein Team mit 10 schlechten colocated Entwicklern besser als ein Team mit guten 5 Entwicklern in räumlicher Distanz? Das bezweifle ich.
    Wer Colocation als so wichtig ansieht, verschenkt aber die Chance, ein verteiltes Team aus 5 guten Entwicklern zu geringeren Kosten einzusetzen. Er kann das nicht mal denken, denn räumliche Nähe ist ja “messbar & belegbar” besser.

image So einfach ist es also nicht mit dem Profit und der Colocation, würde ich sagen. Colocation “best practice” steht vielmehr der besten Lösung im Weg. Die findet nämlich nur, wer wahre Werte und keine Tools in den Blick nimmt.

Wie ist es also mit dem Profit? Wann ist der hoch? Es tragen u.a. bei zum Profit:

  • Flüssige Kommunikation
  • “Barrierefreiheit”, d.h. weitgehende Abwesenheit von Hierarchien als Kommunikationslenker
  • Hohe Motivation
  • Hohe Kompetenz
  • Hohe Kohärenz, d.h. die Ausrichtung der Kräfte auf ein gemeinsames Ziel

Diese Punkte schlage ich mal als Wertesystem hinter der Frage nach Nähe oder Distanz von Teammitglieder vor. Wem da Werte fehlen, mag sie gern vorschlagen.

Wer also über Colocation oder nicht entscheiden muss, sollte sich überlegen, inwiefern die Entscheidung dafür/dagegen diesen Werten dient.

Der Misserfolg von Offshoring ist mit diesen Werten zum Beispiel recht leicht zu erklären, würde ich sagen: Das typische “leichtfertig” Offshoring-Projekt widerspricht den Werten wie folgt:

  • Keine flüssige Kommunikation aufgrund von verschiedenen Zeitzonen, Kulturen und Sprachen
  • Mangelnde Barrierefreiheit, weil Teams in anderen Kulturen oft nicht genauso egalitär arbeiten wie Europäer
  • Mangelnde Motivation, weil Entwickler beim Auftraggeber oft die letzten Übriggebliebenen sind und Entwickler in der Ferne keinen Bezug zur Problemdomäne haben. Angst, Unsicherheit, zähe Kommunikation und Entfremdung sind schlechte Motivatoren.
  • Mangelnde Kompetenz, weil Entwickler in der Ferne bei allem Marketing der Offshore-Firmen eben doch nicht so kompetent sind, wie man es gern hätte. Davon abgesehen ist die Fluktuation bei Offshoring-Firmen oft sehr hoch, so dass kompetente Kollegen in der Ferne bald aus dem Projekt verschwinden.
  • Geringe Kohärenz, da Kulturunterschiede und Problemdomänenferne es schwer machen, Entwickler in Nah und Fern zu einem Team zusammenzuschweißen, das eine gemeinsame Vision hat.

Und bei der nachgebeteten Colocation ist es nicht viel anders. Auch sie läuft Gefahr, den Werten für mehr Profit zu widersprechen – trotz aller guten Intention. Das Ergebnis sieht dann schnell so aus – ohne, dass es jemand merken würde, weil ja keine Messung/kein Vergleich durchgeführt wird, da man sich ja im Einvernehmen mit den agilen Vordenkern sieht:

  • Flüssige Kommunikation im interdisziplinären Team. Super! Pluspunkt für Colocation.
  • Barrierefreiheit mag gegeben sein. Pluspunkt für Colocation.
  • Aber: Suboptimale Motivation, weil die Autonomie gering ist. Menschen, die viel leisten müssen, wollen viel Freiheit darüber haben, wie sie diese Leistung erbringen. Das gehört zur conditio humana. Je größer die Belastung, desto höher sollte die Autonomie sein, über die “Lösungsverhältnisse” zu bestimmen. Die ist bei Colocation per definitionem aber sehr gering.
  • Und: Suboptimale Kompetenz. Die Führung nimmt die Entwickler, die sich “zusammenpferchen lassen”, statt die besten für den Job zu suchen. Da Personalkosten der größte Kostenfaktor in der Softwareentwicklung ist und es Leistungsunterschied im Zehnerpotenzbereich gibt, sind die Kosten durch suboptimale Kompetenz womöglich ungeahnt groß.
    Wer auf Colocation setzt – aus welchen Gründen auch immer –, der verschenkt schlicht die Chance, bares Geld zu sparen mit Entwicklern, die nicht in der Nähe sind, aber deutlich besser. Und sei es auch nur für “Spezialaufträge”, die ab und an anfallen.
  • Schließlich: Munteres Geplauder im Team Room und am Kicker ist eine schöne Sache – kann allerdings auch darüber hinwegtäuschen, dass Kohärenz fehlt. Wo Kommunikation stetig so unbewusst abläuft wie bei der Colocation fehlt es womöglich an Signalen für Missverständnisse.

Ich will damit nicht sagen, dass Colocation schlecht sei. Mir geht es nur darum, sie nicht zu beweihräuchern. Colocation gehört nicht auf den Altar. Auch sie ist nur ein Tool, das man in bestimmten Situationen einsetzen sollte und in anderen eben nicht.

Darüber hinaus möchte ich aber auch motivieren, räumlich verteilte Teams als Ziel zu haben. Wenn das nicht erreicht werden kann, dann ist Colocation auch ok. Aber ich halte die räumliche Verteilung als default Modus eines Teams für unverzichtbar. Warum? Weil mit Motivation und Kompetenz so wichtig sind.

Wer schonmal gute Entwickler gesucht hat, der weiß, wie schwer es sit, gute Leute zu finden. Warum also die Suche erschweren dadurch, dass man neue Kollegen dazu zwingt, umzuziehen? Aber auch wenn das heute noch nicht genügend Grund für viele Manager sein wird, räumliche Distanz im Team zu akzeptieren… ich denke, in 5 oder 10 Jahren ist das anders. Der Fachkräftemangel wird nur größer, nicht kleiner. Keine Sorge. In 10 Jahren ist ein Team in München froh, einen Entwickler in Schwerin sitzen zu haben; und das Team in Paderborn ist froh über den Kollegen auf Madeira.

Voraussetzung für erfolgreiche Distanz

Entwickeln in räumlicher Distanz mag von mir aus aber sogar die zweitbeste Lösung sein. Am Ende hilft es nichts: Wir kommen nicht drumherum, die Verteilung “ins Programm aufzunehmen”. Entwickler werden sie fordern. Sie ist in vielen Fällen ökonomischer und ökologischer für alle Beteiligten. Und auf lange Sicht lassen sich benötigte Kompetenzen nicht mehr anders finden.

imageEs scheint mir daher angezeigt zu überlegen, was denn dann die Bedingungen für die Möglichkeit gelingender Teamentwicklung in Distanz sein kann. Statt dass wir uns in “Ich hab recht, du hast unrecht”-Zuwürfen ergehen oder im Lamentieren verharren, wie schlecht denn die letzte Erfahrung mit der räumlichen Distanz gewesen sei, sollten wir notwendige Kriterien für erfolgreiche verteilte Projekte finden. Hier meine Vorschläge:

  • Erreichbarkeit: Wer nicht im selben Raum sitzt, soll im selben Space sitzen. Alle Teammitglieder müssen einander erreichen können mit Chat, Email, Telefon, Desktopsharing und evtl auch Video und Wiki. Diese Medien müssen denkbar einfach zu bedienen sein. Diese Medien müssen ständig zur Verfügung stehen.
    Wo ein Teammitglied arbeitet, ist letztlich egal. Aber da wo es arbeitet, muss es erreichbar sein. Je nach Medium sollten die Antwortzeiten unterschiedlich, aber medienbezogen kurz sein.
  • Kommunikationskompetenz: Teammitglieder müssen die Medien nicht nur bedienen können, sondern auch wollen. Manche Menschen tun sich schwer mit der elektronischen Kommunikation – auch in unserem Job. Mit denen ist verteilte Arbeit schwierig. Bei der Besetzung eines verteilten Teams ist daher darauf zu achten, ob die Mitglieder “etwas anfangen können” mit den Medien. Lippenbekenntnisse dazu sorgen schnell für Friktionen. Also Obacht!
  • Verlässlichkeit: Wer technisch und charakterlich kompetent ist in der notwendigen elektronischen Kommunikation, der muss auch dem Erwartungsniveau entsprechen. Er muss verlässlich kommunizieren. Und er muss natürlich auch verlässlich arbeiten außerhalb der Kommunikation. Dazu braucht es sicher einige teamspezifische Regeln, auf jeden Fall aber klare Aufgabenzuordnungen.
  • Vertrauen: Aus Verlässlichkeit speist sich dann ganz natürlich Vertrauen. Wer verlässlich arbeitet, gewinn Vertrauen. Da braucht es gar keine Kickerturniere oder Hochseilgärten. Nichts ist teambildender als Verlässlichkeit in der Kommunikation und Arbeit.
    Aber wo das Management es bezahlen kann, da dürfen natürlich weitere vertrauensbildende Maßnahmen hinzukommen. Vor allem aber sollte alles unternommen werden, Vertrauen nicht zu zerstören durch künstliche Fluktuationen im Team.
  • Moduswechsel: Manches lässt sich dann doch einfacher im persönlichen Gespräch klären. Dafür mag es angezeigt sein, gelegentlich Teile eines Teams oder das ganze Team in einem Raum zu sammeln. Eine gesunde Balance zwischen Nähe und Distanz ist also nötig.
    Wann ist eine Colocation-Phase sinnvoll? Wenn einer der obigen Werte droht kompromittiert zu werden. Colocation könnte nötig sein, um doch mal die Motivation zu heben oder die Kohärenz zu stärken oder Kompetenz aufzubauen oder noch zügiger zu kommunizieren.

Soweit meine Vorschläge für eine Diskussion über Nähe vs Distanz für Entwicklerteams. Lasst uns nicht auf Autoritäten verweisen, lasst uns nicht Überkommenes wiederkäuen, sondern mit freiem Geist darüber nachdenken. Es gibt viel zu gewinnen durch eine Befreiung aus der unverbrüchlichen “best practice” Colocation. Denn ich glaube, es gibt nur wenige Entwickler, die mehr Autonomie bei der Wahl ihrer Arbeitszeit und ihres Arbeitsplatzes ablehnen. Oder?

Also sollten wir mehr persönliche Autonomie in den Blick nehmen und schauen, wie wir das Arbeitsumfeld, d.h. die Teamorganisation darauf ausrichten können. Der Mensch ist das Maß, nicht das Projekt.

Dienstag, 10. August 2010

Leiden am Domänenobjektmodell [OOP 2010]

Ist die Objektorientierung wirklich auf dem richtigen Weg? Grad habe ich wieder so eine Phase, wo ich mich das frage – und immer auf die Antwort komme: Es ist etwas grundsätzlich falsch mit der Objektorientierung.

Halt! Stopp! Ich meine nicht, dass wir keine Klassen mehr haben sollten. Auch die Vererbung muss nicht abgeschafft werden. Ebensowenig Polymorphie oder Interfaces. Alles wunderbar. Diese Mittel zur Formulierung von Code dürfen gern erhalten bleiben. (Und andere dürfen gern hinzu kommen.) Das meine ich auch mit Objektorientierung nicht.

Mit geht es um, tja, die Anwendung dieser Mittel. Ich habe sozusagen ein Problem mit ihrer Auslegung. Die Antwort auf “Was sollen wir mit diesen Mitteln tun?” ist für mich in einigen Bereichen kontraproduktiv.

Gibt es dafür einen Begriff? Muss ich dann fragen “Ist OOAD wirklich auf dem richtigen Weg?” Ja, vielleicht wäre das präziser. Denn die armen Objekte bzw. Klassen können ja nichts dafür, wenn man sie suboptimal einsetzt.

Also: Mein wachsendes Gefühl ist, dass OOAD auf dem falschen Weg ist. Und darüber hinaus scheinen mir sogar bekannte Leute wie Martin Fowler, Eric Evans oder Jimmy Nilsson mal auf der falschen Fährte. Ja, ich erlaube mir heute mal – es regnet in Hamburg – Kritik an Gurus zu üben.

Hier mein Stein des Anstoßes: Domänenobjektmodell. Darüber stolpere ich immer wieder. Ich kann mich nicht mit der Defintion der Gurus anfreunden. Vielleicht spüren Sie, was ich fühle, wenn Sie mal über diese Frage nachdenken:

Wann haben Sie zuletzt einen Kunden gefragt, ob sein Kreditlimit ok sei?

Oder diese Frage:

Wann haben Sie zuletzt einen Auftrag (bzw. den beauftragenden Kunden) gefragt, ob er in dieser Größenordnung ok ist?

imageEs mag meiner begrenzten Erfahrung anzulasten sein, aber ich habe soetwas noch nie einen Kunden (oder Auftrag) gefragt. Lassen Sie es mich wissen, wenn Sie es anders kennen und der Kunde selbst zu seiner Bonität Auskunft geben darf.

Ahnen Sie, worauf ich hinaus will? Mir gehts um die Geschäftslogik oder allgemeiner die Domänenlogik. Wo ist die am besten aufgehohen. Guru Jimmy Nilsson hat in seinem vielbeachteten Buch “Applying Domain-Driven Design and Patterns: With Examples in C# and .NET” dazu diesen Vorschlag gemacht. Es ist seine Skizze eines Domänenobjektmodells für eine kleine Liste von Anforderungen:

image

Sehen Sie dort die Fragen, die ich gestellt habe? Die Klasse Customer hat eine Funktion HasOkCreditLimit() und die Klasse Order eine Funktion IsOkAccordingToSize(). Die entsprechen meinen Fragen, denn Customer und Order repräsentieren Kunde und Auftrag in der realen Welt. Das ist ja gewollt. Das macht OOAD und auch Domänenobjektmodelle aus.

Die Welt könnte also in Ordnung sein. Und für viele Entwickler ist sie so auch in Ordnung. Nur für mich nicht mehr. Ich kann so nicht mehr denken. Echt. Mir wird dann schlecht.

Martin Fowler hingegen findet das wunderbar. Für ihn ist das Ausdruck der “basic idea of object-oriented design; which is to combine data and process together.” (Er sagt das allerdings im Zusammenhang mit dem anämischen Domänenmodell. Aber dazu unten mehr.)

Gegen diese Grundidee des objektorientierten Designs habe ich auch nichts. Ich liebe die Möglichkeit, Daten und Operationen auf ihnen zusammenfassen zu können. Einen Stack so bedienen zu können, ist unschätzbar:

var s = new Stack<int>();
s.Push(99);
s.Push(42);
Assert.AreEqual(2, s.Pop());

Wie umständlich ist dagegen die prozedurale Programmierung wie mit Pascal oder C. Brrrr, da schüttelt es mich auch.

Dass wir Operationen und Daten nun allerdings zu Klassen zusammenfassen können, bedeutet nicht automatisch, dass jede solche Zusammenfassung auch eine gute ist. Wir brauchen also Kriterien, um die Qualität eines Objektmodells beurteilen zu können.

Beim Stack sind wir uns einig. Er nutzt die objektorientierten Mittel angemessen, um einen abstrakten Datentyp zu implementieren. Wie steht es aber mit Jimmy Nilssons Domänenmodell?

Bevor ich näher auf diese Frage eingehe, scheint es mir angezeigt, zum Begriff Domänenobjektmodell etwas zu sagen und es abzugrenzen gegen andere

Objektmodelle

image Das Ergebnis eines objektorientierten Designs ist immer ein Objektmodell. Es beschreibt Klassen mit ihren Zusammenhängen (Beziehungen). Ob das Customer und Address wie bei Jimmy sind oder Dictionary<> und KeyValuePair<> oder Form und TextBox bzw. Control ist, das ist einerlei. Wir können also gar nicht anders, als mit Objektmodellen zu arbeiten.

Domänenobjektmodelle

Wir können auch nicht anders, als Domänen zu modellieren. Wenn der Begriff im Zusammenhang mit Domänenobjektmodell gebraucht wird, dann bezieht er sich allerdings vor allem auf das Geschäftsfeld eines Kunden. In dem kommen Customer und Address vor, nicht aber Form und TextBox.

Wenn wir es genau nehmen, dann stellen allerdings auch Form und TextBox ein Domänenobjektmodell dar. Nur ist es keines aus der Geschäftswelt, sondern eines aus der Infrastrukturwelt. WinForms ist eine Infrastruktur zur Darstellung von Formularen. Also ist es das Domänenobjektmodell für Infrastrukturentwickler.

Was die Domäne ist, ist mithin relativ. In Bezug auf eine ganze Anwendung ist allerdings die Problemdomäne der Anwendung gemeint. Das mag bei Jimmy Nilsson eine Warenwirtschaft sein.

Wenn in der Anwendung weitere Objektmodelle vorkommen, z.B. für die Security oder Caching oder oder oder, dann sind das keine Domänenobjektmodelle im engeren Sinn.

Diese Unterscheidung wird interessant, wenn wir das DataSet in den Blick nehmen. Es ist unzweifelhaft ein Objektmodell bestehend aus DataSet, DataTablee, DataRow, DataColumn usw. Aber es ist kein Domänenobjektmodell, auch – und das ist wichtig! – wenn es benutzt wird, um Domänendaten zu halten.

In einem DataSet können Sie genauso Kunden, Aufträge, Auftragspositionen, Adressen usw. ablegen wie in einem Domänenobjektmodell. Allerdings ist dieses Objektmodell unspezifisch. Es enthält ja keine Klassen, die sich auf die Domäne beziehen. Deshalb nennt man es nicht Domänenobjektmodell. Der Begriff ist Objektmodellen vorbehalten, die eben aus ganz eindeutig domänenbezogenen Klassen bestehen wie Customer, Address, Order usw.

Anämische Domänenobjektmodelle

Den Gurus ist es nun nicht genug, dass Sie zur Verwaltung von Domänendaten ein spezifisches Objektmodell definieren, statt ein generisches zu verwenden. Sie sagen, ein Objektmodell sei nur wahrhaft ein Domänenobjektmodell, wenn die Objekte nicht nur Daten enthalten, sondern auch domänenrelevante Operationen auf diesen Daten anbieten.

Fehlen solche Operationen, dann spricht Martin Fowler von einem anämischen, d.h. blutarmen Domänenobjektmodell. Ich denke, er könnte auch sagen, dann sei das Objektmodell ein reines Datenobjektmodell.

Es gilt also: Domänenobjektmodelle sind auch Datenobjektmodelle; aber Objektmodelle sind nicht unbedingt Domänenobjektmodelle, auch wenn sie aus domänenspezifischen Klassen bestehen.

Kritik des Domänenobjektmodells

Was habe ich nun an dem ganzen auszusetzen? Mein Einwand ist prinzipieller Natur: Aus meiner Sicht widerspricht ein Domänenobjektmodell wie das obige von Jimmy mehreren Prinzipien der Softwareentwicklung.

Widerspruch gegen SRP

Wenn Sie nur einen ganz flüchtigen Blick auf das Domänenobjektmodell werfen, was sehen Sie da? Sie sehen Kästchen und Linien, d.h. Klassen und Beziehungen. Sie sehen also ein Datenmodell.

Datenmodell zu sein bzw. Daten zu halten, ist eine Verantwortlichkeit. Ich hoffe, da stimmen Sie mir zu. Verändert sich die Daten(beziehungen) einer Domäne – z.B. könnte jeder Kunde mehrere Adressen haben –, dann ist die Klasse Customer zu verändern.

Customer muss sich aber auch verändern, wenn sich die Daten nicht verändern. Es könnte z.B. sein, dass nicht nur aufgrund eines Kreditlimits entschieden werden soll, ob ein Kunde ok ist, sondern es soll auch entschieden werden, ob einem Kunden Sonderangebote gemacht werden können. Eine Funktion EligbleForPromotionalOfferings() schiene im Sinne des Nilssonschen Verständnis von Domänenobjektmodellierung doch angezeigt, oder?

Wenn Sie zustimmen, dass stimmen Sie einem Widerspruch gegen das SRP zu. Denn Sonderangebote haben nichts mit den Daten zu tun, die im Domänenobjektmodell zu sehen sind. Sonderangebote wären ein zweiter Grund für Veränderungen an der Klasse Customer. So wie auch schon die Kreditlimitprüfung.

image

Nochmal: Ich bin für Methoden auf Datenklassen. AddOrderLine() ist z.B. eine Methode, die ich absolut passend finde für das Domänenobjektmodell. So wie Push() und Pull() angemessene Methoden für die Problemdomäne Stack sind.

Methoden sollen aber zur Verantwortlichkeit passen. Die ist bei dem Objektmodell eindeutig “Datenhaltung”. Sonst gäbe es keine Properties und keine Beziehungen. Jimmys Objektmodell ist in jedem Fall ein Datenobjektmodell. Wunderbar. Kein Problem. Dann sollte es sich aber auch darauf konzentrieren.

Methoden, die helfen, die Daten konsistent zu halten, sind willkommen. Methoden, die darüber hinaus gehen, halte ich für falsch.

Natürlich soll die Anwendung entscheiden, ob ein Kunde ein ausreichendes Kreditlimit hat. Doch diese Entscheidung sollte nicht dem Kunden in Form eines Customer-Objektes überlassen werden. Das ist nicht intuitiv. Und es widersprich dem SRP. Und es macht auch noch eine “arme Datenklasse” abhängig von einem Service:

image

Mit Verlaub: Das ist Mist. Da mag Martin Fowler noch so lang die Objektorientierung beschwören, die das möglich macht. Nicht alles, was möglich ist, ist halt auch angemessen.

Zwar gehört ein Kreditlimit zum Kunden. Das bedeutet jedoch nicht, dass seine Prüfung aber auch über den Kunden stattfinden muss. Wie gesagt, im realen Leben tut das niemand. Warum sollten wir damit anfangen, wenn wir Software schreiben? Nur weil das überhaupt geht? Quatsch.

Da wird auch nichts einfacher testbar. Und es wird nichts evolvierbarer. Die Prüfung ist eh in einen Service ausgelagert. Ich behaupte, dass im Gegenteil die Evolvierbarkeit sinkt, weil eine “dumme” Datenklassen ohne Not aufgepeppt wird mit Funktionalität, die nichts mit ihrem primären Zweck zu tun hat.

Wenn die Validierungen auf Customer und Order wegfielen, ja, dann würde das Objektmodell weniger Operationen enthalten. Ist es deshalb aber blutarm? Sollte es in dieser Weise als krank diagnostiziert werden? Ich könnte dagegen halten und es “fokussiert”.

Aus meiner Sicht bleibt Martin Fowler in seiner Kritik anämischer Domänenmodelle den Beweis eines unausweichlichen pathologischen Effekts auch schuldig. Da schwadroniert er vom Widerspruch zu Grundlagen der Objektorientierung. Da beschwört er eine faulige Nähe zur prozeduralen Programmierung. Da warnt er vor Aufwand mit geringem Nutzen für die Persistenz.

Beweise sind das aus meiner Sicht aber nicht. Vor allem fehlt mir einer der Hauptnutzen eins Domänenobjektmodells: die Repräsentation der Domäne im Code. Ein Domänenobjektmodell macht auch ohne großartige Domänenfunktionen Code lesbarer und sicherer als ein generisches Objektmodell wie das DataSet.

Außerdem ist auch ein anämisches Domänenobjektmodell mit heutigen Tools recht schnell aufgesetzt und persistiert.

Und schließlich: Sein Argument, es würde Domänenlogik ganz unsinnig in prozedurale Services rausgezogen, führt er selbst ad absurdum, indem er lobende Worte für Jimmys Buch findet, das trotz aller Domänenobjektmodellhaftigkeit eben genau das tut. Denn was ist denn sein CreditService, den Customer referenziert? Selbst Martin Fowler würde doch nicht behaupten wollen, dass der Code dieses Service in der Klasse Customer stehen sollte, oder?

Widerspruch gegen Law of Demeter

Nach dem Law of Demeter (LoD) sind “train wrecks” ein code smell, z.B.

someOrder.OrderLines[n].Product.Description

Wieso haben wir dann aber überhaupt Mittel in der Objektorientierung, um solche offensichtlichen Stinkereien zu produzieren? Properties laden dazu ja ein.

Das Law of Demeter ist halt differenziert zu verstehen. Was ist sein Zweck? Es will exzessive Abhängigkeiten vermeiden. Ein sehr löbliches Ansinnen. Aber eines, das im Widerspruch dazu steht, was Datenobjektmodelle wollen. Die wollen nämlich gerade Abhängigkeiten repräsentieren. Mithin gilt das Law of Demeter nicht für Datenmodelle.

Ist ein Objektmodell ein Datenobjektmodell, dann sind “train wrecks” ok, ja geradezu unvermeidbar.

Indem nun jedoch Jimmy Nilsson sich weigert, ein reines Datenobjektmodell zu definieren, widerspricht er dem LoD. Er entscheidet sich nicht für ein Datenobjektmodell, weil er ja der Domänenobjektmodelllehre folgen will – aber er legt trotzdem die Beziehungen offen, so dass man “train wrecks” erzeugen kann.

Das bedeutet im Umkehrschluss: Domänenobjektmodelle dürfen eben keine Datenobjektmodelle sein. Wo “hochwertige” Domänenlogik definiert ist, dürfen keine Datenbeziehungen herauslecken.

Zwischenstand

Die in prominenten Werken wie

image image

beschrieben Domänenobjektmodelle sehen plausibel aus. In ihnen scheint sich Objektorientierung pur und zum Besten zu manifestieren.

Leider hilft das nichts, denn es hilft mir nicht. Objektorientierung ist kein Selbstzweck. Wenn mit eine bestimmte Nutzung objektorientierter Mittel es nicht leicht macht, meine Software zu entwerfen und zu pflegen, dann besteht nicht nur die Möglichkeit, dass ich es immer noch falsch mache (Inkompetenz), sondern auch die Möglichkeit, dass die Empfehlungen suboptimal sind.

Nach jahrelangem Bemühen bin ich nun an einem Punkt, wo ich meine, dass ich so fürchterlich inkompetent nicht sein kann. Also erlaube ich mir, die andere Alternative in Betracht zu ziehen. Vielleicht ist das mit den Domänenobjektmodellen doch nicht so pauschal eine gute Idee wie Fowler, Nilsson & Co es darstellen?

Ich habe viel übrig für Objektorientierung. Abstrakte Datentypen mit ihren Mitteln zu bauen, ist wunderbar. Die sind jedoch in sich abgeschlossen. Ihre Operationen beziehen sich auf sich selbst. Das wünsche ich auch jedem Datenobjektmodell. Es soll nicht blutleer sein. Operationen, die die Konsistenz von Daten im Sinne der expliziten Beziehungen sicherstellen, dürfen, nein, sollen auf den Klassen des Datenobjektmodells definiert sein.

imageJimmys Domänenobjektmodell geht darüber jedoch weit hinaus. Da beginnt dann mein Schmerz. Aber was tun? Eine Idee habe ich. Mal schauen, dass ich die in einem weiteren Blogartikel darstelle. Meine Interpretation von Domain Driven Design (DDD) ist anders. Eric Evans hält sich ja auch bedeckt, was die Übersetzung seiner Modelle in Klassen angeht. Da ist Interpretationsspielraum. Jimmy hat Evans´ Metamodell in einer Weise interpretiert. Ich tue es in einer anderen Weise.

Dienstag, 3. August 2010

Was treibt uns an?

Mit der Softwarequalität steht´s nicht zum Besten. Könnte das aber nicht nur an den ewig suboptimalen Programmiersprachen, unzulänglichen Werkzeugen, imperfekten Konzepten und inkompetenten Entwicklern liegen, sondern womöglich daran, dass viele die Arbeit als nicht so befriedigend empfinden, wie sie sein sollte, um wirklich gute Ergebnisse abzuliefern?

imageIn einem Artikel meiner Sandbox-Reihe in der dotnetpro habe ich mir darüber einmal Gedanken gemacht. Den gibt es heute exklusiv vorab zu lesen. Auch für die, die keine dotnetpro-Abonnenten sind.

Ich glaube, dass wir einiges an unseren Produkten und auch in unserem (Arbeits)Leben verbessern können, wenn wir uns Gedanken machen, warum wir eigentlich arbeiten. Was treibt uns da wirklich an?

Wen meine Überlegungen dazu interessieren, der kann ja mal in meinen Artikel reinschauen. Und dann diskutieren wir hier. Würde mich freuen.

Den Artikel zu lesen, kostet nichts – außer eine Tweet. Wie ist das? Ein fairer Preis?

Sonntag, 18. Juli 2010

Quando, quando, quando? Vom Zeitpunkt für automatisierte Tests

Grad lese ich Thomas Bandts öffentliche Gedanken zur Frage, ob und wann TDD denn sinnvoll sei. Die treibt ja viele um. Immer wieder. Und so glaube ich, dass irgendwas an ihr noch nicht stimmt. Irgendwie ist die Perspektive falsch.

Wenn es in einer Partnerschaft krieselt und sie klagt “Immer bringst du den Müll nicht runter!” worauf er fragt, “Wann soll ich den Müll denn runterbringen? Wenn die Tüte richtig voll ist oder früher?”, dann ist das auch die falsche Frage. Sie nimmt nur ein Symptom in den Blick, nicht das Wurzelproblem. Das könnte nämlich darin bestehen, dass sie sich in ihrem Bemühen um eine schöne Wohnung nicht anerkannt fühlt.

Also: Was könnte falsch an der Perspektive hinter der ewigen Frage nach “Soll ich eigentlich immer (!) mit TDD arbeiten?” falsch sein?

Widerstand gegen die Silberkugel

Ich glaube, die Frage ist Ausdruck einer großen Skepsis. Entwickler sind zurecht skeptisch, dass es eine Praktik wie TDD gibt, die “alles” besser machen könnte. Aber insofern ist die Frage auch Ausdruck eines Missverständnisses. TDD will gar nicht “alles” besser machen. Nur manches.

Die Frage ist damit falsch gestellt, weil sie suggeriert, TDD würde einen Absolutheitsanspruch formulieren. TDD ist “nur” eine Methode, um ein Ziel zu erreichen: korrekten Code mit einer hohen Testabdeckung und gut evolvierbarem Design.

Evolvierbares Design ist die Voraussetzung für langlebige Software.

Hohe Testabdeckung ist eine gute Grundlage dafür, dass Bugs sich nicht verkriechen können in ungetesteten Code. Verkriechen ist aber nur möglich bei Code, der entweder nicht trivial ist oder der nur sehr selten ausgeführt wird. Ist beides nicht gegeben, dann stolpert man bei Integrations- oder Akzeptanztests sehr schnell über den Bug. Und sehr wahrscheinlich ist dann auch sofort klar, wo und warum es geknallt hat.

Beispiele für solchen Code sind Getter/Setter oder Mappings. Die funktionieren entweder -- oder eben nicht.

Wer also Code schreibt, der nicht evolvierbar sein muss oder der weitgehend aus Trivialitäten besteht… der muss TDD nicht einsetzen. Forms-over-Data Szenarien fallen für mich z.B. in diesen Bereich.

Widerstand gegen Veränderung

Entwickler widersetzen sich aber nicht nur Silberkugeln, sondern auch Veränderungsansinnen. Und nichts anderes ist TDD. TDD will den Entwickler verändern. Und zwar sehr tiefgreifend. Alte Gewohnheiten sollen abgestreift werden, ja geradezu Gewohnheitsrecht soll aufgegeben werden.

Bisher war “Softwareentwicklung ist Spaß beim codieren von spannender Funktionalität codieren”, nun soll aber sein “Softwareentwicklung ist nicht nur spannende Funktionalität programmieren, sondern auch langweilige Tests, die das eigene Werk in Frage stellen und langsam machen.” Das ist ein tiefgehender Eingriff. Denn erstens wird dem Entwickler der Spaß an der Programmierung vergellt. Zweitens setzt er ihn noch mehr unter Druck, denn der Chef ist gewohnt, schnell rausgeschossene Funktionalität zu sehen. Mit TDD kann die Feuerfrequenz nur sinken.

Das Recht auf Spaß und Spannung, das Recht auf Entlastung durch Fokus auf Funktionalität soll mit TDD beschnitten werden. TDD bedeutet also einige Veränderung. Und die macht nie Freude. Deshalb setzt sich der Entwickler zur Wehr wie jeder es tut, dem ungewollte Veränderungen ins Haus stehen.

Doch auch hier lauern Missverständnisse. Erstens ist das Schreiben von Tests nicht pure Langeweile. Im Gegenteil! Es kann sogar eine größere intellektuelle Herausforderung darstellen, als Funktionalität zu programmieren. Code testbar machen, ist oft keine Kleinigkeit. Wer also was auf sich hält als Entwickler, der sollte mehr Tests schreiben.

Zweitens verlängern automatisierte Tests die Softwareentwicklung nicht per se. Es kommt vielmehr auf die Perspektive an. Wer nur auf diesen Moment schaut, der sieht natürlich die Zeit dahin rinnen, wenn er nicht nur 3 Zeilen Funktionalität, sondern auch 6 Zeilen Test schreiben muss.

Diese Perspektive ist jedoch kurzsichtig. Sie folgt dem Gedanken eines Rechnungswesens, das immer nur auf die Kosten stiert. Und dahinter steht die Vorstellung, dass ein optimales Gesamtergebnis erzielt wird, wenn jeder Einzelschritt optimal ist.

Das stimmt aber nicht. So funktioniert die Welt nicht. Oder nur in besonderen Situationen tut sie das.

Diese Perspektive verliert schlicht den Gesamtprozess und auch die Zeit im Großen aus dem Blick. Denn nicht nur die Zeit eines Entwicklers ist wichtig. Viel wichtiger ist der Gesamtaufwand an Zeit über längere Zeit.

Wer nur auf hier und jetzt schaut, kann das nicht sehen. Wirtschaftlichkeit bedeutet nicht, jetzt etwas in einer bestimmten kurzen Zeit zu produzieren, sondern langfristig zu überleben.

Tests heute beim Entwickeln dienen nun dieser Wirtschaftlichkeit. Denn sie verringern die Wahrscheinlichkeit, dass Bugs in der Zukunft die Arbeit behindern. Das tun sie nämlich in zweierlei Hinsicht: Erstens halten Sie davon ab, Funktionalität zu produzieren. Zweitens stören Sie den Fokus des Teams. Beides ist kontraproduktiv.

Auf längere Zeit gesehen, sind automatisierte Tests (bzw. TDD) also wirtschaftlicher. Das ergibt sich auch ganz leicht aus einer simplen Rechnung: Nach jeder Veränderungen am Code müssen alle relevanten Tests wiederholt werden, um sicherzustellen, dass nichts kaputtgegangen ist. Das sind umso mehr, je verquarzter Code ist. Damit ist es umso teurer, je mehr manuelle Tests gefahren werden müssen. Die Ausführung von automatischen Tests hingegen kostet nichts.

Umgekehrt bedeutet das allerdings: Wer sich um längere Zeit nicht scheren muss, der kann auf autom. Tests/TDD verzichten.

Allerdings würde ich immer hinterfragen wollen, ob einer, der aus dem Grunde TDD von der Hand weist, wirklich eine Ahnung davon hat, was in der Zukunft mit seiner Software passiert. Aus so mancher Software, die für einmaligen Gebrauch gedacht war, ist ja schon eine unternehmenskritische Anwendung geworden.

Niemand will ja das Brownfield. Das ist, glaube ich, unzweifelhaft. Aber warum ist es dann überall? Warum gibt es solche Massen an unwartbarem Code, dessen Korrektheit niemand beweisen kann. Nach jeder Änderungen hoffen und bangen alle nur, dass nichts vormals Korrektes nun kaputt ist.

Widerstand gegen Angriffspunkt

Nicht nur gegen Veränderungen und Silberkugeln wehren sich Entwickler mit der kritischen Frage, ob TDD denn wirklich sein müsse. Sie wehren sich auch dagegen, angreifbar zu werden. Sie wehren sich dagegen, sich erklären zu müssen.

imageWer bisher keine Tests geschrieben hat und nun damit anfängt und schon deshalb, weil er ein Anfänger ist, länger braucht, der sieht sich Stirnrunzeln ausgesetzt. “Muss das mit diesen neumodischen Tests denn sein?” fragt der Chef?

Darauf dann selbstbewusst mit “Ja, das muss sein” zu reagieren, fällt vielen schwer. Sie können sich auf keinen Berufsethos zurückziehen. Sie haben keine Branche hinter sich, in der es alle so tun. Die Ausbildung steht nicht hinter ihnen. Sie sind auf sich gestellt. Ihre Überzeugung steht gegen die Weisungsmacht des Chefs, der von Softwareentwicklung allzuoft nichts versteht und Experimente so gar nicht mag.

Das sind verständlicherweise unschöne Aussichten. Also ist der Reflex die sehr skeptische Frage, ob denn TDD überhaupt etwas brächte.

Fazit

Natürlich hat die Frage, ob und wann TDD denn sein müsse, eine Sachebene. Die Frage ist sachlich berechtigt. Und die Antwort sollte nicht pauschal sein, sondern differenziert. Entscheidungskriterien sind gefragt.

Lebensdauer und Kompliziertheit und Art von Code gehören für mich als Aspekte in die Antwort. Nicht für jeden Bau sind statische Berechnungen im selben Umfang nötig. Nicht für jedes Unternehmen ist ein Rechnungswesen im selben Umfang nötig. Deshalb: Nicht für jedes Softwareprojekt sind automatisierte Tests im selben Umfang nötig.

Kontraproduktiv im Sinne einer differenzierten Antwort finde ich es jedoch, sich über die wahren Beweggründe der Frage nicht klar zu sein. Jeder, der so fragt, prüfe sich da also am besten erstmal selbst. Das ist schwierig – aber wenn man es nicht tut, läuft man immer wieder gegen Wände aus Missverständnissen. Und nichts ist lähmender als Konflikte, deren Ursachen man nicht kennt bzw. verschleiert.

Ebenfalls kontraproduktiv empfinde ich es, ganz grundsätzlich den Nutzen von automatisierten Tests zu hinterfragen. Das widerspricht aus meiner Sicht der Zivilisationsgeschichte der letzten 1000 Jahre, die ganz klar zeigt, dass Automatisierung wo immer es geht, Vorteile bringt. Menschen ersparen sich langweilige, fehlerträchtige, gefährliche Arbeit, indem sie automatisieren. Warum sollte das nicht auch für Tests gelten?

Nein, da ist für mich die Grenze erreicht. Wer Testautomatisierung pauschal in Frage stellt, der widerspricht grundlegenden Prinzipien. Und dazu sagten schon die Lateiner: contra principia negantem disputari non potest (Mit dem, der die gemeinsamen Prinzipien leugnet, ist nicht diskutieren.) Den Diskussionsaufwand spare ich mir dann.

Also: Wenn die Frage sachlich gestellt ist und nicht als Rationalisierung, d.h. als Abwehrmaßnahme dient, wenn ernstlicher Erkenntnisgewinn gewollt ist und die grundsätzliche Möglichkeit der Beiträge von automatisierten Tests/TDD zu Produktionseffizienz, Korrektheit und Evolvierbarkeit anerkannt sind… dann lässt sich auch eine Antwort für jedes Projekt finden. Und die lautet nicht immer “Ja! TDD musst du machen auf Teufel komm raus.”

Jedes Projekt hat sein Mix an automatisierten Tests wie Akzeptanztests, Integrationstests, Performancetests, Lasttests, Unit Tests. Und ob die nach TDD oder anders geschrieben sind… das ist durchaus auch immer wieder anders.

Nur eines halte ich für unverbrüchlich: Den Willen zur Automatisierung. Denn alles, was automatisiert getan wird, das ist quasi kostenlos wiederholbar, das ist nachvollziehbar, das ist von keinem bestimmten Teammitglied abhängig, das ist schneller als von Hand getan. Wer wollte diese Vorteile der Automatisierung ausschlagen?