Les objets de domaine communs ont des propriétés qui peuvent être représentées par un type intégré mais dont les valeurs valides sont un sous-ensemble des valeurs qui peuvent être représentées par ce type.
Dans ces cas, la valeur peut être stockée en utilisant le type intégré mais il est nécessaire de s'assurer que les valeurs sont toujours validées au point d'entrée, sinon nous pourrions finir par travailler avec une valeur non valide.
Une façon de résoudre ce problème consiste à stocker la valeur en tant que personnalisé struct
qui possède un seul private readonly
champ de sauvegarde du type intégré et dont le constructeur valide la valeur fournie. On peut alors toujours être sûr de n'utiliser que des valeurs validées en utilisant ce struct
type.
Nous pouvons également fournir des opérateurs de transtypage depuis et vers le type intégré sous-jacent afin que les valeurs puissent entrer et sortir en toute transparence en tant que type sous-jacent.
Prenons comme exemple une situation dans laquelle nous devons représenter le nom d'un objet de domaine, et les valeurs valides sont n'importe quelle chaîne de 1 à 255 caractères inclus. Nous pourrions représenter cela en utilisant la structure suivante:
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;
}
}
L'exemple montre que le string
cast comme implicit
cela ne peut jamais échouer mais le string
cast comme explicit
car cela lancera des valeurs invalides, mais bien sûr, ces deux pourraient être soit implicit
ou explicit
.
Notez également que l'on ne peut initialiser cette structure qu'au moyen d'une conversion à partir de string
, mais on peut tester si une telle conversion échouera à l'avance en utilisant la IsValid
static
méthode.
Cela semblerait être un bon modèle pour appliquer la validation des valeurs de domaine qui peuvent être représentées par des types simples, mais je ne le vois pas utilisé souvent ou suggéré et je suis curieux de savoir pourquoi.
Ma question est donc la suivante: quels sont selon vous les avantages et les inconvénients de l'utilisation de ce modèle, et pourquoi?
Si vous pensez que c'est un mauvais schéma, j'aimerais comprendre pourquoi et ce que vous ressentez est la meilleure alternative.
NB: j'ai initialement posé cette question sur Stack Overflow, mais elle a été suspendue car elle était principalement basée sur l'opinion (ironiquement subjective en soi) - j'espère qu'elle pourra avoir plus de succès ici.
Ci-dessus se trouve le texte original, ci-dessous quelques réflexions supplémentaires, en partie en réponse aux réponses reçues avant sa mise en attente:
- L'un des principaux points soulevés par les réponses concernait la quantité de code de plaque de chaudière nécessaire pour le modèle ci-dessus, en particulier lorsque de nombreux types de ce type sont nécessaires. Cependant, pour défendre le modèle, cela pourrait être largement automatisé à l'aide de modèles et en fait, cela ne me semble pas trop mal de toute façon, mais c'est juste mon opinion.
- D'un point de vue conceptuel, ne semble-t-il pas étrange, lorsque l'on travaille avec un langage fortement typé tel que C #, d'appliquer uniquement le principe fortement typé aux valeurs composites, plutôt que de l'étendre à des valeurs qui peuvent être représentées par une instance d'un type intégré?
Réponses:
Ceci est assez courant dans les langages de style ML comme ML / OCaml / F # / Haskell standard où il est beaucoup plus facile de créer les types d'encapsuleurs. Il vous offre deux avantages:
ValidatedName
jamais contient une valeur non valide, vous savez que l'erreur est dans laIsValid
méthode.Si vous obtenez la
IsValid
bonne méthode, vous avez la garantie que toute fonction qui reçoit un reçoitValidatedName
en fait un nom validé.Si vous devez effectuer des manipulations de chaîne, vous pouvez ajouter une méthode publique qui accepte une fonction qui prend une chaîne (la valeur de
ValidatedName
) et renvoie une chaîne (la nouvelle valeur) et valide le résultat de l'application de la fonction. Cela élimine le passe-partout d'obtenir la valeur de chaîne sous-jacente et de la reconditionner.Une utilisation connexe pour les valeurs d'emballage est de suivre leur provenance. Par exemple, les API de système d'exploitation basées sur C donnent parfois des descripteurs de ressources sous forme d'entiers. Vous pouvez encapsuler les API du système d'exploitation pour utiliser à la place une
Handle
structure et fournir uniquement l'accès au constructeur à cette partie du code. Si le code qui produit leHandle
s est correct, alors seuls les descripteurs valides seront utilisés.la source
Bon :
ValidatedString
rend beaucoup plus clair la sémantique de l'appel.Mauvais :
IsValid
avant le casting est un peu maladroit.ValidatedString
n'est pas valide / validée.J'ai vu ce genre de choses plus souvent avec
User
et ceAuthenticatedUser
genre de choses, où l'objet change réellement. Cela peut être une bonne approche, bien qu'elle semble hors de propos en C #.la source
Votre chemin est assez lourd et intensif. Je définis généralement des entités de domaine comme:
Dans le constructeur de l'entité, la validation est déclenchée à l'aide de FluentValidation.NET, pour vous assurer que vous ne pouvez pas créer une entité avec un état non valide. Notez que les propriétés sont toutes en lecture seule - vous ne pouvez les définir que via le constructeur ou les opérations de domaine dédié.
La validation de cette entité est une classe distincte:
Ces validateurs peuvent également être facilement réutilisés et vous écrivez moins de code standard. Et un autre avantage est qu'il est lisible.
la source
J'aime cette approche des types de valeur. Le concept est génial, mais j'ai quelques suggestions / plaintes concernant la mise en œuvre.
Casting : Je n'aime pas utiliser le casting dans ce cas. La conversion explicite à partir d'une chaîne n'est pas un problème, mais il n'y a pas beaucoup de différence entre
(ValidatedName)nameValue
et newValidatedName(nameValue)
. Cela semble donc un peu inutile. La conversion implicite en chaîne est le pire problème. Je pense que l'obtention de la valeur réelle de la chaîne devrait être plus explicite, car elle pourrait accidentellement être affectée à la chaîne et le compilateur ne vous avertira pas d'une éventuelle "perte de précision". Ce type de perte de précision doit être explicite.ToString : Je préfère utiliser des
ToString
surcharges uniquement à des fins de débogage. Et je ne pense pas que renvoyer la valeur brute car c'est une bonne idée. C'est le même problème qu'avec la conversion implicite en chaîne. L'obtention de la valeur interne doit être une opération explicite. Je crois que vous essayez de faire en sorte que la structure se comporte comme une chaîne normale avec le code extérieur, mais je pense qu'en faisant cela, vous perdez une partie de la valeur que vous obtenez en implémentant ce type de type.Equals et GetHashCode : les structures utilisent l'égalité structurelle par défaut. Donc, votre
Equals
etGetHashCode
dupliquez ce comportement par défaut. Vous pouvez les supprimer et ce sera à peu près la même chose.la source