Följande korta serier av artiklar är avsedda för lite erfarna Ruby-utvecklare och förrättare. Jag hade intrycket av att koden luktar och deras refactorings kan vara väldigt skrämmande och skrämmande för nybörjare, särskilt om de inte har det lyckliga stället att ha mentorer som kan göra mystiska programmeringskoncept till lysande glödlampor.
Jag har självklart gått i dessa skor själv, jag kom ihåg att det kände sig onödigt duggigt att komma in i kodlukt och refactorings.
Å ena sidan författare förväntar sig en viss nivå av kompetens och kan därför inte känna sig övertygad för att ge läsaren samma mängd sammanhang som en nybörjare kan behöva bekvämt dyka in i denna värld tidigare.
Som en följd därav, kanske nybörjare indikerar att de ska vänta lite längre tills de är mer avancerade att lära sig om lukt och refactorings. Jag håller inte med det här tillvägagångssättet och tycker att det blir mer tillvägagångssätt för detta ämne att hjälpa dem att utforma bättre programvara tidigare i sin karriär. Jag hoppas åtminstone att det hjälper till att ge junior peeps med en solid start.
Så vad pratar vi om exakt när folk nämner kodlufter? Är det alltid ett problem i din kod? Inte nödvändigtvis! Kan du undvika dem helt? Jag tror inte det! Menar du att kodlukt leder till trasig kod? Tja, ibland och ibland inte. Bör det vara min prioritet att fixa dem genast? Samma svar, jag fruktar: Ibland ja och ibland borde du verkligen steka större fisk först. Är du galen? Rättvis fråga på denna punkt!
Innan du fortsätter att dyka in i hela denna illaluktande verksamhet, kom ihåg att ta bort en sak från allt detta: Försök inte att fixa varje lukt du stöter på - det här är verkligen ett slöseri med din tid!
Det verkar som om koden luktar är lite svårt att sätta upp i en snyggt märkt låda. Det finns alla typer av dofter med olika olika alternativ för att ta itu med dem. Olika programmeringsspråk och ramverk är också benägen för olika typer av luktar, men det finns definitivt många gemensamma "genetiska" stammar bland dem. Mitt försök att beskriva kodluktar är att jämföra dem med medicinska symtom som berättar att du kan ha problem. De kan peka på alla möjliga latenta problem och få en mängd olika lösningar om de diagnostiseras.
Lyckligtvis är de överlag inte så komplicerade som att hantera människokroppen och psyken förstås. Det är en rättvis jämförelse, eftersom vissa av dessa symtom behöver behandlas genast och vissa andra ger dig gott om tid för att komma fram till en lösning som är bäst för patientens övergripande välbefinnande. Om du har en fungerande kod och du stöter på något illaluktande måste du fatta det svåra beslutet om det är värt dags att hitta en lösning och om den refactoring förbättrar stabiliteten för din app.
Med det sagt, om du snubblar på kod som du kan förbättra direkt, är det bra att lämna koden bakom lite bättre än tidigare - även en liten bit förbättras väsentligt över tiden.
Kvaliteten på din kod blir tvivelaktig om införandet av ny kod blir svårare att bestämma var du ska sätta ny kod är en smärta eller kommer med mycket rippeleffekter i hela ditt kodbas. Detta kallas motstånd.
Som riktlinje för kodkvalitet kan du alltid mäta hur enkelt det är att införa ändringar. Om det blir svårare och svårare, är det definitivt dags att refactor och ta den sista delen av röd-grön-Refactor mer seriöst i framtiden.
Låt oss börja med något snyggt ljudande - "Gudsklasser" - eftersom jag tycker att de är särskilt lätta att förstå för nybörjare. Gudsklasser är ett speciellt fall av en kodlucka som heter Stor klass. I det här avsnittet ska jag ta itu med dem båda. Om du har spenderat lite tid i Rails land har du förmodligen sett dem så ofta att de ser normala ut för dig.
Kommer du säkert ihåg "mantramodellen", "skinny controller"? Jo faktiskt, mager är bra för alla dessa klasser, men som riktlinje är det bra råd för nybörjare jag antar.
Guds klasser är föremål som lockar all slags kunskap och beteende som ett svart hål. Dina vanliga misstänkta innehåller oftast användarmodellen och vad som helst problem (förhoppningsvis!) Din app försöker lösa - först och främst minst. En todo app kan massera upp på Todos modell, en shopping app på Produkter, en bild app på foton-du får driften.
Människor kallar dem gudklasser för att de vet för mycket. De har för många kontakter med andra klasser, främst för att någon modellerade dem lättsamt. Det är dock svårt att hålla gudklasser i kontroll. De gör det väldigt enkelt att dumpa mer ansvar på dem, och som många grekiska hjältar skulle bevisa, tar det lite skicklighet att dela och erövra "gudar".
Problemet med dem är att de blir hårdare och svårare att förstå, speciellt för nya lagmedlemmar, svårare att förändra och återanvändning blir mindre och mindre av ett alternativ, desto mer gravitation har de samlat. Åh ja, du har rätt, dina test är onödigt svårare att skriva också. Kort sagt, det finns inte riktigt en uppsida till att ha stora klasser, och speciellt gudklasser.
Det finns några vanliga symptom / tecken på att din klass behöver lite hjältemodell / kirurgi:
Också, om du squint på din klass och tänker "Eh? Ew! "Du kanske också är med på något. Om allt som låter bekant är chansen bra att du hittade dig ett bra exemplar.
"Ruby Class CastingInviter EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+.)+[a-z]2,)\z/
attr_reader: message,: invitees,: casting
def initialize (attributes = ) @message = attribut [: message] || "@invitees = attribut [: invitees] ||" @sender = attribut [: avsändare] @casting = attribut [: casting] end
def valid? valid_message? && valid_invitees? avsluta def leverera om det är giltigt? invitee_list.each do | email | inbjudan = create_invitation (email) Mailer.invitation_notification (inbjudan, @message) slut annars failure_message = "Ditt # @casting meddelande kunde inte skickas. Bjud in e-post eller meddelande är ogiltigt" invitation = create_invitation (@sender) Mailer.invitation_notification (inbjudan, misslyckande) slutänden privat def invalid_invitees @invalid_invitees || = invitee_list.map do | item | om inte item.match (EMAIL_REGEX) slutar slut.compact slut def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, ") split (/ [\ n,;] + /) slut def valid_message? @ message.present? slut def valid_invitees? invalid_invitees.empty? slutet
def create_invitation (email) Invitation.create (casting: @casting, avsändare: @sender, invitee_email: email, status: 'waiting') slutet slutet "
Ugly fella, va? Kan du se hur mycket nastiness är buntad här? Självklart lägger jag lite körsbär på toppen, men du kommer att köra in i kod så här förr eller senare. Låt oss tänka på vad ansvaret här CastingInviter
klassen måste jonglera.
Ska allt detta dumpas på en klass som bara vill leverera ett casting-samtal via leverera
? Absolut inte! Om din metod för inbjudan ändras kan du räkna med att springa in i någon hagelgevärsoperation. CastingInviter behöver inte veta de flesta av dessa detaljer. Det är mer ansvaret för någon klass som är specialiserad på att hantera e-postrelaterade saker. I framtiden hittar du många anledningar att ändra din kod här också.
Så hur ska vi hantera detta? Att extrahera en klass är ofta ett praktiskt refaktormönster som kommer att presentera sig som en rimlig lösning på sådana problem som stora, fördunklade klasser - särskilt när den aktuella klassen hanterar flera ansvarsområden.
Privata metoder är ofta bra kandidater för att börja med och enkelt betyg. Ibland behöver du extrahera ännu mer än en klass från en sådan dålig pojke - gör bara inte allt i ett steg. När du väl har hittat tillräckligt koherent kött som verkar höra i ett specialiserat objekt, kan du extrahera den funktionen till en ny klass.
Du skapar en ny klass och flyttar gradvis funktionaliteten över en efter en. Flytta varje metod separat, och byt namn på dem om du ser en anledning till. Hänvisa sedan till den nya klassen i originalet och delegera den nödvändiga funktionaliteten. Bra du har testdäck (förhoppningsvis!) Som låter dig kontrollera om sakerna fortfarande fungerar ordentligt varje steg på vägen. Syfta att kunna återanvända dina utdragna klasser också. Det är lättare att se hur det görs i åtanke, så låt oss läsa en del kod:
"Ruby Class CastingInviter
attr_reader: message,: invitees,: casting
def initialize (attributes = ) @message = attribut [: message] || "@invitees = attribut [: invitees] ||" @casting = attribut [: casting] @sender = attribut
def valid? casting_email_handler.valid? slutet
def leverera casting_email_handler.deliver slutet
privat
def casting_email_handler @casting_email_handler || = CastingEmailHandler.new (meddelande: meddelande, inbjudna: inbjudningar, gjutning: gjutning, avsändare: @sender) änden "
rubin-klassen CastingEmailHandler EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+.)+[a-z]2,)\z/
def initiera (attr = ) @message = attr [: meddelande] || "@invitees = attr [: invitees] ||" @casting = attr [: casting] @sender = attr [: avsändare] slut
def valid? valid_message? && valid_invitees? slutet
def leverera om det är giltigt? invitee_list.each do | email | inbjudan = create_invitation (email) Mailer.invitation_notification (inbjudan, @message) slut annars failure_message = "Ditt # @casting meddelande kunde inte skickas. Bjud in e-postmeddelanden eller meddelande är ogiltiga "invitation = create_invitation (@sender) Mailer.invitation_notification (inbjudan, misslyckande) slutändan
privat
def invalid_invitees @invalid_invitees || = invitee_list.map do | item | om inte item.match (EMAIL_REGEX) objektet slutar slut.kompakta slut
def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,; + +) slutet
def valid_invitees? invalid_invitees.empty? slutet
def valid_message? @ Message.present? slutet
def create_invitation (email) Invitation.create (casting: @casting, avsändare: @sender, invitee_email: email, status: 'waiting') slutet slutet "
I den här lösningen ser du inte bara hur denna uppdelning av problem påverkar din kodkvalitet, den läser också mycket bättre och blir lättare att smälta.
Här delegerar vi metoder till en ny klass som är specialiserad på att hantera dessa inbjudningar via e-post. Du har en dedikerad plats som kontrollerar om meddelandena och inbjudna är giltiga och hur de behöver levereras. CastingInviter
behöver inte veta något om dessa detaljer, så vi delegerar dessa ansvarsområden till en ny klass CastingEmailHandler
.
Kunskapen om hur man levererar och kontrollerar giltigheten hos dessa gjutningsinbjudningar skickas nu alla i vår nya extraherade klass. Har vi mer kod nu? Det kan du ge dig på! Var det värt att skilja problem? Ganska säker! Kan vi gå utöver det och refactor CastingEmailHandler
lite mer? Absolut! Varsågod!
Om du undrar om giltig?
metod på CastingEmailHandler
och CastingInviter
, den här är för RSpec att skapa en anpassad matcher. Detta låter mig skriva något som:
rubin förväntar sig (casting_inviter) .för att vara ogiltig
Ganska bra, tror jag.
Det finns fler tekniker för att hantera stora klasser / gudobjekt, och under loppet av denna serie lär du dig några sätt att refaktorera sådana föremål.
Det finns inget fast recept för att hantera dessa fall - det beror alltid på det, och det är ett fall från fall till dom domen om du behöver ta med de stora pistolerna eller om mindre inkrementella refactoringtekniker tvingar bäst. Jag vet, lite frustrerande ibland. Att följa principen om ett ansvarsområde (SRP) kommer dock att gå en lång väg, och det är en bra näsa att följa.
Att ha metoder som blev lite stora är en av de vanligaste sakerna du stöter på som utvecklare. I allmänhet vill du veta en överblick vad en metod ska göra. Det bör också ha en enda nestningsnivå eller en abstraktionsnivå. Kort sagt, undvik att skriva komplicerade metoder.
Jag vet att det låter hårt, och det är ofta. En lösning som uppkommer ofta ökar delar av metoden till en eller flera nya funktioner. Denna refaktoringsteknik kallas extraktmetod-Det är en av de enklaste men ändå mycket effektiva. Som en bra bieffekt blir din kod mer läsbar om du namnger dina metoder på ett lämpligt sätt.
Låt oss ta en titt på funktionsspecifikationer där du behöver denna teknik mycket. Jag kommer ihåg att bli introducerad till extraktmetod medan du skriver sådana egenskaper och hur fantastiskt det kändes när lampan fortsatte. Eftersom funktionsspecifikationer som detta är lätta att förstå, är de en bra kandidat för demonstration. Plus du kommer att köra in i liknande scenarier om och om igen när du skriver dina specifikationer.
spec / funktioner / some_feature_spec.rb
"rubin kräver" rails_helper "
funktionen 'M marks mission as complete' gör scenariot 'framgångsrikt' gör visit_root_path fill_in 'Email' med: '[email protected]' click_button 'Skicka' besök missions_path click_on 'Skapa uppdrag' fill_in 'Mission Name' med: 'Project Moonraker 'klicka på' Skicka '
inom "li: innehåller (" Project Moonraker ")" klick_on "Uppdraget slutfört" slutfört (sidan) .at har_css 'ul.missions li.mission-name.completed', text: 'Project Moonraker' endänden "
Som du lätt kan se finns det mycket kvar i detta scenario. Du går till indexsidan, loggar in och skapar ett uppdrag för inställningen, och sedan övar du genom att markera uppdraget som fullständigt och slutligen verifierar du beteendet. Ingen raketvetenskap, men inte heller ren och definitivt inte sammansatt för återanvändning. Vi kan göra bättre än det:
spec / funktioner / some_feature_spec.rb
"rubin kräver" rails_helper "
funktionen 'M marks mission as complete' gör scenariot 'framgångsrikt' gör sign_in_as '[email protected]' create_classified_mission_named 'Project Moonraker'
mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Project Moonraker' slutet
def create_classified_mission_named (uppdragsnamn) besöker missions_path click_on "Skapa uppdrag" fill_in "Uppdragsnamn" med: uppdragsnamn click_button "Submit" slutet
def mark_mission_as_complete (mission_name) inom "li: contains ('# mission_name')" klick_on "Mission completed" slutet
def agent_sees_completed_mission (mission_name) expect (page) .to have_css 'ul.missions li.mission-name.completed', text: uppdragsnamn slut
def sign_in_as (email) besök root_path fill_in 'Email', med: email click_button 'Submit' end "
Här extraherade vi fyra metoder som lätt kan återanvändas i andra test nu. Jag hoppas det är klart att vi träffar tre fåglar med en sten. Funktionen är mycket mer koncis, den läser bättre, och den består av extraherade komponenter utan dubblering.
Låt oss föreställa dig att du hade skrivit alla typer av liknande scenarier utan att extrahera dessa metoder och du ville ändra någon implementering. Nu önskar du att du hade tagit tid att refactor dina tester och hade en central plats att tillämpa dina ändringar.
Visst, det finns ett ännu bättre sätt att hantera funktionsspecifikationer som detta-sidobjekt, till exempel, men det är inte vårt räckvidd för idag. Jag antar att det är allt du behöver veta om extraheringsmetoder. Du kan tillämpa detta refactoringmönster överallt i din kod, inte bara i specifikationer, förstås. När det gäller användningsfrekvensen är min gissning att det blir din nummer ett teknik för att förbättra kvaliteten på din kod. Ha så kul!
Låt oss stänga den här artikeln med ett exempel på hur du kan smala ner dina parametrar. Det blir tråkigt ganska snabbt när du måste mata dina metoder mer än ett eller två argument. Skulle det inte vara trevligt att släppa i ett objekt istället? Det är precis vad du kan göra om du introducerar en parameterobjekt.
Alla dessa parametrar är inte bara en smärta att skriva och hålla i ordning, men kan också leda till koddubbling - och vi vill verkligen undvika det där det är möjligt. Vad jag särskilt tycker om denna refactoringteknik är hur det påverkar andra metoder också. Du kan ofta bli av med en massa parameterskräp i matkedjan.
Låt oss gå över det här enkla exemplet. M kan tilldela ett nytt uppdrag och behöver ett uppdragsnamn, en agent och ett mål. M kan också byta agentens dubbla 0-status, vilket innebär att deras licens ska döda.
"ruby class M def assign_new_mission (mission_name, agent_name, objective, license_to_kill: nil) print" Uppdrag # mission_name har tilldelats till # agent_name med målet att # objective. "om licensen_to_kill print" Licensen att döda har beviljats. "annars utskrift" Licensen att döda har inte beviljats. "Slutänden
m = Mw. m.assign_new_mission ('Octopussy', 'James Bond', 'hitta den kärntekniska enheten', licence_to_kill: true) # => Mission Octopussy har tilldelats James Bond med målet att hitta den kärntekniska enheten. Licensen att döda har beviljats. "
När du tittar på detta och frågar vad som händer när uppdragets "parametrar" växer i komplexitet, är du redan på något. Det är en smärtpunkt som du bara kan lösa om du passerar i ett enda objekt som har all information du behöver. Ofta hjälper det dig också att hålla sig borta från att ändra metoden om parameterns objekt ändras av någon anledning.
"ruby class Mission attr_reader: mission_name,: agent_name,: objective,: licence_to_kill
def initiera (uppdragsnamn: uppdragsnamn, agentnamn: agent_namn, mål: objektiv, licens till_kill: licence_to_kill) @mission_name = mission_name @agent_name = agent_name @objective = objective @licence_to_kill = licence_to_kill end
def tilldela utskrift "Mission # mission_name har tilldelats till # agent_name med målet att # objective." om licensen_to_kill print "Licensen att döda har beviljats." annars skriv ut "Licensen att döda har inte beviljats." ändänden
klass M def assign_new_mission (uppdrag) mission.assign slutänden
m = Märkeuppdrag = Mission.new (uppdragsnamn: 'Octopussy', agentnamn: 'James Bond', mål: 'hitta den kärntekniska enheten', licence_to_kill: true) m.assign_new_mission (mission) # => Mission Octopussy har varit tilldelad James Bond med målet att hitta den kärntekniska enheten. Licensen att döda har beviljats. "
Så vi skapade ett nytt objekt, Uppdrag
, som är enbart inriktade på att tillhandahålla M
med den information som behövs för att tilldela ett nytt uppdrag och tillhandahålla #assign_new_mission
med ett enskilt parameterobjekt. Du behöver inte passera i dessa irriterande parametrar själv. I stället berättar du att objektet ska avslöja den information du behöver inom själva metoden. Dessutom extraherade vi också ett visst beteende - informationen hur man skriver in i det nya Uppdrag
objekt.
Varför skulle M
behöver veta om hur man skriver ut uppdragsuppdrag? Den nya #tilldela
har också gynnats av extraktion genom att förlora lite vikt eftersom vi inte behövde passera i parameterobjektet - så det är inte nödvändigt att skriva saker som mission.mission_name
, mission.agent_name
och så vidare. Nu använder vi bara vår attr_reader
(s), som är mycket renare än utan extraktionen. Du gräver?
Vad är också praktiskt om detta är det Uppdrag
kan samla alla typer av ytterligare metoder eller stater som är snyggt inkapslade på ett ställe och redo för dig att komma åt.
Med den här tekniken kommer du att sluta med metoder som är mer koncisa, tenderar att läsa bättre och undviker att upprepa samma parameterparametrar överallt. Ganska bra affär! Att bli av med identiska parametrar är också en viktig strategi för DRY-kod.
Försök att se ut för att extrahera mer än bara dina data. Om du också kan placera beteende i den nya klassen har du objekt som är mer användbara - annars kommer de snart att börja luktas också.
Visst, du kommer oftast att gå in i mer komplicerade versioner av det - och dina test kommer säkert också att behöva anpassas samtidigt under sådana refactorings-men om du har det enkla exemplet under ditt bälte, är du redo för handling.
Jag ska titta på den nya Bond nu. Hörde det är inte så bra, men ...
Uppdatering: Saw Specter. Min dom: jämfört med Skyfall-som var MEH imho-Specter var wawawiwa!