Selon Quand l'obsession primitive n'est-elle pas une odeur de code? , Je devrais créer un objet ZipCode pour représenter un code postal au lieu d’un objet String.
Cependant, dans mon expérience, je préfère voir
public class Address{
public String zipCode;
}
au lieu de
public class Address{
public ZipCode zipCode;
}
parce que je pense que la dernière demande que je passe à la classe ZipCode pour comprendre le programme.
Et je crois que j'ai besoin de passer d'une classe à l'autre pour voir la définition si tous les champs de données primitifs étaient remplacés par une classe, ce qui donne l'impression de souffrir du problème du yo-yo (un anti-motif).
Je voudrais donc déplacer les méthodes ZipCode dans une nouvelle classe, par exemple:
Vieux:
public class ZipCode{
public boolean validate(String zipCode){
}
}
Nouveau:
public class ZipCodeHelper{
public static boolean validate(String zipCode){
}
}
de sorte que seul celui qui doit valider le code postal dépend de la classe ZipCodeHelper. Et j’ai trouvé un autre «avantage» à garder l’obsession primitive: elle garde la classe comme si elle se présentait sous une forme sérialisée, par exemple: une table d’adresses avec une colonne de chaîne zipCode.
Ma question est la suivante: "éviter le problème du yo-yo" (passer d'une définition de classe à une autre) est-il une raison valable d'autoriser "l'obsession primitive"?
la source
Réponses:
L'hypothèse est qu'il n'est pas nécessaire de passer à la classe ZipCode pour comprendre la classe Address. Si ZipCode est bien conçu, le résultat doit être évident en lisant simplement la classe Address.
Les programmes ne sont pas lus de bout en bout - généralement, les programmes sont beaucoup trop complexes pour rendre cela possible. Vous ne pouvez pas garder tout le code d'un programme dans votre esprit en même temps. Nous utilisons donc des abstractions et des encapsulations pour "diviser" le programme en unités significatives, de sorte que vous puissiez consulter une partie du programme (par exemple, la classe Address) sans avoir à lire tout le code dont il dépend.
Par exemple, je suis sûr que vous ne lisez pas le code source de String chaque fois que vous rencontrez String dans le code.
Renommer la classe de ZipCode en ZipCodeHelper suggère désormais deux concepts distincts: un code postal et un assistant de code postal. Donc, deux fois plus complexe. Et maintenant, le système de types ne peut pas vous aider à faire la distinction entre une chaîne arbitraire et un code postal valide car ils ont le même type. C'est là que "l'obsession" est appropriée: vous suggérez une alternative plus complexe et moins sûre simplement parce que vous voulez éviter un simple type d'enveloppe autour d'une primitive.
L'utilisation d'une primitive est justifiée à mon humble avis dans les cas où il n'y a aucune validation ou autre logique dépendant de ce type particulier. Mais dès que vous ajoutez une logique, il est beaucoup plus simple que cette logique soit encapsulée avec le type.
En ce qui concerne la sérialisation, je pense que cela ressemble à une limitation du cadre que vous utilisez. Vous devriez sûrement pouvoir sérialiser un ZipCode sur une chaîne ou le mapper sur une colonne dans une base de données.
la source
ZipCodeHelper
(que je préférerais appelerZipCodeValidator
) pourrait très bien établir une connexion à un service Web pour faire son travail. Cela ne ferait pas partie de la responsabilité unique "détenir les données du code postal". Rendre le système de types non autorisé par des codes postaux invalides peut toujours être obtenu en rendant leZipCode
constructeur l'équivalent du paquet-privé de Java et en l'appelant avec unZipCodeFactory
qui appelle toujours le validateur.ZipCode
serait une "structure de données". etZipCodeHelper
un "objet". Dans tous les cas, je pense que nous convenons qu'il ne devrait pas être nécessaire de passer des connexions Web au constructeur ZipCode.int
comme identifiant permet de multiplier un identifiant par un identifiant ...)Foo
etFooValidator
classes. Nous pourrions avoir uneZipCode
classe qui valide le format et une classe qui rencontre unZipCodeValidator
service Web pour vérifier qu'une mise en forme correcteZipCode
est bien actuelle. Nous savons que les codes postaux changent. Mais dans la pratique, nous allons avoir une liste de codes ZIP valides encapsulés dansZipCode
ou dans une base de données locale.Si peut faire:
Et le constructeur de ZipCode fait:
Ensuite, vous avez cassé l’encapsulation et ajouté une dépendance plutôt stupide à la classe ZipCode. Si le constructeur n'appelle pas,
ZipCodeHelper.validate(...)
vous avez une logique isolée dans son propre îlot sans l'appliquer réellement. Vous pouvez créer des codes postaux invalides.La
validate
méthode doit être une méthode statique sur la classe ZipCode. Maintenant, la connaissance d'un code postal "valide" est intégrée à la classe ZipCode. Étant donné que vos exemples de code ressemblent à Java, le constructeur de ZipCode devrait émettre une exception si un format incorrect est donné:Le constructeur vérifie le format et lève une exception, empêchant ainsi la création de codes postaux non valides. La
validate
méthode statique étant disponible pour d'autres codes, la logique de vérification du format est encapsulée dans la classe ZipCode.Il n'y a pas de "yo-yo" dans cette variante de la classe ZipCode. C'est ce qu'on appelle la programmation orientée objet.
Nous allons également ignorer l'internationalisation ici, ce qui peut nécessiter une autre classe appelée ZipCodeFormat ou PostalService (par exemple, PostalService.isValidPostalCode (...), PostalService.parsePostalCode (...), etc.).
la source
ZipCode.validate
méthode est la pré-vérification qui peut être effectuée avant d'appeler un constructeur qui lève une exception.Si vous vous posez beaucoup de questions sur cette question, le langage que vous utilisez n'est peut-être pas le bon outil pour le poste. Ce type de "primitives de type domaine" est trivialement facile à exprimer, par exemple, en F #.
Vous pouvez y écrire, par exemple:
Ceci est vraiment utile pour éviter les erreurs courantes, comme comparer les identifiants d'entités différentes. Et comme ces primitives typées sont beaucoup plus légères qu'une classe C # ou Java, vous finirez par les utiliser.
la source
ZipCode
?La réponse dépend entièrement de ce que vous voulez réellement faire avec les codes postaux. Voici deux possibilités extrêmes:
(1) Il est garanti que toutes les adresses se trouvent dans un seul pays. Aucune exception du tout. (Par exemple, aucun client étranger ou aucun employé dont l'adresse personnelle est à l'étranger alors qu'ils travaillent pour un client étranger.) Ce pays possède des codes postaux et on peut s'attendre à ce qu'ils ne soient jamais sérieusement problématiques (c'est-à-dire qu'ils ne nécessitent pas de saisie libre comme "actuellement D4B 6N2, mais cela change toutes les 2 semaines"). Les codes postaux ne sont pas utilisés uniquement pour l'adressage, mais également pour la validation des informations de paiement ou à des fins similaires. - Dans ces circonstances, une classe de code postal a beaucoup de sens.
(2) Les adresses pouvant se trouver dans presque tous les pays, des dizaines ou des centaines de schémas d'adressage avec ou sans code postal (et avec des milliers d'exceptions étranges et de cas particuliers) sont pertinents. Un code "ZIP" est uniquement demandé pour rappeler aux gens des pays où les codes ZIP sont utilisés de ne pas oublier de fournir les leurs. Les adresses ne sont utilisées que de sorte que si une personne perd l'accès à son compte et peut prouver son nom et son adresse, l'accès sera restauré. - Dans ces conditions, les classes de code postal pour tous les pays concernés constitueraient un effort énorme. Heureusement, ils ne sont pas du tout nécessaires.
la source
Les autres réponses ont parlé de la modélisation de domaine OO et de l'utilisation d'un type plus riche pour représenter votre valeur.
Je ne suis pas en désaccord, surtout à cause de l'exemple de code que vous avez posté.
Mais je me demande également si cela répond effectivement au titre de votre question.
Considérez le scénario suivant (tiré d'un projet réel sur lequel je travaille):
Vous avez une application distante sur un appareil de terrain qui communique avec votre serveur central. L'un des champs de la base de données pour l'entrée de périphérique est un code postal pour l'adresse à laquelle se trouve l'appareil de terrain. Vous ne vous souciez pas du code postal (ni du reste de l'adresse, d'ailleurs). Toutes les personnes qui s’intéressent à cette question se trouvent de l’autre côté de la frontière HTTP: vous n’êtes que la seule source de vérité des données. Il n'a pas sa place dans votre modélisation de domaine. Vous devez simplement l'enregistrer, le valider, le stocker et, sur demande, le mélanger dans un blob JSON à des points situés ailleurs.
Dans ce scénario, faire beaucoup plus que valider l'insertion avec une contrainte regex SQL (ou son équivalent ORM) constitue probablement une perte excessive de la variété YAGNI.
la source
RegexValidatedString
, contenant la chaîne elle-même et l'expression régulière utilisée pour la valider. Mais à moins que chaque instance ait une expression rationnelle unique (ce qui est possible mais peu probable), cela semble un peu idiot et gaspille en mémoire (et éventuellement en temps de compilation regex). Donc, soit vous placez l'expression rationnelle dans une table distincte et vous laissez une clé de recherche dans chaque instance pour la trouver (ce qui est sans doute pire en raison de l'indirection), soit vous trouvez un moyen de la stocker une fois pour chaque type courant de valeur partageant cette règle - - par exemple. un champ statique sur un type de domaine, ou une méthode équivalente, comme dit IMSoP.L'
ZipCode
abstraction ne peut avoir un sens que si votreAddress
classe n'a pas également uneTownName
propriété. Sinon, vous avez une demi-abstraction: le code postal désigne la ville, mais ces deux informations connexes se trouvent dans des classes différentes. Cela n'a pas vraiment de sens.Cependant, même dans ce cas, ce n'est toujours pas une application correcte (ou plutôt une solution à) l'obsession primitive; qui, si je comprends bien, se concentre principalement sur deux choses:
Votre cas est ni. Une adresse est un concept bien défini avec des propriétés clairement nécessaires (rue, numéro, code postal, ville, état, pays, ...). Il y a peu à aucune raison de briser ces données car il a une seule responsabilité: désigner un endroit sur Terre. Une adresse nécessite tous ces champs pour être significative. Une demi-adresse est inutile.
C’est ainsi que vous savez que vous n’avez pas besoin de subdiviser davantage: une nouvelle division compromettrait l’intention fonctionnelle de la
Address
classe. De même, vous n'avez pas besoin d'utiliser uneName
sous - classe dans laPerson
classe, à moins queName
(sans personne attachée) ne soit un concept significatif dans votre domaine. Ce qui n'est (généralement) pas. Les noms sont utilisés pour identifier les personnes, ils n'ont généralement aucune valeur par eux-mêmes.la source
Name
sous - classe pour être utilisée dans laPerson
classe, à moins que Nom (sans personne attachée) ne soit un concept significatif dans votre domaine ." Lorsque vous avez une validation personnalisée pour les noms, le nom est devenu un concept significatif dans votre domaine. que j'ai explicitement mentionné comme cas d'utilisation valide pour utiliser un sous-type. Deuxièmement, pour la validation du code postal, vous introduisez des hypothèses supplémentaires, telles que les codes postaux devant suivre le format d'un pays donné. Vous abordez un sujet qui dépasse de loin l'objectif de la question d'OP.De l'article:
Le code source est lu beaucoup plus souvent qu'il n'est écrit. Par conséquent, le problème du yo-yo, qui consiste à basculer entre plusieurs fichiers, est une préoccupation.
Cependant, non , le problème du yo-yo semble beaucoup plus pertinent lorsqu'il s'agit de modules ou de classes fortement interdépendants (qui s'appellent entre eux). Ce sont des cauchemars spéciaux à lire, et c’est probablement ce que l’intéressant du problème du yo-yo avait en tête.
Cependant - oui , il est important d'éviter de trop nombreuses couches d'abstraction!
Par exemple, je ne souscris pas à l'hypothèse formulée dans la réponse de mmmaaa selon laquelle "il n'est pas nécessaire de lire la classe ZipCode pour comprendre la classe d'adresse". Mon expérience a été que vous faites - au moins les premières fois où vous lisez le code. Cependant, comme d' autres l' ont noté, il y a des moments où une
ZipCode
classe est appropriée.YAGNI (il n'y en aura pas besoin) est un meilleur modèle à suivre pour éviter le code de lasagne (code avec trop de couches) - les abstractions, telles que les types et les classes sont là pour aider le programmeur, et ne doivent pas être utilisées à moins qu'elles ne le soient une aide.
Je vise personnellement à "enregistrer des lignes de code" (et bien sûr à "enregistrer des fichiers / modules / classes", etc.). Je suis persuadé que certains voudront bien appliquer l'épithète d '"obsédé primitif" - je trouve qu'il est plus important d'avoir un code sur lequel il est facile de raisonner que de s'inquiéter des étiquettes, des motifs et des anti-motifs. Le choix correct du moment où créer une fonction, un module / fichier / classe ou placer une fonction à un emplacement commun est très situationnel. Je vise environ 3-100 fonctions de ligne, 80-500 fichiers de ligne et "1, 2, n" pour un code de bibliothèque réutilisable ( SLOC - sans commentaires ni passe-partout; je souhaite généralement au moins un minimum de SLOC supplémentaire par ligne obligatoire passe-partout).
La plupart des tendances positives sont apparues lorsque les développeurs ont fait exactement cela, quand ils en avaient besoin . Il est beaucoup plus important d'apprendre à écrire du code lisible que d'essayer d'appliquer des modèles sans résoudre le même problème. Tout bon développeur peut implémenter le modèle d'usine sans l'avoir vu auparavant, dans les cas peu communs où il convient parfaitement à son problème. J'ai utilisé le modèle d'usine, le modèle d'observateur, et probablement des centaines d'autres, sans connaître leur nom (c'est-à-dire, existe-t-il un "modèle d'affectation variable"?). Pour une expérience amusante (voyez combien de modèles GoF sont intégrés au langage JS ) , j'ai arrêté de compter après environ 12-15 ans en 2009. Le modèle Factory est aussi simple que de renvoyer un objet depuis un constructeur JS, par exemple - nul besoin de un WidgetFactory.
Donc, oui , parfois,
ZipCode
c'est une bonne classe. Cependant, non , le problème du yo-yo n'est pas strictement pertinent.la source
Le problème du yo-yo n’est pertinent que si vous devez retourner en arrière. Cela est causé par une ou deux choses (parfois les deux):
Si vous pouvez regarder le nom et avoir une idée raisonnable de ce qu'il fait, et que les méthodes et les propriétés font des choses raisonnablement évidentes, vous n'avez pas besoin d'aller voir le code, vous pouvez simplement l'utiliser. C'est tout le sens des classes en premier lieu: ce sont des morceaux de code modulaires qui peuvent être utilisés et développés séparément. Si vous devez regarder autre chose que l'API de la classe pour voir ce qu'elle fait, c'est au mieux un échec partiel.
la source
Rappelez-vous, il n'y a pas de solution miracle. Si vous écrivez une application extrêmement simple qui doit être analysée rapidement, une simple chaîne peut suffire. Toutefois, dans 98% des cas, un objet de valeur, tel que décrit par Eric Evans dans DDD, conviendrait parfaitement. Vous pouvez facilement voir tous les avantages procurés par les objets de valeur en les parcourant.
la source