Laut Wann ist primitive Obsession kein Code-Geruch? Ich sollte ein ZipCode-Objekt erstellen, um eine Postleitzahl anstelle eines String-Objekts darzustellen.
Nach meiner Erfahrung ziehe ich es jedoch vor, etwas zu sehen
public class Address{
public String zipCode;
}
Anstatt von
public class Address{
public ZipCode zipCode;
}
weil ich denke, dass Letzteres erfordert, dass ich zur ZipCode-Klasse wechsle, um das Programm zu verstehen.
Und ich glaube, ich muss zwischen vielen Klassen wechseln, um die Definition zu sehen, wenn alle primitiven Datenfelder durch eine Klasse ersetzt wurden, die das Gefühl hat, unter dem Jojo-Problem (einem Anti-Muster) zu leiden .
Deshalb möchte ich die ZipCode-Methoden in eine neue Klasse verschieben, zum Beispiel:
Alt:
public class ZipCode{
public boolean validate(String zipCode){
}
}
Neu:
public class ZipCodeHelper{
public static boolean validate(String zipCode){
}
}
Damit nur derjenige, der die Postleitzahl validieren muss, von der ZipCodeHelper-Klasse abhängt. Und ich habe einen weiteren "Vorteil" bei der Beibehaltung der primitiven Besessenheit gefunden: Die Klasse sieht aus wie die serialisierte Form, wenn überhaupt, zum Beispiel: eine Adresstabelle mit der Zeichenfolgenspalte zipCode.
Meine Frage ist, ist "Vermeiden des Jojo-Problems" (Verschieben zwischen Klassendefinitionen) ein gültiger Grund, die "primitive Besessenheit" zuzulassen?
quelle
Antworten:
Es wird davon ausgegangen, dass Sie nicht zur ZipCode-Klasse wechseln müssen, um die Address-Klasse zu verstehen. Wenn ZipCode gut entworfen ist, sollte es offensichtlich sein, was es tut, indem es nur die Adressenklasse liest.
Programme werden nicht durchgehend gelesen - normalerweise sind Programme viel zu komplex, um dies zu ermöglichen. Sie können nicht den gesamten Code eines Programms gleichzeitig im Kopf behalten. Wir verwenden also Abstraktionen und Kapseln, um das Programm in sinnvolle Einheiten zu unterteilen, sodass Sie sich einen Teil des Programms (z. B. die Address-Klasse) ansehen können, ohne den gesamten Code lesen zu müssen, von dem es abhängt.
Ich bin mir zum Beispiel sicher, dass Sie nicht jedes Mal den Quellcode für String lesen müssen, wenn Sie auf String im Code stoßen.
Das Umbenennen der Klasse von ZipCode in ZipCodeHelper schlägt vor, dass es jetzt zwei separate Konzepte gibt: eine Postleitzahl und eine Postleitzahlhilfe. Also doppelt so komplex. Und jetzt kann Ihnen das Typensystem nicht helfen, zwischen einer beliebigen Zeichenfolge und einer gültigen Postleitzahl zu unterscheiden, da sie den gleichen Typ haben. Hier ist "Besessenheit" angebracht: Sie schlagen eine komplexere und weniger sichere Alternative vor, nur weil Sie einen einfachen Wrapper-Typ um ein Primitiv vermeiden möchten.
Die Verwendung eines Grundelements ist meiner Meinung nach in den Fällen gerechtfertigt, in denen es keine Validierung oder andere Logik gibt, die von diesem bestimmten Typ abhängt. Sobald Sie jedoch eine Logik hinzufügen, ist es viel einfacher, wenn diese Logik mit dem Typ gekapselt ist.
Was die Serialisierung angeht, klingt dies nach einer Einschränkung des verwendeten Frameworks. Sicherlich sollten Sie in der Lage sein, einen ZipCode zu einer Zeichenfolge zu serialisieren oder einer Spalte in einer Datenbank zuzuordnen.
quelle
ZipCodeHelper
(was ich eher nennen würdeZipCodeValidator
) könnte sehr gut eine Verbindung zu einem Webdienst herstellen, um seine Arbeit zu erledigen. Das wäre nicht Teil der Einzelverantwortung "Halten Sie die Postleitzahl-Daten". Wenn Sie das Typsystem so einstellen, dass ungültige Postleitzahlen nicht zulässig sind, können Sie denZipCode
Konstruktor weiterhin als Äquivalent zu Javas package-private definieren und diesen mit einem aufrufen,ZipCodeFactory
der immer den Validator aufruft.ZipCode
wäre eine "Datenstruktur". undZipCodeHelper
ein "Objekt". In jedem Fall sind wir uns einig, dass wir keine Webverbindungen an den ZipCode-Konstruktor übergeben müssen.int
eine ID als ID verwenden, können Sie diese mit einer ID multiplizieren ...)Foo
undFooValidator
Klassen. Wir könnten eineZipCode
Klasse haben, die das FormatZipCodeValidator
überprüft und einen Webservice aufruft, um zu überprüfen, ob eine korrekt formatierteZipCode
Version aktuell ist. Wir wissen, dass sich Postleitzahlen ändern. In der Praxis wird es jedoch eine Liste gültiger Postleitzahlen geben, die inZipCode
oder in einer lokalen Datenbank eingekapselt sind .Wenn das geht:
Und der Konstruktor für ZipCode macht:
Dann haben Sie die Kapselung unterbrochen und der ZipCode-Klasse eine ziemlich alberne Abhängigkeit hinzugefügt. Wenn der Konstruktor nicht aufruft
ZipCodeHelper.validate(...)
, haben Sie isolierte Logik auf seiner eigenen Insel, ohne sie tatsächlich zu erzwingen. Sie können ungültige Postleitzahlen erstellen.Die
validate
Methode sollte eine statische Methode für die ZipCode-Klasse sein. Jetzt wird das Wissen über eine "gültige" Postleitzahl zusammen mit der ZipCode-Klasse gebündelt. Da Ihre Codebeispiele wie Java aussehen, sollte der Konstruktor von ZipCode eine Ausnahme auslösen, wenn ein falsches Format angegeben wird:Der Konstruktor überprüft das Format und löst eine Ausnahme aus, wodurch verhindert wird, dass ungültige Postleitzahlen erstellt werden. Die statische
validate
Methode steht anderen Codes zur Verfügung, sodass die Logik zum Überprüfen des Formats in der ZipCode-Klasse eingekapselt ist.In dieser Variante der ZipCode-Klasse gibt es kein "Jo-Jo". Es heißt nur richtige objektorientierte Programmierung.
Wir werden auch die Internationalisierung hier ignorieren, was möglicherweise eine andere Klasse mit dem Namen ZipCodeFormat oder PostalService erforderlich macht (z. B. PostalService.isValidPostalCode (...), PostalService.parsePostalCode (...) usw.).
quelle
ZipCode.validate
Methode ist die Vorprüfung, die durchgeführt werden kann, bevor ein Konstruktor aufgerufen wird, der eine Ausnahme auslöst .Wenn Sie sich viel mit dieser Frage auseinandersetzen, ist die von Ihnen verwendete Sprache möglicherweise nicht das richtige Werkzeug für den Job? Diese Art von "domänentypisierten Primitiven" ist trivial einfach in beispielsweise F # auszudrücken.
Dort könnten Sie zum Beispiel schreiben:
Dies ist sehr nützlich, um häufige Fehler zu vermeiden, z. B. den Vergleich von IDs verschiedener Entitäten. Und da diese typisierten Grundelemente viel leichter sind als eine C # - oder Java-Klasse, werden Sie sie letztendlich tatsächlich verwenden.
quelle
ZipCode
?Die Antwort hängt ganz davon ab, was Sie tatsächlich mit den Postleitzahlen machen möchten. Hier sind zwei extreme Möglichkeiten:
(1) Alle Adressen befinden sich garantiert in einem einzelnen Land. Überhaupt keine Ausnahmen. (ZB keine ausländischen Kunden oder keine Mitarbeiter, deren Privatadresse sich im Ausland befindet, während sie für einen ausländischen Kunden arbeiten.) In diesem Land gibt es Postleitzahlen, von denen zu erwarten ist, dass sie niemals ernsthafte Probleme verursachen (dh sie erfordern keine Freiform-Eingabe) wie "derzeit D4B 6N2, dies ändert sich jedoch alle 2 Wochen"). Die Postleitzahlen werden nicht nur zur Adressierung, sondern auch zur Validierung von Zahlungsinformationen oder ähnlichen Zwecken verwendet. - Unter diesen Umständen ist eine Postleitzahlklasse sehr sinnvoll.
(2) Adressen können sich in fast jedem Land befinden, daher sind Dutzende oder Hunderte von Adressierungsschemata mit oder ohne Postleitzahl (und mit Tausenden von Ausnahmen und Sonderfällen) relevant. Eine "Postleitzahl" wird eigentlich nur benötigt, um Menschen aus Ländern, in denen Postleitzahlen verwendet werden, daran zu erinnern, dass sie vergessen haben, ihre Postleitzahl anzugeben. Die Adressen werden nur verwendet, damit der Zugriff wiederhergestellt wird, wenn jemand den Zugriff auf sein Konto verliert und seinen Namen und seine Adresse nachweisen kann. - Unter diesen Umständen wären Postleitzahlklassen für alle relevanten Länder ein enormer Aufwand. Zum Glück werden sie gar nicht benötigt.
quelle
In den anderen Antworten wurde über die Modellierung von OO-Domänen und die Verwendung eines umfassenderen Typs zur Darstellung Ihres Werts gesprochen.
Ich bin nicht anderer Meinung, insbesondere angesichts des von Ihnen geposteten Beispielcodes.
Aber ich frage mich auch, ob das tatsächlich den Titel Ihrer Frage beantwortet.
Stellen Sie sich das folgende Szenario vor (aus einem aktuellen Projekt, an dem ich arbeite):
Sie haben eine Remote-Anwendung auf einem Feldgerät, die mit Ihrem zentralen Server kommuniziert. Eines der DB-Felder für den Geräteeintrag ist eine Postleitzahl für die Adresse, unter der sich das Feldgerät befindet. Sie interessieren sich nicht für die Postleitzahl (oder den Rest der Adresse für diese Angelegenheit). Alle Menschen, die sich dafür interessieren, befinden sich auf der anderen Seite einer HTTP-Grenze: Sie sind einfach die einzige Quelle der Wahrheit für die Daten. Es hat keinen Platz in Ihrer Domain-Modellierung. Sie zeichnen es einfach auf, validieren es, speichern es und mischen es auf Anfrage in einem JSON-Blob zu anderen Stellen.
In diesem Szenario ist es wahrscheinlich ein Overkill der YAGNI-Variante , weit über die Validierung der Einfügung mit einer SQL-Regex-Einschränkung (oder ihrer ORM-Entsprechung) hinauszugehen.
quelle
RegexValidatedString
erstellen, das die Zeichenfolge selbst und den zur Validierung verwendeten regulären Ausdruck enthält. Aber es sei denn, jede Instanz hat eine eindeutige reguläre Ausdrücke (was möglich, aber unwahrscheinlich ist), scheint dies ein bisschen albern und speicherverschwendend (und möglicherweise die Kompilierungszeit für reguläre Ausdrücke). Sie legen den regulären Ausdruck entweder in eine separate Tabelle und hinterlassen in jedem Fall einen Nachschlageschlüssel, um ihn zu finden (was aufgrund der Indirektion wahrscheinlich schlimmer ist), oder Sie finden eine Möglichkeit, ihn einmal für jeden gemeinsamen Werttyp zu speichern, der diese Regel teilt. - z.B. ein statisches Feld für einen Domänentyp oder eine äquivalente Methode, wie IMSoP sagte.Die
ZipCode
Abstraktion kann nur sinnvoll sein, wenn IhreAddress
Klasse keineTownName
Eigenschaft hat. Ansonsten haben Sie eine halbe Abstraktion: Die Postleitzahl bezeichnet die Stadt, aber diese beiden verwandten Informationen befinden sich in verschiedenen Klassen. Das ergibt keinen Sinn.Aber selbst dann ist es immer noch keine richtige Anwendung (oder vielmehr Lösung für) primitive Obsession. Das konzentriert sich meines Wissens hauptsächlich auf zwei Dinge:
Ihr Fall ist keiner. Eine Adresse ist ein genau definiertes Konzept mit eindeutig erforderlichen Eigenschaften (Straße, Hausnummer, Postleitzahl, Ort, Bundesland, Land, ...). Es gibt kaum einen Grund, diese Daten aufzulösen, da sie eine einzige Verantwortung haben: einen Ort auf der Erde zu bestimmen. Eine Adresse benötigt alle diese Felder, um aussagekräftig zu sein. Eine halbe Adresse ist sinnlos.
So wissen Sie, dass Sie nicht weiter unterteilen müssen: Eine weitere Zerlegung würde die funktionale Absicht der
Address
Klasse beeinträchtigen . Ebenso brauchen Sie keineName
Unterklasse, um in derPerson
Klasse verwendet zu werden, es sei denn,Name
(ohne eine Person im Anhang) ist ein sinnvolles Konzept in Ihrer Domäne. Was es (normalerweise) nicht ist. Namen werden zur Identifizierung von Personen verwendet, sie haben normalerweise keinen eigenen Wert.quelle
Name
Unterklasse, um in derPerson
Klasse verwendet zu werden, es sei denn, Name (ohne Person) ist ein sinnvolles Konzept in Ihrer Domain ." Wenn Sie eine benutzerdefinierte Überprüfung für die Namen vorgenommen haben, ist der Name zu einem aussagekräftigen Konzept in Ihrer Domain geworden. was ich ausdrücklich als gültigen Anwendungsfall für die Verwendung eines Subtyps erwähnt habe. Zweitens führen Sie für die Postleitzahlvalidierung zusätzliche Annahmen ein, z. B. Postleitzahlen, die dem Format eines bestimmten Landes entsprechen müssen. Sie sprechen ein Thema an, das viel breiter ist als die Absicht der OP-Frage.Aus dem Artikel:
Quellcode wird weitaus häufiger gelesen als geschrieben. Daher ist das Jojo-Problem, zwischen vielen Dateien wechseln zu müssen, ein Problem.
Allerdings nicht , das Jo-Jo - Problem fühlt sich viel mehr relevant , wenn sie mit tief voneinander abhängigen Module oder Klassen (die hin und her zwischen ihnen nennen) zu tun. Das ist ein Albtraum der besonderen Art, den es zu lesen gilt, und wahrscheinlich hatte der Geber des Jojo-Problems das im Sinn.
Aber - ja , es ist wichtig, zu viele Abstraktionsebenen zu vermeiden!
Ich bin zum Beispiel nicht einverstanden mit der Annahme in der Antwort von mmmaaa, dass "Sie kein Jo-Jo benötigen, um die ZipCode-Klasse zu [(besuchen)], um die Address-Klasse zu verstehen". Ich habe die Erfahrung gemacht, dass Sie dies tun - zumindest die ersten Male, wenn Sie den Code lesen. Wie andere angemerkt haben, gibt es jedoch Zeiten, in denen eine
ZipCode
Klasse angemessen ist.YAGNI (Ya wird es nicht brauchen) ist ein besseres Muster, um Lasagne-Code (Code mit zu vielen Ebenen) zu vermeiden. Abstraktionen wie Typen und Klassen dienen dem Programmierer und sollten nicht verwendet werden, es sei denn, sie sind vorhanden eine Hilfe.
Ich persönlich möchte "Codezeilen speichern" (und natürlich die zugehörigen "Dateien / Module / Klassen speichern" usw.). Ich bin zuversichtlich, dass es einige gibt, die mir den Beinamen "primitiv besessen" geben würden - ich finde es wichtiger, Code zu haben, über den man leicht nachdenken kann, als sich um Etiketten, Muster und Anti-Muster zu kümmern. Die richtige Wahl, wann eine Funktion, ein Modul / eine Datei / eine Klasse oder eine Funktion an einem gemeinsamen Ort erstellt werden soll, ist sehr situationsabhängig. Ich strebe in etwa 3-100 Zeilenfunktionen, 80-500 Zeilendateien und "1, 2, n" für wiederverwendbaren Bibliothekscode an ( SLOC - ohne Kommentare oder Boilerplate; ich möchte in der Regel mindestens 1 zusätzliches SLOC-Minimum pro obligatorischer Zeile Kochplatte).
Die meisten positiven Muster sind von Entwicklern entstanden, die genau das taten, wann sie es brauchten . Es ist viel wichtiger zu lernen, wie man lesbaren Code schreibt, als zu versuchen, Muster anzuwenden, ohne dass das gleiche Problem zu lösen ist. Jeder gute Entwickler kann das Factory-Muster implementieren, ohne es zuvor gesehen zu haben, in dem seltenen Fall, dass es für sein Problem die richtige Lösung ist. Ich habe das Factory-Muster, das Beobachter-Muster und wahrscheinlich Hunderte davon verwendet, ohne ihren Namen zu kennen (dh gibt es ein "variables Zuweisungsmuster"?). Für ein unterhaltsames Experiment - sehen Sie, wie viele GoF-Muster in die JS- Sprache integriert sind - habe ich 2009 nach ca. 12-15 aufgehört zu zählen. Das Factory-Muster ist so einfach wie das Zurückgeben eines Objekts aus einem JS-Konstruktor - es ist nicht erforderlich eine WidgetFactory.
Also - ja , manchmal
ZipCode
ist eine gute Klasse. Jedoch nicht , ist die Jo-Jo - Problem nicht unbedingt relevant.quelle
Das Jojo-Problem ist nur relevant, wenn Sie hin und her drehen müssen. Das wird durch ein oder zwei Dinge verursacht (manchmal beides):
Wenn Sie sich den Namen ansehen und eine vernünftige Vorstellung davon haben, was er tut, und die Methoden und Eigenschaften einigermaßen offensichtliche Dinge tun, müssen Sie sich den Code nicht ansehen, sondern können ihn einfach verwenden. Das ist in erster Linie der springende Punkt bei Klassen - es handelt sich um modulare Codeteile, die isoliert verwendet und entwickelt werden können. Wenn Sie sich etwas anderes als die API ansehen müssen, um zu sehen, was die Klasse tut, ist dies bestenfalls ein teilweiser Fehler.
quelle
Denken Sie daran, es gibt keine Silberkugel. Wenn Sie eine extrem einfache App schreiben, die schnell durchsucht werden muss, kann eine einfache Zeichenfolge die Aufgabe übernehmen. In 98% der Fälle würde jedoch ein Wertobjekt, wie es von Eric Evans in DDD beschrieben wurde, perfekt passen. Sie können alle Vorteile, die Value-Objekte bieten, leicht erkennen, wenn Sie sich umsehen.
quelle