Sollen wir Typen für alles definieren?

141

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?

Ziv
quelle
4
Vielleicht interessiert es Sie, über kleine Typen zu lesen. Ich habe ein bisschen damit gespielt und würde es nicht empfehlen, aber es macht Spaß darüber nachzudenken. darrenhobbs.com/2007/04/11/tiny-types
Jeroen Vannevel
40
Sobald Sie so etwas wie Haskell verwenden, werden Sie feststellen, inwieweit die Verwendung von Typen hilfreich ist. Typen helfen dabei, ein korrektes Programm zu gewährleisten.
Nate Symer
3
Sogar eine dynamische Sprache wie Erlang profitiert von einem Typensystem, weshalb sie trotz ihrer dynamischen Sprache über ein Typenspezifikationssystem und ein Typcheck-Tool verfügt, das Haskellers vertraut ist. Sprachen wie C / C ++, in denen der eigentliche Typ von etwas eine Ganzzahl ist, die wir mit dem Compiler vereinbaren, um eine bestimmte Art und Weise zu behandeln, oder Java, in dem Sie beinahe Typen haben, wenn Sie so tun, als wären Klassen Typen, die entweder ein semantisches Überladungsproblem darstellen Sprache (ist dies eine "Klasse" oder ein "Typ"?) oder Typ-Mehrdeutigkeit (ist dies eine "URL", "Zeichenfolge" oder "UPC"?). Verwenden Sie am Ende alle Tools, die Sie zur Disambiguierung verwenden können.
zxq9
7
Ich bin nicht sicher, woher die Idee kommt, dass C ++ einen Overhead für Typen hat. In C ++ 11 sah keiner der Compiler-Hersteller ein Problem darin, jedem Lambda einen eigenen Typ zuzuweisen. Aber bei TBH kann man nicht wirklich erwarten, in einem Kommentar viel Einsicht über das "C / C ++ - Typensystem" zu finden, als ob die beiden ein Typensystem teilen.
MSalters
2
@JDB - nein, tut es nicht. Nicht einmal ähnlich.
Davor Ždralo

Antworten:

152

Grundelemente wie stringoder inthaben 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:

Regel 3: Wickeln Sie alle Primitiven und Strings um

In der Java-Sprache ist int ein Primitiv und kein reales Objekt. Daher befolgt es andere Regeln als Objekte. Es wird mit einer Syntax verwendet, die nicht objektorientiert ist. Noch wichtiger ist, dass ein Int für sich genommen nur ein Skalar ist und daher keine Bedeutung hat. Wenn eine Methode einen Int als Parameter verwendet, muss der Methodenname die gesamte Arbeit zum Ausdrücken der Intention erledigen. Wenn dieselbe Methode eine Stunde als Parameter verwendet, ist es viel einfacher zu sehen, was los ist.

Das gleiche Dokument erklärt, dass es einen zusätzlichen Vorteil gibt:

Kleine Objekte wie Hour oder Money bieten uns auch einen offensichtlichen Ort, um Verhaltensweisen zu platzieren, die ansonsten in anderen Klassen verstreut gewesen wären .

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: MoneyKlasse gibt, ist es natürlich, die Überprüfung der Reichweite im Inneren zu finden. Wenn stattdessen ein int(schlechter als ein double) 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 ProductIdhat shortals zugrunde liegende Typ und später muss ich verwenden intstattdessen, 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 Productenthält, ProductPricewelche Erben von PositivePricewelchen Erben von Pricewelchen wiederum erben Money, 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:

  • Der Eigenschaft Getter,
  • Sein Unterstützungsfeld,
  • Ein Konstruktor, der dem Hintergrundfeld den Wert zuweist,
  • Ein Brauch ToString(),
  • XML-Dokumentationskommentare (die viele Zeilen umfassen),
  • A Equalsund A GetHashCodeüberschreiben (auch viel LOC).

und gegebenenfalls:

  • Ein DebuggerDisplay- Attribut
  • Eine Außerkraftsetzung von ==und !=Operatoren,
  • Eventuell eine Überlastung des impliziten Konvertierungsoperators, um nahtlos in und aus dem gekapselten Typ zu konvertieren.
  • Codeverträge (einschließlich der Invariante, die eine ziemlich lange Methode mit ihren drei Attributen ist),
  • Mehrere Konverter, die während der XML-Serialisierung, JSON-Serialisierung oder beim Speichern / Laden eines Werts in eine / aus einer Datenbank verwendet werden.

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 ProductIdin 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 stringoder intvöllig in Ordnung ist.

In der Praxis kann es hilfreich sein, public string CreateNewThing()eine Instanz der ThingIdKlasse zurückzugeben, anstatt string, 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 ThingKlasse zurück. Wenn der Benutzer nur die ID benötigt, kann dies leicht durchgeführt werden mit:

    var thing = this.CreateNewThing();
    var id = thing.Id;
    
Arseni Mourzenko
quelle
33
Ich denke, die Möglichkeit, den zugrunde liegenden Typ frei zu ändern, ist hier wichtig. Es ist heute eine Zeichenfolge, aber vielleicht eine GUID oder ein langes Morgen. Durch das Erstellen des Typwrappers werden diese Informationen ausgeblendet, was später zu weniger Änderungen führt. ++ Gute Antwort hier.
RubberDuck
4
Vergewissern Sie sich im Falle von Geld, dass die Währung entweder im Money-Objekt enthalten ist oder dass Ihr gesamtes Programm dieselbe Währung verwendet (die dann dokumentiert werden sollte).
Paŭlo Ebermann
5
@Blrfl: Während es gültige Fälle gibt, in denen eine tiefe Vererbung erforderlich ist, sehe ich eine solche Vererbung normalerweise in überarbeitetem Code, der ohne Rücksicht auf die Lesbarkeit geschrieben wurde. So ist der negative Charakter dieses Teils meiner Antwort.
Arseni Mourzenko
3
@ArTs: Das ist das Argument "lasst uns das Tool verurteilen, weil einige Leute es falsch benutzen".
Freitag,
6
@MainMa Ein Beispiel dafür, wo die Bedeutung einen kostspieligen Fehler hätte vermeiden können: en.wikipedia.org/wiki/Mars_Climate_Orbiter#Cause_of_failure
cbojar
60

Ich würde den Gültigkeitsbereich als Faustregel verwenden: Je enger der Umfang der Generierung und des Verbrauchs valuesist, desto unwahrscheinlicher ist es, dass Sie ein Objekt erstellen, das diesen Wert darstellt.

Angenommen, Sie haben den folgenden Pseudocode

id = generateProcessId();
doFancyOtherstuff();
job.do(id);

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.

Thomas Junk
quelle
14
Ein sehr guter Punkt. Explizite Typen sind ein wichtiges Werkzeug für die Kommunikation von Absichten , was über Objekt- und Dienstgrenzen hinweg besonders wichtig ist.
Avner Shahar-Kashtan
+1, obwohl Sie den gesamten Code von noch nicht doFancyOtherstuff()in eine Unterroutine umgestaltet haben , denken Sie möglicherweise, dass die job.do(id)Referenz nicht lokal genug ist, um als einfache Zeichenfolge gespeichert zu werden.
Mark Hurd
Das ist absolut wahr! Dies ist der Fall, wenn Sie eine private Methode haben, bei der Sie wissen, dass die Außenwelt diese Methode niemals anwenden wird. Danach können Sie ein bisschen mehr darüber nachdenken, wann die Klasse geschützt ist und wann die Klasse öffentlich ist, aber nicht Teil einer öffentlichen API. Geltungsbereich Geltungsbereich und viel Kunst, um die Linie an der richtigen Position zwischen zu vielen primitiven und zu vielen benutzerdefinierten Typen zu platzieren.
Samuel
34

Bei statischen Systemen geht es darum, eine falsche Verwendung von Daten zu verhindern.

Es gibt offensichtliche Beispiele für Typen, die dies tun:

  • Sie können den Monat einer UUID nicht abrufen
  • Sie können nicht zwei Zeichenfolgen multiplizieren.

Es gibt subtilere Beispiele

  • Mit der Länge des Schreibtisches kann man nichts bezahlen
  • Sie können keine HTTP-Anfrage mit dem Namen einer Person als URL stellen.

Wir könnten versucht sein, doublesowohl für den Preis als auch für die Länge a zu verwenden, oder stringsowohl 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.

Die Wörter oder die Sprache, wie sie geschrieben oder gesprochen werden, scheinen in meinem Denkmechanismus keine Rolle zu spielen.

Albert Einstein

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 gewesen Uri- oder noch schlimmer string-, 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, Operations 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

  1. wie viel Klarheit es Ihrem Code hinzufügt

  2. 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.

Paul Draper
quelle
21
"Strings werden oft zum 'universellen' Datentyp" - das ist der Ursprung des Monikers "Stringly Typed Languages".
MSalters
@MSalters, ha ha, sehr nett.
Paul Draper
4
Und natürlich, was bedeutet 05-03-2015 bedeuten , wenn sie als Datum interpretiert ?
ein Lebenslauf
10

Halten Sie es aus Sicherheitsgründen für eine gute Praxis, einfache Typen in einer Klasse zu verpacken?

Manchmal.

Dies ist einer der Fälle, in denen Sie die Probleme abwägen müssen, die bei der Verwendung von a stringanstelle von a auftreten können OperationIdentifier. 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.

Telastyn
quelle
10

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:

  • Performance. Ich muss mich hier auf die eigentlichen Sprachen beziehen. Wenn eine Client-ID in C # kurz ist und Sie sie in eine Klasse einschließen, haben Sie gerade viel Overhead erstellt: Speicher, da sie jetzt auf einem 64-Bit-System 8 Byte beträgt, und Geschwindigkeit, da sie jetzt auf dem Heap zugeordnet ist.
  • Wenn Sie Annahmen über den Typ treffen. Wenn eine Client-ID eine Kurzform ist und Sie eine Logik haben, die sie auf irgendeine Weise packt, treffen Sie normalerweise bereits Annahmen über den Typ. An all diesen Orten musst du jetzt die Abstraktion brechen. Wenn es eine Stelle ist, ist es keine große Sache, wenn es überall ist, finden Sie möglicherweise die Hälfte der Zeit, in der Sie das Primitive verwenden.
  • Weil nicht jede Sprache typedef hat. Und für Sprachen, die dies nicht tun, und für Code, der bereits geschrieben wurde, könnte eine solche Änderung eine große Aufgabe sein, die sogar zu Fehlern führen kann (aber jeder hat eine Testsuite mit guter Abdeckung, oder?).
  • In einigen Fällen verringert sich die Lesbarkeit. Wie drucke ich das aus? Wie überprüfe ich das? Sollte ich auf null prüfen? Sind alle Fragen, die Sie in die Definition des Typs bohren müssen.
ytoledano
quelle
3
Wenn es sich bei dem Wrapper-Typ um eine Struktur und nicht um eine Klasse handelt, hat a short(zum Beispiel) die gleichen Kosten wie zuvor. (?)
MathematicalOrchid
1
Wäre es nicht ein gutes Design für einen Typ, seine eigene Validierung einzuschließen? Oder um einen Hilfstyp zur Validierung zu erstellen?
Nick Udell
1
Unnötiges Packen oder Mungieren ist ein Anti-Muster. Informationen sollten transparent durch den Anwendungscode fließen, es sei denn, eine Transformation ist ausdrücklich und wirklich erforderlich . Typen sind gut, da sie in der Regel eine kleine Menge an gutem Code ersetzen, der sich an einer einzigen Stelle befindet, und große Mengen an schlechter Implementierung ersetzen, die über das gesamte System verteilt sind.
Thomas W
7

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, usingBlock), 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.

Kenny Evitt
quelle
5

Oberflächlich betrachtet müssen Sie lediglich eine Operation identifizieren.

Zusätzlich sagen Sie, was eine Operation tun soll:

um den Vorgang zu starten [und], um sein Ende zu überwachen

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

das befehlsmuster kapselt alle informationen, die zur durchführung einer aktion oder zur auslösung eines ereignisses zu einem späteren zeitpunkt benötigt werden .

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 Operationim abstrakten Sinne zurück, beispielsweise einen Zeiger auf ein Objekt dieser Klasse in oop.

In Bezug auf den Kommentar

Es wäre wtf-y, diese Klasse einen Befehl zu nennen und dann nicht die Logik darin zu haben.

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.

  1. Wenn Sie all diese Logik im Befehl hätten, wäre es aufgebläht und chaotisch. Sie würden denken "na ja, all diese Funktionen sollten in getrennten Klassen sein." Sie würden Refactoring die Logik aus dem Befehl.
  2. Wenn Sie all diese Logik im Befehl hätten, wäre es schwierig zu testen und sich beim Testen in die Quere zu kommen. "Heiliger Mist, warum muss ich diesen Befehlsquatsch verwenden? Ich möchte nur testen, ob diese Funktion 1 ausspuckt oder nicht, ich möchte es nicht später aufrufen, ich möchte es jetzt testen!"
  3. Wenn Sie all diese Logik im Befehl hätten, wäre es nicht allzu sinnvoll, eine Rückmeldung zu erstellen, wenn Sie fertig sind. Wenn Sie über eine Funktion nachdenken, die standardmäßig synchron ausgeführt werden soll, ist es nicht sinnvoll, benachrichtigt zu werden, wenn die Ausführung abgeschlossen ist. Vielleicht starten Sie einen anderen Thread, weil Ihr Vorgang tatsächlich darin besteht, einen Avatar im Kinoformat zu rendern (möglicherweise wird ein zusätzlicher Thread für Himbeer-Pi benötigt), vielleicht müssen Sie einige Daten von einem Server abrufen, ... was auch immer es ist, wenn es einen Grund dafür gibt Nach Abschluss der Berichterstellung kann dies daran liegen, dass eine gewisse Asynchronität (ist das ein Wort?) vorliegt. Ich denke, das Ausführen eines Threads oder das Kontaktieren eines Servers ist eine Logik, die nicht in Ihrem Befehl enthalten sein sollte. Dies ist eher ein Meta-Argument und möglicherweise umstritten. Wenn Sie der Meinung sind, dass dies keinen Sinn ergibt, teilen Sie es mir bitte in den Kommentaren mit.

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.

Null
quelle
Sie haben Recht, ich habe nicht so darüber nachgedacht. Das Befehlsmuster passt jedoch nicht zu 100%, da die Logik der Operation in einer anderen Klasse gekapselt ist und ich sie nicht in das Objekt einfügen kann, das der Befehl ist. Es wäre wtf-y, diese Klasse einen Befehl zu nennen und dann nicht die Logik darin zu haben.
Ziv
Das macht sehr viel Sinn. Erwägen Sie, eine Operation und keinen OperationIdentifier zurückzugeben. Dies ähnelt der C # -TPL, in der Sie eine Task oder Task <T> zurückgeben. @ Ziv: Sie sagen "die Logik der Operation ist in einer anderen Klasse gekapselt." Aber heißt das wirklich, dass Sie es auch von hier aus nicht bereitstellen können?
Moby Disk
@ Ziv Ich bitte um etwas anderes. Sehen Sie sich die Gründe an, die ich zu meiner Antwort hinzugefügt habe, und prüfen Sie, ob sie für Sie einen Sinn ergeben. =)
null
5

Idee hinter dem Umhüllen von primitiven Typen,

  • Eine domänenspezifische Sprache festlegen
  • Begrenzen Sie Fehler, die von Benutzern begangen werden, indem Sie falsche Werte übergeben

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,

Order
{
   long OrderId { get; set; }
   string InvoiceNumber { get; set; }
   string Description { get; set; }
   double Amount { get; set; }
   string CurrencyCode { get; set; }
}

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:

    Order
    {
       OrderIdType OrderId { get; set; }
       InvoiceNumberType InvoiceNumber { get; set; }
       string Description { get; set; }
       Currency Amount { get; set; }
    }

Es gibt also keinen Grund, alles einzuwickeln, sondern Dinge, die wirklich wichtig sind.

Tieffliegender Pelikan
quelle
4

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 uintkann eine UserId perfekt darstellen, wir können weiterhin die in Java eingebauten Operatoren für uints (wie ==) verwenden und wir benötigen keine Geschäftslogik, um den 'internen Status' der Benutzer-ID zu schützen.

Roy T.
quelle
Zustimmen. Die gesamte Programmierphilosophie ist in Ordnung, aber in der Praxis muss es auch funktionieren
Ewan
Wenn Sie Werbung für Kredite an die ärmsten Menschen in Großbritannien gesehen haben, werden Sie feststellen, dass der Prozentsatz, der ein Double im Bereich von 0..1 einwickelt, nicht funktioniert ...
gnasher729
1
In C / C ++ / Objective C können Sie ein typedef verwenden, das Ihnen nur einen neuen Namen für einen vorhandenen primitiven Typ gibt. Andererseits sollte Ihr Code unabhängig vom zugrunde liegenden Typ funktionieren, z. B. wenn ich OrderId von uint32_t in uint64_t ändere (weil ich mehr als 4 Milliarden Bestellungen verarbeiten muss).
gnasher729
Ich werde Sie nicht für Ihre Tapferkeit ablehnen, aber Pseudotypen und die in der Antwort definierten Typen sind unterschiedlich. Das sind Typen, die geschäftsspezifisch sind. Pseudotypen sind Typen, die immer noch generisch sind.
0fnt
@ gnasher729: C ++ 11 verfügt auch über benutzerdefinierte Literale, die den Umbruchprozess für den Benutzer sehr angenehm machen: Distance d = 36.0_mi + 42.0_km; . msdn.microsoft.com/en-us/library/dn919277.aspx
damix911
3

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 Structund Classnicht immer Class.

Phil Soady
quelle
2
Was ///bedeutet
Gabe
1
/// ist der Dokumentationskommentar in C #, mit dem Intellisense die Dokumentation der Signatur am Aufrufmethodenpunkt anzeigt. Überlegen Sie, wie Sie Code am besten dokumentieren können. Kann mit Werkzeugen bewirtschaftet werden und ein Erreichbarkeits-Intelligenz-Verhalten bieten. msdn.microsoft.com/en-us/library/tkxs89c5%28v=vs.71%29.aspx
phil soady