web-dev-qa-db-fra.com

Quand l'obsession primitive n'est-elle pas une odeur de code?

J'ai lu beaucoup d'articles récemment qui décrivent obsession primitive comme une odeur de code.

Il y a deux avantages à éviter l'obsession primitive:

  1. Cela rend le modèle de domaine plus explicite. Par exemple, je peux parler à un analyste commercial d'un code postal au lieu d'une chaîne qui contient un code postal.

  2. Toute la validation se fait en un seul endroit plutôt qu'à travers l'application.

Il y a beaucoup d'articles qui décrivent quand c'est une odeur de code. Par exemple, je peux voir l'avantage de supprimer l'obsession primitive pour un code postal comme celui-ci:

public class Address
{
    public ZipCode ZipCode { get; set; }
}

Voici le constructeur du ZipCode:

public ZipCode(string value)
    {
        // Perform regex matching to verify XXXXX or XXXXX-XXXX format
        _value = value;
    }

Vous briseriez le principe SEC mettant cette logique de validation partout où un code postal est utilisé.

Mais qu'en est-il des objets suivants:

  1. Date de naissance: vérifiez que la date est supérieure à celle indiquée et inférieure à la date d'aujourd'hui.

  2. Salaire: Vérifiez que supérieur ou égal à zéro.

Souhaitez-vous créer un objet DateOfBirth et un objet Salary? L'avantage est que vous pouvez en parler lors de la description du modèle de domaine. Cependant, est-ce un cas de sur-ingénierie car il n'y a pas beaucoup de validation. Existe-t-il une règle qui décrit quand et quand ne pas supprimer l'obsession primitive ou devez-vous toujours le faire si possible?

Je suppose que je pourrais créer un alias de type au lieu d'une classe, ce qui aiderait au point un ci-dessus.

22
w0051977

L'obsession primitive utilise des types de données primitifs pour représenter des idées de domaine.

L'inverse serait la "modélisation de domaine", ou peut-être "sur l'ingénierie".

Souhaitez-vous créer un objet DateOfBirth et un objet Salary?

L'introduction d'un objet Salary peut être une bonne idée pour la raison suivante: les nombres sont rarement seuls dans le modèle de domaine, ils ont presque toujours une dimension et une unité. Normalement, nous ne modélisons rien d'utile si nous ajoutons une longueur à un temps ou à une masse, et nous obtenons rarement de bons résultats lorsque nous mélangeons mètres et pieds

Quant à DateOfBirth, probablement - il y a deux questions à considérer. Tout d'abord, la création d'une date non primitive vous permet de centrer toutes les préoccupations étranges liées aux mathématiques de la date. De nombreuses langues en fournissent une prête à l'emploi; DateTime , Java.util.Date . Ce sont des implémentations de dates indépendantes du domaine, mais ce ne sont pas pas primitives.

Deuxièmement, DateOfBirth n'est pas vraiment une date-heure; ici aux États-Unis, la "date de naissance" est une construction culturelle/fiction juridique. Nous avons tendance à mesurer la date de naissance à partir de la date locale de naissance d'une personne; Bob, né en Californie, pourrait avoir une date de naissance "antérieure" à Alice, née à New York, même s'il est le plus jeune des deux.

Existe-t-il une règle qui décrit quand et quand ne pas supprimer l'obsession primitive ou devez-vous toujours le faire si possible.

Certainement pas toujours; aux limites, les applications ne sont pas orientées objet . Il est assez courant de voir des primitives utilisées pour décrire les comportements dans tests .

17
VoiceOfUnreason

Pour être honnête: cela dépend.

Il y a toujours un risque de sur-ingénierie de votre code. Dans quelle mesure DateOfBirth et Salary seront-ils utilisés? Les utiliserez-vous uniquement dans trois classes étroitement couplées, ou seront-elles utilisées partout dans l'application? Pourriez-vous les "encapsuler" simplement dans leur propre type/classe pour appliquer cette contrainte, ou pouvez-vous penser à plus de contraintes/fonctions qui y appartiennent réellement?

Prenons l'exemple du Salaire: avez-vous des opérations avec "Salaire" (par exemple, gérer différentes devises, ou peut-être une fonction toString ())? Considérez ce qu'est/fait Salary lorsque vous ne le regardez pas comme une simple primitive, et il y a de bonnes chances pour que Salary soit sa propre classe.

7
CharonX

Une règle de base possible peut dépendre de la couche du programme. Pour --- Domain (DDD) aka Entities Layer (Martin, 2018), cela pourrait aussi bien être "pour éviter les primitives pour tout ce qui représente un domaine/concept d'entreprise". Les justifications sont celles énoncées par le PO: un modèle de domaine plus expressif, la validation des règles métier, rendant les concepts implicites explicites (Evans, 2004).

Un alias de type peut être une alternative légère (Ghosh, 2017) et refactorisé en classe d'entité si nécessaire. Par exemple, nous pouvons d'abord exiger que Salary soit >=0, Puis décider de interdire $100.33333 Et tout ce qui dépasse $10,000,000 (Ce qui mettrait le client en faillite). L'utilisation de la primitive Nonnegative pour représenter Salary et d'autres concepts compliquerait cette refactorisation.

L'évitement des primitives peut également aider à éviter une ingénierie excessive. Supposons que nous devons combiner Salary et Date of Birth dans une structure de données: par exemple, pour avoir moins de paramètres de méthode ou pour passer des données entre les modules. Ensuite, nous pouvons utiliser un Tuple de type (Salary, DateOfBirth). En effet, un Tuple avec des primitives, (Nonnegative, Nonnegative), N'est pas informatif, alors que certains class EmployeeData Gonflés masqueraient entre autres les champs requis. La signature dans say calcPension(d: (Salary, DateOfBirth)) est plus ciblée que dans calcPension(d: EmployeeData), ce qui viole le principe de ségrégation d'interface. De même, un class SalaryAndDateOfBirth Spécialisé semble maladroit et est probablement une surpuissance. Plus tard, nous pouvons choisir de définir une classe de données; les tuples et les types de domaines élémentaires nous permettent de différer ces décisions.

Dans une couche externe (par exemple GUI), il peut être judicieux de "dépouiller" les entités jusqu'à leurs primitives constitutives (par exemple pour les mettre dans un DAO). Cela empêche les fuites d'abstractions de domaine dans les couches externes, comme le préconise Martin (2018).

Références
E. Evans, "Conception pilotée par le domaine", 2004
RÉ. Ghosh, "Modélisation fonctionnelle et réactive des domaines", 2017
R. C. Martin, "Architecture propre", 2018

5
esc_space

Mieux vaut souffrir de Obsession primitive ou être Astronaute d'architecture?

Les deux cas sont pathologiques, dans un cas, vous avez trop peu d'abstractions, ce qui conduit à la répétition et à confondre facilement un Apple avec une orange, et dans l'autre, vous avez déjà oublié de vous arrêter et de commencer à obtenir des choses fait, ce qui rend difficile de faire quoi que ce soit.

Comme presque toujours, vous voulez la modération, une voie médiane, espérons-le, bien réfléchie.

N'oubliez pas qu'une propriété a un nom, en plus d'un type. En outre, la décomposition d'une adresse en ses parties constituantes peut être trop contraignante si elle est toujours effectuée de la même manière. Tout le monde n'est pas au centre-ville de New York.

4
Deduplicator

Si vous aviez une classe Salary, elle pourrait avoir des méthodes comme ApplyRaise.

D'autre part, votre classe ZipCode n'a pas besoin d'avoir une validation interne pour éviter de dupliquer la validation partout où vous pourriez avoir une classe ZipCodeValidator qui pourrait être injectée, donc si votre système doit fonctionner à la fois sur les adresses américaines et britanniques, vous pouvez simplement injecter le validateur correct et lorsque vous devez gérer les adresses AUS, vous pouvez simplement ajouter un nouveau validateur.

Une autre préoccupation est que si vous devez écrire des données dans une base de données via EntityFramework, il devra savoir comment gérer le salaire ou le ZipCode.

Il n'y a pas de réponse claire pour savoir où tracer la ligne entre les classes intelligentes, mais je dirai que j'ai tendance à déplacer la logique métier, comme la validation, vers des classes de logique métier dont les classes de données sont des données pures comme cela semble pour mieux travailler avec EntityFramework.

Quant à l'utilisation des alias de type, le nom de membre/propriété devrait donner toutes les informations nécessaires sur le contenu, donc je n'utiliserais pas d'alias de type.

3
Bent

(Quelle est probablement la question)

Quand l'utilisation du type primitif n'est-elle pas une odeur de code?

(Réponse)

Lorsque le paramètre n'a pas de règles, utilisez un type primitif.

Utilisez un type primitif pour:

htmlEntityEncode(string value)

Utilisez un objet comme:

numberOfDaysSinceUnixEpoch(SimpleDate value)

Ce dernier exemple contient des règles, c'est-à-dire que l'objet SimpleDate est composé de Year, Month et Day. Grâce à l'utilisation d'Object dans ce cas, le concept de SimpleDate étant valide peut être encapsulé dans l'objet.

2

Mis à part les exemples canoniques d'adresses e-mail ou de codes postaux donnés ailleurs dans cette question, la refactorisation loin de Obsession primitive peut être particulièrement utile avec les identifiants d'entité (voir https://andrewlock.net/using-strongly -typed-entity-ids-to-éviter-primitive-obsession-part-1 / pour un exemple de la façon de le faire dans .NET).

J'ai perdu le compte du nombre de fois où un bogue s'est glissé parce qu'une méthode avait une signature comme celle-ci:

int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
  // implementation here
}

Compile très bien, et si vous n'êtes pas rigoureux avec vos tests unitaires, cela peut aussi réussir. Cependant, refactorisez ces ID d'entité dans des classes spécifiques au domaine, et hé bien, les erreurs de compilation:

LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
  // implementation here
}

Imaginez que cette méthode soit mise à l'échelle jusqu'à 10 paramètres ou plus, tous les types de données int (sans parler de l'odeur de code Long Parameter List ). Cela devient encore pire lorsque vous utilisez quelque chose comme AutoMapper pour permutez entre les objets de domaine et les DTO, et une refactorisation que vous faites n'est pas prise en compte par le mappage automagique.

1
David Keaveny

Vous violeriez le principe DRY en mettant cette logique de validation partout où un code postal est utilisé.

D'un autre côté, lorsque vous traitez avec de nombreux pays différents et leurs différents systèmes de code postal, cela signifie que vous ne pouvez pas valider un code postal sans connaître le pays en question. Votre classe ZipCode doit donc également stocker le pays.

Mais stockez-vous ensuite séparément le pays en tant que partie du Address (dont le code postal fait également partie) et partie du code postal (pour validation)?

  • Si vous le faites, vous violez également DRY. Même si vous ne l'appelez pas une violation DRY (car chaque instance sert un objectif différent) , cela prend encore inutilement de la mémoire supplémentaire, en plus d'ouvrir la porte aux bogues lorsque les deux valeurs de pays sont différentes (ce qu'elles ne devraient logiquement jamais être).
    • Ou, alternativement, cela vous oblige à synchroniser les deux points de données pour vous assurer qu'ils sont toujours les mêmes, ce qui suggère que vous devriez vraiment stocker ces données en un seul point, ce qui va à l'encontre de l'objectif.
  • Si ce n'est pas le cas, ce n'est pas une classe ZipCode mais une classe Address, qui contiendra à nouveau un string ZipCode Ce qui signifie que nous avons bouclé la boucle.

Par exemple, je peux parler à un analyste commercial d'un code postal au lieu d'une chaîne qui contient un code postal.

L'avantage est que vous pouvez en parler lors de la description du modèle de domaine.

Je ne comprends pas votre affirmation sous-jacente selon laquelle lorsqu'un élément d'information a un type de variable donné, vous êtes en quelque sorte obligé de mentionner ce type chaque fois que vous parlez à un analyste commercial.

Pourquoi? Pourquoi ne pouvez-vous pas simplement parler du "code postal" et omettre complètement le type spécifique? Quel genre de discussions avez-vous avec votre analyste commercial (pas technique!) Où le type de propriété est la quintessence de la conversation?

D'où je viens, les codes postaux sont toujours numériques. Nous avons donc le choix, nous pourrions le stocker sous forme de int ou de string. Nous avons tendance à utiliser une chaîne car il n'y a aucune attente d'opérations mathématiques sur les données, mais jamais un analyste d'entreprise m'a dit que cela devait être une chaîne. Cette décision est laissée au développeur (ou sans doute à l'analyste technique, bien que d'après mon expérience, ils ne traitent pas directement avec le vif du sujet).

Un analyste métier ne se soucie pas du type de données, tant que l'application fait ce qu'elle est censée faire.


La validation est une bête délicate à affronter, car elle repose sur ce que les humains attendent.

D'une part, je ne suis pas d'accord avec l'argument de validation comme moyen de montrer pourquoi l'obsession primitive devrait être évitée, car je ne suis pas d'accord que (en tant que vérité universelle) les données doivent toujours être validées à tout moment.

Par exemple, que se passe-t-il s'il s'agit d'une recherche plus compliquée? Plutôt qu'une simple vérification de format, que se passe-t-il si votre validation implique de contacter une API externe et d'attendre une réponse? Voulez-vous vraiment forcer votre application à appeler cette API externe pour chaque ZipCode objet que vous instanciez?
Peut-être que c'est une exigence commerciale stricte, et alors c'est bien sûr justifiable. Mais ce n'est pas une vérité universelle. Il y aura de nombreux cas d'utilisation où cela représente plus un fardeau qu'une solution.

Comme deuxième exemple, lorsque vous entrez votre adresse dans un formulaire, il est courant de saisir votre code postal avant votre pays. Bien qu'il soit agréable d'avoir un retour de validation immédiat dans l'interface utilisateur, ce serait en fait un obstacle pour moi (en tant qu'utilisateur) si l'application m'avertissait d'un "mauvais" format de code postal, car la véritable source du problème est (par exemple) que mon pays n'est pas le pays sélectionné par défaut, et donc la validation s'est produite pour le mauvais pays.
C'est le mauvais message d'erreur, ce qui distrait l'utilisateur et provoque une confusion inutile.

Tout comme la validation perpétuelle n'est pas une vérité universelle, mes exemples ne le sont pas non plus. C'est contextuel. Certains domaines d'application nécessitent avant tout la validation des données. D'autres domaines ne placent pas la validation aussi haut dans la liste des priorités parce que les tracas qu'il entraîne entrent en conflit avec leurs priorités réelles (par exemple, l'expérience utilisateur ou la capacité de stocker initialement des données erronées afin qu'elles puissent être corrigées au lieu de ne jamais les autoriser). stockée)

Date de naissance: vérifiez que la date est supérieure à celle indiquée et inférieure à la date d'aujourd'hui.
Salaire: Vérifiez qu'il est supérieur ou égal à zéro.

Le problème avec ces validations est qu'elles sont incomplètes, redondantes ou indicatives d'un problème beaucoup plus important.

Vérifier qu'une date est supérieure à la mentalité est redondant. La mentalité signifie littéralement que c'est la date la plus petite possible. D'ailleurs, où tracez-vous la ligne de pertinence? Quel est l'intérêt d'empêcher DateTime.MinDate Mais d'autoriser DateTime.MinDate.AddSeconds(1)? Vous choisissez une valeur particulière qui n'est pas particulièrement fausse par rapport à de nombreuses autres valeurs.

Mon anniversaire est le 2 janvier 1978 (ce n'est pas le cas, mais supposons que ce soit le cas). Mais disons que les données de votre application sont fausses, et à la place, il est dit que mon anniversaire est:

  • 1er janvier 1978
  • 1 janv.1722
  • 1 janv.2355

Toutes ces dates sont fausses. Aucun d'eux n'est "plus juste" que l'autre. Mais votre règle de validation n'attraperait que n de ces trois exemples.

Vous avez également complètement omis le contexte de la façon dont vous utilisez ces données. Si ceci est utilisé par ex. un bot de rappel d'anniversaire, je dirais que la validation est inutile car il n'y a pas de mauvaise conséquence particulière à remplir une mauvaise date.
. et vous devez entièrement valider les données. La validation proposée que vous avez maintenant n'est pas adéquate.

Pour un salaire, il y a du bon sens en ce sens qu'il ne peut pas être négatif. Mais si vous vous attendez de manière réaliste à ce que des données absurdes soient entrées, je vous suggère de rechercher la source de ces données absurdes. Parce que s'ils ne peuvent pas leur faire confiance pour entrer des données sensibles, vous ne pouvez pas non plus leur faire confiance pour entrer des données correctes.

Si, à la place, le salaire est calculé par votre application, et qu'il est possible de se retrouver avec un nombre négatif (et correct)), une meilleure approche serait de faire Math.Max(myValue, 0) pour transformer les nombres négatifs en 0, plutôt qu'échouer la validation. Parce que si votre logique a décidé que le résultat est un nombre négatif, l'échec de la validation signifie qu'il devra refaire le calcul, et il n'y a aucune raison de penser qu'il fournira un nombre différent la deuxième fois.
Et s'il arrive avec un nombre différent, cela vous amène à nouveau à soupçonner que le calcul n'est pas cohérent et ne peut donc pas être fiable.

Cela ne veut pas dire que la validation n'est pas utile. Mais la validation inutile est mauvaise, à la fois parce qu'elle demande des efforts sans vraiment résoudre un problème et donne aux gens un faux sentiment de sécurité.

0
Flater