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 Zip au lieu de un objet String.
Cependant, selon mon expérience, je préfère voir
public class Address{
public String zipCode;
}
au lieu de
public class Address{
public ZipCode zipCode;
}
car je pense que ce dernier me demande de passer à la classe ZipCode pour comprendre le programme.
Et je crois que je dois passer d'une classe à l'autre pour voir la définition si tous les champs de données primitifs ont été remplacés par une classe, ce qui donne l'impression de souffrir du problème yo-yo (un anti-modèle).
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 a besoin de valider le code postal dépendra de la classe ZipCodeHelper. Et j'ai trouvé un autre "avantage" à garder l'obsession primitive: il garde la classe ressemble à sa forme sérialisée, le cas échéant, 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 à l'autre) est-il une raison valable pour autoriser "l'obsession primitive"?
L'hypothèse est que vous ne pas devez yo-yo à la classe ZipCode pour comprendre la classe Address. Si ZipCode est bien conçu, il devrait être évident de ce qu'il fait simplement en lisant la classe Address.
Les programmes ne sont pas lus de bout en bout - généralement, les programmes sont beaucoup trop complexes pour que cela soit 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 "découper" le programme en unités significatives, de sorte que vous pouvez regarder 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 faites pas de yo-yo pour lire le code source de String à chaque fois que vous rencontrez String dans le code.
Renommer la classe de ZipCode en ZipCodeHelper suggère qu'il existe maintenant deux concepts distincts: un code postal et un assistant de code postal. Donc, deux fois plus complexe. Et maintenant, le système de type 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 proposez une alternative plus complexe et moins sûre simplement parce que vous voulez éviter un type de wrapper simple autour d'une primitive.
L'utilisation d'une primitive est IMHO justifiée dans les cas où il n'y a pas de validation ou autre logique selon ce type particulier. Mais dès que vous ajoutez une logique, c'est beaucoup plus simple si cette logique est encapsulée avec le type.
Quant à la sérialisation, je pense que cela ressemble à une limitation dans le cadre que vous utilisez. Vous devriez sûrement être en mesure de sérialiser un ZipCode en une chaîne ou de le mapper à une colonne dans une base de données.
Si peut faire:
new ZipCode("totally invalid Zip code");
Et le constructeur de ZipCode fait:
ZipCodeHelper.validate("totally invalid Zip code");
Ensuite, vous avez rompu l'encapsulation et ajouté une dépendance assez stupide à la classe ZipCode. Si le constructeur ne le fait pas appelle ZipCodeHelper.validate(...)
alors vous avez une logique isolée dans son propre îlot sans vraiment l'appliquer. Vous pouvez créer des codes postaux invalides.
La méthode validate
doit être une méthode statique sur la classe ZipCode. Désormais, la connaissance d'un code postal "valide" est regroupée avec la classe ZipCode. Étant donné que vos exemples de code ressemblent à Java, le constructeur de ZipCode devrait lever une exception si un format incorrect est donné:
public class ZipCode {
private String zipCode;
public ZipCode(string zipCode) {
if (!validate(zipCode))
throw new IllegalFormatException("Invalid Zip code");
this.zipCode = zipCode;
}
public static bool validate(String zipCode) {
// logic to check format
}
@Override
public String toString() {
return zipCode;
}
}
Le constructeur vérifie le format et lève une exception, empêchant ainsi la création de codes postaux non valides, et la méthode statique validate
est disponible pour d'autres codes, de sorte que 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 appropriée.
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.).
Si vous luttez beaucoup avec cette question, peut-être que la langue que vous utilisez n'est pas le bon outil pour le travail? Ce type de "primitives de type domaine" est trivialement facile à exprimer, par exemple, en F #.
Là, vous pourriez, par exemple, écrire:
type ZipCode = ZipCode of string
type Town = Town of string
type Adress = {
zipCode: ZipCode
town: Town
//etc
}
let adress1 = {
zipCode = ZipCode "90210"
town = Town "Beverly Hills"
}
let faultyAdress = {
zipCode = "12345" // <-Compiler error
town = adress1.zipCode // <- Compiler error
}
C'est vraiment utile pour éviter les erreurs courantes, comme comparer les identifiants des différentes entités. Et puisque ces primitives typées sont beaucoup plus légères qu'une classe C # ou Java, vous finirez par les utiliser.
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) Toutes les adresses sont garanties dans un seul pays. Aucune exception du tout. (Par exemple, aucun client étranger, ou aucun employé dont l'adresse privée est à l'étranger alors qu'il travaille pour un client étranger.) Ce pays a 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 sous forme libre comme "actuellement D4B 6N2, mais cela change toutes les 2 semaines"). Les codes postaux sont utilisés non seulement pour l'adressage, mais 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 peuvent être dans presque tous les pays, donc des dizaines ou des centaines de schémas d'adressage avec ou sans codes postaux (et avec des milliers d'exceptions étranges et de cas spéciaux) sont pertinents. Un code postal n'est vraiment demandé que pour rappeler aux personnes des pays où les codes postaux sont utilisés de ne pas oublier de fournir le leur. Les adresses ne sont utilisées que si quelqu'un perd l'accès à son compte et peut prouver son nom et son adresse, l'accès sera rétabli. - Dans ces circonstances, les classes de code postal pour tous les pays concernés seraient un énorme effort. Heureusement, ils ne sont pas du tout nécessaires.
Les autres réponses ont parlé de OO modélisation de domaine et utilisation d'un type plus riche pour représenter votre valeur.
Je ne suis pas en désaccord, surtout compte tenu de l'exemple de code que vous avez publié.
Mais je me demande aussi si cela répond réellement au titre de votre question.
Considérez le scénario suivant (tiré d'un projet réel sur lequel je travaille):
Vous disposez d'une application distante sur un appareil de terrain qui communique avec votre serveur central. L'un des champs de base de données pour l'entrée de l'appareil est un code postal pour l'adresse à laquelle se trouve l'appareil de terrain. Vous ne vous souciez pas du code postal (ou du reste de l'adresse d'ailleurs). Toutes les personnes qui s'en soucient sont de l'autre côté d'une frontière HTTP: il se trouve que vous êtes la seule source de vérité pour les données. Il n'a pas sa place dans la modélisation de votre domaine. Il vous suffit de l'enregistrer, de le valider, de le stocker et, sur demande, de le mélanger dans un blob JSON vers des points ailleurs.
Dans ce scénario, faire bien plus que valider l'insertion avec une contrainte d'expression régulière SQL (ou son équivalent ORM) est probablement exagéré de la variété YAGNI.
L'abstraction ZipCode
ne pouvait avoir de sens que si votre classe Address
n'avait pas non plus de propriété TownName
. 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 alors, 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 n'est ni l'un ni l'autre. Une adresse est un concept bien défini avec des propriétés clairement nécessaires (rue, numéro, Zip, ville, état, pays, ...). Il y a peu ou pas de raison de diviser ces données car elles ont une responsabilité nique: désigner un emplacement sur Terre. Une adresse nécessite tous de 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: le décomposer davantage nuirait à l'intention fonctionnelle de la classe Address
. De même, vous n'avez pas besoin d'une sous-classe Name
à utiliser dans la classe Person
, à moins que Name
(sans personne attachée) soit un concept significatif dans votre domaine. Ce qui n'est pas (généralement). Les noms sont utilisés pour identifier les gens, ils n'ont généralement aucune valeur en eux-mêmes.
De l'article:
Plus généralement, le problème du yo-yo peut également se référer à toute situation dans laquelle une personne doit continuer à basculer entre différentes sources d'information afin de comprendre un concept.
Le code source est lu beaucoup plus souvent qu'il n'est écrit. Ainsi, le problème de yo-yo, d'avoir à basculer entre plusieurs fichiers est une préoccupation.
Cependant, no , le problème yo-yo semble beaucoup plus pertinent lorsqu'il s'agit de modules ou de classes profondément interdépendants (qui font des va-et-vient entre eux). Ce sont des cauchemars spéciaux à lire, et c'est probablement ce que le rédacteur du problème yo-yo avait en tête.
Cependant - oui , il est important d'éviter trop de couches d'abstraction!
Toutes les abstractions non triviales, dans une certaine mesure, sont fuyantes. - la loi des abstractions qui fuient.
Par exemple, je ne suis pas d'accord avec l'hypothèse faite dans la réponse de mmmaaa que "vous n'avez pas besoin de yo-yo pour [ (visitez)] la classe ZipCode pour comprendre la classe Address ". D'après mon expérience, vous faites - au moins les premières fois que vous lisez le code. Cependant, comme d'autres l'ont noté, il y a fois quand une classe ZipCode
est appropriée.
YAGNI (Ya Ain't Gonna Need It) est un meilleur modèle à suivre pour éviter le code Lasagna (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 sauf si elles sont une aide.
Je vise personnellement à "enregistrer des lignes de code" (et bien sûr les "fichiers/modules/classes" associés, etc.). Je suis convaincu qu'il y en a qui appliqueraient à moi l'épithète de "primitif obsédé" - je trouve plus important d'avoir du code qui est facile à raisonner que de se préoccuper des étiquettes, des motifs et des anti-motifs. Le bon choix du moment de créer une fonction, un module/fichier/classe, ou de placer une fonction dans un emplacement commun est très situationnel. Je vise à peu près les fonctions de ligne 3-100, les fichiers de ligne 80-500 et "1, 2, n" pour le code de bibliothèque réutilisable ( SLOC - non compris commentaires ou passe-partout; je veux généralement au moins 1 SLOC supplémentaire minimum par ligne de passe-partout obligatoire). L'important est de garder à l'esprit et de respecter les limites de la cognition humaine lors de l'écriture de code.
La plupart des schémas positifs sont nés des développeurs qui font 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 le cas inhabituel où il convient à son problème. J'ai utilisé le modèle d'usine, le modèle d'observateur, et probablement des centaines de plus, sans connaître leur nom (c'est-à-dire, y a-t-il un "modèle d'affectation variable"?). Pour une expérience amusante - voyez combien de modèles GoF sont intégrés dans le langage JS - J'ai arrêté de compter après environ 12-15 en 2009. Le modèle Factory est aussi simple que de renvoyer un objet à partir d'un constructeur JS, par exemple - pas besoin de WidgetFactory.
Donc - oui , parfois ZipCode
est un bon classe. Cependant, non , le problème yo-yo n'est pas strictement pertinent.
Le problème du yo-yo n'est pertinent que si vous devez basculer d'avant en arrière. Cela est dû à 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 regarder le code, vous pouvez simplement l'utiliser. C'est tout l'intérêt des classes en premier lieu - ce sont des morceaux de code modulaires qui peuvent être utilisés et développés de manière isolée. Si vous devez regarder autre chose que l'API pour que la classe puisse voir ce qu'elle fait, c'est au mieux un échec partiel.