Follow my new blog

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.

Kommentare:

Dirk Rodermund hat gesagt…

Ralf, danke, Du sprichst mir aus dem Herzen!
Schon ein Weile habe ich bei mir selbst die Tendenz erkannt eher ein - wie Du so schön sagst - "fokusiertes" Datenmodell zu bevorzugen. Nicht nur um den CCD Prinzipien zu folgen, sonder vor allem, weil es sich für mich deutlich besser anfühlt ;-)

Anonym hat gesagt…

Hallo Ralf,
ich sehe Domänenmodelle gemischt.

(1) Ich habe es selbst erlebt, wie schmerzhaft solche kombinierten Modelle aus Operationen und Datenhaltungen sein kann. Das schlimmste Beispiel war ein Klasse Product, die auch Preisermittlung für einen Online Shop gemacht hat. Da gab es Preismatrize, Einzelrabatte, Rabattkarten, Kundengruppen mit Sonderrabatte, Preise für Firmen und Endkunden und wahrscheinlich ist mittlerweile noch mehr dazu gekommen. Das ist der Super GAU. Kaum testbar, da die Beziehungen unklar sind (um ein Endkunden-Preis mit einer Preismatrix zu berechnen muss Bedingungen A-Z erfüllt sein), versteckte Abhängigkeiten und unnötige Abhängigkeiten und eine schlechte Darstellung von Businessoperationen (Workflows wären in meinem Beispiel viel deutlicher gewesen).

(2) Allerdings waren diese Domänenobjekte auch wirklich leicht zu verwenden.
a) Man musste sich nicht um irgendwelche Services Gedanken machen.
b) Abhängigkeiten und benötigte Komponenten müssen nicht zusammengesucht werden.
c) Einfache Methoden berechnen mir den Preis ohne Hintergrund-Informationen zu benötigen.

Diese Klassen waren für die Darstellung und zum Teil auch Workflows schon extrem hilfreich. Ich würde mir also einen gesunden Mix wünschen, das heißt:
a) Seperate Datenobjekte (auch z.T. direkt generiert)
b) Seperate Operationen über Komponenten, Services, Workflows.
c) Eine einfache API (müssen keine Domänenobjekte sein) für Schnittstellen (Darstellung, Services, Importer, Exporter...)

Christian / PolarKreis hat gesagt…

Jupp, kann mich nur Dirk anschließen.

Erfreulicherweise sind wir hier im Kundenprojekt bereits im Januar zu diesem Schluß gekommen, haben aber durch Deinen Blogartikel nochmals Argumente bekommen. Großartig!

cu
Christian

Ralf Westphal - One Man Think Tank hat gesagt…

Wer übrigens ein Szenario hat, zu dem er mal eine Modellierung sehen möchte, melde sich.

Ich kann mir auch was ausdenken - aber das ist dann vielleicht zu "künstlich".

Also: Ich bin auf der Suche nach überschaubaren Problemszenarien aus eurer Praxis. Die Anforderungen sollten auf 1 DIN A4 Seiten vollständig passen. Eine Formulierung als User Stories sollte ausreichen. (Und, naja, dahinter darf dann keine AS400 als Backend vorausgesetzt werden ;-)

Wer hätte da was?

Sebastian Jancke hat gesagt…

Ralf,

"Customer muss sich aber auch verändern, wenn sich die Daten nicht verändern."

hast du nicht zuletzt gegen Änderungsgründe als Indikator für SRP-Verletzungen argumentiert?

Ich teile deine Sicht auf das Modell von Nilsson. Für micht besteht das grundlegende Problem darin, dass diese Art Modell "zwei Herren dienen muss": Der GUI und der Domänenlogik. Trennt man Modelle für beide Zwecke, kommt man nach meinem Verständnis zu schmerzfreieren Domänen- und Datenmodellen. Alle Daten die rein "informativ" gespeichert werden, aber keinen Eingang in das Domänenverhalten finden, gehören meiner Meinung nach nicht in ein Domänenobjektmodell, sondern nur in das Datenmodell.

Dominik Schmid hat gesagt…

Ich schließe mich der Kritik im konkreten Fall an, halte das aber nicht für ein grundsätzliches Problem von DDD oder gar OOP.

Nur weil "hasOKCreditLimit" in diesem Fall fehl am Platze ist, heißt das nicht, dass es keine anderen domänenspezifischen Methoden in der Customer-Klasse könnte, die sie "aus Bordmitteln" durchaus sinnvoll bestreiten könnte. Es läuft also nicht zwangsläufig auf ein anämisches Objektmodell hinaus. Und für alle nicht eindeutig zuzuordnenden Verantwortlichkeiten empfiehlt Evans in seinem Buch ja auch explizit, diese als separate "Domain Services" zu modellieren.

Was das LoD-Problem betrifft, ist im Beispiel einfach keine saubere Unterscheidung zwischen Objekt und Datenstruktur gegeben. Das heißt aber wohl nicht, dass das grundsätzlich unmöglich wäre. Leider versäumt es gerade Fowler (und offensichtlich auch andere) in seinen Büchern oft, auf die Notwendigkeit dieser Unterscheidung hinzuweisen (siehe z.B. "Active Record" in PoEAA). Vielleicht ist das auch nur der Versuch, die Beispiele so einfach wie möglich zu halten.

Ralf Westphal - One Man Think Tank hat gesagt…

@Sebastian: Ja, mir gefällt das Kriterium "nur ein Änderungsgrund" nicht. Aber ich habe es hier bewusst genommen, um der Literatur zu folgen.

Ob das Domänenmodell den Herren Frontend und Domänenlogik dient, lass ich mal dahingestellt. In jedem Fall dient es zwei Zwecken: Datenhaltung und Domänenlogik. Wer mit den Daten umgeht... keine Ahnung ;-)

@Dominik: Auch eine Methode AddCustomer() gehört zur Domäne, wenn sie der Konsistenz der Datenmodells dient. Ich behaupte nicht, dass es kein Domänenobjektmodell gibt. Ich sage nur, dass man es nicht so schnell "krankreden" sollte (Anämie).

Evans habe ich bewusst weitgehend aus meinem Posting raus gelassen. Ihn sehe ich trotz seines lobenden Vorworts bei Nilsson etwas anders positioniert. Das wird klar in meinem nächsten Posting zum Thema.

Dass Fowler wenig auf Prinzipien eingeht, ist auch meine Kritik. Deshalb finde ich seine Aussagen zum Domänenobjektmodell und zum anämischen D. auch so schwammig.

ChinaFan hat gesagt…

Ich muss ein wenig schmunzeln, weil ich vor kurzem gerade für den entgegengesetzten Weg begeistert wurde. Also weg von den reinen Datenobjekten, hin zu den "gesunden" Objekten. ;-)

Die Technik nennt sich "East Oriented Programming". (siehe auch dort: A Design Compass: East Oriented; East: Clean and DRY …; More East …)

Es gibt da im Grunde nur eine Regel, die man beachten muss, wenn man so programmieren will:
Jede öffentliche Methode hat nur folgende Rückgabewerte: void, bool oder this.
(Factories und dergleichen bilden eine Ausnahme.)

Das Beispiel mit dem Customer.HasOkCreditLimit() wäre aus meiner Sicht OK. Um jedoch dem "East-Prinzip" gerecht zu werden, könnte man die Methode besser so formulieren: Customer.HasOkCreditLimitByRule(anyRuleObject)

Es ist somit nicht mehr der Status des Objektes wichtig, sondern das Verhalten.

Was nach dem "East-Prinzip" nicht erlaubt wäre, ist z.B. sowas: Customer.GetDateOfBirth(). Denn hier muss man sich die Frage stellen: "Wozu benötige ich denn das Datum? Was will ich erreichen?"
Legitim wäre dagegen z.B. diese Formulierung: Customer.PutDateOfBirthTo(AgeCalculator)

Und das liest sich, wie ich finde, sehr angenehm.

mfg

Christian

Ralf Westphal - One Man Think Tank hat gesagt…

@ChinaFan: Danke für den Hinweis auf das East-Programming. Das ist ein hübscher Dreh. Ersetzt für mich aber nicht EBC.

Und ich sehe zumindest in deiner Anwendung auf Jimmys Domänenobjektmodell keine wesentliche Verbesserung. Denn mein Punkt ist schlicht, dass einem Datenobjekt wie Customer die Frage nach der Bonität gar nicht gestellt werden darf. Ob man da nun ne Strategie reinreicht, es nach "Osten ausrichtet" oder auf einen Service zugreifen lässt.

Ein Datenobjekt wie Customer als Wurzel eines Objektgraphen lässt den Zugriff auf alle Details zu. Also kann sich bedienen, wer will. Das ist Sinn und Zweck von Datenobjekten.

ChinaFan hat gesagt…

Aber warum muss Customer ein reines Datenobjekt sein?

Ein Objekt ist ja in erster Linie nur eine abstrakte Repräsentation von etwas.
Der Customer kann Daten beinhalten. Man kann aber auch mit ihm agieren.

Zitat:
Denn mein Punkt ist schlicht, dass einem Datenobjekt wie Customer die Frage nach der Bonität gar nicht gestellt werden darf.

Wenn du es aus der Sicht des Customers betrachtest, dann hast du Recht! Denn ich als Kunde wende mich an die Bank, wenn ich wissen will, wie es um die Kreditwürdigkeit bestellt ist.

Allerdings entwickle ich in dem Beispiel eine Software, wo ich an einer bestimmten Stelle im Programm wissen will, ob der aktuelle Customer kreditwürdig ist.
Ich muss die Problemstellung als Programmierer betrachten und darf nicht in die Rolle des Customer schlüpfen. Damit lege ich mir hinterher nur Steine in den Weg, weil ich meine Möglichkeiten der Abstraktion zu Gunsten der Realität beschneide.

Oder:
Viele Methoden des primitven "string"-Datentyps wären auch Fehl am Platz. Denn ein String hält streng genommen nur einzelne Zeichen zusammen. Aber ich finde dennoch Methoden, wie z.B. .Contains(); .EndsWith(); .IsNormalized(); etc.
Aus der Sicht der Zeichenkette gehören diese Methoden IMHO nicht hinein, aber ich, als Programmierer, finde es äußerst praktischt, dass es diese Methoden gibt. Es hält den Quellcode schlank und vor allem leserlich.

Noch ein Extrem ist z.B. Ruby (siehe z.B. dort: KLICK)
Da ist z.B. so ein Konstrukt möglich, um eine Textausgabe 5 mal zu wiederholen:

5.times { print "Wir *lieben* Ruby -- es ist ungeheuerlich!" }

Der Ausdruck .times ist direkter Bestandteil vom Integer. Aus der Sicht des Integer völlig unsinnig, ist es aber aus der Sicht des Programmierers irgendwie cool. Als ich dies zum ersten mal sah, war ich etwas irritiert. Aber was spricht dagegen, wenn es der Lesbarkeit dienlich ist? "Five times {...}" liest sich z.B. schöner, als "for (int i = 0; i<5; i++) {...}".


Wie gesagt:
Worauf ich hinaus will, ist, dass man eventuell wieder beginnen sollte "richtig" objektorientiert zu denken. Das heißt, wieder aus der Sicht des Programmierers und nicht aus der Sicht eines Domänenobjektes.

mfg

Christian

Ralf Westphal - One Man Think Tank hat gesagt…

@ChinaFan: Natürlich kann man mit einem Customer auch agieren. Ich habe nichts gegen eine AddOrder() Methode.

Alles weitere jedoch... da wird es kritisch. Das ist ja gerade mein Punkt.

Ich komme von Prinzipien wie SRP und LoD. Und ich komme aus der Realität. Da frage ich einen Kunden nicht nach seinem Kreditlimit.

Du willst es anders, so "richtig objektorientiert"? Ich frage dich: Was ist das?

Wir definieren, was "richtig" ist. Das ist nicht einfach so gottgegeben. Wir definieren es und es muss zu Designs führen, deren Korrektheit man leicht prüfen kann und die evolvierbar sind. Dazu muss man sie leicht verstehen können. Und dazu ist es gut, wenn sie Prinzipien folgen, die das zusichern.

Es ist nett, 5.Times schreiben zu können. Dagegen hab ich nix.

Aber dass die Sicht des Programmierers am Ende siegen sollte... das halte ich für eine naive Vorstellung. Denn dann siegt auch wieder shape.Draw(). Und das ist - darüber sollten wir nicht mehr diskutieren müssen - schlechte Objektorientierung.

SRP, SoC, LoD sind für mich Leitsterne, die sich im Sinne hoher Evolvierbarkeit bewehrt haben. Wo ich sie verletzt sehe, da ist es mir egal, ob das einer besonders objektorientiert findet. Es dient dann nicht meinen Werten.

ChinaFan hat gesagt…

@Ralf:
Du willst es anders, so "richtig objektorientiert"? Ich frage dich: Was ist das?

Z.B. das Vermeiden von komplizierten Konstrukten bzw. komplizierte Sachverhalte in einfacher Form formulieren. Die Lesbarkeit und somit die Verständlichkeit des Quellcodes erhöhen.

Aber dass die Sicht des Programmierers am Ende siegen sollte... das halte ich für eine naive Vorstellung.

Hmm, aber warum? Ich meine, wir sind es doch letztlich, welche die Software entwickeln. :-)

Denn dann siegt auch wieder shape.Draw(). Und das ist - darüber sollten wir nicht mehr diskutieren müssen - schlechte Objektorientierung.

Das kommt drauf an. Ein beliebiges "shape" kann ja gezeichnet werden. Es benötigt aber ein "surface", worauf es abgebildet werden kann. So könnte man ja sagen "shape.DrawTo(surface)" oder auch "shape.DrawWith(shapePainter)". Bei Letzterem könnte die Wahl des "surfaces" ganz dem "shapePainter" überlassen werden.
Das ist echtes "Tell, Don't Ask!".

SRP, SoC, LoD sind für mich Leitsterne, die sich im Sinne hoher Evolvierbarkeit bewehrt haben. Wo ich sie verletzt sehe, da ist es mir egal, ob das einer besonders objektorientiert findet. Es dient dann nicht meinen Werten.

Das sehe ich doch auch so! Aber wo siehst du bei den Beispielen das SRP verletzt?
Das "shape"-Objekt wäre nach wie vor nur für die eigenen Geometrie-Daten verantwortlich. Die Darstellung würde z.B. der "shapePainter" übernehmen und nicht das "shape" selber.

Ralf Westphal - One Man Think Tank hat gesagt…

@ChinaFan: Wenn shape-Objekte für das Zeichnen gemacht sind, dann mögen sie ein Draw() haben.

Wenn Sie - wie in den üblichen OO-Beispielen - jedoch eher Modellaufgaben haben, dann ist das Zeichnen nicht ihr Concern.

Darum geht es mir: Trenne die Concerns! Wo der Concern Datenhaltung ist - Properties, Beziehungen -, da ist Geschäftslogik fehl am Platze, weil sie eben ein separater Concern ist.

Und es sind gerade die Entwickler, die davon profitieren, saubere Klassen vor sich zu haben, die nur je 1 Verantwortlichkeit/Concern dienen.

Markus Zywitza hat gesagt…

Hallo Ralf,

zum LoD bleibt zu sagen, dass man es im Kontext sehen muss. In dynamischen Sprachen, bei denen durch tiefe Zugriffspfade Laufzeitfehler nach Refactorings entstehen können, macht die Begrenzung der Zugriffstiefe mehr Sinn als in C#.

Die Bonitätsabfrage ist ein klassischer Schnitzer in der Domänenmodellierung. Wenn man sich eng an die Fachdomäne hält, entwickelt man Prozessklassen analog zu den Fachprozessen. Das riecht dann zwar ein wenig nach Transaction Script, schafft aber eine Ubiquitious Language, da die Anwender in der Regel auch in Prozessen denken.
Bei komplexen Prozessen ist es mit einer Klasse meistens nicht getan, so dass für einen einzigen Prozess ein spezifisches Domänenobjektmodell aufgebaut werden muss.
Von da aus kann man auch zu CQRS nach Greg Young und Udi Dahan wechseln, wenn die Komplexität es erfordert, muss aber nicht.

-Markus

Ralf Westphal - One Man Think Tank hat gesagt…

@Markus: Auch bei C# sind "train wrecks" ein Problem. Sie machen weite Teile des Codes abhängig von Strukturen.

Die Abhängigkeit ist das eigentliche Problem. Nicht der dynamische Zugriff. Ob der klappt, zeigen ganz schnell Unit Tests.

Bei der Ubiquitous Language und den Prozessen bin ich deiner Meinung.

ThomasZeman hat gesagt…

Wenn man schon Metaphern wie "blutarm" wählt und dann auch noch betont, dass es ein wichtiges Konzept ist, indem man den medizinischen Fachbegriff dafür nimmt, sollte man sich schon sicher sein dass die Analogie auch passt.
In meinen Augen wäre nämlich eine andere viel passender: Diese Kreditprüfmethode könnte ich auch als Fett oder Flachse bezeichnen und demnach mit einem reinen Datenmodell ein mageres Fleisch haben. Jetzt kann jeder selber wählen: Rinderfilet oder Kotlett.
Mit den lebenswichtigen Aufgaben von Blut hat eine Methode in einer Klasse jedenfalls wenig zu tun.

Ralf Westphal - One Man Think Tank hat gesagt…

@ThomasZemann: Als "anämisch" hat Martin Fowler Domänenobjektmodelle bezeichnet, die seinen Gütekriterien in bestimmter Weise nicht entsprechen. Am besten schreibst du ihm mal ;-)

Letztlich ist das konkrete Wort aber einerlei, würde ich sagen. Es geht darum, dass Fowler etwas als pathologisch ansieht, was ich für nicht ganz so ungesund halte.

Reden wir über mehr oder weniger Fett und es ist Geschmackssache? Zu einem gewissen Grad mag das sein. Dennoch möchte ich mehr Guideline liefern als "Es kommt darauf an!"

Timo hat gesagt…

Hallo Ralf,

Offtopic: Ich bin noch recht unerfahren was Softwareentwicklung angeht. Bin zur Zeit Student und habe wenig Praxiserfahrung. Aber ohne dumme Fragen zu stellen ändert sich das nicht. Entschuldigung!

Das Domänenobjektmodelle sollte für mich von den konkreten Funktionalitäten/Anforderungen möglichst unabhängig sein. Da die Domäneobjekte von vielen Teilen der Anwendung benutzt werden.

Ich will mit einem Beispiel erklären warum:
Ich will doch nicht jedesmal die ganze Anwendung ändern, wenn ich zum Beispiel jetzt für einen Kunden auch noch die Termine verwalten will. Ich erwarte doch dann einfach ein Modul schreiben zu können, welches ich der Anwendung hinzufüge.
Dann würde ich einen extra TerminService schreiben, welcher in der Anwendung registriert wird. Dieser Service hätte bei mir dann auch so Funktionalitäten wie "AddTerminForCustomer". Die Termin Ansicht der Benutzerschnittstelle würde diesen Service(über Interface), die Customer-Klasse und die Termin-Klasse kennen. Die Referenz müsste dann durch die Termin gehalten werden. Da ich den Customer ja nicht ändern will. Wenn die Benutzerschnittstelle dann gesagt bekommt zeige Termin für Customer x an. Würde diese die neue Ansicht öffnen und den Termin-Service nach den Terminen für Customer x fragen....

Wenn ich mehr objektorientiert arbeiten will, schreib ich halt für das Termin Modul einen Wrapper für Customer oder schreibe Erweiterungsmethoden(oder oder). Das macht für mich nicht so den Unterschied. Da diese dann in dem Modul liegen, welche auch die Funktionalität bereitstellen.

Wäre das aus deiner Sicht der richtige Weg. Ich habe mich jedenfalls durch dein Posting bestätigt gefühlt. Vielleicht bin ich zu weit vom Thema abgetriftet(Dann tut es mir Leid).

Ich hoffe ich konnte mich verständlich ausdrücken und hoffe auf Feedback.

Gruß Timo

p.S.: Danke für deine Arbeit, vieles was ich über SE weiß(oder glaub zu wissen *g), habe ich bei dir gelesen.(blog/dotnetPro)

Manfred Steyer hat gesagt…

Hallo Herr Westphal,

ich bin da voll und ganz bei Ihnen. Objektorientierung darf kein Sebstzweck sein. Ich hab' festgestellt, dass viele Leute das auch intuitiv erkannt haben und Worker-Klassen, um z. B. gegen Kreditlimits zu prüfen, verwenden.

Ein gutes Beispiel hierfür ist auch die Evolution des MVC-Patterns. Ursprünglich war ja vorgesehen, dass das Model die Daten sowie die daran gebundene Geschäftslogik kapselt. Heutzutage sind Models meist reine Datencontainer, wenn man es nicht gerade mit View-Models bzw. Presenter zu tun hat. Die Logiken sind in eigenen Worker-Klassen (und bei schlechtem Design direkt im Controller :-) ) zu finden.

Viele Grüße
Manfred

Matthias Jauernig hat gesagt…

Hallo,

Der Artikel spricht mir quasi aus der Seele bzw. habe ich dieselbe Diskussion schon ein paar Mal mit Kollegen geführt.

Das Argument, dass mit einem anämischen Modell die Prinzipien der Objektorientierung untergraben werden, zählt in der Tat nicht. Und die Nachteile eines mit Logik vollgestopften Datenmodells wiegen meiner Meinung nach schwerer.

Ich bin eher ein Freund davon Domänenobjekte möglichst dumm zu lassen und dann ähnlich zu normalen Datentypen als DTOs über Schichten hinweg nutzen zu können. Es macht für mich überhaupt keinen Sinn, wenn meine Datenobjekte Abhängigkeiten zu irgendwelchen Services oder sonstigen Komponenten haben.

Wenn ich die Datenobjekte als DTOs verwende, ist von externen Komponenten abhängige Logik äußerst kontraproduktiv. DTOs nutze ich in mehreren Schichten einer Anwendung, Logik ist in der Regel allerdings an eine Schicht gebunden. Wenn ich die Logik auf Datenobjekten aus allen Schichten heraus aufrufen kann, so führt das zu Chaos.

Meine DTOs enthalten schon noch eine gewissen Logik, allerdings nur solche, die sich auf das Datenobjekt selbst bezieht und keine externen Abhängigkeiten benötigt.

Anonym hat gesagt…

Hallo Zusammen,

ích finde das Thema sehr interressant und wollte auch meine Meinung dazu außern.

Wenn man aus der Realität kommt: Man absrahiert nicht der Customer als eine Person bzw. Lebewesen sondern die Daten der Customer und seine Verhalten, die für den Geschäftsfeld und die FAchabteilung relevant sind.

Ich stelle mir vor, dass es in der Realität eine Akte für einen Customer gibt mit der Informationen über ihn und einen Notiz oder Ergänzung zur seiner Bonität.

Wenn man die Akte öffnet, man hat die Information zur Bonität auf den gleichen Ebene wie die Information über den Name.

Die Daten zur Bonität eines Customers und sein Name bilden für mich ein zusammengehörendes Konzept. Daher ist es gut für das Gewährleisten der "hohe Kohäsion" wenn eine Eigenschaft auf Customer über seine aktuelle Bonität sich in der Customer Klasse befindet.

Das sollte man nicht als "Abfragen der Bonität des Customer" interpretieren.

Die Tatsache, dass man die Information nicht über das Objekt selbst aber über einen externen Dienstlester bekommt, kann nicht entscheidend für das Abstrahieren eines fachlichen Konzeptes in einer Klasse sein.

Was hier wichtig ist, dass man die Logik, wie man eine Bonität für eine Kunde ermittelt, in einer Serviceklasse implementiert und über eine klar definierte Schnittstelle zur Verfügung stellt.


Wenn man die Klassen nach diesem prinzip entwirft, was macht man mit einer Klasse VersichertePerson
deren Alter nach tariflichen Regeln bestimmt sind, deren Krankenversichertennummer und Steuernummer über einen automatischen Dienst abgefragt werden. Wird die Klasse VersichertePerson keine Aler und KKVErscherungsnummer Eigenschaften haben, nur weil diese Informationen über einen externen Diensleister bezogen werden?

SRP ist ein wichtiges Prinzip in OOAD aber hohe Kohäsion ist auch wichtig und soll nicht geopfert werden.

Ich habe auch Bauchschmerzen mit Martin Fowler's Definition "anemic Domainmodell" und halte ich nicht %100 OOP-Konform, aber die strikte Trennung der Datenhaltungslogik und die Geschäftslogik, die ein zusammenhängendes Konzept mit dem Objket bildet, macht mir eben Bauchschmerzen.

Mit freundlichen Grüßen aus Dortmund
Özgür Ergel

Ralf Westphal - One Man Think Tank hat gesagt…

@Özgür: Ich kann schon verstehen, warum du so argumentierst. Es ist ja eigentlich die traditionelle OOP-Sicht.

Für mich spricht zweierlei dagegen:

1. Die de facto Ergebnisse. So haben es alle in den letzten 20 Jahre gemacht. So haben sich alle in die Unwartbarkeit gefahren.

2. Der Begriff "hohe Kohäsion" ist nicht weiter definiert. Hört sich gut an - aber woran erkennst du sie? Das SRP hingegen ist vergleichsweise präzise. Wenn "Dinge" sich getrennt von einander ändern können, sollten sie getrennt codiert werden. Ich nenne diese "getrennten Dinge" Aspekte. Logik und Datenstruktur sind zwei solche für mich fundamentalen Aspekte - die deshalb eben nicht in dieselbe Klasse gehören. SRP over high cohesion :-) Hohe Kohäsion wird durch SRP erst erkennbar bzw. ist eine andere Formulierung für das SRP.

In der dotnetpro habe ich zwei Artikel, die das Thema Domänendatenmodell nochmal näher beleuchten. Einer ist schon erschienen Anfang 2012, ein weiterer kommt in der nächsten Ausgabe oder so. Schau mal rein.

oXmoX hat gesagt…

Hallo,

jetzt mal ganz naiv:

Wenn der Kunde nicht dafür verantwortlich ist, seine eigene Kreditwürdigkeit zu prüfen, wer ist es dann? Leider kenne ich den Sachverhalt aus dem Beispiel nicht, aber ich könnte mir vorstellen, dass diese Verantwortlichkeit gut in eine neue Klasse "Creditor" (Kreditgeber) passen würde. Wer das Domainmodel nicht zu sehr aufblähen will (es soll ja ein "Modell" bleiben), steckt die Methode vlt. lieber gleich in den CreditService (z.B. CreditService.CheckCreditLimit(Customer)). Was spricht dagegen? Wie gesagt: für eine Refaktorisierung des Modells müsste man eigentlich die Randbedingungen besser kennen.

Was ich sagen will:

Wenn eine Methode im Kontext eines gegebenen Domain Models und im Sinne des SRP nicht in eine gegebene Klasse passt dann ist das für mich eher ein Grund, das konkrete Modell anzuzweifeln, nicht aber das Prinzip des DDD.

Die Erkenntnis, dass Daten und Funktionalitäten logisch zusammengehören, ist m.E. die wichtigste Errungenschaft des OOSE. Erst dadurch lassen sich auch sehr komplexe Sachverhalte intuitiv modellieren. Damit ein Objektmodell aber auch wirklich intuitiv wird, muss es bestimmte Regeln befolgen, wie die von Kohäsion und Kopplung und des SRP. Im Beispiel ist dieses wohl nicht gegeben.

Und natürlich wird es in jedem Modell Klassen geben, die eher einen reinen Datencharakter haben und wenig Funktionalität bieten. DDD bedeutet ja nicht, dass ich zwanghaft versuche, irgendwelche Methoden in meine Klassen unterzubringen, die dort eigentlich nichts zu suchen haben, nur um sicherzugehen, dass mein Domain Model auch nicht unter blutarmut leidet. Ein "gesundes" (Rich) Domain Model liegt irgendwo zwischen den Extremen, denn Polyglobulie klingt mindestens genauso krank wie Anämie. Nur wenn eine Funktionalität auch wirklich zu einem Objekt passt, sollte man keine Hemmungen haben, sie auch dort zu hinterlegen (M.E. sollten man sich dabei keinesfalls nur auf solche Methoden beschränken, die die Konsistenz der Daten sicherstellen). Ich finde das sehr intuitiv. Findet man dagegen für eine Funktionalität kein adequates Objekt, so sollte man über eine neue Klasse oder gar ein Refactoring des Modells nachdenken. Sicherlich erfodert es viel Disziplin, damit ein Rich Domain Modell im Laufe seiner Entwicklung nicht degeneriert. Das gilt natürlich für Applikationen mit reinen Datenmodellen genauso.

Am Ende ist es vielleicht auch eine Frage des Geschmacks. Gruppiere ich lieber nach fachlichen/logischen Aspekten und lasse zu dass sich dabei Daten und Funktionen vermischen (DDD) oder lege ich mehr Wert auf deren saubere Trennung und nehme dafür in Kauf, dass sich fachlich/logisch zusammengehörendes an verschiedenen Orten wiederfindet. Mein Favorit ist ganz klar Ersteres.

Der eine sortiert seine Plattensammlung ja auch lieber alphabetisch, der andere chronologisch. Ich sortiere meine übrigens autobiographisch. :-P

Ahoi