=> «La mission Octopussy a été confiée à James Bond dans le but de trouver le dispositif nucléaire. Le permis de tuer a été accordé.

Ceci est la partie 2 d'une petite série sur les odeurs de code et les refactorings possibles. Le public cible auquel je pensais était celui des débutants qui avaient entendu parler de ce sujet et qui souhaitaient peut-être attendre un peu avant d'entrer dans ces eaux avancées. L'article suivant traite de «Feature Envy», «Shotgun Surgery» et «Divergent Change»..

Les sujets

  • Envie de fonctionnalité
  • Chirurgie au fusil
  • Changement Divergent

Ce que vous réaliserez rapidement avec les odeurs de code, c’est que certains d’entre eux sont des cousins ​​très proches. Même leurs refactorings sont parfois liés, par exemple, Classe en ligne et Extrait de classe ne sont pas si différents.

En insérant une classe, par exemple, vous extrayez la classe entière tout en supprimant celle d'origine. Donc, retirez un peu la classe avec un peu de torsion. Ce que j'essaie de dire, c'est que vous ne devriez pas vous sentir submergé par le nombre d'odeurs et de refactorisation, et que vos noms intelligents ne devraient certainement pas vous décourager. Des choses comme la chirurgie au fusil à pompe, l'envie de fonctionnalité et le changement divergent peuvent sembler fantaisistes et intimidantes pour les personnes qui viennent de commencer. Peut-être que je me trompe, bien sûr.

Si vous plongez un peu dans ce sujet et jouez avec quelques refactorisations pour que les codes sentent par vous-même, vous verrez rapidement qu'ils se retrouvent souvent dans le même stade. Un grand nombre de refactorisations sont simplement des stratégies différentes pour aboutir à des cours concis, bien organisés et axés sur un petit nombre de responsabilités. Je pense qu'il est juste de dire que si vous pouvez y parvenir, vous serez en avance sur le peloton la plupart du temps - il n'est pas important d'être en avance sur les autres, mais un tel design de classe manque tout simplement souvent dans le code des personnes avant qu'elles ne le soient sommes considérés comme des «experts».

Alors, pourquoi ne pas entrer dans le jeu tôt et construire une base concrète pour la conception de votre code. Ne croyez pas, selon votre propre récit, qu'il s'agit d'un sujet avancé que vous devriez remettre à plus tard jusqu'à ce que vous soyez prêt. Même si vous êtes un débutant, si vous faites de petits pas, vous pouvez envelopper la tête autour des odeurs et de leur refactorisation beaucoup plus tôt que vous ne le pensez.

Avant de plonger dans la mécanique, je veux répéter un point important du premier article. Toutes les odeurs ne sont pas intrinsèquement mauvaises, et toutes les refactorisations n'en valent pas toujours la peine. Vous devez décider sur le lieu, lorsque vous disposez de toutes les informations pertinentes, si votre code est plus stable après un refactoring et si cela vaut la peine de votre temps pour corriger l'odeur..

Envie de fonctionnalité

Reprenons un exemple de l'article précédent. Nous avons extrait une longue liste de paramètres pour #assign_new_mission dans une objet paramètre via le Mission classe. Jusqu'ici tellement cool.

M avec envie

"Ruby class M def assign_new_mission (mission) print" La mission # mission.mission_name a été affectée à # mission.agent_name avec pour objectif # # mission.objective. "si mission.licence_to_kill affiche print" Le permis de tuer a été accordé. "else print" La licence de mise à mort n'a pas été accordée. "end end end

Classe Mission_lecteur_attribut: nom_mission,: nom_agent,: objectif,: licence_to_kill

def initialize (nom_mission: nom_mission, nom_agent: nom_agent, objectif: objectif, licence_de_tait: licence_tait_kill) @nom_mission = nom_session # nom_agent = nom_agent @objective = objectif @licence_to_kill = licence_to_kill fin

m = M.new

mission = mission.new (mission_name: 'Octopussy', agent_name: 'James Bond', objectif: 'trouver le dispositif nucléaire', licence_to_kill: true)

m.assign_new_mission (mission)

=> «La mission Octopussy a été confiée à James Bond dans le but de trouver le dispositif nucléaire. Le permis de tuer a été accordé.

"

J'ai brièvement mentionné comment simplifier la M classe encore plus en déplaçant la méthode #assign_new_mission à la nouvelle classe pour l'objet paramètre. Ce que je n'ai pas abordé, c'est le fait que M avait une forme facilement curable de fonctionnalité envie ainsi que. M était trop curieux sur les attributs de Mission. En d'autres termes, elle a posé beaucoup trop de «questions» sur l'objet de mission. Ce n'est pas seulement un mauvais cas de microgestion mais aussi une odeur de code très répandue.

Laissez-moi vous montrer ce que je veux dire. Dans M # assign_new_mission, M est «envieux» à propos des données du nouvel objet paramètre et veut y avoir accès partout.

  • mission.nom_mission
  • mission.nom_agent
  • Objectif de mission
  • mission.licence_to_kill

En plus de cela, vous avez également un objet de paramètre Mission qui est uniquement responsable des données pour le moment - ce qui est une autre odeur, un Classe de données.

Toute cette situation vous dit fondamentalement que #assign_new_mission veut être ailleurs et M n'a pas besoin de connaître les détails de la façon dont les missions sont assignées. Après tout, pourquoi ne serait-il pas de la responsabilité de la mission d’affecter de nouvelles missions? N'oubliez pas de toujours mettre ensemble des choses qui changent ensemble.

M sans envie

"classe ruby ​​M def assign_new_mission (mission) mission.assign end end

Classe Mission_lecteur_attribut: nom_mission,: nom_agent,: objectif,: licence_to_kill

def initialize (nom_mission: nom_mission, nom_agent: nom_agent, objectif: objectif, licence_de_tait: licence_traitement) @nom_mission = nom_mission = nom_agent @ = nom_agent @objective = objectif @licence_to_kill = licence_to_kill fin

def assign print «La mission # mission_name a été affectée à # agent_name avec pour objectif # # objectif.» si licence_to_kill affiche «La licence de tuer a été accordée.» sinon affiche «La licence de tuer n'a pas été accordé. "fin fin fin

m = M.new mission = nouvelle.fonction (mission_name: 'Octopussy', agent_name: 'James Bond', objectif: 'trouver le dispositif nucléaire', licence_to_kill: true) m.assign_new_mission (mission) "

Comme vous pouvez le constater, nous avons simplifié un peu les choses. La méthode s'est considérablement réduite et délègue le comportement à l'objet en charge. M ne demande plus les détails de la mission et évite de s’impliquer dans l’impression des devoirs. Désormais, elle peut se concentrer sur son vrai travail et ne doit pas être dérangée si des détails concernant les missions confiées changent. Plus de temps pour les jeux d'esprit et la chasse aux agents voyous. Gagnant-gagnant!

La jalousie des fonctionnalités engendre l’enchevêtrement - je ne parle pas du bon genre, celle qui permet à l’information de voyager plus vite que la lumière - je parle de celle qui, au fil du temps, pourrait laisser votre élan de développement s’arrêter de plus en plus. Pas bon! Pourquoi Les effets d'entraînement dans votre code vont créer une résistance! Un changement dans un endroit papillon à travers toutes sortes de choses et vous vous retrouvez comme un cerf-volant dans un ouragan. (Ok, un peu trop dramatique, mais je me donne un B + pour la référence de Bond ici.)

En tant qu'antidote général contre l'envie de fonctionnalités, vous souhaitez concevoir des classes qui se préoccupent principalement de leurs propres problèmes et qui ont, si possible, des responsabilités uniques. En bref, les classes devraient ressembler à des otakus amicaux. Socialement, ce ne sont peut-être pas les comportements les plus sains, mais pour la conception de cours, il est souvent raisonnable de garder votre élan là où il devrait être: aller de l'avant.!

Chirurgie au fusil

Le nom est un peu bête, non? Mais en même temps, c'est une description assez précise. Cela ressemble à une affaire sérieuse, et c'est le cas! Heureusement, ce n’est pas si difficile à comprendre, mais c’est néanmoins l’une des odeurs les plus odieuses du code. Pourquoi? Parce que cela engendre la duplication à nul autre pareil et qu'il est facile de perdre de vue tous les changements que vous auriez besoin de faire pour régler le problème. Ce qui se passe pendant une opération chirurgicale au canon, c'est que vous modifiez une classe / un fichier et que vous devez également toucher de nombreuses autres classes / fichiers qui doivent être mis à jour. J'espère que ça ne sonne pas comme un bon moment.

Par exemple, vous pouvez penser que vous pouvez vous contenter d'un petit changement à un endroit, puis vous rendre compte que vous devez parcourir tout un tas de fichiers pour apporter le même changement ou réparer quelque chose qui ne fonctionne pas à cause de cela.. ne pas bon, pas du tout! Cela ressemble plus à une bonne raison pour laquelle les gens commencent à détester le code avec lequel ils traitent.

Si vous avez un spectre avec du code DRY d'un côté, le code qui nécessite souvent une intervention chirurgicale au fusil de chasse est plutôt à l'opposé. Ne soyez pas paresseux et laissez-vous entrer sur ce territoire. Je suis sûr que vous préférez ouvrir un fichier, y appliquer vos modifications et en finir. C'est le genre de paresseux que vous devriez rechercher!

Pour éviter cette odeur, voici une courte liste de symptômes à surveiller:

  • Envie de fonctionnalité
  • Couplage serré
  • Long Parameter List
  • Toute forme de duplication de code

Que voulons-nous dire quand nous parlons de code couplé? Disons que nous avons des objets UNE et B. S'ils ne sont pas couplés, vous pouvez changer l'un d'eux sans affecter l'autre. Sinon, vous devrez le plus souvent également traiter l'autre objet.

C'est un problème, et la chirurgie au fusil est un symptôme d'un couplage serré. Veillez donc toujours à changer facilement votre code. Si c'est relativement facile, cela signifie que votre niveau de couplage est acceptable. Cela dit, je me rends compte que vos attentes seraient irréalistes si vous vous attendez à pouvoir éviter tout couplage à tout moment. Ça ne va pas arriver! Vous trouverez de bonnes raisons de vous opposer à ce remplacement du conditionnel par une Polymorphisme. Dans un tel cas, un peu de couplage, d'opération au fusil à pompe et de synchronisation de l'API des objets vaut bien de se débarrasser d'une tonne de déclarations de cas via un Objet nul (plus à ce sujet dans une pièce ultérieure).

Le plus souvent, vous pouvez appliquer l'une des modifications suivantes pour soigner les plaies:

  • Déplacer le champ
  • Classe en ligne
  • Extrait de classe
  • Méthode de déplacement

Regardons du code. Cet exemple montre comment une application Spectre gère les paiements entre ses sous-traitants et ses clients malveillants. J'ai un peu simplifié les paiements en ayant des honoraires standard pour les entrepreneurs et les clients. Peu importe que Spectre soit chargé d'enlever un chat ou d'extorquer tout un pays: les frais restent les mêmes. La même chose vaut pour ce qu'ils paient leurs entrepreneurs. Dans les rares cas, une opération va vers le sud et une autre 2 doit littéralement sauter le requin, Spectre offre un remboursement complet pour garder les clients pervers heureux. Spectre utilise un joyau de paiement exclusif, qui est essentiellement un espace réservé pour tout type de processeur de paiement..

Dans le premier exemple ci-dessous, ce serait difficile si Specter décidait d'utiliser une autre bibliothèque pour gérer les paiements. Il y aurait plus de parties mobiles impliquées, mais je pense que pour démontrer la chirurgie au fusil de chasse, cette complexité

Exemple avec une odeur de chirurgie de fusil de chasse:

"classe de rubis EvilClient #…

STANDARD_CHARGE = 10000000 BONUS_CHARGE = 10000000

def accept_new_client PaymentGem.create_client (email) end

def charge_for_initializing_operation evil_client_id = PaymentGem.find_client (email) .payments_id PaymentGem.charge (evil_client_id, STANDARD_CHARGE) fin

def charge_for_successful_operation evil_client_id = PaymentGem.find_client (email) .payments_id PaymentGem.charge (evil_client_id, BONUS_CHARGE) end end

Classe Opération #…

REFUND_AMOUNT = 10000000

def refund transaction_id = PaymentGem.find_transaction (payments_id) PaymentGem.refund (transaction_id, REFUND_AMOUNT) end end

classe Contractant #…

STANDARD_PAYOUT = 200000 BONUS_PAYOUT = 1000000

def process_payout spectre_agent_id = PaymentGem.find_contractor (email) .payments_id if operation.enemy_agent == 'James Bond' && operation.enemy_agent_status == 'Tué à l'action' PaymentGem.transfer_funds fin fin fin "

Quand vous regardez ce code, vous devriez vous demander: “Est-ce que le Mauvais clients que la classe soit vraiment préoccupée par la façon dont le processeur de paiement accepte les nouveaux clients pervers et par la facturation de leurs opérations? »Bien sûr que non! Est-ce une bonne idée d’étaler les différents montants à payer partout? Les détails de mise en œuvre du processeur de paiement doivent-ils apparaître dans l'une de ces classes? Très certainement pas!

Regardez ça de cette façon. Si vous voulez changer quelque chose dans la façon dont vous traitez les paiements, pourquoi devriez-vous ouvrir le Mauvais client classe? Dans d'autres cas, il peut s'agir d'un utilisateur ou d'un client. Si vous y réfléchissez, cela n'a aucun sens de les familiariser avec ce processus.

Dans cet exemple, il devrait être facile de voir que les changements apportés à la manière dont vous acceptez et transférez les paiements créent des effets d'entraînement dans votre code. En outre, si vous souhaitez modifier le montant que vous facturez ou transférez à vos sous-traitants, des modifications supplémentaires sont nécessaires. Premiers exemples d'opérations au fusil à pompe. Et dans ce cas, nous ne traitons que de trois classes. Imaginez votre douleur si une complexité un peu plus réaliste est impliquée. Oui, c'est de quoi les cauchemars sont faits. Regardons un exemple un peu plus sain d'esprit:

Exemple sans odeur de chirurgie de fusil de chasse et classe extraite:

"ruby class PaymentHandler STANDARD_CHARGE = 10000000 BONUS_CHARGE = 10000000 REFUND_AMOUNT = 10000000 STANDARD_CONTRACTOR_PAYOUT = 200000 BONUS_CONTRACTOR_PAYOUT = 1000000

def initialize (payment_handler = PaymentGem) @payment_handler = payment_handler end

def accept_new_client (evil_client) @ payment_handler.create_client (evil_client.email) end

def charge_for_initializing_operation (evil_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (evil_client_id, STANDARD_CHARGE) end

def charge_for_successful_operation (evil_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (evil_client_id, BONUS_CHARGE) end

def refund (operation) transaction_id = @ payment_handler.find_transaction (operation.payments_id) @ payment_handler.refund (transaction_id, REFUND_AMOUNT) end

def contractant_payout (contractant) spectre_agent_id = @ payment_handler.find_contractor (contractant.email) .payments_id if operation.enemy_agent == 'James Bond' && operation.enemy_agent_status == 'Tué à l'action' payment_handler.transfer_funds (spectre_agent_id, STANDARD_CONTRACTOR_PAYOUT) fin fin fin

classe EvilClient #…

def accept_new_client PaymentHandler.new.accept_new_client (self) end

def charge_for_initializing_operation PaymentHandler.new.charge_for_initializing_operation (self) end

def charge_for_successful_operation (operation) PaymentHandler.new.charge_for_successful_operation (self) end end

Classe Opération #…

def refund PaymentHandler.new.refund (self) end end

classe Contractant #…

def process_payout PaymentHandler.new.contractor_payout (self) end end "

Ce que nous avons fait ici est d’envelopper le PaymentGem API dans notre propre classe. Maintenant, nous avons un endroit central où nous appliquons nos changements si nous décidons que, par exemple, un SpectrePaymentGem fonctionnerait mieux pour nous. Plus besoin de toucher les fichiers internes des paiements multiples liés aux paiements si nous devons nous adapter aux changements. Dans les classes traitant des paiements, nous avons simplement instancié la PaymentHandler et déléguer la fonctionnalité nécessaire. Facile, stable et sans raison de changer.

Et non seulement nous avons tout contenu dans un seul fichier. Dans le PaiementsHandler classe, il n’ya qu’un seul endroit où nous devons échanger et référencer un nouveau processeur de paiement possible. initialiser. C'est rad dans mon livre. Bien sûr, si le nouveau service de paiement a une API complètement différente, vous devez modifier le corps de quelques méthodes dans PaymentHandler. C'est un prix minime à payer comparé à une opération à la carabine complète - cela ressemble plus à une intervention chirurgicale pour une petite écharde au doigt. Bonne affaire!

Si vous ne faites pas attention lorsque vous écrivez des tests pour un processeur de paiement comme celui-ci, ou pour tout service externe sur lequel vous devez compter, vous risquez fort d'être confronté à de graves maux de tête lors du changement d'API. Ils "souffrent de changement" aussi, bien sûr. Et la question est de ne pas changer leur API, seulement quand.

Grâce à notre encapsulation, nous sommes dans une bien meilleure position pour remplacer nos méthodes pour le processeur de paiement. Pourquoi? Parce que les méthodes que nous utilisons sont les nôtres et qu’elles ne changent que lorsque nous le voulons. C'est une grande victoire. Si vous êtes novice en matière de test et que cela n’est pas tout à fait clair pour vous, ne vous inquiétez pas. Prends ton temps; ce sujet peut être délicat au début. Parce que c'est un tel avantage, je voulais juste le mentionner par souci d'exhaustivité.

Comme vous pouvez le constater, j’ai beaucoup simplifié le traitement des paiements dans cet exemple ridicule. J'aurais pu nettoyer le résultat final un peu plus, mais le but était de démontrer clairement l'odeur et comment vous pouvez vous en débarrasser par abstraction..

Si vous n'êtes pas complètement satisfait de ce cours et que vous voyez des possibilités de refactorisation, je vous salue et suis heureux de le prendre pour cela. Je vous recommande de vous assommer! Un bon début pourrait bien être votre façon de trouver payments_ids. La classe elle-même est déjà un peu encombrée…

Changement Divergent

Les changements divergents sont, d’une certaine manière, l’opposé de la chirurgie à la bombe: vous voulez changer une chose et la faire exploser à travers un tas de fichiers différents. Ici, une classe est souvent modifiée pour différentes raisons et de différentes manières. Ma recommandation est d'identifier les parties qui changent ensemble et de les extraire dans une classe séparée pouvant se concentrer sur cette responsabilité unique. Ces classes, à leur tour, ne devraient pas avoir plus d’une raison de changer, sinon une autre odeur de changement divergente attend fort probablement.

Les classes qui subissent des changements divergents sont celles qui changent beaucoup. Avec des outils tels que Churn, vous pouvez mesurer la fréquence à laquelle des parties particulières de votre code ont dû être modifiées par le passé. Plus vous trouvez de points dans une classe, plus grande est la probabilité que des changements divergents se produisent. Je ne serais pas non plus surpris si exactement ces classes sont celles qui causent le plus de bugs au total.

Ne vous méprenez pas: se changer souvent n'est pas directement une odeur. C'est un symptôme utile, cependant. Un autre symptôme très commun et plus explicite est que cet objet doit jongler avec plus d'une responsabilité. le principe de responsabilité unique SRP est un excellent guide pour empêcher cette odeur de code et pour écrire du code plus stable en général. Cela peut être difficile à suivre, mais cela vaut quand même la peine.

Regardons cet exemple méchant ci-dessous. J'ai légèrement modifié l'exemple de la chirurgie au fusil à pompe. Blofeld, responsable de Spectre, est peut-être connu pour la microgestion, mais je doute qu’il soit intéressé par la moitié des choses dans lesquelles ce cours est impliqué..

"classe de rubis Spectre

STANDARD_CHARGE = 10000000 STANDARD_PAYOUT = 200000

def charge_for_initializing_operation (client) evil_client_id = PaymentGem.find_client (client.email) .payments_id PaymentGem.charge (evil_client_id, STANDARD_CHARGE) fin

def Contractor_payout (entrepreneur) spectre_agent_id = PaymentGem.find_contractor (entrepreneur.email) .payments_id PaymentGem.transfer_funds (spectre_agent_id, STANDARD_PAYOUT) fin

def assign_new_operation (operation) operation.contractor = 'Un mec diabolique' operation.objective = 'Voler un chargement de biens de valeur' ​​operation.deadline = 'Minuit, le 18 novembre'

def print_operation_assignment (operation) print “# opération.contracteur est affecté à # opération.objectif. La date limite de la mission se termine à # operation.deadline. ”Fin

def dispose_of_agent (spectre_agent) met «Vous avez déçu cette organisation. Vous savez comment Specter gère les échecs. Au revoir # spectre_agent.code_name! ”Fin fin"

le Spectre la classe a trop de choses différentes qui le préoccupent:

  • Assigner de nouvelles opérations
  • Facturer leur sale boulot
  • Imprimer des missions
  • Tuer des agents spectres infructueux
  • Traiter avec les internes de PaymentGem
  • Payer leurs agents / entrepreneurs Spectre
  • Il connaît également les montants d'argent pour le chargement et le paiement

Sept responsabilités différentes sur une même classe. Pas bon! Vous devez changer la manière dont les agents sont éliminés? Un vecteur pour changer le Spectre classe. Vous voulez gérer les paiements différemment? Un autre vecteur. Vous obtenez la dérive.

Bien que cet exemple soit loin d’être réaliste, il montre tout de même à quel point il est facile d’amener inutilement un comportement qui doit changer fréquemment à un seul endroit. On peut faire mieux!

"classe de rubis Spectre #…

def dispose_of_agent (spectre_agent) met «Vous avez déçu cette organisation. Vous savez comment Specter gère les échecs. Au revoir # spectre_agent.code_name! ”Fin fin

class PaymentHandler STANDARD_CHARGE = 10000000 STANDARD_CONTRACTOR_PAYOUT = 200000

#…

def initialize (payment_handler = PaymentGem) @payment_handler = payment_handler end

def charge_for_initializing_operation (evil_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (evil_client_id, STANDARD_CHARGE) end

def entrepreneur_payout (entrepreneur) spectre_agent_id = @ paiement_handler.find_contracteur (entrepreneur.email) .payments_id @ paiement_handler.transfer_funds (spectre_agent_id, STANDARD_CONTRACTOR_PAYOUT) end end end

classe EvilClient #…

def charge_for_initializing_operation PaymentHandler.new.charge_for_initializing_operation (self) end end

classe Contractant #…

def process_payout PaymentHandler.new.contractor_payout (self) end end

Classe Operation attr_accessor: contractant,: objectif,: date limite

def initialise (attrs = ) @contractor = attrs [: entrepreneur] @objective = attrs [: objectif] @deadline = attrs [: date-limite] end

def print_operation_assignment print “# entrepreneur est attribué à # objectif. La date limite de la mission se termine à # date-limite. "Fin fin"

Ici, nous avons extrait plusieurs classes et leur avons donné leurs propres responsabilités - et donc leur propre raison de changer..

Vous souhaitez gérer les paiements différemment? Maintenant, vous n’aurez plus besoin de toucher le Spectre classe. Vous devez facturer ou payer différemment? Encore une fois, pas besoin d'ouvrir le fichier pour Spectre. L'impression des affectations d'opération est maintenant l'activité commerciale, à laquelle elle appartient. C'est tout. Pas trop compliqué, je pense, mais certainement l’une des odeurs les plus communes que vous devriez apprendre à manipuler tôt.

Dernières pensées

J'espère que vous en êtes arrivé au point où vous vous sentirez prêt à essayer ces refactorisations dans votre propre code et à identifier plus facilement les odeurs de code qui vous entourent. Attention, nous venons tout juste de commencer, mais vous en avez déjà abordé quelques-unes. Je parie que ce n'était pas aussi compliqué qu'on aurait pu le penser!

Bien sûr, les exemples du monde réel seront beaucoup plus difficiles, mais si vous avez compris les mécanismes et les modèles de détection des odeurs, vous serez sûrement en mesure de vous adapter rapidement aux complexités réalistes..