Follow my new blog

Dienstag, 7. September 2010

Abhängigkeiten bewusster wahrnehmen

Abhängigkeiten sind eine der größten Geißeln der Softwareentwicklung. Wenn es ein allgemein anerkanntes Prinzip gibt, dann ist es, Abhängigkeiten zu minimieren. Jede Hilfe ist da willkommen. Was können Sie also tun, um Abhängigkeiten im Code los zu werden?
Für die Suche nach einer Lösung und die Beurteilung von Hilfsangeboten ist es nützlich, Abhängigkeiten zu kategorisieren. Nicht alle Abhängigkeiten sind gleich. Worüber reden wir also eigentlich?

Abhängigkeit

Eine Abhängigkeit ist vorhanden, wenn einer etwas braucht, ohne dass er seine Arbeit nicht verrichten kann. Eine Funktionseinheit kann eine andere Funktionseinheit brauchen; oder sie braucht nur eine Datei an einem bestimmten Ort. Oder sie ist von einem bestimmten Format von Daten abhängig, die als Zeichenkette an sie übergeben werden.
Abhängigkeiten lauern also überall.
Mir geht es allerdings vor allem um Abhängigkeiten zwischen den grundlegenden Funktionseinheiten Assembly, Klasse und Methode.

Statische Abhängigkeit

Wenn eine Funktionseinheit (FE) schon zur Compilezeit von einer anderen abhängig ist, dann nennt man das statische Abhängigkeit oder auch statische Kopplung. Hier ein Beispiel: Klasse Client ist von Klasse Service statisch abhängig.

   1: class Client
   2: {
   3:     private Service s;
   4: }
   5:  
   6: class Service
   7: {}

Oder hier eine Methode, die mit einer anderen statisch gekoppelt ist:



   1: void Foo()
   2: {
   3:     Bar();
   4: }
   5:  
   6: void Bar()
   7: {}

Oder hier eine statische Abhängigkeit von Client nicht zur Klasse Service, sondern auch noch zu einem Member von Service:



   1: class Client
   2: {
   3:     private Service s; 
   4:     
   5:     void Foo()
   6:     {
   7:         s.Text = "hello";
   8:     }
   9: }
  10:  
  11: class Service
  12: {
  13:     public string Text;
  14: }


Oder hier statische Kopplung zwischen Assemblies:

image

Ohne, dass die Funktionseinheiten, zu denen eine Kopplung besteht, bei der Compilation vorhanden sind, ist keine fehlerfreie Übersetzung möglich. Das macht statische Abhängigkeiten vergleichsweise zahm. Es fällt einfach schnell auf, ob eine Abhängigkeit nicht erfüllt ist.

Ted Faison hat in seinem Buch “Event-based Programming” für Abhängigkeiten eine Notation eingeführt, die ich hier auch benutzen möchte. Abhängigkeiten bezeichnet er dabei so:

image

Der Pfeil zeigt vom Abhängigen zum Unabhängigen und das Symbol (Zeichen Ou des erweiterten Lateinischen Alphabets) macht klar, dass der Pfeil ein Abhängigkeitspfeil ist.


Eine statische Abhängigkeit kann dann so qualifiziert werden:

image 


Dynamische Abhängigkeit


Dynamisch ist eine Abhängigkeit, wenn erst zur Laufzeit die eigentliche unabhängige Funktionseinheit verfügbar ist; der abhängige Code kann zur Compilezeit dann nämlich nur Annahmen darüber treffen, wie das zur Laufzeit “nachgereichte” Unabhängige aussieht.

Ein typsiches Beispiel sind Abhängigkeiten von Klassen, die zur Compilezeit durch Interfaces vertreten werden:

   1: class Client
   2: {
   3:     public IService s; 
   4:     
   5:     void Foo()
   6:     {
   7:         s.Text = "hello";
   8:     }
   9: }
  10:  
  11: interface IService
  12: {
  13:     string Text { get; set; }
  14: }
  15:  
  16: class Service : IService
  17: {
  18:     public string Text
  19:     {
  20:         get { ...  } 
  21:         set { ... }
  22:     }
  23: }

Hier ist die Klasse Client von der Klasse Service dynamisch abhängig. Vom Interface IService und dessen Property Text hingegen ist sie statisch abhängig.

image

Das macht klar, wie Komponentenorientierung funktioniert: Die Assembly, die Client implementiert, muss die referenzieren, die IService implementiert – aber nicht die Assembly von Service. Client und Service können also getrennt von einander entwickelt werden. Aber die IService-Assembly, der Kontrakt von Service,muss vorher da sein, weil die Client-Assembly wie die Service-Assembly daran statisch gekoppelt sind.

Statische Abhängigkeiten erfordern mithin Colocation von Code in derselben Assembly oder zumindest, dass Assemblies einander statisch referenzieren. Die unabhängige Funktionseinheit muss dann vor der abhängigen implementiert werden.

Ein typisches Muster, um diese Form der Abhängigkeitskonstellation zwischen Klassen und Interfaces auszudrücken, ist die Inversion of Control mit der ctor-Injection:

   1: class Client
   2:     {
   3:         private IService s; 
   4:         public Client(IService s)
   5:         {
   6:             this.s = s;
   7:         } 
   8:         
   9:         void Foo()
  10:         {
  11:             s.Text = "hello";
  12:         }
  13:     }
  14:  
  15:     class Program
  16:     {
  17:         static void Main(string[] args)
  18:         {
  19:             Client c = new Client(new Service());
  20:         }
  21:     }

So werden Klassen entkoppelt, um einfacher testbar zu werden, um Implementationen austauschen zu können und um produktiver bei der Entwicklung zu sein.

Dynamische Kopplung ist loser als statische – das ist vorteilhaft. Allerdings werden Fehler erst zur Laufzeit sichtbar. Die Entscheidung zwischen statischen und dynamischen Abhängigkeiten ist also eine zwischen Flexibilität/Entkopplung und Gewissheit.

Wer flexibel sein will, der setzt z.B. die neue dynamische Typisierung ein:

   1: class Client
   2: {
   3:     public dynamic s; 
   4:     
   5:     public void Foo()
   6:     {
   7:         s.Text = "hello";
   8:     }
   9: }

Zur Entwicklungszeit muss man sich dann nicht entscheiden, wie die Instanzen von s aussehen sollen. Sie müssen lediglich eine Property oder ein Feld Text bieten.

Das funktioniert dann gut mit dem bisherigen Service:

   1: static void Main(string[] args)
   2: {
   3:     Client c = new Client(); 
   4:     c.s = new Service(); 
   5:     c.Foo();
   6: }

Ohne Kontrolle durch den Compiler kann aber auch jeder andere Typ zugewiesen werden:

   1: static void Main(string[] args)
   2: {
   3:     Client c = new Client(); 
   4:     c.s = 42; 
   5:     c.Foo();
   6: }

Und das funktioniert dann gar nicht mehr gut.

Also: Vorsicht mit dynamischen Abhängigkeiten. Flexibilität, Entkopplung hat ihren Preis. Dynamische Abhängigkeiten lassen sich schlechter kontrollieren als statische. Sollte sich am Unabhängigen bzw. bei der Bedienung einer Abhängigkeit etwas ändern, ist erst viel später klar, auf welche Abhängigen das eine Auswirkung hat.


Logische Abhängigkeit


Der Compiler meldet kein Problem und Sie haben auch Ihre dynamischen Abhängigkeiten im Griff? Das ist gut – aber Ihre Software ist dann noch nicht in trockenen Tüchern, was die Abhängigkeiten angeht. Denn da sind noch die logischen Abhängigkeiten. Und das sind die fiesesten.

Hier eine kleine Denksportaufgabe. Welche Abhängigkeiten enthält diese Funktion, die die Nachkommastellen einer Zahl abtrennen soll:

   1: static double Trunc(double n)
   2: {
   3:     var s = n.ToString(); 
   4:     s = s.Substring(0, s.IndexOf(',')); 
   5:     return double.Parse(s);
   6: }

Gibt es statische Abhängigkeiten? Klar. Der Code ist ja streng typisiert.

Gibt es dynamische Abhängigkeiten? Nein. Hier wird zur Laufzeit nichts konkretisiert, nachgereicht.

Dennoch gibt es im Code eine Abhängigkeit. Und ob die Erwartung daran erfüllt wird, zeigt sich auch erst zur Laufzeit.

Trunc() ist davon abhängig, dass die String-Repräsentation  einer double-Zahl ein Komma enthält, das die Nachkommastellen abtrennt. Zahlen müssen diesem Format folgen:

Zahl ::= Vorkommastellen [ “,” Nachkommastellen ].

Vorkommastellen, Nachkommastellen ::= Ziffer { Ziffer }.
Das ist eine legitime Annahme für eine Software, die nur in Deutschland laufen soll – für eine internationale Software hingegen sollte sie nicht getroffen werden.

Eine solche Abhängigkeit nennt man logische Abhängigkeit. Sie besteht immer dann, wenn zwei Funktionseinheiten Annahmen übereinander machen, insbesondere Annahmen über ihre Funktionsweise. Deshalb drücken sich logische Abhängigkeiten oft in Daten aus, da die das Bindeglied zwischen Funktionseinheiten sind.

Hier besteht die Annahme der Funktionseinheit Trunc() darin, dass die Funktionseinheit double.ToString() ein Komma vor die Nachkommastellen setzt.

image

Als Kontrast ein Beispiel logischer Unabhängigkeit:

   1: static int GetIndexOf(string item, string[] list)
   2: {
   3:     for (var i = 0; i < list.Length; i++)            
   4:         if (list[i] == item) return i; 
   5:     return -1;
   6: }

Die Funktion ist nicht (!) abhängig davon, dass die Einträge in der Liste in einer bestimmten Reihenfolge stehen. Sie bestimmt mit und ohne Ordnung der Listenelemente den Index des gesuchten korrekt. So ist diese Funktion logisch unabhängig von potenziellen Quellen für Listen.

Keine Annahme über die Reihenfolge der Einträge zu machen, dient also der Entkopplung. Das ist gut für die Evolvierbarkeit (oder auch Wiederverwendbarkeit), hat jedoch seinen Preis. Die Performance dieses Verfahrens ist nicht optimal. Ob das allerdings schlimm ist… das hängt vom Verwendungszusammenhang ab. Im Sinne der Prinzipien KISS und Beware-of-Premature-Optimization (BoPO) mag es sinnvoll sein, keine Annahmen zu machen und die lose Kopplung einzustreichen – bis das an eine Grenze stößt.

Logische Abhängigkeiten machen die Softwareentwicklung wahrhaft komplex. Denn erstens wird erst zur Laufzeit sichtbar, ob sie erfüllt werden. Und zweitens führt ihre Nichterfüllung nicht immer und sofort zu einem Fehler.

Trunc() kann während der Entwicklung alle automatisierten Tests bestehen. Software, die die Funktion einsetzt, kann bei Hunderten Kunden fehlerfrei laufen. Doch dann, eines Tages, kommt es zu einem Fehler. Warum? Weil ein Anwender erstmalig auf die Idee gekommen ist, die Software auf einem Rechner mit anderer default Einstellung für das Dezimaltrennzeichen laufen zu lassen.

Logische Abhängigkeiten können sehr subtil sein. Kein Compiler zeigt sie an. Kein Laufzeitsystem deckt sie auf. Womöglich werden sie nur sporadisch nicht erfüllt.

Vorsicht also an den Schnittstellen zwischen Funktionseinheiten. Dort lauern immer wieder Annahmen über empfangene Daten, die die Funktionseinheiten mehr oder weniger offensichtlich und oft unerwartet stark miteinander logisch koppeln.


Zusammenschau


Abhängigkeiten machen das Softwareentwicklerleben schwer. Denn wo Abhängigkeiten bestehen, besteht immer die Gefahr, dass sich Änderungen am Unabhängigen auf Abhängige auswirken.

Sind die Abhängigkeiten nur statisch, dann zeigt ein Übersetzungslauf an, wo Erwartungen bei Abhängigen enttäuscht werden. Insofern sind statische Abhängigkeiten unkritisch, was die Entdeckung von Nichterfüllung angeht. Diese Effizienz, diese Sicherheit wird jedoch durch Inflexibilität erkauft. Statische Abhängigkeit bedeutet immer enge Kopplung.

Dynamische Abhängigkeiten koppeln loser – lassen sich jedoch erst zur Laufzeit entdecken. Durch die heutzutage sehr einfach mögliche Automatisierung von Tests lassen sich Probleme bei der Erfüllung dynamischer Abhängigkeiten jedoch auch fast so schnell erkennen wie bei statischen Abhängigkeiten.

Programme geschrieben in dynamischen Sprachen wie Python oder Ruby leiden daher auch nicht unter größerer Fehlerhäufigkeit als Programme geschrieben in einer streng typisierten Sprache wie C#. Wer mit einer dynamischen Sprache entwickelt, stützt sich einfach nur weniger auf den Compiler und setzt stattdessen mehr auf eine Sammlung von automatisierten Tests.

Die Komponentenorientierung hilft ebenfalls weiter, da sie dynamische Abhängigkeiten bewusst plant und Kontrakte auf beiden Seiten der Abhängigkeit für Stabilität sorgen. So kann lose Kopplung als Vorteil dynamischer Abhängigkeit eingestrichen werden, ohne die Sicherheit statischer Kopplung aufzugeben.

Logische Abhängigkeiten teilen mit den dynamischen Abhängigkeiten, dass sie erst zur Laufzeit festgestellt werden können. Sie gehen in ihrer Gefährlichkeit jedoch darüber hinaus. Sie sind oft unsichtbar, sie sind oft subtil, sie machen womöglich nur sporadisch Probleme. Es hilft aber nichts: Wir müssen mit ihnen leben.

Ohne logische Abhängigkeiten keine Zusammenarbeit. Wer sich nicht mindestens logische abhängig macht, steht allein.

Bei Planung, Test und Review Ihres Codes achten Sie daher besonders auf logische Abhängigkeiten. Wo Sie sie erkennen, dokumentieren Sie sie. Am besten mit einem Test. Beispiel:

   1: [Test]
   2: public void GetIndexOf_does_not_require_the_list_to_be_sorted()
   3: {
   4:     Assert.AreEqual(2, 
   5:                     Helpers.GetIndexOf("x", 
   6:                                        new[] { "k", "f", "x", "s" }));
   7: }

Das kann ein Unit Test wie hier sein. Das kann aber auch ein Integrationstest sein, der live die Funktionseinheiten, die Annahmen übereinander machen, zusammenspielen lässt.

.NET 4 Code Contracts könnten auch helfen, um logische Abhängigkeiten zu dokumentieren.

In jedem Fall gilt jedoch: zentralisieren Sie logische Abhängigkeiten. Lassen Sie möglichst nur eine Funktionseinheit in einer bestimmten Hinsicht logisch von einer anderen abhängig sein.

Ein Beispiel dafür ist ein Proxy in der verteilten Kommunikation. Der zentralisiert die logische Abhängigkeit zwischen Client und Server in Bezug auf das Datenformat auf der Leitung. Der Client nimmt z.B. an, dass der Server SOAP-Nachrichten versteht. Statt nun aber diese Annahme an vielen Stellen im Code zu treffen, zentralisiert man sie im Proxy. Sollte sich das Datenformat ändern, trifft die Annahme also nicht mehr zu, dann ist nur eine Funktionseinheit betroffen.

Der Proxy ist sozusagen dafür zuständig, eine logische Abhängigkeit in eine statische zu verwandeln. Denn wo der Proxy von SOAP-Nachrichten logisch abhängt, da hängt Code, der ihn nutzt, z.B. nur von einem POCO ab, das der Proxy umwandelt in einen Teil einer SOAP-Nachricht.

Seien Sie also wachsam, was die Abhängigkeiten in Ihrer Software angeht. Unterscheiden Sie zwischen statischen, dynamischen und logischen. Werden Sie sich der Abhängigkeiten in Ihrem Code bewusst. Wählen Sie die eine oder andere Form mit Bedacht. Prüfen Sie die Erfüllung von Abhängigkeiten automatisiert. Dann sind Sie auf einem guten Weg, die Komplexität Ihrer Software zu verringern.

Sonntag, 5. September 2010

Lesen heute für Softwareentwickler

Neulich wurde ich gefragt, ob das CCD-Wiki es ernst meine mit der Empfehlung, 6 Fachbücher pro Jahr zu lesen. Das sei doch wohl etwas viel verlangt.

image Hm… ist das wirklich viel, ja, zuviel verlangt von geplagten Softwareentwicklern? 6 Fachbücher lesen pro Jahr, also alle 2 Monate ein anderes. Oder wenn wir mal 400 Seiten pro Fachbuch annehmen, knapp 7 Seiten pro Tag lesen. Jahrein, jahraus… Das würde bei 2 Minuten Lesezeit, oder sagen wir 3, damit das Lesen gründlich ist, pro Tag 21 Minuten kosten, also immerhin 1,5% der Tageszeit und wackere 2,5% der Wachzeit.

Tja, wie sieht es aus? Sind 21 Minuten Fachbuchlektüre pro Werk-, Sonn- und Feiertag jahrein, jahraus zuviel verlangt? Dazu kommen ja noch andere Quellen, die wir auch empfehlen zu lesen. Blogartikel, Zeitschriftenartikel, Twitternachrichten… Wenn wir da mal annehmen, CCD würde empfehlen, pro Woche – horribile dictu! – auch noch 4 Fachartikel zu lesen… - Moment, ich rechne, 4 * 5 Seiten à 3 Minuten = 60 Minuten pro Woche oder knapp 9 Minuten pro Tag… - Wir wären am Ende bei 21 + 9 = 30 Minuten pro Wochentag jahrein, jahraus… Puh… Ist das nicht ein bisschen viel?

Das ist die Frage: Sind 6 Fachbücher und 208 Fachartikel pro Jahr zuviel für einen Softwareentwickler? Setzt das sein Zeitbudget unter inakzeptablen Stress? Bringt das seine Aufnahmefähigkeit an ihre Grenzen? 3% der Wachzeit hingegeben an Fachlektüre?

Meine persönliche kurze Antwort: Nein. Das ist natürlich nicht zuviel.

Das kostet weder zuviel Zeit, noch sollte es einen erwachsenen Menschen an sein Aufnahmelimit führen. Oder wenn, dann erwäge man ein Umsatteln auf den ehrenwerten Beruf des Bäckereifachverkäufers.

Und jetzt die lange Antwort:

Verantwortung

30 Minuten Fachlektüre pro Tag dürfen nicht zuviel sein. Denn jede Minute Fachlektüre ist eine Investition in die Erhaltung und sogar die Verbesserung der Vermittlungsfähigkeit. Angesichts von Entity Framework, Code Contracts, TPL, WF, Scrum, WPF, dynamische Typen, Rx, Fit, EBC, WCF, TDD, AppSpace, Complex Event Processing, Kanban und was der Technologien/Konzepte/Methoden noch mehr sein mag, angesichts dieses Sperrfeuers konstanter Neuerungen und Veränderungen sind 30 Minuten täglich ein Tropfen auf den heißen Stein, um auch nur annähernd uptodate zu bleiben.

Muss man denn aber uptodate bleiben? Ja, aus zwei Gründen:

Erstens sollte es jedem am Herzen liegen, seine Kompetenz hoch zu halten. Denn hohe Kompetenz bedeutet Freiheit. Wer kompetent ist, kann sich im Notfall oder auch ohne Not den Job aussuchen. Wer mit veraltetem Wissen auf seinem Sessel sitzt, der hat diese Freiheit nicht.

Das ist wie mit alten Leuten, die stolpern. Warum brechen die sich so leicht etwas? Weil sie keine “Freiheit” haben. Sie sind nicht mehr reaktionsschnell, sie haben keine elastischen Knochen mehr und sie haben wenig Kraft. Das Resultat ist ein ungebremster Fall, der direkt auf einen spröden Knochen trifft.

Dasselbe gilt für “eingerostete” Kompetenz. Statt eines gebrochenen Knochens, der in einigen Wochen wieder heilt, ist das Ergebnis hier jedoch schlimmer, weil es viel länger anhält. Die Unsicherheit nimmt zu, die Motivation nimmt ab, alles dauert länger, die Zahl der Konflikte mit anderen steigt, Veränderungen werden schwieriger. Bottom line: “Eingerostete” Kompetenz erzeugt Stress. Garantiert.

Zweitens ist die Erhaltung bzw. Vergrößerung eine Frage des Verantwortungsgefühls gegenüber Kunde und Arbeitgeber. Bezahlt wird für gute Arbeit. Und das bedeutet nicht nur, dass die Software irgendwie funktioniert. Das bedeutet auch, dass sie auf der Höhe der Zeit ist. Oder zumindest, dass der Entwickler weiß, was state-of-the-art ist, wenn er sich gegen eine Lösung auf der Höhe der Zeit entscheidet. Für etwas anderes würde ich als Kunde oder Arbeitgeber jedenfalls kein Geld ausgeben. Und ich denke, denselben Anspruch stellen Sie an einen Bauingenieur, Architekten, Elektrotechniker oder Landwirt.

Wer sich also nicht fit hält, der erfüllt den Anspruch an ihn nicht. 30 Minuten Lektüre pro Tag – die selbstverständlich in der Arbeitszeit liegen – sollten also nicht auf Widersprich stoßen.

Anspruch

Zähneknirschend mögen Sie sich nun in die Lektür schicken. Bei allem Verantwortungsgefühl erscheinen Ihnen die knapp 3400 Seiten pro Jahr “Zwangslesen” aber immer noch enorm.

Dazu kann ich nur sagen: Sehen Sie das Lesen nicht so eng.

image Sie mögen noch “Großvaters Anspruch” ans Lesen im Kopf haben. Zu dem gehört, dass jedes Buch ganz gelesen werden muss, dass man ein Buch zur Zeit liest, dass man ein Buch fertig liest und erst dann das nächste anfängt.

Vergessen Sie diesen Anspruch. Er gehört wie Großvater einer vergangenen Epoche an. Er ist entstanden in einer Mangelsituation. Früher gab es einfach nicht soviel zu lesen. Und Lesestoff war vergleichsweise teuer.

Heute haben wir Fachlektüre im Überfluss und zu kleinen Preisen. (6 * 35 EUR = 210 EUR/Jahr für Fachbücher – wenn Sie die denn selbst bezahlen müssen – halte ich für nicht viel Geld angesichts dessen, was auf dem Spiel steht: ihr Gehalt bzw. ihre Gehaltserhöhung. Kosten für Artikel setze ich mal gar nicht an. Davon gibt es soviele kostenlos im Internet; aber selbst wenn Sie noch das eine oder andere Zeitschriftenabo haben sollten, ändert sich die Größenordnung der Fachlektüreausgaben nicht.) Also können und sollen wir anders mit ihr umgehen.

Hier einige Tipps, nach denen ich lese:

  • Lesen Sie, was Sie interessiert. Versuchen Sie nicht, überall uptodate zu sein. Fokussieren Sie sich besonders auf 2-3 Schwerpunkte, die Sie besonders mögen. Aber lesen Sie auch sonst, was Ihr Interesse weckt.
    Und lassen Sie soweit es geht aus, was Sie langweilt. Denn nur bei hoher Lesemotivation nützt das Lesen etwas. Außerdem geht es dann schneller.
    Glauben Sie übrigens nicht, dass Sie auf diese selektive Leseweise etwas Wichtiges verpassen. Was wirklich, wirklich wichtig ist, kommt wieder und drängt sich früher oder später auch in Ihren Interessenshorizont.
  • Lesen Sie soweit es Sie interessiert, soweit Sie mitkommen. Zwingen Sie sich nicht, einen Artikel oder ein Buch zuende zu lesen. Geben Sie dem Text eine Chance, halten Sie durchaus auch einen Moment aus, wenn es mal zäh wird – aber prügeln Sie sich nichts rein. Brechen Sie also Lektüre guten Gewissens ab. Vielleicht kommen Sie später wieder zurück und lesen weiter. Vielleicht aber auch nicht. Manche Themen müssen sich in Ihnen erst entwickeln. “Ist der Schüler bereit, kommt der Lehrer” heißt es.
  • Lesen Sie quer. Springen Sie im Text. Verschaffen Sie sich einen Überblick (Inhaltsverzeichnis, Bilder, Überschriften) und lesen Sie dann, was Sie anzieht. Lesen Sie in der Mitte oder am Ende. Oder von allem ein bisschen.
  • Lesen Sie mehrere Fachbücher parallel. 50 Seiten in einem, dann 100 Seiten im anderen, dann wieder 70 Seiten im ersten, 150 Seiten in einem Dritten. Wie es Ihnen Spaß macht. Die Lektüre kann sich gegenseitig befruchten. Und Sie gewinnen Inkubationszeit: Nach 50 Seiten im ersten Buch mögen Sie erstmal genug haben. Das Thema muss sich bei Ihnen setzen. Schieben Sie es in den Hinterkopf und fangen Sie etwas Neues an. Später hat sich das erste Thema in Ihnen weiterentwickelt, dann lesen Sie ab Seite 51 weiter.
  • Verfolgen Sie viele Quellen (Blogs, Zeitschriften, Fachbücher), aber lesen Sie nicht alles. Vertrauen Sie auf Ihr Unterbewusstsein, dass es aus dem Informationsstrom für Sie Relevantes hervorhebt. Lassen Sie sich von Themen “anspringen”. Blättern Sie durch und verweilen Sie, wo es Sie hält.
  • Haben Sie nicht den Anspruch, alles, was Sie lesen, auch auszuprobieren. Tun Sie das, wenn es Ihnen wirklich wichtig erscheint oder Spaß verheißt. Ansonsten lassen Sie es sein und beobachten ggf. das Thema weiter.

Wenn Sie sich ein Thema konkret “draufschaffen” wollen, müssen Sie Ihren Modus natürlich etwas verändern. Um jedoch uptodate zu bleiben, reicht der hinter diesen Tipps stehende Anspruch: Lesen Sie selektiv. Hoffen Sie nicht auf das eine Buch, das es bringt. Surfen Sie die Welle des Überflusses.

Texte durcharbeiten, sich an ihnen abarbeiten, sie bis ins Letzte zu verstehen… das war gestern. (Nein, das kann natürlich auch heute noch sein. Aber solche Texte sind relativ selten. Setzen Sie stattdessen darauf, dass am Ende mehrere Texte zu einem Thema Ihnen den Durchblick mit weniger Mühe verschaffen, den Sie sich früher mit dem “Studium” eines Textes hätten mühsam erarbeiten müssen. Sie haben heute den Vorteil, Themen von vielen Autoren beleuchtet sehen zu können, wo früher der Quellenmangel sie auf einen festgenagelt hat.)

Was denken Sie nun? Ist die CCD-Forderung von 6 Fachbüchern pro Jahr wirklich so gewaltig? Oder sah sie nur so groß aus, weil Sie sie im Lichte eines veralteten Leseanspruchs verstanden haben?

Ich hoffe, Sie können nun entspannt(er) denken: “Alles halb so schlimm.” Und vor allem: “Das Lesen bringt mich weiter. Ich übernehme damit Verantwortung für meine Kompetenz. Es macht mich zu einem besseren Softwareentwickler. Und besser zu werden, das macht auch Spaß.”

Freitag, 27. August 2010

Die Weite des Merkmalhorizonts

Golo will die räumliche Nähe für Teammitglieder nicht weiter überschätzen, Ilker will sie nicht unterschätzen. Mit seinem “Gemeinsam für die gemeinsame Sache” drückt er sogar aus, dass bei räumlicher Distanz keine Gemeinsamkeit mehr ent-/bestehen könne.

Ja, was denn nun? Golos Fahne folgen in das bisher weniger erforschte Land verteilter Teams oder eher in der bisherigen Reisegruppe der Colocated Developers bleiben und Ilkers Regenschirm folgen?

Wie schon hier ausgeführt, plädiere ich für eine “werteorientierte” Diskussion. Niemand sollte sich also aus Sympathie oder Antipathie für den einen oder andere Proponenten in diese oder jene Richtung wünsche. Ohne Zorn und Eifer ist vielmehr zu entscheiden, wie für den Kunden und (!) – das ist mir wichtig – die einzelnen Projektbeteiligten eine möglichst große Befriedigung von Bedürfnissen erreicht werden kann.

Ist jetzt nicht aber schon alles zum Thema gesagt? Nein, mein Gefühl ist, dass die Diskussion nicht wirklich weiter kommt. Sie verharrt in der Emotionalität. Da helfen auch nicht die wiederholten Verweise auf die Autoritäten oder die eigenen Erfahrungen. Denn ohne Wertesystem, ohne Beurteilungssystem, bleiben Autoritäten und eigene Erfahrungen im anekdotischen. Denn wir sollten uns im Klaren sein, dass die Diskussion von keiner Seite wissenschaftlich geführt wird. Experimente, die das eine oder das andere belegen und dann auch noch erklären, warum das so ist, sehe ich nämlich nicht. Dazu braucht es nämlich mehr als persönliche Erfahrungen der Art “Dort haben wir verteilt gearbeitet und es war scheiße. Heute arbeite ich colocated und alles ist gut.”

Wie kommen wir also weiter? Ich würde sagen, wie sollten einfach auf jedes Plädoyer verzichten. Es geht nicht um ein endgültiges Urteil, ob Nähe oder Distanz einfürallemal besser sei. Die Welt ist leider nicht so einfach, dass wir an Colocation (oder “versprengte Teams”) einen Haken machen können. Neue Situationen erfordern immer wieder neue Beurteilungen. Und selbst eingefahrene Situationen können von einer Neubeurteilung profitieren.

Die Frage “Wenn es im Team hakt, könnte das an der Colocation liegen?” ist genauso berechtigt wie “Wenn es im Team hakt, könnte das daran liegen, dass es versprengt ist?”

Um das aber denken zu können, muss man einen weiten Horizont haben; man muss sich über die Natur von Qualität Gedanken machen. Hier eine Analogie:

image Wenn Software allein auf einem Prozessor läuft, ist sie am schnellsten. Multithreading macht Software also ga-ran-tiert langsamer.

Ist Multithreading deshalb nicht per se schlechter als Singlethreading?

Wie kann es da jedoch sein, dass Windows und Mac OS und Linux und Unix Multithreading-Betriebssysteme sind?

Die Antwort ist ganz einfach: Weil es kein so einfaches, pauschales "schlechter" gibt. Gut und schlecht sind Ausdrücke für Qualität. Qualität ist die Ausprägung eines Merkmals im Hinblick auf eine Anforderung. Performance ist aber nur ein Merkmal von vielen für Software. Responsiveness, Usability, Scalability usw. sind andere Merkmale. 

Multithreading kann daher sehr wohl Sinn ergeben, wenn man nicht nur starr auf das Merkmal Performance blickt, sondern auch andere in den Blick nimmt. Das tun Betriebssysteme. Sie nehmen geringere als optimale Performance für ein Programm in Kauf, um dem User z.B. auch noch Responsiveness zu bieten.

Die Qualität eines nicht trivialen Produktes insgesamt ist somit immer die Summe der Qualitäten vieler Merkmale. Selbst beim Kauf eines Mixers haben Sie bestimmt einen weiteren Merkmalhorizont als den Preis. Sie werden auch noch Anforderungen an die Merkmale Lautstärke, Umdrehungszahl, Fassungsvermögen, Usability usw. haben.

Als mündiger Konsument werden Sie deshalb…

  1. Ihren Merkmalhorizont bestimmen
  2. Für jedes Merkmal Ihre Anforderung spezifizieren
  3. Den ersten Mixer kaufen, der Ihren Anforderungen entspricht

Das ist ein effektives wie effizientes Vorgehen – und besonders befriedigend in Situationen mit unübersichtlichem Angebot.

Nicht den billigsten Mixer zu kaufen, kann sinnvoll sein. Software nicht mit Singlethreading zu betreiben, kann sinnvoll sein. Und ein Team nicht in einem Raum “zu betreiben”, kann sinnvoll sein.

Ob und wann es sinnvoll ist, ein Team zu verteilen, das hängt davon ab, wie Ihre Anforderungen sind. Nicht Golo, nicht Ilker, nicht Martin Fowler bestimmen das, sondern allein Sie.

Lassen Sie sich nicht von emotionalen Appellen beeindrucken. Anekdotische Berichte sind gut, wenn Sie sie für das nehmen, was sie sind: persönliche Erfahrungen in ganz bestimmten Kontexten. Nicht mehr, nicht weniger.

Ihre Situation ist aber anders. Sie mag der von Golo oder Ilker ähneln. Letztlich ist sie jedoch immer anders. Verantwortlich handeln Sie daher nur, wenn Sie die Merkmale kennen, die effektive und effiziente Softwareentwicklung ausmachen und zu jedem Ihre Anforderungen formulieren. Und dann müssen Sie ganz bewusst bestimmen, inwiefern Colocation oder Verteilung in welcher Form diesen Anforderungen genügen.

Mit welchem Arbeitsmodus maximieren Sie die Gesamtqualität der Softwareentwicklung?

Sehen Sie diese Diskussion daher als Horizonterweiterung an. Sie will einen Impuls geben. Seien Sie nicht einfach so mit Ihrer bisherigen default Entscheidungen für Colocation zufrieden, die womöglich keine war, weil Sie sich nie Gedanken über Alternativen gemacht haben. Hinterfragen Sie sie. Es besteht die Chance, dass ein anderer Arbeitsmodus Vorteile hat. Geben Sie der Möglichkeit, dass Motivation und Kompetenz durch Verteilung steigen könnten, eine Chance. Mehr hat Golo nicht gewollt. Mehr will ich auch nicht.

Hinterfragen Sie nicht nur immer das Neue kritisch. “Kann die neue Technologie XYZ beweisen, ob sie wirklich Vorteile hat?”

Hinterfragen Sie auch das Etablierte. “Was verschenken wir, wenn wir weiterhin auf Colocation und 9-15h Kernarbeitszeit von Mo-Fr bestehen?” Das ist eine legitime Frage. Das ist eine Frage, die sich jeder bewusste Manager stellen sollte.

Und wenn man dann vor einem weiten Horizont an Qualitätsmerkmalen die Situation und die Alternative beleuchtet hat und entscheidet, alles bleibt beim Alten… dann ist es auch ok.

Und jetzt hoffentlich Schluss mit der Diskussion. Machen Sie etwas draus. Allerdings: “Unschuldig” sind Sie nun nicht mehr. Die alternativlose Ruhe ist vorbei. Sie können nicht mehr behaupten, Sie hätten nichts davon gewusst, dass es auch anders erfolgreich (oder gar erfolgreicher) geht. Es gibt mehr als Singlethreading. Es gibt mehr als Colocation.

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.