Récemment, j'ai eu un problème de lisibilité de mon code.
J'avais une fonction qui effectuait une opération et renvoyait une chaîne représentant l'ID de cette opération pour référence future (un peu comme OpenFile dans Windows renvoyant un descripteur). L'utilisateur utiliserait cet identifiant ultérieurement pour lancer l'opération et surveiller son achèvement.
L'ID doit ê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 floue. J'ai pensé à envelopper cette chaîne dans un autre type qui rend son sens plus clair comme suit:
public OperationIdentifier CreateNewThing()
Le type ne contiendra que la chaîne et sera utilisé chaque fois que cette chaîne sera utilisée.
Il est évident que l’avantage de ce mode de fonctionnement est une sécurité de type et une intention plus claires, 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 aussi beaucoup d'encombrement.
Pensez-vous qu’il est une bonne pratique d’envelopper des types simples dans une classe pour des raisons de sécurité?
Réponses:
Les primitives telles que
string
ouint
n'ont aucune signification dans un domaine commercial. Une conséquence directe de cela est que vous pouvez utiliser par erreur une URL lorsqu'un ID de produit est attendu ou utiliser une quantité lorsque vous attendez un prix .C'est aussi la raison pour laquelle le défi Object Calisthenics présente les primitives comme l'une de ses règles:
Le même document explique qu'il existe un avantage supplémentaire:
En effet, lorsque des primitives sont utilisées, il est généralement extrêmement difficile de suivre l'emplacement exact du code associé à ces types, ce qui conduit souvent à une duplication importante du code . S'il y a
Price: Money
classe, il est naturel de trouver la plage en vérifiant l'intérieur. Si, au lieu de cela, unint
(pire, undouble
) est utilisé pour stocker les prix des produits, qui doit valider la fourchette? 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 type
ProductId
estshort
sous-jacent et que je devrais plus tard utiliserint
, il est probable que le code à modifier ne couvre pas l'intégralité de la base de codes.L'inconvénient - et le même argument s'applique à toutes les règles de l'exercice de la calisthénie des objets - est que si cela devient rapidement trop pénible pour créer une classe pour tout . Si
Product
contientProductPrice
ce qui hérite de qui hérite dePositivePrice
qui héritePrice
à son tourMoney
, ce n'est pas une architecture propre, mais plutôt un gâchis total dans lequel, pour trouver une seule chose, un responsable doit 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 habituellement), cela signifie que si nous prenons C #, vous devez avoir, au moins dans les wrappers:
ToString()
,Equals
et aGetHashCode
outrepasse (aussi beaucoup de LOC).et éventuellement, le cas échéant:
==
et des!=
opérateurs,Une centaine de LOC pour un emballage simple le rend assez prohibitif, ce qui explique également pourquoi vous pouvez être absolument 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. Ecrire une centaine de LOCs pour représenter une
ProductId
utilisation sur 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 même méthode est beaucoup plus discutable.Conclusion:
Enveloppez les primitives dans des classes qui ont une signification dans un domaine commercial 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'emballez pas automatiquement toutes les primitives que vous trouvez dans votre code: il existe de nombreux cas d'utilisation
string
ou d' utilisationint
parfaite.En pratique,
public string CreateNewThing()
renvoyer une instance deThingId
classe au lieu destring
peut aider, mais vous pouvez aussi:Renvoie une instance de
Id<string>
classe, 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 conserver de nombreux types.Renvoie une instance de
Thing
classe. Si l'utilisateur n'a besoin que de l'identifiant, vous pouvez le faire facilement avec:la source
J'utiliserais la portée comme règle empirique: plus la génération et la consommation d'une telle portée sont restreintes
values
, moins vous aurez de chances de créer un objet représentant cette valeur.Disons que vous avez le pseudocode suivant
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 que vous le transmettez à un autre calque (ou même à un autre objet), il serait alors parfaitement logique de créer un type pour cela.
la source
doFancyOtherstuff()
dans un sous-programme, vous pourriez penser que lajob.do(id)
référence n'est pas assez locale pour être conservée comme une simple chaîne.Les systèmes de types statiques ont tous pour objectif de prévenir 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
à la fois le prix et la longueur, ou d’utiliser unstring
pour un nom et une URL. Mais cela compromet notre génial système de caractères et permet à ces utilisations abusives de passer les contrôles statiques du langage.Obtenir une livre-seconde confondue avec Newton-seconde peut avoir des résultats médiocres au moment de l'exécution .
Ceci est particulièrement un problème avec les chaînes. Ils deviennent souvent le "type de données universel".
Nous sommes habitués principalement aux interfaces de texte avec des ordinateurs, et nous étendons souvent ces interfaces utilisateur (IU) aux interfaces de programmation (API). Nous pensons à 34.25 comme aux personnages 34.25 . Nous pensons à une date comme les personnages 05-03-2015 . Nous pensons à un UUID comme les caractères 75e945ee-f1e9-11e4-b9b2-1697f925ec7b .
Mais ce modèle mental nuit à l'abstraction des API.
De même, les représentations textuelles ne doivent jouer aucun rôle dans la conception des types et des API. Attention le
string
! (et autres types "primitifs" trop génériques)Les types communiquent "quelles opérations ont un sens".
Par exemple, j'ai déjà travaillé sur un client pour une API HTTP REST. REST, correctement fait, utilise des entités hypermédia, qui ont des hyperliens pointant vers des entités apparentées. Dans ce client, non seulement les entités tapées (par exemple, utilisateur, compte, abonnement), les liens vers ces entités ont également été tapés (UserLink, AccountLink, SubscriptionLink). Les liens n'étaient guère plus que des wrappers
Uri
, mais les types distincts rendaient impossible l'utilisation d'un AccountLink pour récupérer un utilisateur. Si tout avait été simpleUri
ou pire,string
ces erreurs n’auraient été constatées qu’au moment de l’exécution.De même, dans votre cas, vous disposez de données utilisées dans un seul but: identifier un fichier
Operation
. Il ne devrait pas être utilisé pour autre chose, et nous ne devrions pas essayer d'identifier lesOperation
s avec des chaînes aléatoires que nous avons inventées. La création d'une classe séparée ajoute la lisibilité et la sécurité à votre code.Bien sûr, toutes les bonnes choses peuvent être utilisées à l'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 interfaces de code, il constitue un très bon candidat pour être une classe séparée, aux dépens de la verbosité.
la source
05-03-2015
signifie quand interprété comme une date ?Quelquefois.
C’est l’un de ces cas où vous devez peser les problèmes qui pourraient survenir si vous utilisiez
string
un système plus concretOperationIdentifier
. Quelle est leur gravité? Quelle est leur probabilité?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 économiserez du temps et des efforts en utilisant un type de béton intéressant. Dans d'autres cas, ça 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 de l'avoir comme type propre, car cette entité est plus susceptible de changer / de se développer avec les activités.
la source
Je conviens généralement que vous devez souvent 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 / pour ne pas ne pas:
la source
short
(par exemple) a le même coût, qu'il soit encapsulé ou non. (?)Non, vous ne devez pas définir de types (classes) pour "tout".
Mais, comme le soulignent d’autres réponses, il est souvent utile de le faire. Vous devriez développer - consciemment, si possible - un sentiment ou une sensation de trop de friction dû à l' absence d'un type ou d'une classe appropriée, lorsque vous écrivez, testez et mettez à jour votre code. Pour moi, trop de frictions surviennent lorsque je veux consolider plusieurs valeurs primitives en tant que valeur unique ou lorsque je dois valider les valeurs (c'est-à-dire déterminer laquelle de toutes les valeurs possibles du type primitif correspond aux valeurs valides de 'type implicite').
J'ai constaté que des considérations telles que ce que vous avez demandé dans votre question sont responsables de trop de 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 le savez, vous pouvez facilement refactoriser votre code et ajouter des types ou des classes si cela procure des avantages nets pour le développement et la maintenance continus de votre code .
Les réponses de Telastyn et de Thomas Junk soulignent toutes les deux le très bon point de vue sur le contexte et l'utilisation du code correspondant. Si vous utilisez une valeur dans un seul bloc de code (par exemple, une méthode, une boucle, un
using
bloc), il est préférable d'utiliser simplement un type primitif. Il est même bon d'utiliser un type primitif si vous utilisez un ensemble de valeurs à plusieurs reprises et dans 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 devriez envisager d'encapsuler les valeurs dans une classe ou un type.la source
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 supposée faire:
Vous dites cela comme si c'était "exactement 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 motif appelé "motif de commande" qui convient très bien. .
Ça dit
Je pense que cela ressemble beaucoup à 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
élément abstrait, un pointeur sur un objet de cette classe dans oop, par exemple.Concernant le commentaire
Non ce ne serait pas. Rappelez-vous que les motifs sont très abstraits , si abstraits qu’ils sont un peu méta. En d’autres termes, ils font souvent abstraction de la programmation elle-même et non d’un concept 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 de pause juste après avoir passé les valeurs des paramètres et juste avant l'exécution, pour le reprendre plus tard.
Ce qui suit est une réflexion sur 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 la fonctionnalité dans un objet, pour l’exécuter ultérieurement. Par souci de modularité (la fonctionnalité existe indépendamment du fait qu'elle soit exécutée via une commande ou non), la testabilité (la fonctionnalité doit être testable sans commande) et tous les 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.
Les motifs étant abstraits, il peut être difficile de créer 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 sur la télévision à 12 heures pour que je ne manque pas les simpsons sur le canal 1?"
Ma grand-mère ne sait pas ce qui se passe techniquement quand elle appuie sur le bouton d'enregistrement. La logique est ailleurs (à la télé). Et c'est une bonne chose. La fonctionnalité est encapsulée et les informations masquées de la commande, il s'agit d'un utilisateur de l'API, qui ne fait pas nécessairement partie de la logique.
la source
Idée derrière les types primitifs,
De toute évidence, il serait très difficile et peu pratique de le faire partout, mais il est important d’envelopper les types le cas échéant,
Par exemple, si vous avez la classe Order,
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, et 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 le wrapping OrderId, InvoiceNumber et l'introduction d'un composite pour la devise ont du sens dans ce scénario et probablement une description du wrapping n'a pas de sens. Résultat donc préféré pourrait ressembler,
Il n'y a donc aucune raison de tout envelopper, mais des choses qui comptent vraiment.
la source
Opinion impopulaire:
Vous ne devriez généralement pas définir un nouveau type!
Définir un nouveau type pour envelopper une primitive ou une classe de base est parfois appelé définir un pseudo-type. IBM décrit cela comme une mauvaise pratique ici (ils se concentrent spécifiquement sur l'utilisation abusive de génériques dans ce cas).
Les pseudo-types rendent les fonctionnalités de bibliothèque communes inutiles.
Les fonctions mathématiques de Java peuvent fonctionner avec toutes les primitives numériques. Mais si vous définissez une nouvelle classe Pourcentage (un double pouvant être compris entre 0 et 1), toutes ces fonctions sont inutiles et doivent être entourées par des classes qui (encore pire) doivent connaître la représentation interne de la classe Pourcentage. .
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 de pourcentage, 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 de la création de ce type), soit vous devrez créer ces classes. accessible à l’autre bibliothèque également. Infecter la nouvelle bibliothèque avec vos types là où un simple double aurait suffi.
Enlever le message
Tant que le type que vous emballez n'a pas besoin de beaucoup de logique métier, je vous conseillerais de ne pas l'envelopper dans une pseudo-classe. Vous ne devriez envelopper une classe que s'il existe de sérieuses contraintes commerciales. Dans d’autres cas, nommer correctement vos variables devrait permettre de véhiculer un sens.
Un exemple:
A
uint
peut parfaitement représenter un UserId, nous pouvons continuer à utiliser les opérateurs intégrés de Java pouruint
s (tels que ==) et nous n'avons pas besoin de logique métier pour protéger "l'état interne" de l'ID utilisateur.la source
Comme pour tous les conseils, il faut savoir quand appliquer les règles. Si vous créez vos propres types dans une langue de type, 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 clairement exprimer l'intention.
Mais /// est toujours utile.
Alors oui, je dirais créer de nombreux types lorsque leur durée de vie dépasse les limites d'une classe. Pensez également à l’utilisation des deux
Struct
etClass
pas toujours de la classe.la source
///
veut dire?