Ü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 readonly
Sicherungsfeld 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 struct
Typ 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 implicit
dies niemals fehlschlagen kann, aber den string
Cast, explicit
da dies für ungültige Werte wirft, aber natürlich können beide entweder implicit
oder 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
static
Methode 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?
Antworten:
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:
ValidatedName
jemals einen ungültigen Wert enthält, wissen Sie, dass der Fehler in derIsValid
Methode liegt.Wenn Sie die
IsValid
Methode richtig verstehen, haben Sie die Garantie, dass jede Funktion, die a empfängt,ValidatedName
tatsä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
Handle
Struktur zu verwenden und nur auf diesen Teil des Codes Zugriff auf den Konstruktor zu gewähren. Wenn der Code, der dasHandle
s erzeugt, korrekt ist, werden immer nur gültige Handles verwendet.quelle
Gut :
ValidatedString
nimmt, wird die Semantik des Aufrufs viel klarer.Schlecht :
IsValid
Vor der Besetzung zu tun ist ein wenig unfreundlich.ValidatedString
ist nicht gültig / validiert.Ich habe solche Dinge öfter mit
User
und solchenAuthenticatedUser
Dingen 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.quelle
Dein Weg ist ziemlich schwer und intensiv. Normalerweise definiere ich Domänenentitäten wie:
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:
Diese Validatoren können auch problemlos wiederverwendet werden, und Sie schreiben weniger Boilerplate-Code. Ein weiterer Vorteil ist, dass es lesbar ist.
quelle
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)nameValue
und neuValidatedName(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
Equals
undGetHashCode
dieses Standardverhalten. Sie können sie entfernen und es wird so ziemlich dasselbe sein.quelle