Utilisation de struct pour appliquer la validation du type intégré

9

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é structqui possède un seul private readonlychamp 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 structtype.

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 stringcast comme implicitcela ne peut jamais échouer mais le stringcast comme explicitcar cela lancera des valeurs invalides, mais bien sûr, ces deux pourraient être soit implicitou 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 staticmé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é?
gmoody1979
la source
vous pourriez faire une version basée sur un modèle qui prend un bool (T) lambda
ratchet freak

Réponses:

4

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:

  • Il permet à un morceau de code d'imposer qu'une chaîne a subi une validation, sans avoir à prendre soin de cette validation elle-même.
  • Il vous permet de localiser le code de validation en un seul endroit. Si ValidatedNamejamais contient une valeur non valide, vous savez que l'erreur est dans la IsValidméthode.

Si vous obtenez la IsValidbonne méthode, vous avez la garantie que toute fonction qui reçoit un reçoit ValidatedNameen 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 Handlestructure et fournir uniquement l'accès au constructeur à cette partie du code. Si le code qui produit le Handles est correct, alors seuls les descripteurs valides seront utilisés.

Doval
la source
1

quels sont selon vous les avantages et les inconvénients de l'utilisation de ce modèle, et pourquoi?

Bon :

  • Il est autonome. Trop de bits de validation ont des vrilles atteignant différents endroits.
  • Il aide à l'auto-documentation. Voir une méthode prendre un ValidatedStringrend beaucoup plus clair la sémantique de l'appel.
  • Il permet de limiter la validation à un seul endroit plutôt que de devoir être dupliqué entre les méthodes publiques.

Mauvais :

  • La ruse du casting est cachée. Ce n'est pas idiomatique en C #, cela peut donc prêter à confusion lors de la lecture du code.
  • Ça jette. Avoir des chaînes qui ne répondent pas à la validation n'est pas un scénario exceptionnel. Faire IsValidavant le casting est un peu maladroit.
  • Il ne peut pas vous dire pourquoi quelque chose n'est pas valide.
  • La valeur par défaut ValidatedStringn'est pas valide / validée.

J'ai vu ce genre de choses plus souvent avec Useret ce AuthenticatedUsergenre de choses, où l'objet change réellement. Cela peut être une bonne approche, bien qu'elle semble hors de propos en C #.

Telastyn
la source
1
Merci, je pense que votre quatrième "con" est l'argument le plus convaincant à ce jour - l'utilisation de default ou d'un tableau du type pourrait vous donner des valeurs invalides (selon que la chaîne zéro / nulle est une valeur valide bien sûr). Ce sont (je pense) les deux seules façons de se retrouver avec une valeur invalide. Mais alors, si nous n'utilisions PAS ce modèle, ces deux choses nous donneraient toujours des valeurs invalides, mais je suppose qu'au moins nous saurions qu'elles devaient être validées. Donc, cela pourrait potentiellement invalider l'approche où la valeur par défaut du type sous-jacent n'est pas valide pour notre type.
gmoody1979
Tous les inconvénients sont des problèmes de mise en œuvre plutôt que des problèmes avec le concept. De plus, je trouve que les «exceptions devraient être exceptionnelles» est un concept flou et mal défini. L'approche la plus pragmatique consiste à fournir à la fois une méthode basée sur les exceptions et non basée sur les exceptions et à laisser l'appelant choisir.
Doval
@Doval Je suis d'accord sauf comme indiqué dans mon autre commentaire. L'intérêt du modèle est de savoir avec certitude que si nous avons un ValidatedName, il doit être valide. Cela tombe en panne si la valeur par défaut du type sous-jacent n'est pas également une valeur valide du type de domaine. Cela dépend bien sûr du domaine mais est plus susceptible d'être le cas (j'aurais pensé) pour les types basés sur des chaînes que pour les types numériques. Le modèle fonctionne mieux lorsque la valeur par défaut du type sous-jacent convient également comme valeur par défaut du type de domaine.
gmoody1979
@Doval - Je suis généralement d'accord. Le concept lui-même est bien, mais il essaie effectivement de raffiner les types de chausse-pied dans un langage qui ne les prend pas en charge. Il y aura toujours des problèmes de mise en œuvre.
Telastyn
Cela dit, je suppose que vous pouvez vérifier la valeur par défaut sur le casting "sortant" et à tout autre endroit nécessaire dans les méthodes de la structure et lancer s'il n'est pas initialisé, mais cela commence à devenir désordonné.
gmoody1979
0

Votre chemin est assez lourd et intensif. Je définis généralement des entités de domaine comme:

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
}

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:

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);
    }  
}

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.

L-Four
la source
Est-ce que l'électeur se soucierait d'expliquer pourquoi ma réponse a été rejetée?
L-Four
La question portait sur une structure pour contraindre les types de valeur, et vous êtes passé à une classe sans expliquer POURQUOI. (Pas un downvoter, juste une suggestion.)
DougM
J'ai expliqué pourquoi je trouvais cela une meilleure alternative, et c'était l'une de ses questions. Merci pour la réponse.
L-Four
0

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)nameValueet new ValidatedName(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 ToStringsurcharges 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 Equalset GetHashCodedupliquez ce comportement par défaut. Vous pouvez les supprimer et ce sera à peu près la même chose.

Euphorique
la source
Casting: Sémantiquement, cela me semble plus comme la transformation d'une chaîne en un ValidatedName plutôt que la création d'un nouveau ValidatedName: nous identifions une chaîne existante comme étant un ValidatedName. Par conséquent, le casting me semble plus correct sémantiquement. D'accord, il y a peu de différence dans la frappe (des doigts sur la variété du clavier). Je ne suis pas d'accord sur le cast to-string: ValidatedName est un sous-ensemble de chaîne, donc il ne peut jamais y avoir de perte de précision ...
gmoody1979
ToString: Je ne suis pas d'accord. Pour moi, ToString est une méthode parfaitement valide à utiliser en dehors des scénarios de débogage, en supposant qu'elle correspond à l'exigence. De plus, dans cette situation où un type est un sous-ensemble d'un autre type, je pense qu'il est logique de transformer la capacité du sous-ensemble au super-ensemble aussi facilement que possible, de sorte que si l'utilisateur le souhaite, il puisse presque le traiter comme du type super-set, c'est-à-dire chaîne ...
gmoody1979
Equals et GetHashCode: Oui, les structures utilisent l'égalité structurelle, mais dans ce cas, il s'agit de comparer la référence de chaîne, pas la valeur de la chaîne. Par conséquent, nous devons remplacer Equals. Je suis d'accord que cela ne serait pas nécessaire si le type sous-jacent était un type de valeur. D'après ma compréhension de l'implémentation GetHashCode par défaut pour les types de valeur (qui est assez limitée), cela donnera la même valeur mais sera plus performant. Je devrais vraiment tester si c'est le cas, mais c'est un problème secondaire par rapport au point principal de la question. Merci pour votre réponse d'ailleurs :-).
gmoody1979
@ gmoody1979 Les structures sont comparées en utilisant Equals sur chaque champ par défaut. Cela ne devrait pas être un problème avec les chaînes. Même chose avec GetHashCode. Quant à la structure étant un sous-ensemble de chaîne. J'aime à considérer le type comme un filet de sécurité. Je ne veux pas travailler avec ValidatedName, puis glisser accidentellement pour utiliser une chaîne. Je préférerais que le compilateur me fasse explicitement spécifier que je veux maintenant travailler avec des données non contrôlées.
Euphoric
Désolé oui, bon point sur Equals. Bien que le remplacement devrait mieux fonctionner étant donné que le comportement par défaut doit utiliser la réflexion pour faire la comparaison. Casting: oui peut-être un bon argument pour en faire un casting explicite.
gmoody1979