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


14

Quelles sont les directives générales ou les règles générales pour savoir quand utiliser un objet spécifique au domaine par rapport à une chaîne ou un nombre ordinaire?

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 OOP 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 les classes simples sont faciles à manipuler et plus sûres. Mais pour les trois premiers, il y a plus de débat.

Je n'ai jamais rencontré de classe "Âge" mais on pourrait dire que cela a du sens étant donné qu'elle doit être non négative (d'accord, je sais que vous pouvez plaider pour des âges négatifs, mais c'est un bon exemple qu'il est presque équivalent à un entier primitif).

La chaîne est courante pour représenter le "prénom" mais elle n'est pas parfaite car une chaîne vide est une chaîne valide mais pas un nom valide. La comparaison serait généralement effectuée en ignorant le cas. Bien sûr, il existe des méthodes pour vérifier le vide, faire une comparaison insensible à la casse, etc., mais cela oblige le consommateur à le faire.

La réponse dépend-elle de l'environnement? Je m'intéresse principalement aux logiciels d'entreprise / à haute valeur ajoutée qui vivront et seront maintenus pendant peut-être plus d'une décennie.

Peut-être que j'y réfléchis trop, mais j'aimerais vraiment savoir si quelqu'un a des règles sur le choix de la classe par rapport à la primitive.


2
La classe d'âge n'est pas une bonne idée si elle peut être remplacée par un entier. Si elle prend une date de naissance dans le constructeur et a des méthodes getCurrentAge () et getAgeAt (Date date), alors la classe a un sens. Mais il ne peut pas être remplacé par un entier.
kiwiron

5
"Une chaîne n'est parfois pas une chaîne. Modélisez-la en conséquence.". Il y a un bon article de Mark Seemann sur «l'obsession primitive»: blog.ploeh.dk/2015/01/19/…
Serhii Shushliapin

4
En fait, une classe d'âge a un sens étrange. La définition occidentale courante de l'âge est zéro à la naissance et ajoutez 1 à votre prochain anniversaire. La méthodologie de l'Asie de l'Est est que vous êtes 1 à la naissance et 1 est ajouté le jour du Nouvel An suivant. Ainsi, selon votre localité, votre âge peut être calculé différemment. EG de en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
Peter M

@PeterM, c'est une bonne info! Dates / heures et vieillit maintenant. Ils devraient être des concepts simples, mais cela prouve qu'il y a beaucoup plus de complexité dans le monde que ce que nous avons l'habitude de gérer.
Machado

6
Je vois beaucoup d'écrits sous cette question, mais la réponse n'est pas vraiment compliquée. Vous utilisez une Ageclasse au lieu d'un entier lorsque vous avez besoin du comportement supplémentaire qu'une telle classe fournirait.
Robert Harvey

Réponses:


20

Quelles sont les directives générales ou les règles générales pour savoir quand utiliser un objet spécifique au domaine par rapport à une chaîne ou un nombre ordinaire?

La règle générale est que vous souhaitez modéliser votre domaine dans une langue spécifique au domaine.

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

Si nous programmions dans un langage agnostique de domaine qui incluait des types primitifs pour l'entier et l' âge, lequel choisiriez-vous?

Ce qui se résume vraiment à la nature "primitive" de certains types, c'est un accident du choix du langage pour notre implémentation.

Les nombres, en particulier, nécessitent généralement un contexte supplémentaire. L'âge n'est pas seulement un nombre mais il a aussi une dimension (temps), des unités (années?), règles d'arrondi! Ajouter des âges ensemble est logique d'une manière qui n'ajoute pas un âge à un argent.

Rendre les types distincts nous permet de modéliser les différences entre un adresse e-mail non vérifiée et une adresse e-mail vérifiée .

L'accident de la façon 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 se soucie pas d'unCustomerId est un int, ou une chaîne, ou un UUID / GUID, ou un nœud JSON. Il veut juste les moyens.

Nous soucions-nous vraiment de savoir si les entiers sont gros ou petits? Sommes-nous soucieux si leList nous avons été passés est une abstraction sur un tableau ou un graphique? Lorsque nous découvrons que l'arithmétique à double précision est inefficace et que nous devons passer à une représentation en virgule flottante, le modèle de domaine devrait-il s'en soucier ?

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 alors conçu pour cacher une telle décision aux autres.

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

L'avantage est donc la modularité - nous obtenons une conception où il est plus facile de gérer la portée d'un changement. L'inconvénient est le coût - il faut plus de travail pour créer les types sur mesure dont vous avez besoin, le choix des types appropriés nécessite une compréhension plus approfondie du domaine. La quantité de travail requise pour créer le module de valeur dépendra de votre dialecte local de Blub .

D'autres termes dans l'équation peuvent inclure la durée de vie prévue de la solution (une modélisation minutieuse des logiciels de script qui seront exécutés une fois que le retour sur investissement est moche), la proximité du domaine avec les compétences de base de l'entreprise.

Un cas particulier que nous pourrions considérer est celui de la communication à travers une frontière . Nous ne voulons pas être dans une situation où les changements apportés à une unité déployable nécessitent des changements coordonnés avec d'autres unités déployables. Les messages ont donc tendance à se concentrer davantage sur les représentations, sans tenir compte des invariants ou des comportements spécifiques au domaine. Nous n'allons pas essayer de communiquer «cette valeur doit être strictement positive» dans le format du message, mais plutôt communiquer sa représentation sur le câble et appliquer la validation à cette représentation à la limite du domaine.


Excellent point sur le fait de cacher (ou d'abstraire) les choses difficiles et susceptibles de changer.
user949300

4

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

Ce qui me semble le plus important, c'est de fournir une abstraction qui regroupe les attributs et leur donne un foyer approprié, comme 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 un BirthDate en tant que date (primitive), ainsi que 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 mise en forme, il s'agit maintenant de deux attributs et mériterait une classe pour le numéro de téléphone (car il aurait probablement un certain comportement également, comme obtenir uniquement les numéros et obtenir le 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ériteraient une classe (comme maintenant vous avez certains comportements tels que l'obtention du nom complet sous forme de chaîne par rapport à l'obtention de plus petits morceaux, titre, etc. .).


1

Vous devez modéliser comme vous le souhaitez, si vous voulez simplement des données, vous pouvez utiliser des types primitifs, 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) un La classe d'âge n'a pas de sens, mais il serait préférable de la remplacer par une classe Birthdate 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, à la place, vous avez une méthode qui accepte n'importe quelle date, un utilisateur peut la remplir avec n'importe quelle date, date de début de la Seconde Guerre mondiale ou date de fin de la guerre froide, de ces dates est toujours une date de naissance valide. vous pouvez le remplacer par Birthdate donc dans ce cas, vous appliquez votre méthode pour accepter une date de naissance pour ne pas accepter de date.

Pour le prénom, je ne vois pas beaucoup d'avantages, UniqueID aussi, PhoneNumber un peu, vous pouvez lui demander de vous donner le pays et la région par exemple parce qu'en général PhoneNumber fait référence aux pays et régions, certains opérateurs utilisent des suffixes prédéfinis pour classer Mobile , Office ... numéros, vous pouvez donc ajouter ces méthodes à cette classe, de même pour DomainName.

Pour plus de détails, je vous recommande de jeter un œil aux objets de valeur dans DDD (Domain Driven Design).


1

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

En bref, s'il s'agit simplement d'une donnée qui est lancée sans que beaucoup de comportements ne lui soient associés, utilisez un type primitif ou, pour les données un peu plus complexes, une structure de données (largement sans comportement) (par exemple une classe avec uniquement des getters et setters).

Si, d'autre part, vous constatez que, pour prendre des décisions, vous vérifiez ou manipulez ces données à différents endroits de votre code, des endroits souvent pas proches les uns des autres - alors transformez-les en un objet approprié en définissant une classe et mettre le comportement pertinent à l'intérieur comme méthodes. Si, pour une raison quelconque, vous estimez qu'il n'est pas logique de définir une telle classe, une alternative consiste à consolider ce comportement dans une sorte de classe "service" qui fonctionne sur ces données.


1

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 la primitive. À un moment donné, vous devrez utiliser une primitive, donc j'économise généralement la complexité lorsque j'en ai réellement besoin.

Par exemple, disons que je fais un jeu et que je n'ai pas à me soucier 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 Ageclasse qui a DateOfBirthet Localecomme propriétés. Cette classe d'âge peut également calculer l'âge actuel à n'importe quel horodatage donné.

Il y a donc des raisons de promouvoir une primitive dans une classe, mais elles sont très spécifiques à l'application. Voici quelques exemples:

  • Vous disposez d'une logique de validation spéciale qui doit être appliquée partout où le type d'informations est utilisé (par exemple, numéros de téléphone, adresses e-mail).
  • Vous disposez d'une logique de formatage spéciale qui est utilisée pour le rendu que les humains peuvent lire (par exemple, les adresses IP4 et IP6)
  • 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 valeurs similaires

Fondamentalement, si vous avez un tas de règles sur la façon dont une valeur doit être traitée 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 car ce n'est pas vraiment critique pour la fonctionnalité, utilisez une primitive.


1

À la fin de la journée, un objet n'est qu'un témoin qu'un processus a été exécuté .

Un primitif int signifie que certains processus ont mis à zéro 4 octets de mémoire, puis ont inversé 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 de l'âge. De même, un (non nul) System.Stringsignifie que certains octets ont été alloués et des bits ont été inversés pour représenter une séquence de caractères Unicode par rapport à un certain codage.

Ainsi, lorsque vous décidez de la façon de représenter votre objet de domaine, vous devez penser au processus pour lequel il sert de témoin. Si unFirstName chaîne doit vraiment être non vide, elle doit témoigner que le système a exécuté un processus qui garantit qu'au moins un caractère non blanc a été créé dans une séquence de caractères qui vous a été remise.

De même, un Age objet devrait probablement être témoin qu'un processus a calculé la différence entre deux dates. Comme intsils 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 voulez 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. Mais, nous pouvons imaginer des syntaxes alternatives qui rendraient 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);

F #, par exemple, a en fait une belle syntaxe sous la forme de 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 le simple fait que votre langage de programmation complique la définition des objets de domaine ne signifie pas que ce soit vraiment une mauvaise idée de le faire.


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


0

Pensez à ce que vous obtenez en utilisant une classe par rapport à une primitive. Si utiliser un cours peut vous aider

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

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

Sachez également que parfois un bon nom peut simplement le gérer (c'est-à-dire une chaîne nommée firstNamevs faire 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 choses.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.