Kürzlich bin ich auf ein Problem mit der Lesbarkeit meines Codes gestoßen.
Ich hatte eine Funktion, die eine Operation ausführte und eine Zeichenfolge zurückgab, die die ID dieser Operation zum späteren Nachschlagen darstellt (ein bisschen wie OpenFile in Windows, das ein Handle zurückgibt). Der Benutzer würde diese ID später verwenden, um den Vorgang zu starten und sein Ende zu überwachen.
Die ID musste aus Gründen der Interoperabilität eine zufällige Zeichenfolge sein. So entstand eine Methode mit einer sehr unklaren Signatur:
public string CreateNewThing()
Dies macht die Absicht des Rückgabetyps unklar. Ich habe mir überlegt, diesen String in einen anderen Typ zu wickeln, der seine Bedeutung so klarer macht:
public OperationIdentifier CreateNewThing()
Der Typ enthält nur die Zeichenfolge und wird verwendet, wenn diese Zeichenfolge verwendet wird.
Es liegt auf der Hand, dass der Vorteil dieser Art der Bedienung in einer höheren Typensicherheit und einer klareren Zielsetzung besteht, aber auch in der Erzeugung von viel mehr Code und Code, der nicht sehr idiomatisch ist. Einerseits mag ich die zusätzliche Sicherheit, aber es schafft auch viel Unordnung.
Halten Sie es aus Sicherheitsgründen für eine gute Praxis, einfache Typen in einer Klasse zu verpacken?
quelle
Antworten:
Grundelemente wie
string
oderint
haben in einer Geschäftsdomäne keine Bedeutung. Eine direkte Folge davon ist, dass Sie möglicherweise fälschlicherweise eine URL verwenden, wenn eine Produkt-ID erwartet wird, oder die Menge verwenden, wenn der Preis erwartet wird .Dies ist auch der Grund, warum Object Calisthenics Challenge das Umschließen von Primitiven als eine seiner Regeln kennzeichnet:
Das gleiche Dokument erklärt, dass es einen zusätzlichen Vorteil gibt:
Tatsächlich ist es bei der Verwendung von Grundelementen in der Regel äußerst schwierig, die genaue Position des Codes in Bezug auf diese Typen zu ermitteln, was häufig zu schwerwiegenden Codeduplikationen führt . Wenn es eine
Price: Money
Klasse gibt, ist es natürlich, die Überprüfung der Reichweite im Inneren zu finden. Wenn stattdessen einint
(schlechter als eindouble
) zum Speichern von Produktpreisen verwendet wird, wer sollte das Sortiment validieren? Das Produkt? Der Rabatt? Der Wagen?Ein dritter Vorteil, der im Dokument nicht erwähnt wird, ist die Möglichkeit, den zugrunde liegenden Typ relativ einfach zu ändern. Wenn heute mein
ProductId
hatshort
als zugrunde liegende Typ und später muss ich verwendenint
stattdessen, sind die Chancen der Code wird nicht die gesamte Codebasis umspannen zu ändern.Der Nachteil - und das gleiche Argument gilt für alle Regeln der Objektkalisthenik - ist, dass es zu schnell zu überwältigend wird, eine Klasse für alles zu erstellen . Wenn es
Product
enthält,ProductPrice
welche Erben vonPositivePrice
welchen Erben vonPrice
welchen wiederum erbenMoney
, handelt es sich nicht um eine saubere Architektur, sondern um ein komplettes Durcheinander, bei dem ein Maintainer jedes Mal ein paar Dutzend Dateien öffnen sollte, um eine einzige Sache zu finden.Ein weiterer zu berücksichtigender Punkt sind die Kosten (in Form von Codezeilen) für die Erstellung zusätzlicher Klassen. Wenn die Wrapper unveränderlich sind (wie sie normalerweise sein sollten), bedeutet dies, dass Sie, wenn wir C # nehmen, mindestens Folgendes im Wrapper haben müssen:
ToString()
,Equals
und AGetHashCode
überschreiben (auch viel LOC).und gegebenenfalls:
==
und!=
Operatoren,Ein Wert von 100 LOC für einen einfachen Wrapper macht ihn ziemlich unerschwinglich, weshalb Sie sich der langfristigen Rentabilität eines solchen Wrappers sicher sein können. Der von Thomas Junk erläuterte Begriff des Geltungsbereichs ist hier besonders relevant. Das Schreiben von einhundert LOCs, um eine
ProductId
in Ihrer gesamten Codebasis verwendete zu repräsentieren, sieht sehr nützlich aus. Das Schreiben einer Klasse dieser Größe für einen Code, der drei Zeilen innerhalb einer einzigen Methode erstellt, ist viel fragwürdiger.Fazit:
Binden Sie Primitive in Klassen ein, die in einem Geschäftsbereich der Anwendung eine Bedeutung haben, wenn (1) dies zur Reduzierung von Fehlern, (2) zur Verringerung des Risikos von Codeduplizierungen oder (3) zur späteren Änderung des zugrunde liegenden Typs beiträgt.
Schließen Sie nicht jedes Primitiv, das Sie in Ihrem Code finden, automatisch um: Es gibt viele Fälle, in denen die Verwendung von
string
oderint
völlig in Ordnung ist.In der Praxis kann es hilfreich sein,
public string CreateNewThing()
eine Instanz derThingId
Klasse zurückzugeben, anstattstring
, aber Sie können auch:Gibt eine Instanz der
Id<string>
Klasse zurück, die ein Objekt vom generischen Typ ist und angibt, dass der zugrunde liegende Typ eine Zeichenfolge ist. Sie profitieren von der Lesbarkeit, ohne dass Sie viele Typen pflegen müssen.Gibt eine Instanz der
Thing
Klasse zurück. Wenn der Benutzer nur die ID benötigt, kann dies leicht durchgeführt werden mit:quelle
Ich würde den Gültigkeitsbereich als Faustregel verwenden: Je enger der Umfang der Generierung und des Verbrauchs
values
ist, desto unwahrscheinlicher ist es, dass Sie ein Objekt erstellen, das diesen Wert darstellt.Angenommen, Sie haben den folgenden Pseudocode
dann ist der Geltungsbereich sehr begrenzt und ich würde keinen Sinn darin sehen, es zu einem Typ zu machen. Angenommen, Sie generieren diesen Wert in einer Ebene und übergeben ihn einer anderen Ebene (oder sogar einem anderen Objekt). Dann wäre es durchaus sinnvoll, einen Typ dafür zu erstellen.
quelle
doFancyOtherstuff()
in eine Unterroutine umgestaltet haben , denken Sie möglicherweise, dass diejob.do(id)
Referenz nicht lokal genug ist, um als einfache Zeichenfolge gespeichert zu werden.Bei statischen Systemen geht es darum, eine falsche Verwendung von Daten zu verhindern.
Es gibt offensichtliche Beispiele für Typen, die dies tun:
Es gibt subtilere Beispiele
Wir könnten versucht sein,
double
sowohl für den Preis als auch für die Länge a zu verwenden, oderstring
sowohl für einen Namen als auch für eine URL. Aber das untergräbt unser großartiges Typensystem und lässt zu, dass diese Missbräuche die statischen Prüfungen der Sprache bestehen.Wenn Pfundsekunden mit Newtonsekunden verwechselt werden, kann dies zur Laufzeit zu schlechten Ergebnissen führen .
Dies ist insbesondere bei Zeichenfolgen ein Problem. Sie werden häufig zum "universellen Datentyp".
Wir sind in erster Linie daran gewöhnt, Textschnittstellen mit Computern zu erstellen, und erweitern diese Benutzerschnittstellen häufig auf Programmierschnittstellen (APIs). Wir betrachten 34.25 als die Zeichen 34.25 . Wir denken an ein Datum als die Zeichen 05-03-2015 . Wir stellen uns eine UUID als die Zeichen 75e945ee-f1e9-11e4-b9b2-1697f925ec7b vor .
Aber dieses mentale Modell schadet der API-Abstraktion.
Ebenso sollten Textdarstellungen beim Entwerfen von Typen und APIs keine Rolle spielen. Vorsicht
string
! (und andere übermäßig generische "primitive" Typen)Typen kommunizieren "welche Operationen sinnvoll sind".
Ich habe zum Beispiel einmal an einem Client für eine HTTP-REST-API gearbeitet. REST verwendet ordnungsgemäß Hypermedia-Entitäten, deren Hyperlinks auf verwandte Entitäten verweisen. In diesem Client wurden nicht nur die Entitäten (z. B. Benutzer, Konto, Abonnement) eingegeben, sondern auch die Links zu diesen Entitäten (Benutzer, Konto, Abonnement). Die Links waren kaum mehr als nur Wrapper
Uri
, aber die unterschiedlichen Typen machten es unmöglich, einen AccountLink zu verwenden, um einen Benutzer abzurufen. Wäre alles klar gewesenUri
- oder noch schlimmerstring
-, würden diese Fehler nur zur Laufzeit gefunden werden.Ebenso verfügen Sie in Ihrer Situation über Daten, die nur zu einem Zweck verwendet werden: zur Identifizierung eines
Operation
. Es sollte für nichts anderes verwendet werden, und wir sollten nicht versuchen,Operation
s mit zufälligen Strings zu identifizieren, die wir erfunden haben. Das Erstellen einer separaten Klasse erhöht die Lesbarkeit und Sicherheit Ihres Codes.Natürlich können alle guten Dinge im Übermaß verwendet werden. Erwägen
wie viel Klarheit es Ihrem Code hinzufügt
wie oft es benutzt wird
Wenn ein "Typ" von Daten (im abstrakten Sinne) häufig für unterschiedliche Zwecke und zwischen Codeschnittstellen verwendet wird, ist dies ein sehr guter Kandidat für die Einstufung als separate Klasse auf Kosten der Ausführlichkeit.
quelle
05-03-2015
bedeuten , wenn sie als Datum interpretiert ?Manchmal.
Dies ist einer der Fälle, in denen Sie die Probleme abwägen müssen, die bei der Verwendung von a
string
anstelle von a auftreten könnenOperationIdentifier
. Was sind ihre Schwere? Wie hoch ist ihre Wahrscheinlichkeit?Dann müssen Sie die Kosten für die Verwendung des anderen Typs berücksichtigen. Wie schmerzhaft ist es zu benutzen? Wie viel Arbeit ist es zu machen?
In einigen Fällen sparen Sie Zeit und Mühe, indem Sie einen guten konkreten Typ haben, gegen den Sie arbeiten können. In anderen Fällen ist es die Mühe nicht wert.
Im Allgemeinen denke ich, dass so etwas mehr getan werden sollte als heute. Wenn Sie eine Entität haben, die etwas in Ihrer Domäne bedeutet, ist es gut, diese als eigenen Typ zu haben, da diese Entität mit der Wahrscheinlichkeit größer / größer wird, dass sie sich mit dem Geschäft ändert.
quelle
Ich stimme im Allgemeinen zu, dass Sie häufig einen Typ für Primitive und Zeichenfolgen erstellen sollten. Da die obigen Antworten jedoch in den meisten Fällen das Erstellen eines Typs empfehlen, werde ich einige Gründe nennen, warum / wann nicht:
quelle
short
(zum Beispiel) die gleichen Kosten wie zuvor. (?)Nein, Sie sollten keine Typen (Klassen) für "alles" definieren.
Aber, wie andere Antworten sagen , ist es oft nützlich, dies zu tun. Sie sollten entwickeln - bewusst, wenn möglich - ein Gefühl oder Gefühl von allzu viel Reibung aufgrund des Fehlens einer geeigneten Art oder Klasse, wie Sie schreiben, zu testen und Ihren Code erhalten. Für mich beginnt zu viel Reibung, wenn ich mehrere Grundwerte zu einem einzigen Wert zusammenfassen möchte oder wenn ich die Werte validieren muss (dh feststellen muss, welche der möglichen Werte des Grundtyps den gültigen Werten des entsprechen 'impliziter Typ').
Ich habe Überlegungen wie das, was Sie in Ihrer Frage gestellt haben, für zu viel Design in meinem Code verantwortlich gemacht. Ich habe mir angewöhnt, bewusst zu vermeiden, mehr Code als nötig zu schreiben. Hoffentlich schreiben Sie gute (automatisierte) Tests für Ihren Code. Wenn Sie dies tun, können Sie Ihren Code einfach umgestalten und Typen oder Klassen hinzufügen, wenn dies einen Nettovorteil für die fortlaufende Entwicklung und Pflege Ihres Codes darstellt .
Die Antwort von Telastyn und die von Thomas Junk machen beide eine sehr gute Aussage über den Kontext und die Verwendung des relevanten Codes. Wenn Sie einen Wert in einem einzelnen Codeblock verwenden (z. B. Methode, Schleife,
using
Block), ist es in Ordnung, nur einen primitiven Typ zu verwenden. Es ist sogar in Ordnung, einen primitiven Typ zu verwenden, wenn Sie eine Reihe von Werten wiederholt und an vielen anderen Stellen verwenden. Je häufiger und umfassender Sie jedoch einen Wertesatz verwenden und je weniger dieser Wertesatz den vom primitiven Typ dargestellten Werten entspricht, desto mehr sollten Sie in Betracht ziehen, die Werte in eine Klasse oder einen Typ einzuschließen.quelle
Oberflächlich betrachtet müssen Sie lediglich eine Operation identifizieren.
Zusätzlich sagen Sie, was eine Operation tun soll:
Sie sagen es auf eine Art und Weise, als ob dies "nur die Art und Weise ist, wie die Identifikation verwendet wird", aber ich würde sagen, dass dies Eigenschaften sind, die eine Operation beschreiben. Das klingt für mich nach einer Typdefinition, es gibt sogar ein Muster namens "Befehlsmuster", das sehr gut passt. .
Es sagt
Ich denke, das ist sehr ähnlich im Vergleich zu dem, was Sie mit Ihren Operationen machen wollen. (Vergleichen Sie die in beiden Anführungszeichen fett dargestellten Ausdrücke.) Anstatt einen String zurückzugeben, geben Sie einen Bezeichner für einen
Operation
im abstrakten Sinne zurück, beispielsweise einen Zeiger auf ein Objekt dieser Klasse in oop.In Bezug auf den Kommentar
Nein, würde es nicht. Denken Sie daran, dass Muster sehr abstrakt sind , in der Tat so abstrakt, dass sie etwas Meta sind. Das heißt, sie abstrahieren oft die Programmierung selbst und nicht ein Konzept der realen Welt. Das Befehlsmuster ist eine Abstraktion eines Funktionsaufrufs. (oder Methode) Als ob jemand die Pause-Taste unmittelbar nach dem Übergeben der Parameterwerte und unmittelbar vor der Ausführung gedrückt hätte, um später fortzufahren.
Im Folgenden wird oop in Betracht gezogen, aber die Motivation dahinter sollte für jedes Paradigma gelten. Ich möchte einige Gründe nennen, warum das Einfügen der Logik in den Befehl als eine schlechte Sache angesehen werden kann.
Fazit: Mit dem Befehlsmuster können Sie Funktionen in ein Objekt einbinden, das später ausgeführt wird. Aus Gründen der Modularität (Funktionalität ist vorhanden, unabhängig davon, ob sie über einen Befehl ausgeführt wird oder nicht), Testbarkeit (Funktionalität sollte ohne Befehl getestet werden können) und all den anderen Modewörtern, die im Wesentlichen die Anforderung zum Schreiben von gutem Code ausdrücken, würden Sie die eigentliche Logik nicht verwenden in den Befehl.
Da Muster abstrakt sind, kann es schwierig sein, gute Metaphern für die reale Welt zu finden. Hier ist ein Versuch:
"Hey Oma, kannst du bitte um 12 auf den Aufnahmeknopf im Fernseher drücken, damit ich die Simpsons auf Kanal 1 nicht verpasse?"
Meine Oma weiß nicht, was technisch passiert, wenn sie diesen Aufnahmeknopf drückt. Die Logik ist anderswo (im Fernsehen). Und das ist auch gut so. Die Funktionalität ist gekapselt und Informationen sind vor dem Befehl verborgen. Es handelt sich um einen Benutzer der API, der nicht unbedingt Teil der Logik ist.
quelle
Idee hinter dem Umhüllen von primitiven Typen,
Offensichtlich wäre es sehr schwierig und nicht praktikabel, es überall zu tun, aber es ist wichtig, die Typen dort einzupacken, wo es erforderlich ist.
Wenn Sie zum Beispiel die Klasse Order haben,
Die wichtigen Eigenschaften zum Nachschlagen einer Bestellung sind meistens OrderId und InvoiceNumber. Und der Betrag und der Währungscode hängen eng zusammen. Wenn jemand den Währungscode ändert, ohne den Betrag zu ändern, kann die Bestellung nicht mehr als gültig angesehen werden.
In diesem Szenario ist es also nur sinnvoll, OrderId, InvoiceNumber und ein zusammengesetztes Währungssymbol einzufügen, und wahrscheinlich macht das Umschließen von description keinen Sinn. So könnte das bevorzugte Ergebnis aussehen:
Es gibt also keinen Grund, alles einzuwickeln, sondern Dinge, die wirklich wichtig sind.
quelle
Unpopuläre Meinung:
Sie sollten normalerweise keinen neuen Typ definieren!
Das Definieren eines neuen Typs zum Umschließen eines Grundelements oder einer Basisklasse wird manchmal als Definieren eines Pseudotyps bezeichnet. IBM beschreibt dies als eine schlechte Praxis hier (sie mit Generika in diesem Fall speziell auf Missbrauch konzentrieren).
Pseudotypen machen die allgemeine Bibliotheksfunktionalität unbrauchbar.
Javas Mathematikfunktionen können mit allen Zahlenprimitiven arbeiten. Wenn Sie jedoch einen neuen Prozentsatz für die Klasse definieren (ein Double umschließen, der im Bereich von 0 bis 1 liegen kann), sind alle diese Funktionen nutzlos und müssen von Klassen umbrochen werden, die (noch schlimmer) über die interne Darstellung der Prozentsatzklasse Bescheid wissen müssen .
Pseudotypen werden viral
Wenn Sie mehrere Bibliotheken erstellen, werden Sie häufig feststellen, dass diese Pseudotypen viral werden. Wenn Sie die oben erwähnte Percentage-Klasse in einer Bibliothek verwenden, müssen Sie sie entweder an der Bibliotheksgrenze konvertieren (wodurch alle Sicherheit / Bedeutung / Logik / andere Gründe, die Sie für das Erstellen dieses Typs hatten, verloren gehen) oder Sie müssen diese Klassen erstellen auch für die andere Bibliothek zugänglich. Infizieren Sie die neue Bibliothek mit Ihren Typen, bei denen ein einfaches Double ausreichen könnte.
Nachricht zum Mitnehmen
Solange der Typ, den Sie einbinden, nicht viel Geschäftslogik benötigt, würde ich davon abraten, ihn in eine Pseudoklasse einzubinden. Sie sollten eine Klasse nur dann abschließen, wenn schwerwiegende geschäftliche Einschränkungen bestehen. In anderen Fällen sollte die korrekte Benennung Ihrer Variablen einen großen Beitrag zur Vermittlung von Bedeutungen leisten.
Ein Beispiel:
A
uint
kann eine UserId perfekt darstellen, wir können weiterhin die in Java eingebauten Operatoren füruint
s (wie ==) verwenden und wir benötigen keine Geschäftslogik, um den 'internen Status' der Benutzer-ID zu schützen.quelle
Wie bei allen Tipps ist es die Fähigkeit, zu wissen, wann die Regeln anzuwenden sind. Wenn Sie Ihre eigenen Typen in einer typgetriebenen Sprache erstellen, erhalten Sie eine Typprüfung. Im Allgemeinen ist das also ein guter Plan.
NamingConvention ist der Schlüssel zur Lesbarkeit. Die beiden zusammen können Absichten klar vermitteln.
Aber /// ist immer noch nützlich.
Also ja, ich würde sagen, erstellen Sie viele eigene Typen, wenn ihre Lebensdauer eine Klassengrenze überschreitet. Berücksichtigen Sie auch die Verwendung von beiden
Struct
undClass
nicht immer Class.quelle
///
bedeutet