Ruby / Rails Code Smell Basics 03

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.

Les sujets

  • Déclarations de cas
  • Polymorphisme
  • Objets Nuls
  • Classe de données

Déclarations de cas

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".

Polymorphisme

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..

Objets Nuls

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:

  • Nil est un fêtard (désolé, il faut le dire).
  • Nil n'est pas utile car il manque de sens.
  • Nil ne répond à rien et viole l’idée de "Duck Typing".
  • Les messages d'erreur nuls sont souvent difficiles à gérer.
  • Nil va te mordre-tôt ou tard.

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:

  • Frais de manutention de 0 les montants.
  • Sachant que les opérations non existantes ne bénéficient également d'aucun rabais.
  • Renvoyer une petite chaîne d'erreur informative lorsque la carte est chargée.

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:

  • Comprendre l'implémentation et le comportement peut devenir délicat car le code est répandu et les objets nuls ne sont pas très explicites sur leur existence.
  • L'ajout d'un nouveau comportement peut nécessiter d'être synchronisé entre les objets null et leurs homologues.

Classe de données

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!

Pensées finales

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.