Primitive vs Class, um ein einfaches Domain-Objekt darzustellen?

14

Was sind allgemeine Richtlinien oder Faustregeln für die Verwendung eines domänenspezifischen Objekts gegenüber einer einfachen Zeichenfolge oder Zahl?

Beispiele:

  • Altersklasse vs Integer?
  • Vorname Klasse vs String?
  • UniqueID vs String
  • PhoneNumber Klasse vs String vs Long?
  • DomainName Klasse vs String?

Ich denke, die meisten OOP-Praktiker würden definitiv bestimmte Klassen für PhoneNumber und DomainName angeben. Je mehr Regeln für die Gültigkeit und den Vergleich von Klassen gelten, desto einfacher und sicherer ist es, mit ihnen umzugehen. Aber für die ersten drei gibt es mehr Debatten.

Ich bin noch nie auf eine "Age" -Klasse gestoßen, aber man könnte argumentieren, dass sie sinnvoll ist, da sie nicht negativ sein darf (okay, ich weiß, Sie können für ein negatives Alter argumentieren, aber es ist ein gutes Beispiel dafür, dass sie fast einer primitiven Ganzzahl entspricht).

Zeichenfolge steht häufig für "Vorname", ist jedoch nicht perfekt, da eine leere Zeichenfolge eine gültige Zeichenfolge, jedoch kein gültiger Name ist. Der Vergleich wird normalerweise ohne Berücksichtigung der Groß- und Kleinschreibung durchgeführt. Sicher, es gibt Methoden, um nach Leerzeichen zu suchen, Vergleiche ohne Berücksichtigung der Groß- und Kleinschreibung usw., aber der Verbraucher muss dies tun.

Hängt die Antwort von der Umgebung ab? Ich befasse mich hauptsächlich mit Unternehmens- / hochwertiger Software, die möglicherweise länger als ein Jahrzehnt Bestand hat und gewartet wird.

Vielleicht überdenke ich das, aber ich würde wirklich gerne wissen, ob jemand Regeln hat, wann man Klasse gegen Primitiv wählt.

Noctonura
quelle
2
Altersklasse ist keine gute Idee, wenn sie durch eine Ganzzahl ersetzt werden kann. Wenn es im Konstruktor ein Geburtsdatum hat und über die Methoden getCurrentAge () und getAgeAt (Date date) verfügt, hat die Klasse eine Bedeutung. Es kann jedoch nicht durch eine Ganzzahl ersetzt werden.
Kiwiron
5
Msgstr "Eine Zeichenfolge ist manchmal keine Zeichenfolge. Modellieren Sie sie entsprechend." Es gibt einen guten Beitrag von Mark Seemann über 'primitive Obsession': blog.ploeh.dk/2015/01/19/…
Serhii Shushliapin 30.10.17
4
Eigentlich macht eine Altersklasse auf seltsame Weise Sinn. Die gängige westliche Definition von Alter ist Null bei der Geburt und addiert 1 bei Ihrem nächsten Geburtstag. Die ostasiatische Methode sieht vor, dass Sie 1 bei der Geburt sind und 1 am nächsten Neujahrstag hinzugefügt wird. Abhängig von Ihrem Gebietsschema kann Ihr Alter daher unterschiedlich berechnet werden. EG von en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
Peter M
@PeterM, das sind gute Infos! Daten / Zeiten und jetzt Alter. Es sollten einfache Konzepte sein, aber das beweist, dass die Welt weitaus komplexer ist, als wir es gewohnt sind.
Machado
6
Ich sehe unter dieser Frage viel zu schreiben, aber die Antwort ist nicht wirklich so kompliziert. Sie verwenden eine AgeKlasse anstelle einer Ganzzahl, wenn Sie das zusätzliche Verhalten benötigen, das eine solche Klasse bieten würde.
Robert Harvey

Antworten:

19

Was sind allgemeine Richtlinien oder Faustregeln für die Verwendung eines domänenspezifischen Objekts gegenüber einer einfachen Zeichenfolge oder Zahl?

Die allgemeine Richtlinie lautet, dass Sie Ihre Domain in einer domänenspezifischen Sprache modellieren möchten.

Überlegen Sie: Warum verwenden wir eine Ganzzahl? Genauso einfach können wir alle ganzen Zahlen mit Strings darstellen. Oder mit Bytes.

Wenn wir in einer domänenunabhängigen Sprache programmieren würden, die primitive Typen für Ganzzahl und Alter enthält, welche würden Sie wählen?

Worauf es wirklich ankommt, ist die "primitive" Natur bestimmter Typen, die ein Zufall bei der Wahl der Sprache für unsere Implementierung ist.

Insbesondere Zahlen erfordern in der Regel einen zusätzlichen Kontext. Alter ist nicht nur eine Zahl, sondern hat auch Dimension (Zeit), Einheiten (Jahre?), Rundungsregeln! Das Zusammenzählen von Altersangaben ist in einer Weise sinnvoll, wie es das Zusammenzählen eines Alters mit einem Geld nicht tut.

Durch die Unterscheidung der Typen können wir die Unterschiede zwischen einer nicht verifizierten E-Mail-Adresse und einer verifizierten E-Mail-Adresse modellieren .

Der Zufall, wie diese Werte im Speicher dargestellt werden, ist einer der am wenigsten interessanten Teile. Das Domänenmodell kümmert sich nicht darum CustomerId, ob es sich um einen int- oder einen String- oder einen UUID / GUID- oder einen JSON-Knoten handelt. Es will nur die Erschwinglichkeiten.

Interessiert es uns wirklich, ob ganze Zahlen Big Endian oder Little Endian sind? Interessiert es uns, ob es sich bei der ListÜbergabe um eine Abstraktion über ein Array oder einen Graphen handelt? Wenn wir feststellen, dass Arithmetik mit doppelter Genauigkeit ineffizient ist und wir zu einer Gleitkommadarstellung wechseln müssen, sollte sich das Domänenmodell darum kümmern ?

Parnas, im Jahr 1972 , schrieb

Wir schlagen stattdessen vor, dass man mit einer Liste schwieriger Entwurfsentscheidungen oder Entwurfsentscheidungen beginnt, die sich wahrscheinlich ändern werden. Jedes Modul soll dann eine solche Entscheidung vor den anderen verbergen.

In gewissem Sinne sind die von uns eingeführten domänenspezifischen Werttypen Module, die unsere Entscheidung darüber, welche zugrunde liegende Darstellung der Daten verwendet werden soll, isolieren.

Der Vorteil ist also die Modularität - wir erhalten ein Design, mit dem sich der Umfang einer Änderung leichter verwalten lässt. Der Nachteil sind die Kosten - es ist mehr Arbeit , die für Sie maßgeschneiderten Typen zu erstellen. Die Auswahl der richtigen Typen erfordert ein tieferes Verständnis der Domäne. Der Arbeitsaufwand für die Erstellung des Wertemoduls hängt von Ihrem lokalen Blub- Dialekt ab .

Andere Begriffe in der Gleichung könnten die erwartete Lebensdauer der Lösung beinhalten (sorgfältige Modellierung für Skript-Software, die einmal ausgeführt wird, hat einen schlechten Return on Investment), wie nah die Domäne an der Kernkompetenz des Geschäfts ist.

Ein besonderer Fall, den wir in Betracht ziehen könnten, ist die grenzüberschreitende Kommunikation . Wir möchten nicht in einer Situation sein, in der Änderungen an einer bereitstellbaren Einheit koordinierte Änderungen mit anderen bereitstellbaren Einheiten erfordern. So Nachrichten neigen dazu, mehr auf Darstellungen fokussiert werden, ohne Berücksichtigung von Invarianten oder domänenspezifischen Verhaltensweisen. Wir werden nicht versuchen, "dieser Wert muss streng positiv sein" im Nachrichtenformat mitzuteilen, sondern die Darstellung auf der Leitung mitzuteilen und diese Darstellung an der Domänengrenze zu validieren.

VoiceOfUnreason
quelle
Hervorragender Punkt, um die Dinge zu verstecken (oder zu abstrahieren), die schwierig sind und sich wahrscheinlich ändern.
user949300
4

Sie denken über Abstraktion nach, und das ist großartig. Allerdings scheinen mir die Dinge, die Sie auflisten, größtenteils individuelle Attribute von etwas Größerem zu sein (natürlich nicht alle gleich).

Was ich für wichtiger halte, ist die Bereitstellung einer Abstraktion, die Attribute gruppiert und ihnen ein angemessenes Zuhause gibt, z. B. eine Personenklasse, sodass der Client-Programmierer nicht mit mehreren Attributen, sondern mit einer Abstraktion auf höherer Ebene arbeiten muss.

Sobald Sie diese Abstraktion haben, benötigen Sie nicht unbedingt zusätzliche Abstraktionen für Attribute, die nur Werte sind. Die Person-Klasse kann ein Geburtsdatum als (primitives) Datum sowie Telefonnummern als Liste von (primitiven) Zeichenfolgen und einen Namen als (primitiven) Zeichenfolge enthalten.

Wenn Sie eine Telefonnummer mit einem Formatierungscode haben, sind dies nun zwei Attribute, die eine Klasse für die Telefonnummer verdienen würden (da dies wahrscheinlich auch ein gewisses Verhalten hätte, z. B. Nur Nummern abrufen oder formatierte Telefonnummer abrufen).

Wenn Sie einen Namen als mehrere Attribute haben (z. B. Präfixe / Titel, Erstes, Letztes, Suffix), die eine Klasse verdienen würden (da Sie jetzt einige Verhaltensweisen haben, wie z. B. den vollständigen Namen als Zeichenfolge zu erhalten, anstatt kleinere Teile, Titel usw. zu erhalten. .).

Erik Eidt
quelle
1

Sie sollten nach Bedarf modellieren. Wenn Sie lediglich einige Daten benötigen, können Sie Grundtypen verwenden. Wenn Sie jedoch weitere Operationen mit diesen Daten ausführen möchten, sollten Sie diese beispielsweise in bestimmten Klassen modellieren (ich stimme @kiwiron in seinem Kommentar zu) Altersklasse hat keinen Sinn, aber es wäre besser, sie durch eine Geburtsdatum-Klasse zu ersetzen und diese Methoden dort abzulegen.

Der Vorteil der Modellierung dieser Konzepte in Klassen besteht darin, dass Sie die Reichhaltigkeit Ihres Modells erzwingen. Stattdessen haben Sie eine Methode, die ein beliebiges Datum akzeptiert. Ein Benutzer kann es mit einem beliebigen Datum, dem Startdatum des Zweiten Weltkriegs oder dem Enddatum des Kalten Krieges füllen. Von diesen Daten ist noch ein gültiges Geburtsdatum. Sie können es durch Geburtsdatum ersetzen. In diesem Fall erzwingen Sie, dass Ihre Methode ein Geburtsdatum akzeptiert und kein Datum akzeptiert.

Für Vorname sehe ich nicht viel Nutzen, UniqueID auch, PhoneNumber ein wenig, Sie können es bitten, Ihnen das Land und die Region zum Beispiel zu geben, weil sich PhoneNumber im Allgemeinen auf Länder und Regionen bezieht, einige Betreiber verwenden vordefinierte Nummernsuffixe, um Mobile zu klassifizieren , Office ... -Nummern, damit Sie diese Methoden zu dieser Klasse hinzufügen können, genau wie bei DomainName.

Für weitere Details empfehle ich Ihnen, sich Value Objects in DDD (Domain Driven Design) anzuschauen.

La VloZ Merrill
quelle
1

Worauf es ankommt, ist, ob Sie ein Verhalten haben (das Sie pflegen und / oder ändern müssen), das mit diesen Daten verknüpft ist oder nicht.

Kurz gesagt, wenn es sich nur um ein Datenelement handelt, das ohne viel damit verbundenes Verhalten herumgewirbelt wird, verwenden Sie einen primitiven Typ oder für etwas komplexere Datenelemente eine (weitgehend verhaltenslose) Datenstruktur (z. B. eine Klasse mit nur Gettern und Setter).

Wenn Sie andererseits feststellen, dass Sie zum Treffen von Entscheidungen diese Daten an verschiedenen Stellen in Ihrem Code überprüfen oder manipulieren, an Stellen, die häufig nicht nah beieinander liegen, dann wandeln Sie sie durch Definieren einer Klasse in ein geeignetes Objekt um und das relevante Verhalten als Methoden in sich aufzunehmen. Wenn Sie aus irgendeinem Grund der Meinung sind, dass es keinen Sinn macht, eine solche Klasse zu definieren, können Sie dieses Verhalten in einer Art "Service" -Klasse zusammenfassen, die diese Daten verarbeitet.

Filip Milovanović
quelle
1

Ihre Beispiele ähneln viel eher Eigenschaften oder Attributen von etwas anderem. Das bedeutet, dass es durchaus sinnvoll wäre, nur mit dem Primitiven zu beginnen. Irgendwann müssen Sie ein Primitiv verwenden, daher spare ich normalerweise Komplexität, wenn ich es tatsächlich benötige.

Nehmen wir zum Beispiel an, ich mache ein Spiel und muss mir keine Gedanken über das Altern von Charakteren machen. Die Verwendung einer Ganzzahl ist absolut sinnvoll. Das Alter kann in einfachen Überprüfungen verwendet werden, um festzustellen, ob der Charakter etwas tun darf oder nicht.

Wie andere betonten, kann das Alter abhängig von Ihrem Gebietsschema ein kompliziertes Thema sein. Wenn Sie Alter an verschiedenen Orten usw. abgleichen müssen, ist es absolut sinnvoll, eine AgeKlasse einzuführen , die DateOfBirthund Localeals Eigenschaften hat. Diese Altersklasse kann auch das aktuelle Alter zu einem bestimmten Zeitpunkt berechnen.

Es gibt also Gründe, ein Primitiv einer Klasse zuzuordnen, aber sie sind sehr anwendungsspezifisch. Hier sind einige Beispiele:

  • Sie haben eine spezielle Validierungslogik, die überall dort angewendet werden muss, wo die Art der Informationen verwendet wird (z. B. Telefonnummern, E-Mail-Adressen).
  • Sie haben eine spezielle Formatierungslogik, die verwendet wird, um das Lesen durch den Menschen zu ermöglichen (z. B. IP4- und IP6-Adressen).
  • Sie haben eine Reihe von Funktionen, die sich alle auf diesen Objekttyp beziehen
  • Sie haben spezielle Regeln für den Vergleich ähnlicher Werttypen

Wenn Sie eine Reihe von Regeln für die Behandlung eines Werts haben, den Sie im gesamten Projekt konsistent verwenden möchten, erstellen Sie eine Klasse. Wenn Sie mit einfachen Werten leben können, weil diese für die Funktionalität nicht wirklich kritisch sind, verwenden Sie ein Grundelement.

Berin Loritsch
quelle
1

Letztendlich ist ein Objekt nur ein Zeuge dafür, dass ein Prozess tatsächlich ausgeführt wurde .

Ein Primitiv intbedeutet, dass ein Prozess 4 Byte Speicher auf Null gesetzt und dann die zur Darstellung einer ganzen Zahl im Bereich von -2.147.483.648 bis 2.147.483.647 erforderlichen Bits gekippt hat. Das ist keine starke Aussage über das Konzept des Alters. Ebenso bedeutet ein (nicht null) System.String, dass einige Bytes zugewiesen und Bits gespiegelt wurden, um eine Sequenz von Unicode-Zeichen in Bezug auf eine bestimmte Codierung darzustellen.

Wenn Sie sich also für die Darstellung Ihres Domänenobjekts entscheiden, sollten Sie über den Prozess nachdenken, für den es als Zeuge dient. Wenn a FirstNamewirklich eine nicht leere Zeichenfolge sein sollte, sollte es als Zeuge gelten, dass das System einen Prozess ausgeführt hat, der sicherstellt, dass mindestens ein Nicht-Leerzeichen-Zeichen in einer Folge von Zeichen erstellt wurde, die Ihnen übergeben wurden.

Ebenso sollte ein AgeObjekt wahrscheinlich ein Zeuge sein, dass ein Prozess die Differenz zwischen zwei Daten berechnet hat. Da sie intsim Allgemeinen nicht das Ergebnis der Berechnung der Differenz zwischen zwei Daten sind, sind sie für die Darstellung des Alters ipso unzureichend.

Daher ist es ziemlich offensichtlich, dass Sie fast immer eine Klasse (die nur ein Programm ist) für jedes Domänenobjekt erstellen möchten. Also, was ist das Problem? Das Problem ist, dass C # (das ich liebe) und Java zu viel Zeremonie beim Erstellen von Klassen erfordern. Wir können uns jedoch alternative Syntaxen vorstellen, mit denen sich die Definition von Domänenobjekten einfacher gestalten lässt:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

F # zum Beispiel hat tatsächlich eine schöne Syntax dafür in Form von Active Patterns :

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

Der Punkt ist, dass nur, weil Ihre Programmiersprache das Definieren von Domänenobjekten umständlich macht, dies nicht bedeutet, dass es jemals eine schlechte Idee ist, dies zu tun.


† Wir bezeichnen diese Dinge auch als Post-Bedingungen , aber ich bevorzuge den Begriff "Zeuge", da er als Beweis dafür dient, dass ein Prozess ausgeführt werden kann und ausgeführt wurde.

Rodrick Chapman
quelle
0

Überlegen Sie, was Sie durch die Verwendung einer Klasse im Vergleich zu einer Grundform erhalten. Wenn Sie eine Klasse verwenden können Sie bekommen

  • Validierung
  • Formatierung
  • andere nützliche Methoden
  • Typensicherheit (dh mit einer PhoneNumberKlasse sollte es mir unmöglich sein, sie einer Zeichenfolge oder einer zufälligen Zeichenfolge (dh "Gurke") zuzuweisen, bei der ich eine Telefonnummer erwarte)
  • irgendwelche anderen Vorteile

und diese Vorteile erleichtern Ihnen das Leben, verbessern die Codequalität, die Lesbarkeit und überwiegen die Schmerzen bei der Verwendung solcher Dinge. Ansonsten bleiben Sie bei den Primativen. Wenn es nah ist, machen Sie einen Urteilsruf.

Beachten Sie auch, dass gute Namen manchmal einfach damit umgehen können (z. B. ein String mit dem Namen oder firstNameeine ganze Klasse, die nur eine String-Eigenschaft umschließt). Klassen sind nicht die einzige Möglichkeit, Dinge zu lösen.

Becuzz
quelle