Récemment, j'ai eu un problème avec la lisibilité de mon code.
J'ai eu une fonction qui a fait une opération et a renvoyé une chaîne représentant l'ID de cette opération pour référence future (un peu comme OpenFile dans Windows renvoyant un handle). L'utilisateur utilisera cet ID plus tard pour démarrer l'opération et pour surveiller sa fin.
L'ID devait être une chaîne aléatoire en raison de problèmes d'interopérabilité. Cela a créé une méthode qui avait une signature très peu claire comme ceci:
public string CreateNewThing()
Cela rend l'intention du type de retour peu claire. J'ai pensé envelopper cette chaîne dans un autre type qui rend son sens plus clair comme ceci:
public OperationIdentifier CreateNewThing()
Le type ne contiendra que la chaîne et sera utilisé chaque fois que cette chaîne est utilisée.
Il est évident que l'avantage de ce mode de fonctionnement est une plus grande sécurité de type et une intention plus claire, mais cela crée également beaucoup plus de code et de code qui n'est pas très idiomatique. D'une part, j'aime la sécurité supplémentaire, mais cela crée également beaucoup d'encombrement.
Pensez-vous que c'est une bonne pratique d'envelopper des types simples dans une classe pour des raisons de sécurité?
Les primitives, telles que string
ou int
, n'ont aucune signification dans un domaine métier. Une conséquence directe de cela est que vous pouvez utiliser par erreur une URL lorsqu'un ID de produit est attendu, ou utiliser la quantité lorsque vous attendez un prix .
C'est aussi pourquoi défi Object Calisthenics présente le conditionnement des primitives comme l'une de ses règles:
Règle 3: Envelopper toutes les primitives et chaînes
Dans le langage Java, int est un objet primitif et non un objet réel, il obéit donc à des règles différentes des objets. Il est utilisé avec une syntaxe qui n'est pas orientée objet. Plus important encore, un int à lui seul n'est qu'un scalaire, il n'a donc aucune signification. Lorsqu'une méthode prend un int comme paramètre, le nom de la méthode doit faire tout le travail pour exprimer l'intention. Si la même méthode prend une heure comme paramètre , il est beaucoup plus facile de voir ce qui se passe.
Le même document explique qu'il y a un avantage supplémentaire:
Les petits objets comme Hour ou Money nous donnent également un endroit évident pour mettre un comportement qui aurait autrement été jeté autour d'autres classes .
En effet, lorsque des primitives sont utilisées, il est généralement extrêmement difficile de suivre l'emplacement exact du code lié à ces types, ce qui conduit souvent à une duplication de code sévère . S'il existe une classe Price: Money
, Il est naturel de trouver la plage à l'intérieur. Si, à la place, un int
(pire, un double
) est utilisé pour stocker les prix des produits, qui devrait valider la gamme? Le produit? Le rabais? Le panier?
Enfin, un troisième avantage non mentionné dans le document est la possibilité de changer relativement facilement le type sous-jacent. Si aujourd'hui, mon ProductId
a short
comme type sous-jacent et que je dois utiliser à la place int
à la place, il y a de fortes chances que le code à modifier ne couvre pas toute la base de code.
L'inconvénient - et le même argument s'applique à chaque règle d'exercice de la gymnastique objet - est que s'il devient rapidement trop écrasant pour créer une classe pour tout . Si Product
contient ProductPrice
qui hérite de PositivePrice
qui hérite de Price
qui à son tour hérite de Money
, ce n'est pas une architecture propre, mais plutôt un gâchis complet où pour trouver une seule chose, un mainteneur devrait ouvrir quelques dizaines de fichiers à chaque fois.
Un autre point à considérer est le coût (en termes de lignes de code) de la création de classes supplémentaires. Si les wrappers sont immuables (comme ils devraient l'être, généralement), cela signifie que, si nous prenons C #, vous devez avoir, au moins dans le wrapper:
ToString()
personnalisée,Equals
et un GetHashCode
remplacent (également beaucoup de LOC).et éventuellement, le cas échéant:
==
Et !=
,Une centaine de LOC pour un emballage simple le rend assez prohibitif, c'est pourquoi vous pouvez être complètement sûr de la rentabilité à long terme d'un tel emballage. La notion de portée expliquée par Thomas Junk est particulièrement pertinente ici. L'écriture d'une centaine de LOC pour représenter un ProductId
utilisé partout dans votre base de code semble très utile. Écrire une classe de cette taille pour un morceau de code qui fait trois lignes dans une seule méthode est beaucoup plus discutable.
Conclusion:
Enveloppez les primitives dans des classes qui ont un sens dans un domaine métier de l'application lorsque (1) cela aide à réduire les erreurs, (2) réduit le risque de duplication de code ou (3) aide à changer le type sous-jacent ultérieurement.
N'encapsulez pas automatiquement toutes les primitives que vous trouvez dans votre code: il existe de nombreux cas où l'utilisation de string
ou int
est parfaitement adaptée.
En pratique, dans public string CreateNewThing()
, le renvoi d'une instance de la classe ThingId
au lieu de string
peut aider, mais vous pouvez également:
Renvoie une instance de la classe Id<string>
, C'est-à-dire un objet de type générique indiquant que le type sous-jacent est une chaîne. Vous avez l'avantage de la lisibilité, sans l'inconvénient de devoir maintenir un grand nombre de types.
Renvoie une instance de la classe Thing
. Si l'utilisateur n'a besoin que de l'ID, cela peut facilement être fait avec:
var thing = this.CreateNewThing();
var id = thing.Id;
J'utiliserais le scope en règle générale: plus l'étendue de la génération et de la consommation de values
est étroite, moins vous avez de chances de créer un objet représentant cette valeur.
Disons que vous avez ce qui suit pseudocode
id = generateProcessId();
doFancyOtherstuff();
job.do(id);
alors la portée est très limitée et je ne verrais aucun sens à en faire un type. Mais disons que vous générez cette valeur dans un calque et la transmettez à un autre calque (ou même à un autre objet), il serait alors parfaitement logique de créer un type pour cela.
Les systèmes de type statique visent à empêcher les utilisations incorrectes des données.
Il existe des exemples évidents de types faisant cela:
Il y a des exemples plus subtils
Nous pouvons être tentés d'utiliser double
pour le prix et la longueur, ou utiliser un string
pour un nom et une URL. Mais cela subvertit notre système de types génial et permet à ces abus de passer les vérifications statiques du langage.
La confusion entre les secondes-livre et les secondes-newton peut avoir résultats médiocres à l'exécution .
C'est particulièrement un problème avec les chaînes. Ils deviennent fréquemment le "type de données universel".
Nous sommes habitués à envoyer principalement des interfaces texte avec des ordinateurs, et nous étendons souvent ces interfaces humaines (IU) à interfaces de programmation (API). Nous considérons 34,25 comme les caractères 34,25. Nous considérons une date comme les caractères 05-03-2015. Nous considérons un UUID comme les caractères 75e945ee-f1e9-11e4-b9b2-1697f925ec7b.
Mais ce modèle mental nuit à l'abstraction de l'API.
Les mots ou la langue, tels qu'ils sont écrits ou parlés, ne semblent jouer aucun rôle dans mon mécanisme de pensée.
Albert Einstein
De même, les représentations textuelles ne devraient jouer aucun rôle dans la conception des types et des API. Attention au string
! (et d'autres types "primitifs" trop génériques)
Types communiquer "quelles opérations ont du sens."
Par exemple, j'ai déjà travaillé sur un client vers un HTTP REST. REST, correctement fait, utilise des entités hypermédias, qui ont des hyperliens pointant vers des entités liées. Dans ce client, non seulement les entités étaient (par exemple, utilisateur, compte, abonnement), les liens vers ces entités ont également été saisis (UserLink, AccountLink, SubscriptionLink). Les liens n'étaient guère plus que des wrappers autour de Uri
, mais les types séparés rendaient impossible d'essayer pour utiliser un AccountLink pour récupérer un utilisateur. Si tout avait été simple Uri
s - ou pire encore, string
- ces erreurs ne seraient détectées qu'au moment de l'exécution.
De même, dans votre situation, vous disposez de données utilisées dans un seul but: identifier un Operation
. Il ne devrait pas être utilisé pour autre chose, et nous ne devrions pas essayer d'identifier les Operation
avec des chaînes aléatoires que nous avons créées. La création d'une classe distincte ajoute la lisibilité et la sécurité à votre code.
Bien sûr, toutes les bonnes choses peuvent être utilisées en excès. Considérer
combien de clarté cela ajoute à votre code
combien de fois il est utilisé
Si un "type" de données (au sens abstrait) est utilisé fréquemment, à des fins distinctes, et entre des interfaces de code, c'est un très bon candidat pour être une classe distincte, au détriment de la verbosité.
Je suis généralement d'accord pour dire que plusieurs fois vous devez créer un type pour les primitives et les chaînes, mais comme les réponses ci-dessus recommandent de créer un type dans la plupart des cas, je vais énumérer quelques raisons pour lesquelles/quand ne pas:
Pensez-vous que c'est une bonne pratique d'envelopper des types simples dans une classe pour des raisons de sécurité?
Parfois.
C'est l'un de ces cas où vous devez peser les problèmes qui peuvent survenir en utilisant un string
au lieu d'un _ plus concret OperationIdentifier
. Quelle est leur gravité? Quelle est leur ressemblance?
Ensuite, vous devez considérer le coût d'utilisation de l'autre type. Est-ce douloureux à utiliser? Combien de travail faut-il faire?
Dans certains cas, vous vous épargnerez du temps et des efforts en ayant un type de béton Nice contre lequel travailler. Dans d'autres, cela ne vaudra pas la peine.
En général, je pense que ce genre de chose devrait être fait plus qu’aujourd’hui. Si vous avez une entité qui signifie quelque chose dans votre domaine, il est bon d'avoir cela comme son propre type, car cette entité est plus susceptible de changer/croître avec l'entreprise.
Non, vous ne devez pas définir de types (classes) pour "tout".
Mais, comme d'autres réponses l'indiquent, cela souvent est utile pour le faire. Vous devez développer - consciemment, si possible - un sentiment ou un sentiment de trop de friction dû à l'absence d'un type ou d'une classe appropriée, pendant que vous écrivez, testez et maintenez votre code. Pour moi, le début de trop de friction est lorsque je veux consolider plusieurs valeurs primitives en une seule valeur ou lorsque je dois valider les valeurs (c'est-à-dire déterminer laquelle de toutes les valeurs possibles du type primitif correspond à des valeurs valides de la 'type implicite').
J'ai trouvé des considérations telles que ce que vous avez posé dans votre question pour être responsable de trop conception dans mon code. J'ai pris l'habitude d'éviter délibérément d'écrire plus de code que nécessaire. J'espère que vous écrivez de bons tests (automatisés) pour votre code - si vous l'êtes, vous pouvez facilement refactoriser votre code et ajouter des types ou des classes si cela fournit des avantages nets pour le développement continu et maintenance de votre code .
réponse de Telastyn et réponse de Thomas Junk font tous deux un très bon point sur le contexte et l'utilisation du code pertinent. Si vous utilisez une valeur à l'intérieur d'un seul bloc de code (par exemple, méthode, boucle, bloc using
), il est préférable d'utiliser simplement un type primitif. Il est même possible d'utiliser un type primitif si vous utilisez un ensemble de valeurs à plusieurs reprises et à de nombreux autres endroits. Mais plus vous utilisez fréquemment et largement un ensemble de valeurs, et moins cet ensemble de valeurs correspond aux valeurs représentées par le type primitif, plus vous devez envisager d'encapsuler les valeurs dans une classe ou un type.
Idée derrière l'emballage de types primitifs,
Évidemment, ce serait très difficile et pas pratique de le faire partout, mais il est important d'envelopper les types là où c'est nécessaire,
Par exemple, si vous avez la classe Order,
Order
{
long OrderId { get; set; }
string InvoiceNumber { get; set; }
string Description { get; set; }
double Amount { get; set; }
string CurrencyCode { get; set; }
}
Les propriétés importantes pour rechercher une commande sont principalement OrderId et InvoiceNumber. Et le montant et le code de devise sont étroitement liés, où si quelqu'un modifie le code de devise sans modifier le montant, la commande ne peut plus être considérée comme valide.
Ainsi, seuls l'habillage OrderId, InvoiceNumber et l'introduction d'un composite pour la devise sont logiques dans ce scénario et probablement l'habillage de la description n'a aucun sens. Donc, le résultat préféré pourrait ressembler,
Order
{
OrderIdType OrderId { get; set; }
InvoiceNumberType InvoiceNumber { get; set; }
string Description { get; set; }
Currency Amount { get; set; }
}
Il n'y a donc aucune raison de tout emballer, mais des choses qui comptent vraiment.
En surface, il semble que tout ce que vous avez à faire est d'identifier une opération.
De plus, vous dites ce qu'une opération est censée faire:
pour démarrer l'opération [et] pour surveiller sa fin
Vous le dites d'une manière comme si c'était "juste comment l'identification est utilisée", mais je dirais que ce sont des propriétés décrivant une opération. Cela ressemble à une définition de type pour moi, il y a même un modèle appelé "modèle de commande" qui correspond très bien. .
Ça dit
le modèle de commande [...] est utilisé pour encapsuler toutes les informations nécessaires pour exécuter une action ou déclencher un événement à une date ultérieure .
Je pense que c'est très similaire par rapport à ce que vous voulez faire avec vos opérations. (Comparez les phrases que j'ai mises en gras dans les deux guillemets) Au lieu de renvoyer une chaîne, renvoyez un identifiant pour un Operation
dans le sens abstrait, un pointeur vers un objet de cette classe dans oop par exemple.
Concernant le commentaire
Il serait wtf-y d'appeler cette classe une commande sans avoir la logique à l'intérieur.
Non, ce ne serait pas le cas. N'oubliez pas que les motifs sont très abstraits , si abstraits en fait qu'ils sont quelque peu méta. Autrement dit, ils font souvent abstraction de la programmation elle-même et non d'un concept du monde réel. Le modèle de commande est une abstraction d'un appel de fonction. (ou méthode) Comme si quelqu'un appuyait sur le bouton pause juste après avoir passé les valeurs des paramètres et juste avant l'exécution, pour reprendre plus tard.
Ce qui suit envisage oop, mais la motivation derrière cela devrait être vraie pour tout paradigme. J'aime donner quelques raisons pour lesquelles placer la logique dans la commande peut être considéré comme une mauvaise chose.
Pour récapituler: le modèle de commande vous permet d'envelopper des fonctionnalités dans un objet, à exécuter plus tard. Par souci de modularité (la fonctionnalité existe indépendamment de l'exécution via une commande ou non), la testabilité (la fonctionnalité devrait être testable sans commande) et tous ces autres mots à la mode qui expriment essentiellement la demande d'écrire du bon code, vous ne mettriez pas la logique réelle dans la commande.
Parce que les motifs sont abstraits, il peut être difficile de trouver de bonnes métaphores du monde réel. Voici une tentative:
"Hé grand-mère, pourriez-vous s'il vous plaît appuyer sur le bouton d'enregistrement du téléviseur à 12h pour ne pas manquer les simpsons sur le canal 1?"
Ma grand-mère ne sait pas ce qui se passe techniquement lorsqu'elle frappe ce bouton d'enregistrement. La logique est ailleurs (dans la télé). Et c'est une bonne chose. La fonctionnalité est encapsulée et les informations sont cachées à la commande, c'est un utilisateur de l'API, pas nécessairement une partie de la logique ... oh jeez je babille à nouveau des mots à la mode, je ferais mieux de terminer cette édition maintenant.
Opinion impopulaire:
Vous ne devriez généralement pas définir de nouveau type!
La définition d'un nouveau type pour encapsuler une classe primitive ou de base est parfois appelée définir un pseudo-type. IBM décrit cela comme une mauvaise pratique ici (ils se concentrent spécifiquement sur l'utilisation abusive des génériques dans ce cas).
Les pseudo-types rendent la fonctionnalité de bibliothèque commune inutile.
Les fonctions mathématiques de Java peuvent fonctionner avec toutes les primitives numériques. Mais si vous définissez une nouvelle classe Percentage (encapsulant un double pouvant être compris entre 0 et 1), toutes ces fonctions sont inutiles et doivent être encapsulées par des classes qui (pire encore) ont besoin de connaître la représentation interne de la classe Percentage .
Les pseudo-types deviennent viraux
Lorsque vous créez plusieurs bibliothèques, vous constaterez souvent que ces pseudotypes deviennent viraux. Si vous utilisez la classe Percentage mentionnée ci-dessus dans une bibliothèque, vous devrez soit la convertir à la limite de la bibliothèque (en perdant toute sécurité/signification/logique/autre raison que vous aviez pour créer ce type), soit vous devrez créer ces classes également accessible à l'autre bibliothèque. Infecter la nouvelle bibliothèque avec vos types où un simple double aurait suffi.
Message à emporter
Tant que le type que vous encapsulez n'a pas besoin de beaucoup de logique métier, je vous déconseille de l'envelopper dans une pseudo-classe. Vous ne devez boucler une classe que s'il existe de sérieuses contraintes commerciales. Dans d'autres cas, le fait de nommer correctement vos variables devrait contribuer grandement à transmettre un sens.
Un exemple:
Un uint
peut parfaitement représenter un UserId, nous pouvons continuer à utiliser les opérateurs intégrés de Java pour les uint
s (tels que ==) et nous n'avons pas besoin de logique métier pour protéger l '"état interne" du identifiant d'utilisateur.
Comme pour tous les conseils, savoir quand appliquer les règles est la compétence. Si vous créez vos propres types dans un langage axé sur les types, vous obtenez une vérification de type. Donc, en général, ce sera un bon plan.
NamingConvention est la clé de la lisibilité. Les deux combinés peuvent exprimer clairement l'intention.
Mais /// est toujours utile.
Alors oui, je dirais créer de nombreux types propres lorsque leur durée de vie dépasse les limites de la classe. Pensez également à utiliser à la fois Struct
et Class
, pas toujours Class.