Ruby / Rails Code Geur Grondbeginselen 01

Onderwerpen

  • Duikt op
  • Weerstand
  • Grote klasse / Godklasse
  • Klasse extraheren
  • Lange methode
  • Lange parameterlijst

Staat op

De volgende korte reeks artikelen is bedoeld voor enigszins ervaren Ruby-ontwikkelaars en starters. Ik had de indruk dat code ruikt en hun refactorings kunnen heel ontmoedigend en intimiderend zijn voor beginners - vooral als ze niet in de gelukkige positie zijn om mentoren te hebben die mystieke programmeerconcepten kunnen omzetten in glanzende lampen..

Omdat ik duidelijk in deze schoenen heb gewandeld, herinnerde ik me dat het onnodig mistig aanvoelde om in codegeur en refactorings terecht te komen.

Aan de ene kant verwachten auteurs een bepaald niveau van bekwaamheid en voelen ze zich daarom misschien niet supergezind om de lezer dezelfde hoeveelheid context te bieden als een newbie nodig heeft om deze wereld sneller te kunnen ontdekken..

Als een gevolg hiervan, misschien, newbies aan de andere kant de indruk dat ze een beetje langer moeten wachten totdat ze meer geavanceerd zijn om te leren over geuren en refactorings. Ik ben het niet eens met die benadering en denk dat het beter benaderbaar maken van dit onderwerp hen zal helpen betere software te ontwerpen eerder in hun carrière. Ik hoop in ieder geval dat het helpt om junior-peeps een stevige voorsprong te geven.

Dus waar hebben we het over precies wanneer mensen code ruikt? Is het altijd een probleem in je code? Niet noodzakelijk! Kun je ze volledig vermijden? Ik denk het niet! Bedoel je dat codeergeuren tot gebroken code leiden? Wel, soms en soms niet. Moet het mijn prioriteit zijn om ze meteen te repareren? Hetzelfde antwoord, vrees ik: soms wel en soms moet je zeker eerst grotere vissen bakken. Ben je gek? Redelijke vraag op dit punt!

Voordat je verder gaat met duiken in dit hele stinkende bedrijf, onthoud dit om één ding van dit alles weg te nemen: probeer niet elke geur die je tegenkomt te repareren - dit is zeker een verspilling van je tijd!

Het lijkt mij dat de codesgeur een beetje moeilijk is om in te sluiten in een mooi gelabeld doosje. Er zijn allerlei geuren met verschillende opties om ze aan te pakken. Ook zijn verschillende programmeertalen en -kaders gevoelig voor verschillende soorten geuren, maar er zijn absoluut veel gemeenschappelijke "genetische" stammen. Mijn poging om code ruikt is om ze te vergelijken met medische symptomen die u vertellen dat u een probleem zou kunnen hebben. Ze kunnen wijzen op allerlei soorten latente problemen en een breed scala aan oplossingen hebben als ze worden gediagnosticeerd.

Gelukkig zijn ze over het algemeen niet zo ingewikkeld als het omgaan met het menselijk lichaam - en natuurlijk psyche. Het is echter een goede vergelijking, omdat sommige van deze symptomen meteen moeten worden behandeld, en sommige anderen geven je ruim de tijd om een ​​oplossing te bedenken die het beste is voor het algehele welzijn van de patiënt. Als je werkende code hebt en tegen iets stinkt aanloopt, moet je de moeilijke beslissing nemen als het de moeite waard is om een ​​oplossing te vinden en als die refactoring de stabiliteit van je app verbetert.

Dat gezegd hebbende, als je code tegenkomt die je meteen kunt verbeteren, is het een goed advies om de code een beetje beter achter te laten dan voorheen - zelfs een klein beetje beter wordt aanzienlijk beter in de loop van de tijd.

Weerstand

De kwaliteit van je code wordt dubieus als het opnemen van nieuwe code moeilijker wordt, zoals het kiezen van een nieuwe code lastig is of heel veel rimpelingseffecten in bijvoorbeeld je codebase. Dit wordt weerstand genoemd.

Als richtlijn voor de kwaliteit van de code kunt u deze altijd meten aan de hand van het gemak waarmee wijzigingen kunnen worden doorgevoerd. Als dat moeilijker en moeilijker wordt, is het absoluut tijd om te refacteren en het laatste deel van te nemen rood-groen-refactor serieuzer in de toekomst.

Grote klasse / Godklasse

Laten we beginnen met iets bijzonders, "God classes", omdat ik denk dat ze bijzonder gemakkelijk te begrijpen zijn voor beginners. God klassen zijn een speciaal geval van een code geur genoemd Grote klasse. In deze sectie zal ik beide behandelen. Als je een beetje tijd in Railsland hebt doorgebracht, heb je ze waarschijnlijk zo vaak gezien dat ze er normaal uitzien.

Herinner je je de mantra van de 'dikke modellen, magere controller' nog? Nou eigenlijk, skinny is goed voor al deze klassen, maar als richtlijn is het een goed advies voor nieuwkomers, denk ik.

Godscursussen zijn objecten die allerlei soorten kennis en gedrag aantrekken als een zwart gat. Je gebruikelijke verdachten hebben meestal het gebruikersmodel en welk probleem (hopelijk!) Je app probeert op te lossen, in de eerste plaats ten minste. Een todo-app kan bulk krijgen op de Todos model, een winkel-app op producten, een foto-app op foto's-je krijgt de drift.

Mensen noemen ze godslessen omdat ze te veel weten. Ze hebben te veel connecties met andere klassen, voornamelijk omdat iemand ze lui aan het modelleren was. Het is echter hard werken om godscategorieën onder controle te houden. Ze maken het heel gemakkelijk om meer verantwoordelijkheden aan hen te dumpen, en zoals veel Griekse helden zouden beamen, kost het een beetje vaardigheid om "goden" te verdelen en te veroveren..

Het probleem met hen is dat ze moeilijker en moeilijker te begrijpen worden, vooral voor nieuwe teamleden, moeilijker te veranderen en hergebruiken ervan wordt steeds minder een optie, hoe meer zwaartekracht ze hebben vergaard. Oh ja, je hebt gelijk, je testen zijn ook onnodig moeilijker om te schrijven. Kortom, er is niet echt een voordeel bij het hebben van grote klassen, en met name godscursussen.

Er zijn een paar veel voorkomende symptomen / tekenen dat je klas wat heldendaad / chirurgie nodig heeft:

  • Je moet scrollen!
  • Tal van particuliere methoden?
  • Heeft uw klas er zeven of meer methoden op staan?
  • Moeilijk om te vertellen wat je klas echt doet - beknopt!
  • Heeft je klas veel redenen om te veranderen wanneer je code evolueert??

Ook als je naar je klas tuurt en denkt: "Eh? Ew! "Misschien ben je ook iets van plan. Als dat allemaal bekend klinkt, is de kans groot dat je een goed exemplaar hebt gevonden.

"ruby class CastingInviter EMAIL_REGEX = /\A=[^@\s]+)@((?:[-a-z0-9]+.)+[a-z]2,)\z/

attr_reader: message,: invitees,: casting

def initialize (attributes = ) @message = attributes [: message] || "@invitees = attributes [: invitees] ||" @sender = attributen [: afzender] @casting = attributen [: casting] einde

def geldig? valid_message? && valid_invitees? end def deliver indien geldig? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = "Uw # @casting bericht kon niet worden verzonden. Invitees e-mails of berichten zijn ongeldig" invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end private def invalid_invitees @invalid_invitees || = invitee_list.map do | item | tenzij item.match (EMAIL_REGEX) item end endpact 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, afzender: @sender, invitee_email: email, status: 'pending') end end "

Lelijke vent, toch? Zie je hoeveel misselijkheid hier wordt gebundeld? Natuurlijk zet ik er een kersje bovenop, maar zul je vroeg of laat de code tegenkomen. Laten we nadenken over welke verantwoordelijkheden dit is CastingInviter klas moet jongleren.

  • E-mail bezorgen
  • Controleren op geldige berichten en e-mailadressen
  • Witruimte verwijderen
  • E-mailadressen splitsen op komma's en puntkomma's

Moet dit allemaal worden gedumpt in een klas die gewoon een castingoproep wil afleveren leveren? Zeker niet! Als je uitnodigingsmethode verandert, kun je verwachten dat je een operatie tegen een geweer zult tegenkomen. CastingInviter hoeft de meeste van deze details niet te kennen. Dat is meer de verantwoordelijkheid van een klas die gespecialiseerd is in het omgaan met e-mailgerelateerde zaken. In de toekomst zult u hier ook veel redenen vinden om uw code te wijzigen.

Klasse extraheren

Dus hoe moeten we hiermee omgaan? Vaak is het extraheren van een klas een handig refactoringpatroon dat zichzelf presenteert als een redelijke oplossing voor problemen als grote, ingewikkelde klassen, vooral wanneer de betreffende klas meerdere verantwoordelijkheden behandelt..

Particuliere methoden zijn vaak goede kandidaten om mee te beginnen - en ook gemakkelijk markeringen. Soms moet je nog meer dan één klas uit zo'n slechte jongen halen - doe het niet allemaal in één stap. Als je eenmaal genoeg samenhangend vlees hebt gevonden dat lijkt te behoren tot een speciaal gespecialiseerd object, kun je die functionaliteit in een nieuwe klasse uitpakken.

U maakt een nieuwe klasse en verplaatst de functionaliteit geleidelijk één voor één. Verplaats elke methode afzonderlijk en hernoem ze als je daar een reden voor ziet. Refereer dan naar de nieuwe klasse in de originele en delegeer de benodigde functionaliteit. Het is maar goed dat je een testverslag hebt (hopelijk!) Waarmee je kunt controleren of de dingen nog steeds goed werken, elke stap van de weg. Streef ernaar om je geëxtraheerde klassen ook te kunnen hergebruiken. Het is gemakkelijker om te zien hoe het in actie wordt uitgevoerd, dus laten we wat code lezen:

"robijnklasse CastingInviter

attr_reader: message,: invitees,: casting

def initialize (attributes = ) @message = attributes [: message] || "@invitees = attributes [: invitees] ||" @casting = attributes [: casting] @sender = attributes [: afzender] einde

def geldig? casting_email_handler.valid? einde

def lever casting_email_handler.deliver einde

privaat

def casting_email_handler @casting_email_handler || = CastingEmailHandler.new (bericht: bericht, genodigden: genodigden, casting: casting, afzender: @sender) end end "

"ruby class CastingEmailHandler EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+.)+[a-z]2,)\z/

def initialize (attr = ) @message = attr [: message] || "@invitees = attr [: invitees] ||" @casting = attr [: casting] @sender = attr [: afzender] einde

def geldig? valid_message? && valid_invitees? einde

def leveren als geldig? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = "Uw # @casting bericht kon niet worden verzonden. Nodigt e-mails of berichten uit die ongeldig zijn "invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end

privaat

def invalid_invitees @invalid_invitees || = invitee_list.map do | item | tenzij item.match (EMAIL_REGEX) item end endpact einde eindigt

def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;] + /) einde

def valid_invitees? invalid_invitees.empty? einde

def valid_message? @ Message.present? einde

def create_invitation (email) Invitation.create (casting: @casting, afzender: @sender, invitee_email: email, status: 'pending') end end "

In deze oplossing ziet u niet alleen hoe deze scheiding van zorgen uw codekwaliteit beïnvloedt, het leest ook veel beter en wordt gemakkelijker te verteren.

Hier delegeren we methoden naar een nieuwe klasse die gespecialiseerd is in het afhandelen van deze uitnodigingen via e-mail. Je hebt één speciale plaats die controleert of de berichten en genodigden geldig zijn en hoe ze moeten worden afgeleverd. CastingInviter hoeft niets te weten over deze details, dus delegeren we deze verantwoordelijkheden naar een nieuwe klas CastingEmailHandler.

De kennis over hoe te leveren en controleren op de geldigheid van deze e-mails met castinguitnodigingen is nu allemaal opgenomen in onze nieuwe uitgepakte klasse. Hebben we nu meer code? Zeker weten! Was het de moeite waard om zorgen te scheiden? Vrij zeker! Kunnen we verder gaan en refactoren CastingEmailHandler wat meer? Absoluut! Klop jezelf uit!

In het geval dat je je afvraagt ​​over de Geldig? methode op CastingEmailHandler en CastingInviter, deze is voor RSpec om een ​​aangepaste matcher te maken. Hierdoor kan ik iets schrijven als:

robijn verwacht (casting_inviter) .to be_valid

Behoorlijk handig, denk ik.

Er zijn meer technieken om grote klassen / god-objecten aan te pakken, en in de loop van deze serie leer je een aantal manieren om zulke objecten te refactoren.

Er is geen vast recept voor het afhandelen van deze gevallen - het is altijd afhankelijk en het is een beslissing per geval als u de grote wapens moet nemen of als kleinere, incrementele refactoringtechnieken het beste zijn. Ik weet het, soms een beetje frustrerend. Het volgen van het Single Responsibility Principle (SRP) gaat echter een heel eind, en het is een goede neus om te volgen.

Lange methode

Het hebben van methoden die een beetje groot zijn geworden, is een van de meest voorkomende dingen die je als ontwikkelaar tegenkomt. In het algemeen wilt u in een oogopslag weten wat een methode moet doen. Het zou ook slechts één niveau van nesten of één niveau van abstractie moeten hebben. Kortom, vermijd het schrijven van gecompliceerde methoden.

Ik weet dat dit hard klinkt, en dat is het vaak ook. Een oplossing die vaak naar voren komt is het extraheren van delen van de methode in een of meer nieuwe functies. Deze refactoringtechniek wordt de extract methode-het is een van de eenvoudigste maar toch zeer effectief. Als een leuk neveneffect, wordt uw code beter leesbaar als u uw methoden op de juiste manier een naam geeft.

Laten we eens kijken naar functie-specificaties waar je deze techniek veel nodig hebt. Ik herinner me dat ik kennis heb gemaakt met de extract methode tijdens het schrijven van dergelijke functie-specificaties en hoe geweldig het voelde toen de gloeilamp ging branden. Omdat functiespecificaties zoals deze gemakkelijk te begrijpen zijn, zijn ze een goede kandidaat voor demonstratie. Bovendien zul je steeds weer dezelfde scenario's tegenkomen wanneer je je specificaties schrijft.

spec / kenmerken / some_feature_spec.rb

"ruby require 'rails_helper'

functie 'M markeert missie als compleet' do scenario 'succesvol' do visit_root_path vul_in 'E-mail', met: '[email protected]' click_button 'Verzenden' bezoek missies_pad klik_op 'Maak missie' vul in 'Missienaam', met: 'Project Moonraker 'click_button' Verzenden '

binnen "li: contains ('Project Moonraker')" do click_on 'Mission completed' end expect (pagina) .to have_css 'ul.missions li.mission-name.completed', tekst: 'Project Moonraker' end end "

Zoals je gemakkelijk kunt zien, is er veel aan de hand in dit scenario. U gaat naar de indexpagina, logt in en maakt een missie voor de installatie en oefent vervolgens door de missie als voltooid te markeren en ten slotte verifieert u het gedrag. Geen rocket science, maar ook niet schoon en zeker niet gecomposeerd voor herbruikbaarheid. We kunnen het beter dan dat:

spec / kenmerken / some_feature_spec.rb

"ruby require 'rails_helper'

kenmerk 'M markeert missie als compleet' do scenario 'succesvol' do 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 (mission_name) bezoek missions_path click_on 'Create Mission' fill_in 'Mission Name', met: mission_name click_button 'Submit' end

def mark_mission_as_complete (mission_name) binnen "li: contains ('# mission_name')" do click_on 'Mission completed' end end

def agent_sees_completed_mission (mission_name) verwacht (pagina) .to have_css 'ul.missions li.mission-name.completed', text: mission_name end

def sign_in_as (email) bezoek root_path fill_in 'Email', met: email click_button 'Submit' end "

Hier hebben we vier methoden geëxtraheerd die nu gemakkelijk in andere tests kunnen worden hergebruikt. Ik hoop dat het duidelijk is dat we drie vliegen in één klap slaan. De functie is veel beknopter, het leest beter, en het is opgebouwd uit geëxtraheerde componenten zonder duplicatie.

Laten we ons voorstellen dat je allerlei vergelijkbare scenario's hebt geschreven zonder deze methoden te extraheren en je een aantal implementaties wilde wijzigen. Nu zou je willen dat je de tijd had genomen om je tests te verfijnen en dat je een centrale plaats had om je wijzigingen toe te passen.

Natuurlijk is er een nog betere manier om met functiespecificaties zoals dit - pagina-objecten - om te gaan, maar dat is niet onze ruimte voor vandaag. Ik denk dat dat alles is wat je moet weten over extractiemethoden. Je kunt dit refactoring-patroon overal in je code toepassen, niet alleen in specificaties, natuurlijk. Qua gebruiksfrequentie is mijn inschatting dat het je nummer een techniek zal zijn om de kwaliteit van je code te verbeteren. Veel plezier!

Lange parameterlijst

Laten we dit artikel afsluiten met een voorbeeld van hoe u uw parameters kunt afslanken. Het wordt nogal snel als je je methoden meer dan een of twee argumenten moet voeden. Zou het niet leuk zijn om in plaats daarvan in één object te vallen? Dat is precies wat u kunt doen als u een introduceert parameter object.

Al deze parameters zijn niet alleen lastig om te schrijven en te houden, maar kunnen ook leiden tot codeduplicatie - en we willen dat zeker vermijden waar mogelijk. Wat ik vooral leuk vind aan deze refactoringtechniek, is hoe dit ook andere methoden binnenin beïnvloedt. Je bent vaak in staat om veel parameterafval in de voedselketen kwijt te raken.

Laten we dit eenvoudige voorbeeld bespreken. M kan een nieuwe missie toewijzen en heeft een missienaam, een agent en een doel nodig. M kan ook de dubbele 0-status van agenten wijzigen, wat betekent dat ze hun licentie hebben om te doden.

"ruby class M def assign_new_mission (mission_name, agent_name, objective, licence_to_kill: nihil) print" Mission # mission_name is toegewezen aan # agent_name met als doelstelling # objective. "if licence_to_kill print" De licentie om te doden is verleend. "else print" De licentie om te doden is niet toegekend. "end-end-end

m = M.new m.assign_new_mission ('Octopussy', 'James Bond', 'vind het nucleaire apparaat', licence_to_kill: true) # => Missie Octopussy is toegewezen aan James Bond met als doel het nucleaire apparaat te vinden. De licentie om te doden is toegekend. "

Als je dit bekijkt en je vraagt ​​wat er gebeurt als de missieparameters in complexiteit toenemen, ben je al iets van plan. Dat is een pijnpunt dat je alleen kunt oplossen als je een enkel object doorgeeft met alle informatie die je nodig hebt. Vaker wel dan niet helpt dit u ook om weg te blijven van het veranderen van de methode als het parameterobject om een ​​of andere reden verandert.

"ruby class Mission attr_reader: mission_name,: agent_name,: objective,: licence_to_kill

def initialize (mission_name: mission_name, agent_name: agent_name, doelstelling: objective, licence_to_kill: licence_to_kill) @mission_name = mission_name @agent_name = agent_name @objective = objective @licence_to_kill = licence_to_kill end

def assign print "Mission # mission_name is toegewezen aan # agent_name met als doelstelling # objective." als licence_to_kill print "De licentie om te doden is verleend." else print "De licentie om te doden is niet verleend." einde einde 

klasse M def. assign_new_mission (missie) mission.assign end end

m = M.new mission = Mission.new (missienaam: 'Octopussy', agentnaam: 'James Bond', doelstelling: 'vind het nucleaire apparaat', licence_to_kill: true) m.assign_new_mission (missie) # => Missie Octopussy is geweest toegewezen aan James Bond met als doel het nucleaire apparaat te vinden. De licentie om te doden is toegekend. "

Dus we hebben een nieuw object gemaakt, Missie, dat is uitsluitend gericht op het leveren M met de informatie die nodig is om een ​​nieuwe missie toe te wijzen en te voorzien #assign_new_mission met een singulier parameter-object. Het is niet nodig om deze vervelende parameters zelf door te geven. In plaats daarvan vertel je het object om de informatie te onthullen die je nodig hebt binnen de methode zelf. Daarnaast hebben we ook wat gedrag (de informatie hoe je moet afdrukken) in het nieuwe gehaald Missie voorwerp.

Waarom zou M moet je weten hoe je missieopdrachten print? De nieuwe #toewijzen profiteerde ook van extractie door wat gewicht te verliezen, omdat we het parameterobject niet hoefden door te geven - dus geen behoefte om dingen zoals te schrijven mission.mission_name, mission.agent_name enzovoorts. Nu gebruiken we gewoon onze attr_reader(s), die veel schoner is dan zonder de extractie. Jij graaft?

Wat ook handig is, is dat Missie kan allerlei aanvullende methoden of toestanden verzamelen die mooi op één plek zijn ingekapseld en voor u klaarstaan ​​om te openen.

Met deze techniek kom je uit op methoden die beknopter zijn, geneigd zijn beter te lezen en niet dezelfde groep parameters overal hoeven herhalen. Best goede deal! Het wegwerken van identieke groepen parameters is ook een belangrijke strategie voor DRY-code.

Probeer uit te kijken om meer te extraheren dan alleen uw gegevens. Als u ook gedrag in de nieuwe klasse kunt plaatsen, heeft u objecten die nuttiger zijn, anders zullen ze ook snel gaan ruiken.

Natuurlijk kom je meestal ingewikkeldere versies tegen - en je tests zullen zeker ook tegelijkertijd moeten worden aangepast tijdens dergelijke refactorings - maar als je dat eenvoudige voorbeeld onder je riem hebt, ben je klaar voor actie.

Ik ga nu naar de nieuwe Bond kijken. Ik heb gehoord dat het niet zo goed is, hoewel ...

Update: Saw Spectre. Mijn vonnis: vergeleken met Skyfall - dat was MEH imho-Specter was wawawiwa!