La courte série d’articles qui suit est destinée aux développeurs et débutants de Ruby légèrement expérimentés. J'avais l'impression que les odeurs de code et leurs modifications peuvent être très intimidantes et intimidantes pour les débutants, surtout s'ils ne sont pas en position privilégiée d'avoir des mentors capables de transformer des concepts de programmation mystiques en ampoules brillantes..
Ayant manifestement moi-même marché dans ces chaussures, je me suis souvenu qu'il était inutilement embué d'entrer dans le code qui sent et refactorise.
D'un côté, les auteurs s'attendent à un certain niveau de maîtrise et ne se sentiraient donc pas obligés de fournir au lecteur le même contexte qu'un journaliste débutant qui pourrait avoir besoin de plonger confortablement dans ce monde plus tôt..
En conséquence, peut-être que les débutants ont l’impression qu’ils doivent attendre un peu plus longtemps jusqu’à ce qu’ils soient plus avancés pour se renseigner sur les odeurs et le refactoring. Je ne suis pas d'accord avec cette approche et pense que rendre ce sujet plus accessible les aidera à concevoir de meilleurs logiciels plus tôt dans leur carrière. Au moins, j'espère que cela aidera à donner aux jeunes juniors un bon départ.
Alors de quoi parle-t-on exactement quand les gens mentionnent des odeurs de code? Est-ce toujours un problème dans votre code? Pas nécessairement! Pouvez-vous les éviter complètement? Je ne pense pas! Voulez-vous dire que les odeurs de code mènent à un code cassé? Eh bien, parfois et parfois pas. Devrait-il être ma priorité de les réparer tout de suite? Même réponse, je le crains: parfois oui et parfois vous devriez certainement faire frire les gros poissons en premier. Es-tu fou? Juste question à ce stade!
Avant de continuer à vous plonger dans cette affaire odorante, n'oubliez pas de retirer quelque chose de tout cela: n'essayez pas de réparer toutes les odeurs que vous rencontrez, c'est certainement une perte de temps.!
Il me semble que les odeurs de code sont un peu difficiles à résumer dans une boîte bien étiquetée. Il y a toutes sortes d'odeurs avec différentes options pour y remédier. En outre, différents langages et cadres de programmation sont sujets à différents types d'odeurs, mais il existe certainement beaucoup de souches «génétiques» communes parmi eux. Ma tentative pour décrire les odeurs de code est de les comparer à des symptômes médicaux vous indiquant que vous pourriez avoir un problème. Ils peuvent signaler toutes sortes de problèmes latents et avoir une grande variété de solutions s'ils sont diagnostiqués..
Heureusement, ils ne sont globalement pas aussi compliqués que de s'occuper du corps humain et de la psyché, bien sûr. C'est une comparaison juste, cependant, car certains de ces symptômes doivent être traités immédiatement, et d'autres vous donnent amplement le temps de proposer la solution la mieux adaptée au bien-être général du «patient». Si vous avez du code de travail et que vous rencontrez un problème, vous devrez prendre une décision difficile à prendre si cela vaut la peine de trouver une solution et si ce refactoring améliore la stabilité de votre application..
Cela dit, si vous tombez sur du code que vous pouvez améliorer tout de suite, il est judicieux de laisser le code derrière un peu mieux qu'avant, même un tout petit peu s’ajoute considérablement avec le temps..
La qualité de votre code devient discutable si l'inclusion d'un nouveau code devient plus difficile, par exemple, décider où placer le nouveau code est pénible ou produit de nombreux effets d'entraînement dans votre code, par exemple. Ceci s'appelle la résistance.
Pour vous donner une idée de la qualité du code, vous pouvez toujours le mesurer en fonction de la facilité avec laquelle vous apportez des modifications. Si cela devient de plus en plus difficile, il est certainement temps de refactoriser et de prendre la dernière partie de rouge-vert-REFACTOR plus sérieusement à l'avenir.
Commençons par quelque chose d'extraordinaire - “Les classes de Dieu” - car je pense qu'elles sont particulièrement faciles à comprendre pour les débutants. Les classes divines sont un cas particulier d'une odeur de code appelée Grande classe. Dans cette section, je vais aborder les deux. Si vous avez passé un peu de temps dans Rails Land, vous les avez probablement vues si souvent qu'elles vous semblaient normales..
Vous vous souvenez sûrement du mantra «Modèles de graisse, contrôleur maigre»? En fait, maigre est bon pour tous ces cours, mais à titre indicatif, c'est un bon conseil pour les débutants, je suppose.
Les classes divines sont des objets qui attirent toutes sortes de connaissances et de comportements comme un trou noir. Vos suspects habituels incluent le plus souvent le modèle utilisateur et le problème (espérons-le!) Que votre application tente de résoudre, en tout premier lieu. Une application à exécuter peut être volumineuse Todos modèle, une application de shopping sur Des produits, une application photo sur Photos-vous obtenez la dérive.
Les gens les appellent des classes divines parce qu'ils en savent trop. Ils ont trop de liens avec d'autres classes, principalement parce que quelqu'un les modélisait paresseusement. Cependant, il est difficile de contrôler les classes de dieu. Il est très facile de leur imposer plus de responsabilités et, comme l'attestent de nombreux héros grecs, il faut un peu d'habileté pour diviser et conquérir des «dieux»..
Le problème avec eux, c’est qu’ils deviennent de plus en plus difficiles à comprendre, en particulier pour les nouveaux membres de l’équipe, plus difficiles à changer, et que les réutiliser devient de moins en moins une option à mesure que la gravité augmente. Oh oui, vous avez raison, vos tests sont inutilement plus difficiles à écrire également. En bref, il n’ya pas vraiment d’avantage à avoir de grandes classes, et les classes divines en particulier.
Il existe quelques symptômes / signes courants indiquant que votre classe a besoin d'héroisme ou de chirurgie:
Aussi, si vous plissez les yeux sur votre classe et pensez «Eh? Ew! ”Vous pourriez être sur quelque chose aussi. Si tout cela vous semble familier, il y a de bonnes chances que vous vous trouviez un beau spécimen.
"classe de rubis CastingInviter EMAIL_REGEX = /\A([^@\singer+)@((::-a-zz-9-9++.)+&en-z-¬=2 ,une)\z/
attr_reader: message,: invités,: casting
def initialiser (attributs = ) @message = attributs [: message] || "@invités = attributs [: invités] ||" @ expéditeur = attributs [: expéditeur] @casting = attributs [: diffusion] fin
def valide? valid_message? && valid_invitees? fin def livrer si valide? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = "Votre message # @casting n'a pas pu être envoyé. Les courriels ou les messages des invités ne sont pas valides" invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end private def invalid_invitees @invalid_invitees || = invitee_list.map do | item | sauf item.match (EMAIL_REGEX) item end end.compact end def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;]] /) end def valid_message? @ message.present? end def valid_invitees? invalid_invitees.empty? end
def create_invitation (email) Invitation.create (casting: @casting, expéditeur: @ expéditeur, invitee_email: email, statut: 'en attente') end end "
Moche moche, hein? Pouvez-vous voir combien de méchanceté est regroupé ici? Bien sûr, je mets une petite cerise sur le gâteau, mais vous rencontrerez un tel code tôt ou tard. Pensons à quelles responsabilités cela CastingInviter
la classe doit jongler.
Si tout cela doit être jeté sur une classe qui veut juste donner un appel de casting via livrer
? Certainement pas! Si votre méthode d'invitation change, vous pouvez vous attendre à une opération chirurgicale au fusil de chasse. CastingInviter n'a pas besoin de connaître la plupart de ces détails. C’est plutôt la responsabilité d’une classe spécialisée dans le traitement des problèmes liés au courrier électronique. À l'avenir, vous trouverez également de nombreuses raisons de modifier votre code..
Alors, comment devrions-nous gérer cela? Extraire une classe est souvent un modèle de refactorisation pratique qui se présente comme une solution raisonnable à des problèmes tels que les grandes classes alambiquées, en particulier lorsque la classe en question traite de multiples responsabilités..
Les méthodes privées sont souvent de bons candidats pour commencer avec des notes faciles. Parfois, vous aurez besoin d'extraire même plus d'une classe d'un si mauvais garçon. Ne faites pas tout en même temps. Une fois que vous avez trouvé suffisamment de viande cohérente qui semble appartenir à un objet spécialisé, vous pouvez extraire cette fonctionnalité dans une nouvelle classe..
Vous créez une nouvelle classe et déplacez progressivement les fonctionnalités les unes après les autres. Déplacez chaque méthode séparément et renommez-les si vous voyez une raison. Ensuite, référencez la nouvelle classe dans la classe d'origine et déléguez les fonctionnalités nécessaires. C'est une bonne chose que vous ayez une couverture de test (espérons-le!) Qui vous permet de vérifier si tout fonctionne toujours correctement à chaque étape. Essayez de pouvoir également réutiliser vos classes extraites. Il est plus facile de voir comment cela se passe en action, lisons donc du code:
"classe de rubis CastingInviter
attr_reader: message,: invités,: casting
def initialize (attributs = ) @message = attributs [: message] || "@invités = attributs [: invités] ||" @casting = attributs [: diffusion] @sender = attributs [: expéditeur] fin
def valide? casting_email_handler.valid? fin
def deliver casting_email_handler.deliver end
privé
def casting_email_handler @casting_email_handler || = CastingEmailHandler.new (message: message, invités: invités, casting: casting, expéditeur: @ expéditeur) end end "
"classe de rubis CastingEmailHandler EMAIL_REGEX = /\A([^@\saser+ )@((::--zz-9-9++-) .-++--a-***/
def initialise (attr = ) @message = attr [: message] || "@invités = attr [: invités] ||" @casting = attr [: casting] @sender = attr [: expéditeur] end
def valide? valid_message? && valid_invitees? fin
def livrer si valide? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = “Votre message # @casting n'a pas pu être envoyé. Les e-mails ou messages des invités ne sont pas valides »invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end
privé
def invalid_invitees @invalid_invitees || = invitee_list.map do | item | sauf item.match (EMAIL_REGEX) item end end.compact end
def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;] + /) fin
def valid_invites? invalid_invitees.empty? fin
def valid_message? @ message.present? fin
def create_invitation (email) Invitation.create (casting: @casting, expéditeur: @ expéditeur, invitee_email: email, statut: 'en attente') end end "
Dans cette solution, vous verrez non seulement comment cette séparation des problèmes affecte la qualité de votre code, mais elle lit également beaucoup mieux et devient plus facile à digérer..
Ici, nous déléguons des méthodes à une nouvelle classe spécialisée dans la fourniture de ces invitations par courrier électronique. Vous avez un endroit dédié qui vérifie si les messages et les invités sont valides et comment ils doivent être remis.. CastingInviter
n'a pas besoin de savoir quoi que ce soit à propos de ces détails, nous avons donc délégué ces responsabilités à une nouvelle classe CastingEmailHandler
.
La connaissance de la manière de livrer et de vérifier la validité de ces courriels d’invitations à faire partie du casting est maintenant contenue dans notre nouvelle classe extraite. Avons-nous plus de code maintenant? Tu paries! Cela valait-il la peine de séparer les préoccupations? Plutôt sûr! Pouvons-nous aller au-delà de cela et refactor CastingEmailHandler
un peu plus? Absolument! Assommez-vous!
Au cas où vous vous interrogeriez sur le valide?
méthode sur CastingEmailHandler
et CastingInviter
, celui-ci est pour RSpec pour créer un matcher personnalisé. Cela me permet d'écrire quelque chose comme:
ruby expect (casting_inviter) .to_valid
Assez pratique, je pense.
Il existe davantage de techniques permettant de traiter de grandes classes / objets divins. Au cours de cette série, vous apprendrez plusieurs façons de reformuler de tels objets..
Il n'y a pas de prescription fixe pour traiter ces cas - cela dépend toujours, et c'est un jugement au cas par cas si vous devez apporter les gros calibres ou si des techniques de refactoring incrémentielles plus petites obligent à faire mieux. Je sais, un peu frustrant parfois. Suivre le principe de responsabilité unique ira un long chemin, et c'est un bon nez à suivre.
Avoir des méthodes un peu grosses est l’une des choses les plus courantes que vous rencontrez en tant que développeur. En général, vous voulez savoir en un coup d’œil ce qu’une méthode est censée faire. Il devrait également avoir un seul niveau d'imbrication ou un seul niveau d'abstraction. En bref, évitez d’écrire des méthodes compliquées.
Je sais que cela semble difficile, et c'est souvent le cas. Une solution fréquente consiste à extraire des parties de la méthode dans une ou plusieurs nouvelles fonctions. Cette technique de refactoring est appelée la méthode d'extrait-c'est un des plus simples mais néanmoins très efficace. En tant qu'effet secondaire intéressant, votre code devient plus lisible si vous nommez vos méthodes de manière appropriée..
Jetons un coup d'œil aux caractéristiques techniques pour lesquelles vous aurez souvent besoin de cette technique. Je me souviens d'avoir été présenté à la méthode d'extrait tout en écrivant de telles caractéristiques et à quel point c'était incroyable de voir l'ampoule allumée. Parce que les caractéristiques de ce type sont faciles à comprendre, elles constituent un bon candidat pour la démonstration. De plus, vous rencontrerez encore et encore des scénarios similaires lorsque vous rédigerez vos spécifications..
spec / features / some_feature_spec.rb
"ruby nécessite 'rails_helper'
fonctionnalité 'M marque la mission comme' le scénario est terminé 'avec succès' visiter visit_root_path fill_in 'Email', avec: '[email protected]' click_button 'Soumettre' visiter mission_path click_on 'Créer une mission' fill_in 'Nom de la mission', avec: 'Projet Moonraker 'click_button' Soumettre '
dans "li: contient ('Project Moonraker')", cliquez sur "Mission terminée" et attendez (page) .pour avoir_css "ul.missions li.mission-name.completed", texte: "Projet Moonraker" à la fin "
Comme vous pouvez facilement le constater, beaucoup de choses se passent dans ce scénario. Vous accédez à la page d'index, vous vous connectez et créez une mission pour la configuration, puis vous vous exercez via le marquage de la mission comme étant terminée et vous vérifiez enfin le comportement. Aucune science de fusée, mais aussi pas propre et certainement pas composé pour la réutilisation. On peut faire mieux que ça:
spec / features / some_feature_spec.rb
"ruby nécessite 'rails_helper'
fonctionnalité 'M marque la mission comme étant terminée' réalise le scénario 'avec succès' sign_in_as '[email protected]' create_classified_mission_named 'Project Moonraker'
mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Project Moonraker' end end
def create_classified_mission_named (nom_mission) visiter mission_path click_on 'Create Mission' fill_in 'Nom de la mission', avec: nom_mission click_button 'Submit' end
def mark_mission_as_complete (nom_mission) dans “li: contient ('# mission_name' ')” ne cliquez pas sur' Mission terminée 'end end
def agent_sees_completed_mission (nom_mission) attend (page) .to_css 'ul.missions li.mission-nom.completed', texte: nom_mission
def sign_in_as (email) visitez root_path fill_in 'Email', avec: email click_button 'Submit' end "
Ici, nous avons extrait quatre méthodes qui peuvent maintenant être facilement réutilisées dans d’autres tests. J'espère que c'est clair que nous avons frappé d'une pierre trois coups. La fonctionnalité est beaucoup plus concise, elle lit mieux et se compose de composants extraits sans duplication.
Imaginons que vous ayez écrit toutes sortes de scénarios similaires sans extraire ces méthodes et que vous vouliez modifier certaines implémentations. Maintenant, vous souhaitez avoir pris le temps de refactoriser vos tests et de disposer d'un emplacement central pour appliquer vos modifications..
Bien sûr, il existe un moyen encore meilleur de traiter des spécifications de fonctionnalité telles que this-Page Objects, par exemple, mais ce n’est pas notre domaine d’activité actuel. Je suppose que c'est tout ce que vous devez savoir sur l'extraction de méthodes. Vous pouvez appliquer ce modèle de refactoring partout dans votre code, pas seulement dans les spécifications, bien sûr. En termes de fréquence d'utilisation, je suppose que ce sera votre technique numéro un pour améliorer la qualité de votre code. S'amuser!
Fermons cet article avec un exemple de la façon dont vous pouvez affiner vos paramètres. Cela devient vite fastidieux lorsque vous devez alimenter vos méthodes avec plus d'un ou deux arguments. Ne serait-il pas agréable de déposer un objet à la place? C’est exactement ce que vous pouvez faire si vous introduisez un objet paramètre.
Tous ces paramètres sont non seulement pénibles à écrire et à maintenir en ordre, mais peuvent également conduire à une duplication de code - et nous voulons certainement éviter cela autant que possible. Ce que j'aime particulièrement dans cette technique de refactoring, c'est comment cela affecte également d'autres méthodes à l'intérieur. Vous êtes souvent capable de vous débarrasser de beaucoup de paramètres indésirables dans la chaîne alimentaire.
Passons en revue cet exemple simple. M peut attribuer une nouvelle mission et nécessite un nom de mission, un agent et un objectif. M est également capable de changer le double 0 du statut des agents, ce qui signifie que leur licence de tuer.
"classe de ruby M def assign_new_mission (nom_mission, nom_agent, objectif, licence_coil: nil) print" La mission # nom_mission a été affectée à # agent_name avec pour objectif # # objectif. "si licence_to_kill print" La licence de tuer a été accordé. "else print" La licence de mise à mort n'a pas été accordée. "end end end
m = M.new m.assign_new_mission ('Octopussy', 'James Bond', 'trouver le dispositif nucléaire', licence_to_kill: true) # => Mission Octopussy a été affecté à James Bond dans le but de trouver le dispositif nucléaire. Le permis de tuer a été accordé. "
Lorsque vous regardez cela et que vous vous demandez ce qui se passe lorsque la complexité des «paramètres» de la mission augmente, vous êtes déjà sur quelque chose. C'est un problème que vous ne pouvez résoudre que si vous transmettez un seul objet contenant toutes les informations dont vous avez besoin. Plus souvent qu'autrement, cela vous permet également d'éviter de changer de méthode si l'objet paramètre change pour une raison quelconque.
"classe ruby Mission attr_reader: 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é assignée à # agent_name avec pour objectif # # objectif." if licence_to_kill print "La licence de tuer a été accordée." else print "Le permis de tuer n'a pas été accordé." fin fin fin
classe M def assign_new_mission (mission) mission.assign end end
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) # => Mission Octopussy a été attribué à James Bond dans le but de trouver le dispositif nucléaire. Le permis de tuer a été accordé. "
Nous avons donc créé un nouvel objet, Mission
, qui est uniquement axé sur la fourniture M
avec les informations nécessaires pour assigner une nouvelle mission et fournir #assign_new_mission
avec un objet paramètre singulier. Pas besoin de passer vous-même ces paramètres embêtants. Au lieu de cela, vous indiquez à l'objet de révéler les informations dont vous avez besoin dans la méthode elle-même. De plus, nous avons également extrait un comportement - les informations sur la manière d’imprimer - dans le nouveau Mission
objet.
Pourquoi devrais-je M
besoin de savoir comment imprimer des missions? Le nouveau #attribuer
de l’extraction en perdant un peu de poids car nous n’avions pas besoin de passer dans le paramètre object. Il n’est donc pas nécessaire d’écrire des choses comme mission.nom_mission
, mission.nom_agent
etc. Maintenant, nous utilisons simplement notre attr_reader
(s), qui est beaucoup plus propre que sans l'extraction. Tu creuses?
Ce qui est également pratique à ce sujet est que Mission
peut collecter toutes sortes de méthodes ou d’états supplémentaires qui sont joliment encapsulés à un seul endroit et prêts à être consultés.
Avec cette technique, vous obtiendrez des méthodes plus concises, qui tendent à mieux lire et à éviter de répéter le même groupe de paramètres dans tous les sens. Très bonne affaire! Se débarrasser de groupes de paramètres identiques est également une stratégie importante pour le code DRY.
Essayez de ne pas extraire plus que vos données. Si vous pouvez également placer un comportement dans la nouvelle classe, vous aurez des objets plus utiles, sinon ils commenceront à sentir rapidement.
Bien sûr, la plupart du temps, vous rencontrerez des versions plus compliquées de celui-ci, et vos tests devront également être adaptés simultanément au cours de ces restructurations. Cependant, si vous avez cet exemple simple à votre actif, vous serez prêt à passer à l'action..
Je vais regarder le nouveau Bond maintenant. Entendu que ce n'est pas si bon, cependant…
Mise à jour: Saw Specter. Mon verdict: comparé à Skyfall, qui était MEH imho-Spectre était wawawiwa!