Cet article, qui s'adresse aux débutants, couvre une autre série d'odeurs et de refactorisations avec lesquelles vous devriez vous familiariser tôt dans votre carrière. Nous couvrons les instructions de cas, le polymorphisme, les objets nuls et les classes de données.
Celui-ci pourrait également être nommé «odeur de la liste de contrôle» ou quelque chose du genre. Les déclarations de cas sont une odeur, car elles causent des doubles emplois - elles sont souvent aussi inélégantes. Elles peuvent également conduire à des classes inutilement volumineuses, car toutes ces méthodes répondant aux différents scénarios (et potentiellement en croissance) aboutissent souvent dans la même classe, qui assume alors toutes sortes de responsabilités mixtes. Il n’est pas rare que vous disposiez de nombreuses méthodes privées qui seraient mieux loties dans leurs propres classes..
Un gros problème avec les déclarations de cas se produit si vous souhaitez les développer. Ensuite, vous devez changer cette méthode particulière, éventuellement encore et encore. Et pas seulement là-bas, car souvent, ils ont des jumeaux répétés qui nécessitent maintenant une mise à jour. Un excellent moyen de reproduire des insectes à coup sûr. Comme vous vous en souvenez peut-être, nous voulons être ouverts à l'extension mais fermés à la modification. Ici, la modification est inévitable et juste une question de temps.
Vous vous efforcez plus difficilement d’extraire et de réutiliser du code, c’est aussi une bombe à retardement. Le code de visualisation dépend souvent de telles déclarations de cas, qui dupliquent ensuite l'odeur et ouvrent grand la porte pour une prochaine intervention chirurgicale au fusil à pompe. Aie! En outre, interroger un objet avant de trouver la bonne méthode à exécuter est une violation désagréable du principe "Tell-Don't-Ask".
Il existe une bonne technique pour gérer le besoin de déclarations de cas. Mot de fantaisie entrant! Polymorphisme. Cela vous permet de créer la même interface pour différents objets et d'utiliser n'importe quel objet nécessaire pour différents scénarios. Vous pouvez simplement permuter l'objet approprié et celui-ci s'adapte à vos besoins car il utilise les mêmes méthodes. Leur comportement sous ces méthodes est différent, mais tant que les objets répondent à la même interface, Ruby s'en fiche. Par exemple, VegetarianDish.new.order
et VeganDish.new.order
se comporter différemment, mais les deux répondent à #ordre
de la même façon. Vous voulez juste commander et ne répondez pas à des tonnes de questions comme si vous mangiez ou non des œufs.
Le polymorphisme est implémenté en extrayant une classe pour la branche de l'instruction case et en déplaçant cette logique dans une nouvelle méthode sur cette classe. Vous continuez à faire cela pour chaque branche de l'arborescence conditionnelle et vous leur donnez le même nom de méthode. De cette façon, vous encapsulerez ce comportement dans un objet qui conviendrait le mieux pour prendre ce type de décision et qui n’a aucune raison de changer. Vous voyez, de cette façon, vous pouvez éviter toutes ces questions harcelantes sur un objet, vous lui dites simplement ce qu'il est censé faire. Lorsque vous avez besoin de cas plus conditionnels, vous créez simplement une autre classe qui prend en charge cette responsabilité sous le même nom de méthode..
Logique de déclaration de cas
class Operation def case case @mission_tpe quand: counter_intelligence @standard_fee + @counter_intelligence_fee quand: terrorisme @standard_fee + @terrorism_fee quand: vengeance @standard_fee + @revenge_fee quand: extorsion @standard_fee + @terardism_fee quand: vengeance @standard_fee + @revenge_fee quand: extortion : counter_intelligence) counter_intel_op.price terror_op = Operation.new (mission_type:: terror) terror_op.price revenge_op = Operation.new (mission_type:: vengeance) vengeance_op.price extortion_op = Operation.new (mission_type:: extorsion)
Dans notre exemple, nous avons un Opération
classe qui a besoin de poser des questions sur son type de mission
avant qu'il puisse vous dire son prix. Il est facile de voir que cela prix
La méthode attend juste de changer lorsqu'un nouveau type d'opération est ajouté. Lorsque vous souhaitez également afficher cela dans votre affichage, vous devez également appliquer cette modification. (Pour votre information, vous pouvez utiliser des partiels polymorphes dans Rails pour éviter que ces instructions ne soient diffusées dans votre vue.)
Classes Polymorphes
classe CounterIntelligenceOperation def price @standard_fee + @commonting_intelligence_end end class .price terror_op = CounterIntelligenceOperation.new terror_op.price revenge_op = CounterIntelligenceOperation.new revenge_op.price extortion_op = CounterIntelligenceOperation.new extortion_op.price
Ainsi, au lieu de descendre dans ce gouffre, nous avons créé un groupe de classes d'opérations qui ont leur propre connaissance du montant de leurs honoraires correspondant au prix final. Nous pouvons simplement leur dire de nous donner leur prix. Vous ne devez pas toujours vous débarrasser de l'original (Opération
) classe-seulement lorsque vous constatez que vous avez extrait toutes les connaissances dont il disposait.
Je pense que la logique derrière les déclarations de cas est inévitable. Les scénarios dans lesquels vous devez passer par une sorte de liste de contrôle avant de trouver l'objet ou le comportement permettant de faire le travail sont tout simplement trop courants. La question est simplement de savoir comment ils sont mieux gérés. Je suis en faveur de ne pas les répéter et d'utiliser les outils proposés par la programmation orientée objet pour concevoir des classes discrètes pouvant être facilement remplacées par leur interface..
La vérification de zéro partout est un type particulier d'odeur d'affirmation de cas. Demander un objet à propos de zéro est souvent une sorte de déclaration de cas cachée. La manipulation conditionnelle peut prendre la forme de object.nil?
, objet.présente?
, object.try
, puis une sorte d'action dans le cas néant
se présente à votre fête.
Une autre question plus sournoise est de demander à un objet sa véracité, si elle existe ou si elle est nulle, puis de prendre des mesures. Ça a l'air inoffensif, mais c'est juste un déguisement. Ne vous laissez pas berner: opérateurs ternaires ou ||
les opérateurs entrent également dans cette catégorie, bien sûr. Autrement dit, non seulement les conditionnels sont clairement identifiables sauf si
, sinon
ou Cas
déclarations. Ils ont des façons plus subtiles de planter votre parti. Ne demandez pas aux objets leur nullité, mais dites-leur que si l'objet de votre joyeux chemin est absent, un objet null est maintenant en charge de répondre à vos messages..
Les objets nuls sont des classes ordinaires. Il n'y a rien de spécial en eux, juste un "nom de fantaisie". Vous extrayez une logique conditionnelle liée à néant
et ensuite vous vous en occupez de manière polymorphe. Vous contenez ce comportement, contrôlez le flux de votre application via ces classes et avez également des objets ouverts pour d'autres extensions qui leur conviennent. Pensez à comment Procès
(NullSubscription
) la classe pourrait croître avec le temps. Non seulement il est plus sec et yadda-yadda-yadda, il est aussi beaucoup plus descriptif et stable.
L’utilisation d’objets nuls présente un grand avantage: les choses ne peuvent pas exploser aussi facilement. Ces objets répondent aux mêmes messages que les objets émulés (bien entendu, vous n'avez pas toujours besoin de copier l'intégralité de l'API vers des objets nuls), ce qui donne à votre application peu de raisons de devenir fou. Cependant, notez que les objets nuls encapsulent la logique conditionnelle sans la supprimer complètement. Vous venez de trouver une meilleure maison pour cela.
Étant donné qu'il est très contagieux et malsain d'avoir beaucoup d'action liée à zéro dans votre application, j'aime penser aux objets nuls comme au «modèle de confinement nul» (s'il vous plaît, ne me poursuivez pas en justice!). Pourquoi contagieux? Parce que si vous passez néant
autour, quelque part ailleurs dans votre hiérarchie, une autre méthode est tôt ou tard également obligé de demander si néant
est en ville, ce qui mène ensuite à une autre série de contre-mesures pour traiter un tel cas.
Autrement dit, nil n’est pas cool de traîner parce que demander sa présence devient contagieux. Poser des objets pour néant
est probablement toujours le symptôme d’une mauvaise conception - aucune offense et ne vous sentez pas mal! - nous sommes tous passés par là. Je ne veux pas dire bon gré mal gré le fait que rien puisse être hostile, mais il faut mentionner quelques points:
Globalement, le scénario selon lequel un objet manque quelque chose revient très souvent. Un exemple souvent cité est une application qui a un nom enregistré Utilisateur
et un NilUser
. Mais comme les utilisateurs qui n'existent pas sont un concept idiot si cette personne navigue clairement dans votre application, il est probablement plus cool d'avoir un Client
qui n'a pas encore signé. Un abonnement manquant pourrait être un Procès
, une charge zéro pourrait être un Billet de faveur
, etc.
Nommer vos objets nuls est parfois évident et facile, parfois très difficile. Mais essayez de ne pas nommer tous les objets nuls avec le préfixe «Null» ou «No». Tu peux faire mieux! Fournir un peu de contexte va un long chemin, je pense. Choisissez un nom plus spécifique et significatif, quelque chose qui reflète le cas d'utilisation réel. De cette façon, vous communiquez plus clairement avec les autres membres de l’équipe et avec votre futur moi bien sûr.
Il existe plusieurs façons de mettre en œuvre cette technique. Je vais vous en montrer un qui, à mon avis, est simple et adapté aux débutants. J'espère que c'est une bonne base pour comprendre les approches plus impliquées. Lorsque vous rencontrez à plusieurs reprises une sorte de conditionnel qui traite néant
, vous savez qu'il est temps de créer simplement une nouvelle classe et de déplacer ce comportement. Après cela, vous indiquez à la classe d'origine que ce nouveau joueur n'a plus rien.
Dans l'exemple ci-dessous, vous pouvez voir que le Spectre
classe demande un peu trop sur néant
et encombre le code inutilement. Il veut s'assurer que nous avons un opération maléfique
avant qu'il décide de charger. Pouvez-vous voir la violation de «Dites, ne demandez pas»?
Une autre partie problématique est la raison pour laquelle Specter devrait se préoccuper de la mise en œuvre d’un prix zéro. le essayer
La méthode demande également si le opération maléfique
a un prix
gérer le néant via le ou
(||
) déclaration. evil_operation.present?
fait la même erreur. Nous pouvons simplifier ceci:
la classe Spectre include ActiveModel :: Model attr_accessor: credit_card,: evil_operation def charge à moins que evil_operation.nil? evil_operation.charge (credit_card) end end def has has_discount? evil_operation.present? && evil_operation.has_discount? fin def price evil_operation.try (: price) || 0 end end class EvilOperation inclut ActiveModel :: Model attr_accessor: discount,: price def has_discount? discount end def charge (credit_card) credit_card.charge (price) end end
class NoOperation def charge (carte de crédit) "Aucune opération maléfique enregistrée" end def has_discount? false fin def prix 0 fin fin classe Spectre include ActiveModel :: Model attr_accessor: credit_card,: evil_operation def charge evil_operation.charge (credit_card) end def has_discount? evil_operation.has_discount? end def price price evil_operation.price end private def evil_operation @evil_operation || NoOperation.new end end class EvilOperation include ActiveModel :: Model attr_accessor: discount,: price def has_discount? discount end def charge (credit_card) credit_card.charge (price) end end
Je suppose que cet exemple est assez simple pour voir tout de suite à quel point les objets nuls peuvent être élégants. Dans notre cas, nous avons un Pas d'opération
Classe null qui sait comment traiter une opération maléfique non existante via:
0
les montants.Nous avons créé cette nouvelle classe qui traite du néant, un objet qui gère l’absence de contenu et le remplace par l’ancienne classe si néant
révéler. L'API est la clé ici car si elle correspond à celle de la classe d'origine, vous pouvez permuter de manière transparente l'objet null avec les valeurs de retour dont nous avons besoin. C'est exactement ce que nous avons fait dans la méthode privée Spectre # evil_operation
. Si nous avons le opération maléfique
objet, nous avons besoin de nous en servir; sinon, nous utilisons notre néant
caméléon qui sait comment traiter ces messages, donc opération maléfique
ne reviendra jamais nul. Canard dactylographie à son meilleur.
Notre logique conditionnelle problématique est maintenant encapsulée dans un endroit et notre objet null est responsable du comportement Spectre
cherche. SEC! Nous avons un peu reconstruit la même interface à partir de l'objet original que nil ne pouvait pas gérer. Rappelez-vous que nil fait un mauvais travail pour recevoir des messages - personne à la maison, toujours! À partir de maintenant, il s’agit simplement de dire aux objets quoi faire sans leur demander d’abord leur «permission». Ce qui est également cool, c’est qu’il n’était pas nécessaire de toucher le Opération diabolique
classe du tout.
Dernier point mais non le moindre, je me suis débarrassé du chèque si une opération perverse est présente dans Spectre # has_discount?
. Inutile de s’assurer qu’une opération existe pour bénéficier de la remise. À la suite de l'objet null, le Spectre
la classe est beaucoup plus mince et ne partage pas autant les responsabilités des autres classes.
Un bon conseil à retenir est de ne pas vérifier les objets s’ils sont enclins à faire quelque chose. Commandes-les comme un sergent instructeur. Comme c'est souvent le cas, ce n'est probablement pas cool dans la vie réelle, mais c'est un bon conseil pour la programmation orientée objet. Maintenant donne moi 20!
En général, tous les avantages liés à l'utilisation du polymorphisme au lieu des instructions case s'appliquent également aux objets nuls. Après tout. c'est juste un cas particulier de déclarations de cas. La même chose vaut pour les inconvénients:
Fermons cet article avec quelque chose de léger. C'est une odeur, car c'est une classe qui n'a pas de comportement, à l'exception de la récupération et du paramétrage de ses données. Il ne s'agit en réalité que d'un conteneur de données sans méthodes supplémentaires utilisant ces données. Cela les rend passifs par nature car ces données sont conservées là pour être utilisées par d’autres classes. Et nous savons déjà que ce n’est pas un scénario idéal car il crie fonctionnalité envie. De manière générale, nous voulons avoir des classes qui ont des données de signification d'état qu'elles prennent en charge, ainsi qu'un comportement via des méthodes qui peuvent agir sur ces données sans trop de gêne ni de fouine dans les autres classes..
Vous pouvez commencer à refactoriser de telles classes en extrayant éventuellement le comportement d'autres classes agissant sur les données dans votre classe de données. En faisant cela, vous pourriez lentement attirer un comportement utile pour ces données et leur donner une maison appropriée. C'est tout à fait bien si ces classes développent un comportement avec le temps. Parfois, vous pouvez déplacer facilement une méthode entière et parfois, vous devez d'abord extraire des parties d'une méthode plus grande, puis les extraire dans la classe de données. En cas de doute, si vous pouvez réduire l’accès des autres classes à votre classe de données, vous devez absolument tenter le coup et déplacer ce comportement. Votre objectif devrait être de retirer à un moment donné les getters et les setters qui ont donné à d’autres objets l’accès à ces données. En conséquence, vous aurez un couplage plus faible entre les classes, ce qui est toujours gagnant!
Vous voyez, ce n'était pas si compliqué! Il existe beaucoup de jargon sophistiqué et de techniques de sondage compliquées autour des odeurs de code, mais nous espérons que vous vous êtes rendu compte que les odeurs et leur refactorisation partagent également quelques traits qui sont en nombre limité. Les odeurs de code ne disparaîtront jamais ni deviendront inutiles - du moins tant que nos corps ne seront pas renforcés par des IA qui nous laisserons rédiger le genre de code de qualité dont nous sommes actuellement à des années-lumière.
Au cours des trois derniers articles, je vous ai présenté une bonne partie des scénarios d'odeurs de code les plus importants et les plus courants que vous rencontrerez au cours de votre carrière de programmation orientée objet. Je pense que c'était une bonne introduction pour les débutants sur ce sujet, et j'espère que les exemples de code ont été suffisamment détaillés pour pouvoir suivre facilement le fil.
Une fois que vous avez compris ces principes pour la conception de code de qualité, vous détectez de nouvelles odeurs et leur refactorisation en un rien de temps. Si vous avez réussi jusqu'ici et que vous sentez que vous n'avez pas perdu la vue d'ensemble, je pense que vous êtes prêt à aborder le statut de responsable de la hiérarchie hiérarchique au niveau du patron.