Primitive vs Class pour représenter un objet de domaine simple?

Quelles sont les directives générales ou les règles empiriques concernant l’utilisation d’un objet spécifique à un domaine par rapport à une chaîne ou un nombre en clair?

Exemples:

  • Classe d'âge vs Integer?
  • Classe FirstName vs String?
  • UniqueID vs String
  • Classe PhoneNumber vs String vs Long?
  • Classe DomainName vs String?

Je pense que la plupart des praticiens POO diraient certainement des classes spécifiques pour PhoneNumber et DomainName. Plus il y a de règles sur ce qui les rend valides et comment les comparer, plus il est facile et plus sûr de traiter des classes simples. Mais pour les trois premiers, il y a plus de débat.

Je n'ai jamais rencontré de classe "Age", mais on pourrait dire que c'est logique étant donné que cela doit être non négatif (d'accord, je sais que vous pouvez argumenter pour des âges négatifs, mais c'est un bon exemple, cela équivaut presque à un entier primitif).

Une chaîne est commune pour représenter le "prénom" mais ce n'est pas parfait car une chaîne vide est une chaîne valide mais pas un nom valide. La comparaison serait généralement faite en ignorant les cas. Bien sûr, il existe des méthodes pour vérifier les vides, effectuer des comparaisons sans distinction de casse, etc., mais le consommateur doit le faire.

La réponse dépend-elle de l'environnement? Je suis principalement concerné par les logiciels d'entreprise/de grande valeur qui vont vivre et être maintenus pendant peut-être plus d'une décennie.

Je réfléchis peut-être à cela, mais j'aimerais vraiment savoir si quelqu'un a des règles sur le moment où il convient de choisir la classe par rapport à la classe primitive.

13
Je vois beaucoup d’écrits sous cette question, mais la réponse n’est pas si compliquée que cela. Vous utilisez une classe Age au lieu d'un entier lorsque vous avez besoin du comportement supplémentaire qu'une telle classe fournirait.
ajouté l'auteur sgwill, source
@PeterM, c'est une bonne information! Dates/heures et maintenant les âges. Ce devraient être des concepts simples, mais cela prouve qu'il y a dans le monde beaucoup plus de complexité que ce à quoi nous sommes habitués.
ajouté l'auteur Eric Shain, source
Pourquoi diable utiliseriez-vous un entier pour modéliser une période lorsqu'un type de période existe?
ajouté l'auteur CodeCharming, source
"une chaîne vide est une chaîne valide mais pas un nom valide" Les personnes n'ont absolument pas besoin de prénoms ni de noms de famille. Les mononymes sont une chose peu commune, mais réelle.
ajouté l'auteur Jacob, source
En fait, une classe d'âge a un sens étrange. La définition occidentale la plus commune de l'âge est zéro à la naissance et ajoutez 1 à votre prochain anniversaire. Selon la méthodologie est-asiatique, vous êtes 1 à la naissance et 1 est ajouté au prochain jour de l'An. Ainsi, selon votre région, votre âge peut être calculé différemment. EG à partir de en.wikipedia.org/wiki/East_Asian_age_reckoning les personnes peuvent être un ou deux années de plus en Asie que dans le système d’âge occidental
ajouté l'auteur Peter M, source
La classe d'âge n'est pas une bonne idée si elle peut être remplacée par un entier. S'il faut une date de naissance dans le constructeur et que les méthodes getCurrentAge() et getAgeAt (Date date) ont une signification. Mais il ne peut pas être remplacé par un entier.
ajouté l'auteur kiwiron, source
"Une chaîne n'est parfois pas une chaîne. Modélise-la en conséquence.". Mark Seemann a écrit un article intéressant sur «l'obsession primitive»: blog.ploeh.dk/2015/01/19/…
ajouté l'auteur user3316066, source
Super article @SergeyShushlyapin ... BTW Je suis très biaisé en faveur des classes de domaine au lieu des primitives ... ces commentaires et réponses aident à articuler ce que je n'ai pas été capable de bien articuler moi-même.
ajouté l'auteur booya, source
@kiwiron Je suis d'accord avec vous. La classe "Âge" était quelque chose que j'avais imaginé pour avoir des exemples bien configurés allant de "primitif, c'est probablement bien" (Âge) à "primitif, absolument pas correct"? (Nom de domaine, numéro de téléphone).
ajouté l'auteur booya, source

7 Réponses

Quelles sont les directives générales ou les règles empiriques concernant le moment d'utiliser un objet spécifique à un domaine par rapport à une chaîne ou un nombre simple?

La règle générale est que vous souhaitez modéliser votre domaine dans un langage spécifique à ce domaine.

Considérez: pourquoi utilisons-nous un entier? Nous pouvons aussi facilement représenter tous les entiers avec des chaînes. Ou avec des octets.

Si nous programmions dans un langage agnostique de domaine incluant des types primitifs pour les entiers et , lequel choisiriez-vous?

Il s’agit vraiment de la nature "primitive" de certains types est un accident du choix du langage pour notre implémentation.

Les chiffres, en particulier, nécessitent généralement un contexte supplémentaire. L’âge n’est pas simplement un nombre, mais il a aussi une dimension (temps), des unités (années?), Des règles d’arrondi! L’ajout des âges est logique, de même que l’ajout d’un âge à un argent ne l’est pas.

La distinction des types nous permet de modéliser les différences entre un adresse électronique non vérifiée et adresse électronique vérifiée .

L'accident de la manière dont ces valeurs sont représentées en mémoire est l'une des parties les moins intéressantes. Le modèle de domaine ne prend pas en compte qu'un CustomerId soit un entier, une chaîne, un UUID/GUID ou un noeud JSON. Il veut juste les prix.

Est-ce que nous nous soucions vraiment de savoir si les entiers sont big endian ou little endian? Est-ce que cela nous importe que la Liste qui nous a été transmise soit une abstraction sur un tableau ou un graphique? Lorsque nous découvrons que l'arithmétique en double précision est inefficace et qu'il est nécessaire de passer à une représentation en virgule flottante, le modèle de domaine doit-il prendre en charge ?

Parnas, en 1972 , a écrit

Nous proposons plutôt de commencer par une liste de décisions de conception difficiles ou de décisions de conception susceptibles de changer. Chaque module est ensuite conçu pour masquer une telle décision des autres.

En un sens, les types de valeur spécifiques au domaine que nous introduisons sont des modules qui isolent notre décision quant à la représentation sous-jacente des données à utiliser.

L’avantage est donc la modularité - nous obtenons une conception dans laquelle il est plus facile de gérer la portée d’un changement. L'inconvénient est le coût: il faut travailler plus pour créer les types sur mesure dont vous avez besoin. Pour choisir les types appropriés, vous devez acquérir une compréhension plus approfondie du domaine. La quantité de travail nécessaire pour créer le module de valeur dépend de votre dialecte local de Blub .

Les autres termes de l'équation peuvent inclure la durée de vie attendue de la solution (modélisation minutieuse des logiciels de script exécutés une fois le retour sur investissement médiocre), la proximité du domaine par rapport à la compétence clé de l'entreprise.

Un cas particulier que nous pourrions envisager est celui de la communication à travers une frontière. . Nous ne voulons pas être dans une situation où des changements sur une unité déployable nécessitent des modifications coordonnées avec d'autres unités déployables. Ainsi, les messages ont tendance à se concentrer davantage sur les représentations, sans tenir compte des invariants ni des comportements propres à un domaine. Nous n'allons pas essayer de communiquer "cette valeur doit être strictement positive" dans le format du message, mais plutôt de communiquer sa représentation sur le réseau et d'appliquer une validation à cette représentation à la limite du domaine.

18
ajouté
Excellent point sur le fait de cacher (ou d’abstruire) ce qui est difficile et susceptible de changer.
ajouté l'auteur MrG, source

Vous pensez à l'abstraction, et c'est génial. Cependant, les éléments que votre fiche me semble être principalement des attributs individuels de quelque chose de plus grand (pas toujours la même chose, bien sûr).

Ce qui me semble le plus important est de fournir une abstraction qui regroupe les attributs et leur donne un logement approprié, tel qu'une classe Person, afin que le programmeur client n'ait pas à gérer plusieurs attributs, mais plutôt une abstraction de niveau supérieur.

Une fois que vous avez cette abstraction, vous n'avez pas nécessairement besoin d'abstractions supplémentaires pour les attributs qui ne sont que des valeurs. La classe Person peut contenir une Date de naissance en tant que Date (primitive), avec PhoneNumbers en tant que liste de chaînes (primitives) et un Nom en tant que chaîne (primitive).

Si vous avez un numéro de téléphone avec un code de formatage, il s'agit maintenant de deux attributs et mériterait une classe pour le numéro de téléphone (car il aurait probablement aussi un comportement, comme obtenir uniquement des chiffres ou obtenir un numéro de téléphone formaté).

Si vous avez un nom en tant qu'attributs multiples (par exemple, préfixes/titres, premier, dernier, suffixe) qui mériterait une classe (comme maintenant, vous avez certains comportements, comme obtenir le nom complet sous forme de chaîne ou obtenir des morceaux plus petits, un titre, etc. .)

4
ajouté

Vos exemples ressemblent beaucoup plus à des propriétés ou à des attributs d’autre chose. Cela signifie qu'il serait parfaitement raisonnable de commencer par le primitif uniquement. À un moment donné, vous devrez utiliser une primitive. Par conséquent, j’évite généralement la complexité lorsque j’en ai réellement besoin.

Par exemple, disons que je crée un jeu et que je n'ai pas à m'inquiéter du vieillissement des personnages. Utiliser un entier pour cela est parfaitement logique. L'âge pourrait être utilisé dans de simples vérifications pour voir si le personnage est autorisé à faire quelque chose ou non.

Comme d'autres l'ont souligné, l'âge peut être un sujet compliqué en fonction de votre région. Si vous avez besoin de réconcilier les âges dans différents paramètres régionaux, etc., il est parfaitement logique d'introduire une classe Age comportant DateOfBirth et Paramètres régionaux . en tant que propriétés. Cette classe d'âge peut également calculer l'âge actuel à un horodatage donné.

Il existe donc des raisons de promouvoir un primitif dans une classe, mais elles sont très spécifiques à une application. Voici quelques exemples:

  • Vous avez une logique de validation spéciale à appliquer partout où le type d'informations est utilisé (numéros de téléphone, adresses électroniques, par exemple).
  • Vous disposez d'une logique de formatage spéciale utilisée pour que les utilisateurs puissent lire (adresses IP4 et IP6, par exemple)
  • Vous disposez d'un ensemble de fonctions qui se rapportent toutes à ce type d'objet
  • Vous avez des règles spéciales pour comparer des types de valeur similaires

Fondamentalement, si vous avez plusieurs règles sur le traitement d'une valeur que vous souhaitez utiliser de manière cohérente tout au long de votre projet, créez une classe. Si vous pouvez vivre avec des valeurs simples parce que ce n'est pas vraiment critique pour la fonctionnalité, utilisez une primitive.

1
ajouté

En fin de compte, un objet est simplement un témoin du fait qu'un processus a effectivement été exécuté .

Une primitive int signifie qu'un processus a mis à zéro 4 octets de mémoire, puis a retourné les bits nécessaires pour représenter un entier compris entre -2 147 483 648 et 2 147 483 647; ce qui n’est pas une affirmation forte sur le concept d’âge. De même, un System.String (non nul) signifie que certains octets ont été alloués et que les bits ont été retournés pour représenter une séquence de caractères Unicode par rapport à un codage.

Ainsi, lorsque vous décidez comment représenter votre objet de domaine, vous devez réfléchir au processus pour lequel il sert de témoin. Si FirstName doit être une chaîne non vide, le système doit alors être témoin du fait que le système a exécuté un processus garantissant qu'au moins un caractère non blanc a été créé dans une séquence de caractères ont été remis à vous.

De même, un objet Age devrait probablement indiquer qu'un processus a calculé la différence entre deux dates. Puisque ints ne sont généralement pas le résultat du calcul de la différence entre deux dates, ils sont ipso facto insuffisants pour représenter les âges.

Il est donc assez évident que vous souhaitiez presque toujours créer une classe (qui n'est qu'un programme) pour chaque objet de domaine. Donc quel est le problème? Le problème est que C# (que j'adore) et Java demandent trop de cérémonie pour créer des classes. Cependant, nous pouvons imaginer des syntaxes alternatives rendant la définition des objets de domaine beaucoup plus simple:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

Par exemple, F # a effectivement une syntaxe intéressante sous la forme Modèles actifs :

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days/365

Le fait est que ce n'est pas parce que votre langage de programmation est lourd de définir des objets de domaine que c'est une mauvaise idée de le faire.


† Nous appelons également ces choses les conditions de publication , mais je préfère le terme "témoin". car il sert de preuve qu'un processus peut et a été exécuté.

1
ajouté

Vous devez modéliser à votre guise. Si vous voulez simplement quelques données, vous pouvez utiliser des types de primitives, mais si vous voulez effectuer plus d'opérations sur ces données, elles doivent être modélisées dans des classes spécifiques, par exemple (je suis d'accord avec @kiwiron dans son commentaire) et La classe d'âge n'a pas de sens, mais il serait préférable de la remplacer par une classe de date de naissance et vous y mettez ces méthodes.

L’avantage de la modélisation de ces concepts à l’intérieur des classes est que vous appliquez la richesse de votre modèle. Par exemple, vous disposez d’une méthode qui accepte toute date, un utilisateur peut la renseigner avec n’importe quelle date, la date de début WW II ou la date de fin de guerre froide, de ces dates encore une date de naissance valide. vous pouvez le remplacer par Date de naissance afin que, dans ce cas, vous appliquiez votre méthode pour accepter une date de naissance pour n’accepter aucune date.

Pour Firstname, je ne vois pas grand avantage, UniqueID aussi, PhoneNumber un peu, vous pouvez lui demander de vous donner le pays et la région par exemple car en général, PhoneNumber fait référence à des pays et à des régions, certains opérateurs utilisent des suffixes prédéfinis pour classer Mobile. , Office ... numbers, vous pouvez donc ajouter ces méthodes à cette classe, comme pour DomainName.

Pour plus de détails, je vous recommande d’examiner les objets de valeur dans DDD (Domain Driven Design).

1
ajouté

Cela dépend si vous avez un comportement (que vous devez maintenir et/ou modifier) ​​associé à ces données ou non.

En bref, s’il ne s’agit que d’une donnée qui s’agite sans trop de comportement, utilisez un type primitif ou, pour des données un peu plus complexes, une structure de données (en grande partie sans comportement) (par exemple une classe avec uniquement des accesseurs). les setters).

Si, au contraire, vous constatez que, pour prendre des décisions, vous vérifiez ou manipulez ces données à divers endroits de votre code, ces endroits ne sont souvent pas proches les uns des autres - puis transformez-les en un objet approprié en définissant une classe. et mettre le comportement pertinent à l'intérieur en tant que méthodes. Si, pour une raison quelconque, vous estimez qu’il n’est pas logique de définir une telle classe, vous pouvez également consolider ce comportement en une sorte de classe "service" qui exploite ces données.

1
ajouté

Pensez à ce que vous obtenez en utilisant un cours par rapport à un primatif. Si vous utilisez un cours, vous pourrez

  • validation
  • mise en forme
  • autres méthodes utiles
  • type safety (c'est-à-dire avec une classe PhoneNumber , il devrait être impossible pour moi de l'attribuer à une chaîne ou à une chaîne aléatoire (c'est-à-dire "concombre") où j'attends un numéro de téléphone)
  • tout autre avantage

et ces avantages vous simplifient la vie, améliorent la qualité du code, la lisibilité et l'emportent sur la douleur liée à l'utilisation de telles choses, faites-le. Sinon, restez avec les primitifs. Si c'est proche, faites un jugement.

Sachez également que, parfois, une bonne dénomination peut simplement la gérer (c'est-à-dire une chaîne nommée prénom ou la création d'une classe entière qui encapsule simplement une propriété de chaîne). Les cours ne sont pas le seul moyen de résoudre les problèmes.

0
ajouté