Verwenden von struct, um die Validierung des integrierten Typs zu erzwingen

9

Üblicherweise haben Domänenobjekte Eigenschaften, die durch einen integrierten Typ dargestellt werden können, deren gültige Werte jedoch eine Teilmenge der Werte sind, die durch diesen Typ dargestellt werden können.

In diesen Fällen kann der Wert mit dem integrierten Typ gespeichert werden. Es muss jedoch sichergestellt werden, dass die Werte immer am Eingangspunkt validiert werden, da wir sonst möglicherweise mit einem ungültigen Wert arbeiten.

Eine Möglichkeit, dies zu lösen, besteht darin, den Wert als benutzerdefinierten Wert zu speichern, der structüber ein einzelnes private readonlySicherungsfeld des integrierten Typs verfügt und dessen Konstruktor den angegebenen Wert überprüft. Wir können dann immer sicher sein, nur validierte Werte zu verwenden, wenn wir diesen structTyp verwenden.

Wir können auch Cast-Operatoren von und zu dem zugrunde liegenden integrierten Typ bereitstellen, damit Werte nahtlos als zugrunde liegender Typ eingegeben und beendet werden können.

Nehmen Sie als Beispiel eine Situation, in der wir den Namen eines Domänenobjekts darstellen müssen und gültige Werte alle Zeichenfolgen sind, deren Länge zwischen 1 und 255 Zeichen liegt. Wir könnten dies mit der folgenden Struktur darstellen:

public struct ValidatedName : IEquatable<ValidatedName>
{
    private readonly string _value;

    private ValidatedName(string name)
    {
        _value = name;
    }

    public static bool IsValid(string name)
    {
        return !String.IsNullOrEmpty(name) && name.Length <= 255;
    }

    public bool Equals(ValidatedName other)
    {
        return _value == other._value;
    }

    public override bool Equals(object obj)
    {
        if (obj is ValidatedName)
        {
            return Equals((ValidatedName)obj);
        }
        return false;
    }

    public static implicit operator string(ValidatedName x)
    {
        return x.ToString();
    }

    public static explicit operator ValidatedName(string x)
    {
        if (IsValid(x))
        {
            return new ValidatedName(x);
        }
        throw new InvalidCastException();
    }

    public static bool operator ==(ValidatedName x, ValidatedName y)
    {
        return x.Equals(y);
    }

    public static bool operator !=(ValidatedName x, ValidatedName y)
    {
        return !x.Equals(y);
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value;
    }
}

Das Beispiel zeigt den Token string, da implicitdies niemals fehlschlagen kann, aber den stringCast, explicitda dies für ungültige Werte wirft, aber natürlich können beide entweder implicitoder sein explicit.

Beachten Sie auch, dass man diese Struktur nur über eine Umwandlung von initialisieren kann string, aber man kann testen, ob eine solche Umwandlung im Voraus mit der IsValid staticMethode fehlschlägt .

Dies scheint ein gutes Muster zu sein, um die Validierung von Domänenwerten zu erzwingen, die durch einfache Typen dargestellt werden können, aber ich sehe es nicht oft verwendet oder vorgeschlagen, und ich bin interessiert, warum.

Meine Frage lautet also: Was sind für Sie die Vor- und Nachteile der Verwendung dieses Musters und warum?

Wenn Sie der Meinung sind, dass dies ein schlechtes Muster ist, würde ich gerne verstehen, warum und auch was Sie für die beste Alternative halten.

NB Ich habe diese Frage ursprünglich zu Stack Overflow gestellt , sie wurde jedoch als primär meinungsbasiert (ironischerweise subjektiv an sich) zurückgestellt - hoffentlich kann sie hier mehr Erfolg haben.

Oben ist der Originaltext, unten ein paar weitere Gedanken, teilweise als Antwort auf die dort eingegangenen Antworten, bevor er auf Eis gelegt wurde:

  • Einer der Hauptpunkte der Antworten war die Menge an Kesselplattencode, die für das obige Muster erforderlich ist, insbesondere wenn viele solcher Typen erforderlich sind. Zur Verteidigung des Musters könnte dies jedoch mithilfe von Vorlagen weitgehend automatisiert werden, und tatsächlich scheint es mir sowieso nicht schlecht zu sein, aber das ist nur meine Meinung.
  • Aus konzeptioneller Sicht erscheint es bei der Arbeit mit einer stark typisierten Sprache wie C # nicht seltsam, das stark typisierte Prinzip nur auf zusammengesetzte Werte anzuwenden, anstatt es auf Werte zu erweitern, die durch eine Instanz von a dargestellt werden können eingebauter Typ?
gmoody1979
quelle
Sie könnten eine Vorlagenversion machen, die einen Bool (T) Lambda
Ratschenfreak am

Antworten:

4

Dies ist in ML-ähnlichen Sprachen wie Standard ML / OCaml / F # / Haskell ziemlich häufig, wo es viel einfacher ist, die Wrapper-Typen zu erstellen. Es bietet Ihnen zwei Vorteile:

  • Es ermöglicht einem Code, zu erzwingen, dass eine Zeichenfolge validiert wurde, ohne sich selbst um diese Validierung kümmern zu müssen.
  • Sie können den Validierungscode an einer Stelle lokalisieren. Wenn a ValidatedNamejemals einen ungültigen Wert enthält, wissen Sie, dass der Fehler in der IsValidMethode liegt.

Wenn Sie die IsValidMethode richtig verstehen, haben Sie die Garantie, dass jede Funktion, die a empfängt, ValidatedNametatsächlich einen validierten Namen erhält .

Wenn Sie Zeichenfolgenmanipulationen durchführen müssen, können Sie eine öffentliche Methode hinzufügen, die eine Funktion akzeptiert, die eine Zeichenfolge (den Wert von ValidatedName) verwendet, eine Zeichenfolge (den neuen Wert) zurückgibt und das Ergebnis der Anwendung der Funktion überprüft. Dadurch entfällt die Möglichkeit, den zugrunde liegenden String-Wert abzurufen und erneut zu verpacken.

Eine verwandte Verwendung zum Umschließen von Werten ist das Verfolgen ihrer Herkunft. Beispielsweise geben C-basierte Betriebssystem-APIs manchmal Handles für Ressourcen als Ganzzahlen an. Sie können die Betriebssystem-APIs umbrechen, um stattdessen eine HandleStruktur zu verwenden und nur auf diesen Teil des Codes Zugriff auf den Konstruktor zu gewähren. Wenn der Code, der das Handles erzeugt, korrekt ist, werden immer nur gültige Handles verwendet.

Doval
quelle
1

Was sind für Sie die Vor- und Nachteile der Verwendung dieses Musters und warum?

Gut :

  • Es ist in sich geschlossen. Zu viele Validierungsbits haben Ranken, die an verschiedene Stellen reichen.
  • Es hilft bei der Selbstdokumentation. Wenn Sie sehen, wie eine Methode a ValidatedStringnimmt, wird die Semantik des Aufrufs viel klarer.
  • Es hilft dabei, die Validierung auf einen Punkt zu beschränken, anstatt sie über öffentliche Methoden hinweg duplizieren zu müssen.

Schlecht :

  • Der Casting-Trick ist versteckt. Es ist kein idiomatisches C #, kann also beim Lesen des Codes Verwirrung stiften.
  • Es wirft. Zeichenfolgen zu haben, die die Validierung nicht erfüllen, ist kein außergewöhnliches Szenario. IsValidVor der Besetzung zu tun ist ein wenig unfreundlich.
  • Es kann Ihnen nicht sagen, warum etwas ungültig ist.
  • Der Standardwert ValidatedStringist nicht gültig / validiert.

Ich habe solche Dinge öfter mit Userund solchen AuthenticatedUserDingen gesehen, bei denen sich das Objekt tatsächlich ändert. Es kann ein guter Ansatz sein, obwohl er in C # fehl am Platz zu sein scheint.

Telastyn
quelle
1
Vielen Dank, ich denke, Ihr vierter "con" ist das bisher überzeugendste Argument dagegen - die Verwendung von default oder eines Arrays vom Typ kann zu ungültigen Werten führen (abhängig davon, ob die Zeichenfolge null / null natürlich ein gültiger Wert ist). Dies sind (glaube ich) die einzigen zwei Möglichkeiten, um einen ungültigen Wert zu erhalten. Aber wenn wir dieses Muster NICHT verwenden, würden diese beiden Dinge uns immer noch ungültige Werte geben, aber ich nehme an, wir würden zumindest wissen, dass sie validiert werden müssen. Dies könnte möglicherweise den Ansatz ungültig machen, bei dem der Standardwert des zugrunde liegenden Typs für unseren Typ nicht gültig ist.
gmoody1979
Alle Nachteile sind eher Implementierungsprobleme als Probleme mit dem Konzept. Zusätzlich finde ich die "Ausnahmen sollten außergewöhnlich sein" ein unscharfes und schlecht definiertes Konzept. Der pragmatischste Ansatz besteht darin, sowohl eine ausnahmebasierte als auch eine nicht ausnahmebasierte Methode bereitzustellen und den Anrufer wählen zu lassen.
Doval
@Doval Ich stimme zu, außer wie in meinem anderen Kommentar angegeben. Der springende Punkt des Musters ist, sicher zu wissen, dass ein validierter Name gültig sein muss, wenn wir ihn haben. Dies bricht zusammen, wenn der Standardwert des zugrunde liegenden Typs nicht auch ein gültiger Wert des Domänentyps ist. Dies ist natürlich domänenabhängig, ist aber bei stringbasierten Typen eher der Fall (hätte ich gedacht) als bei numerischen Typen. Das Muster funktioniert am besten, wenn der Standard des zugrunde liegenden Typs auch als Standard des Domänentyps geeignet ist.
gmoody1979
@Doval - Ich stimme im Allgemeinen zu. Das Konzept selbst ist in Ordnung, aber es versucht effektiv, Verfeinerungstypen in eine Sprache zu bringen, die sie nicht unterstützt. Es wird immer Implementierungsprobleme geben.
Telastyn
Ich nehme an, Sie könnten den Standardwert in der "ausgehenden" Besetzung und an jeder anderen erforderlichen Stelle innerhalb der Methoden der Struktur überprüfen und werfen, wenn sie nicht initialisiert werden, aber das wird langsam chaotisch.
gmoody1979
0

Dein Weg ist ziemlich schwer und intensiv. Normalerweise definiere ich Domänenentitäten wie:

public class Institution
{
    private Institution() { }

    public Institution(int organizationId, string name)
    {
        OrganizationId = organizationId;            
        Name = name;
        ReplicationKey = Guid.NewGuid();

        new InstitutionValidator().ValidateAndThrow(this);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }        
    public virtual ICollection<Department> Departments { get; private set; }

    ... other properties    

    public Department AddDepartment(string name)
    {
        var department = new Department(Id, name);
        if (Departments == null) Departments = new List<Department>();
        Departments.Add(department);            
        return department;
    }

    ... other domain operations
}

Im Konstruktor der Entität wird die Validierung mit FluentValidation.NET ausgelöst, um sicherzustellen, dass Sie keine Entität mit ungültigem Status erstellen können. Beachten Sie, dass alle Eigenschaften schreibgeschützt sind. Sie können sie nur über den Konstruktor oder dedizierte Domänenoperationen festlegen.

Die Validierung dieser Entität ist eine separate Klasse:

public class InstitutionValidator : AbstractValidator<Institution>
{
    public InstitutionValidator()
    {
        RuleFor(institution => institution.Name).NotNull().Length(1, 100).WithLocalizedName(() =>   Prim.Mgp.Infrastructure.Resources.GlobalResources.InstitutionName);       
        RuleFor(institution => institution.OrganizationId).GreaterThan(0);
        RuleFor(institution => institution.ReplicationKey).NotNull().NotEqual(Guid.Empty);
    }  
}

Diese Validatoren können auch problemlos wiederverwendet werden, und Sie schreiben weniger Boilerplate-Code. Ein weiterer Vorteil ist, dass es lesbar ist.

L-Vier
quelle
Würde der Downvoter gerne erklären, warum meine Antwort abgelehnt wurde?
L-Four
Bei der Frage ging es um eine Struktur zum Einschränken von Werttypen, und Sie sind zu einer Klasse gewechselt, ohne zu erklären, WARUM. (Kein Downvoter, nur ein Vorschlag.)
DougM
Ich erklärte, warum ich dies für eine bessere Alternative halte, und dies war eine seiner Fragen. Danke für die Antwort.
L-Four
0

Ich mag diesen Ansatz für Werttypen. Das Konzept ist großartig, aber ich habe einige Vorschläge / Beschwerden über die Implementierung.

Casting : In diesem Fall mag ich Casting nicht. Die explizite From-String-Besetzung ist kein Problem, aber es gibt keinen großen Unterschied zwischen (ValidatedName)nameValueund neu ValidatedName(nameValue). Es scheint also irgendwie unnötig. Die implizite Besetzung der Zeichenfolge ist das schlimmste Problem. Ich denke, dass das Abrufen des tatsächlichen Zeichenfolgenwerts expliziter sein sollte, da er möglicherweise versehentlich der Zeichenfolge zugewiesen wird und der Compiler Sie nicht vor einem möglichen "Genauigkeitsverlust" warnt. Diese Art von Präzisionsverlust sollte explizit sein.

ToString : Ich bevorzuge die Verwendung von ToStringÜberladungen nur zum Debuggen. Und ich denke nicht, dass es eine gute Idee ist, den Rohwert dafür zurückzugeben. Dies ist das gleiche Problem wie bei der impliziten Konvertierung in Zeichenfolgen. Das Abrufen des internen Werts sollte eine explizite Operation sein. Ich glaube, Sie versuchen, die Struktur wie eine normale Zeichenfolge für den externen Code zu verhalten, aber ich denke, dass Sie dabei einen Teil des Werts verlieren, den Sie durch die Implementierung dieser Art von Typ erhalten.

Equals und GetHashCode : Strukturen verwenden standardmäßig strukturelle Gleichheit. Also duplizieren Sie Equalsund GetHashCodedieses Standardverhalten. Sie können sie entfernen und es wird so ziemlich dasselbe sein.

Euphorisch
quelle
Casting: Semantisch fühlt sich das für mich eher wie die Umwandlung einer Zeichenfolge in einen ValidatedName an als wie die Erstellung eines neuen ValidatedName: Wir identifizieren eine vorhandene Zeichenfolge als ValidatedName. Daher erscheint mir die Besetzung semantisch korrekter. Einverstanden gibt es wenig Unterschied in der Eingabe (der Finger auf der Tastaturvielfalt). Ich bin mit der Besetzung der Zeichenfolge nicht einverstanden: ValidatedName ist eine Teilmenge der Zeichenfolge, daher kann es nie zu einem Genauigkeitsverlust kommen ...
gmoody1979
ToString: Ich bin anderer Meinung. Für mich ist ToString eine absolut gültige Methode, die außerhalb von Debugging-Szenarien verwendet werden kann, vorausgesetzt, sie entspricht den Anforderungen. Auch in dieser Situation, in der ein Typ eine Teilmenge eines anderen Typs ist, halte ich es für sinnvoll, die Fähigkeitsumwandlung von der Teilmenge in die Supermenge so einfach wie möglich zu gestalten, damit der Benutzer sie fast so behandeln kann, wie er es wünscht vom Super-Set-Typ, dh String ...
gmoody1979
Equals und GetHashCode: Ja, Strukturen verwenden strukturelle Gleichheit, aber in diesem Fall wird die Zeichenfolgenreferenz verglichen, nicht der Wert der Zeichenfolge. Daher müssen wir Equals überschreiben. Ich bin damit einverstanden, dass dies nicht erforderlich wäre, wenn der zugrunde liegende Typ ein Werttyp wäre. Nach meinem Verständnis der standardmäßigen GetHashCode-Implementierung für Werttypen (die ziemlich begrenzt ist) ergibt dies den gleichen Wert, ist jedoch leistungsfähiger. Ich sollte wirklich testen, ob dies der Fall ist, aber es ist ein Nebenproblem zum Hauptpunkt der Frage. Danke übrigens für deine Antwort :-).
gmoody1979
@ gmoody1979 Strukturen werden standardmäßig in jedem Feld mit Gleich verglichen. Sollte kein Problem mit Strings sein. Gleiches gilt für GetHashCode. Wie für die Struktur als Teilmenge der Zeichenfolge. Ich stelle mir den Typ gerne als Sicherheitsnetz vor. Ich möchte nicht mit ValidatedName arbeiten und dann versehentlich ausrutschen, um einen String zu verwenden. Ich würde es vorziehen, wenn der Compiler mich explizit angeben würde, dass ich jetzt mit ungeprüften Daten arbeiten möchte.
Euphoric
Sorry ja, guter Punkt auf Equals. Obwohl die Überschreibung angesichts des Standardverhaltens eine bessere Leistung erzielen sollte, muss für den Vergleich die Reflexion verwendet werden. Casting: Ja, möglicherweise ein gutes Argument, um es zu einer expliziten Besetzung zu machen.
gmoody1979