La seguente breve serie di articoli è pensata per sviluppatori e principianti di Ruby leggermente esperti. Ho avuto l'impressione che gli odori del codice e i loro refactoring possano essere molto scoraggianti e intimidatori per i neofiti, specialmente se non sono nella posizione fortunata di avere mentori che possono trasformare i concetti di programmazione mistica in lampadine brillanti.
Avendo ovviamente camminato in queste scarpe me stesso, mi sono ricordato che si sentiva inutilmente annebbiato per entrare negli odori e nei refactoring del codice.
Da un lato, gli autori si aspettano un certo livello di competenza e quindi potrebbero non sentirsi super costretti a fornire al lettore la stessa quantità di contesto che un principiante potrebbe aver bisogno di immergersi comodamente in questo mondo prima.
Di conseguenza, forse, i neofiti d'altra parte danno l'impressione di dover aspettare un po 'di più fino a quando non saranno più avanzati per conoscere gli odori e i refactoring. Non sono d'accordo con questo approccio e penso che rendere questo argomento più accessibile li aiuterà a progettare un software migliore prima della loro carriera. Almeno spero che aiuti a dare ai piccoli capolini un buon vantaggio.
Quindi di cosa stiamo parlando esattamente quando la gente parla di odori di codice? È sempre un problema nel tuo codice? Non necessariamente! Puoi evitarli completamente? Io non la penso così! Intendi che gli odori di codice portano a codice non funzionante? Bene, a volte e qualche volta no. Dovrebbe essere la mia priorità risolverli subito? La stessa risposta, temo: a volte sì, ea volte dovresti prima friggere pesce più grosso. Sei pazzo? Domanda giusta a questo punto!
Prima di continuare a immergerti in questa faccenda maleodorante, ricorda di togliere una cosa da tutto questo: non cercare di aggiustare ogni odore che incontri - questo è sicuramente uno spreco di tempo!
Mi sembra che gli odori del codice siano un po 'difficili da racchiudere in una scatola ben etichettata. Ci sono tutti i tipi di odori con varie opzioni per affrontarli. Inoltre, linguaggi e schemi di programmazione diversi sono inclini a diversi tipi di odori, ma ci sono sicuramente molti ceppi "genetici" comuni tra loro. Il mio tentativo di descrivere gli odori del codice è confrontarli con i sintomi medici che ti dicono che potresti avere un problema. Possono indicare tutti i tipi di problemi latenti e avere una vasta gamma di soluzioni se diagnosticate.
Per fortuna nel complesso non sono così complicati come trattare con il corpo umano - e la psiche, naturalmente. È un paragone equo, però, perché alcuni di questi sintomi devono essere trattati immediatamente e alcuni altri ti danno ampio spazio per trovare una soluzione che sia la migliore per il "benessere generale" del paziente. Se hai un codice funzionante e ti imbatti in qualcosa di puzzolente, dovrai prendere la decisione difficile se vale la pena trovare il tempo necessario e se tale modifica migliora la stabilità della tua app.
Detto questo, se incappi in codice che puoi migliorare subito, è un buon consiglio lasciare il codice un po 'meglio di prima, anche se un po' meglio si aggiunge sostanzialmente nel tempo.
La qualità del tuo codice diventa discutibile se l'inclusione di un nuovo codice diventa più difficile, ad esempio decidere dove mettere il nuovo codice è un problema o avere molti effetti a catena nella base di codice. Questo è chiamato resistenza.
Come linea guida per la qualità del codice, è possibile misurarlo sempre con la facilità con cui introdurre le modifiche. Se questo sta diventando sempre più difficile, è sicuramente il momento di refactoring e di prendere l'ultima parte di rosso-verde-refactoring più seriamente in futuro.
Iniziamo con qualcosa di sofisticato - "lezioni di Dio" - perché penso che siano particolarmente facili da capire per i principianti. Le classi di Dio sono un caso speciale di un codice chiamato odore Grande classe. In questa sezione mi occuperò di entrambi. Se hai trascorso un po 'di tempo in Rails land, probabilmente li hai visti così spesso da sembrare normali per te.
Sicuramente ti ricordi il mantra "Fat Model, Skinny Controller"? Beh, in realtà, skinny è buono per tutte queste classi, ma come linea guida è un buon consiglio per i principianti, suppongo.
Le classi di Dio sono oggetti che attirano ogni sorta di conoscenza e comportamento come un buco nero. I tuoi soliti sospetti includono più spesso il modello Utente e qualsiasi problema (si spera!) Che la tua app sta tentando di risolvere, almeno in primo luogo. Un'app di Todo potrebbe fare il pieno Todos modello, un'app di shopping su Prodotti, una app fotografica su Fotografie-ottieni la deriva.
Le persone li chiamano classi di Dio perché sanno troppo. Hanno troppe connessioni con altre classi, soprattutto perché qualcuno le stava modellando pigramente. È un duro lavoro, tuttavia, tenere sotto controllo le divinità. Rendono molto facile scaricare più responsabilità su di loro, e come molti eroi greci attestano, ci vuole un po 'di abilità per dividere e conquistare gli "dei".
Il problema con loro è che diventano sempre più difficili da capire, soprattutto per i nuovi membri del team, più difficile da cambiare, e riutilizzarli diventa sempre meno un'opzione, maggiore è la gravità che hanno ammassato. Oh sì, hai ragione, i tuoi test sono inutilmente più difficili da scrivere. In breve, non c'è davvero un lato positivo nell'avere classi grandi e in particolare le classi di Dio.
Ci sono un paio di sintomi / segni comuni che la tua classe ha bisogno di eroismo / chirurgia:
Inoltre, se strizzi gli occhi alla tua classe e pensi "Eh? Ew! "Potresti essere coinvolto anche in qualcosa. Se tutto ciò suona familiare, ci sono buone probabilità che tu ti sia trovato un ottimo esemplare.
"classe ruby 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 = attributes [: mittente] @casting = attributes [: casting] end
def valido? valid_message? && valid_invitees? end def consegnare se valido? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = "Non è stato possibile inviare il tuo # @casting messaggio Invita email o messaggio non valido" invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end private def invalid_invitees @invalid_invitees || = invitee_list.map do | item | a meno che 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, mittente: @sender, invitee_email: email, stato: 'in sospeso') end end "
Brutto, huh? Riesci a vedere quanta cattiveria è raggruppata qui? Ovviamente metto un po 'di ciliegina sulla torta, ma prima o poi ti imbatterai in codice come questo. Pensiamo a quali responsabilità questo CastingInviter
la classe deve destreggiarsi.
Tutto questo dovrebbe essere scaricato su una classe che vuole solo effettuare una chiamata di casting via consegnare
? Certamente no! Se il tuo metodo di invito cambia, puoi aspettarti di imbatterti in un intervento chirurgico con fucile a pompa. CastingInviter non ha bisogno di conoscere la maggior parte di questi dettagli. Questa è più la responsabilità di una classe specializzata nell'affrontare le cose relative alle email. In futuro, troverai molti motivi per cambiare il tuo codice anche qui.
Quindi come dovremmo affrontare questo? Spesso, l'estrazione di una classe è un utile pattern di refactoring che si presenterà come una soluzione ragionevole a problemi come classi grandi e complicate, specialmente quando la classe in questione si occupa di più responsabilità.
I metodi privati sono spesso buoni candidati per iniziare e anche i marchi facili. A volte è necessario estrarre anche più di una classe da un ragazzo così cattivo, ma non fare tutto in un solo passaggio. Una volta trovato abbastanza carne coerente che sembra appartenere a un oggetto specializzato a sé stante, è possibile estrarre tale funzionalità in una nuova classe.
Crei una nuova classe e trasferisci gradualmente la funzionalità una per una. Spostare ciascun metodo separatamente e rinominarli se viene visualizzato un motivo. Quindi fare riferimento alla nuova classe in quella originale e delegare la funzionalità necessaria. Per fortuna hai una copertura di prova (si spera!) Che ti permetta di controllare se le cose funzionano ancora correttamente in ogni fase del percorso. Mira a essere in grado di riutilizzare anche le classi estratte. È più facile vedere come è fatto in azione, quindi leggiamo un po 'di codice:
"classe rubino CastingInviter
attr_reader: message,: invitees,: casting
def initialize (attributes = ) @message = attributes [: message] || "@invitees = attributes [: invitees] ||" @casting = attributes [: casting] @sender = attributes [: mittente] end
def valido? casting_email_handler.valid? fine
def consegnare casting_email_handler.deliver end
privato
def casting_email_handler @casting_email_handler || = CastingEmailHandler.new (message: message, invitees: invitees, casting: casting, mittente: @sender) end end "
"classe ruby 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 [: mittente] fine
def valido? valid_message? && valid_invitees? fine
def consegnare se valido? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = "Non è stato possibile inviare il tuo # @casting messaggio. Invita le e-mail o il messaggio non sono validi "invito = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end
privato
def invalid_invitees @invalid_invitees || = invitee_list.map do | item | a meno che item.match (EMAIL_REGEX) non termini end.compact fine
def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;] + /) end
def valid_invitees? invalid_invitees.empty? fine
def valid_message? @ Message.present? fine
def create_invitation (email) Invitation.create (casting: @casting, mittente: @sender, invitee_email: email, stato: 'in sospeso') end end "
In questa soluzione, non solo vedrai come questa separazione di preoccupazioni influisce sulla qualità del tuo codice, ma si legge molto meglio e diventa più facile da digerire.
Qui deleghiamo metodi a una nuova classe specializzata nell'affrontare questi inviti via email. Hai un posto dedicato che controlla se i messaggi e gli inviti sono validi e come devono essere consegnati. CastingInviter
non ha bisogno di sapere nulla di questi dettagli, quindi deleghiamo queste responsabilità a una nuova classe CastingEmailHandler
.
La conoscenza di come consegnare e verificare la validità di queste e-mail di invito al casting è ora contenuta nella nostra nuova classe estratta. Abbiamo più codice ora? Scommetti! Ne è valsa la pena separare le preoccupazioni? Abbastanza sicuro! Possiamo andare oltre e refactoring CastingEmailHandler
qualche altro? Assolutamente! Staccati!
Nel caso ti stia chiedendo il valido?
metodo attivo CastingEmailHandler
e CastingInviter
, questo è per RSpec per creare un matcher personalizzato. Questo mi consente di scrivere qualcosa come:
ruby expect (casting_inviter) .to be_valid
Abbastanza utile, penso.
Ci sono più tecniche per gestire grandi classi / oggetti di dio, e nel corso di questa serie imparerai un paio di modi per refactoring di tali oggetti.
Non esiste una prescrizione fissa per trattare questi casi, dipende sempre, e si tratta di una chiamata di giudizio caso per caso se è necessario portare i grandi cannoni o se le tecniche di refactoring incrementale più piccole sono obbligatorie. Lo so, un po 'frustrante a volte. Seguendo il Principio di Responsabilità Unica (SRP) andrà molto lontano, ed è un buon naso da seguire.
Avere metodi un po 'grandi è una delle cose più comuni che si incontrano come sviluppatori. In generale, vuoi sapere a colpo d'occhio che cosa dovrebbe fare un metodo. Dovrebbe anche avere solo un livello di nidificazione o un livello di astrazione. In breve, evita di scrivere metodi complicati.
So che sembra difficile, e lo è spesso. Una soluzione che emerge frequentemente consiste nell'estrarre parti del metodo in una o più nuove funzioni. Questa tecnica di refactoring è chiamata il metodo di estrazione-è uno dei più semplici ma comunque molto efficace. Come un bell'effetto collaterale, il tuo codice diventa più leggibile se dai un nome ai metodi in modo appropriato.
Diamo un'occhiata alle specifiche delle caratteristiche in cui avrete bisogno di questa tecnica molto. Ricordo di essermi presentato al metodo di estrazione mentre scrivevo le specifiche di questa funzionalità e quanto fosse incredibile quando la lampadina si accendeva. Poiché le caratteristiche tecniche come questa sono facili da capire, sono un buon candidato per la dimostrazione. Inoltre ti imbatterai in scenari simili più e più volte quando scrivi le tue specifiche.
spec / caratteristiche / some_feature_spec.rb
"ruby richiede 'rails_helper'
funzione 'M segna missione come completa' fai scenario 'con successo' fai visit_root_path fill_in 'Email', con: '[email protected]' click_button 'Submit' visita missions_path click_on 'Crea missione' fill_in 'Nome missione' con: 'Progetto Moonraker 'click_button' Submit '
all'interno di "li: contains ('Project Moonraker')" fai click_on 'Missione completata' end expect (pagina) .per avere_css 'ul.missions li.mission-name.completed', text: 'Project Moonraker' end end "
Come puoi facilmente vedere, c'è molto da fare in questo scenario. Vai alla pagina indice, accedi e crea una missione per l'installazione, quindi fai esercizio completando la missione e infine verifichi il comportamento. Nessuna scienza missilistica, ma anche non pulita e sicuramente non composta per la riusabilità. Possiamo fare meglio di così:
spec / caratteristiche / some_feature_spec.rb
"ruby richiede 'rails_helper'
funzione 'M segna missione come completa' fai scenario 'con successo' fai sign_in_as '[email protected]' create_classified_mission_named 'Project Moonraker'
mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Fine del progetto Moonraker'
def create_classified_mission_named (mission_name) visita missions_path click_on 'Crea missione' fill_in 'Mission Name', con: mission_name click_button 'Submit' fine
def mark_mission_as_complete (nome_azione) all'interno di "li: contains ('# mission_name')" fai clic su "Fine missione completata"
def agent_sees_completed_mission (mission_name) expect (page) .per have_css 'ul.missions li.mission-name.completed', text: nome_azione fine
def sign_in_as (email) visita root_path fill_in 'Email', con: email click_button 'Submit' end "
Qui abbiamo estratto quattro metodi che possono essere facilmente riutilizzati in altri test ora. Spero sia chiaro che abbiamo colpito tre piccioni con una fava. La funzione è molto più concisa, si legge meglio ed è composta da componenti estratti senza duplicazione.
Immaginiamo che avresti scritto tutti i tipi di scenari simili senza estrarre questi metodi e volevi cambiare qualche implementazione. Ora vorresti che avessi preso il tempo di rifattorizzare i tuoi test e avevi un posto centrale per applicare le tue modifiche.
Certo, c'è un modo ancora migliore per gestire le specifiche di funzionalità come questo Page-Objects, ad esempio, ma questo non è il nostro obiettivo per oggi. Immagino sia tutto ciò che devi sapere sull'estrazione dei metodi. Puoi applicare questo modello di refactoring ovunque nel tuo codice, non solo nelle specifiche, ovviamente. In termini di frequenza di utilizzo, la mia ipotesi è che sarà la tecnica numero uno per migliorare la qualità del codice. Divertiti!
Chiudiamo questo articolo con un esempio di come ridurre i parametri. Diventa noioso piuttosto veloce quando devi alimentare i tuoi metodi più di uno o due argomenti. Non sarebbe carino cadere in un oggetto invece? Questo è esattamente quello che puoi fare se introduci un oggetto parametro.
Tutti questi parametri non sono solo un dolore da scrivere e da tenere in ordine, ma possono anche portare alla duplicazione del codice, e noi certamente vogliamo evitarlo laddove possibile. Quello che mi piace soprattutto di questa tecnica di refactoring è come questo influenzi anche altri metodi all'interno. Spesso sei in grado di sbarazzarti di molti parametri spazzatura nella catena alimentare.
Esaminiamo questo semplice esempio. M può assegnare una nuova missione e ha bisogno di un nome di missione, di un agente e di un obiettivo. M è anche in grado di cambiare lo stato del doppio 0 degli agenti, ovvero la loro licenza di uccidere.
"ruby class M def assign_new_mission (nome_azione, nome_agente, obiettivo, licence_to_kill: nil) stampa" Missione # nome_azione è stato assegnato a # nome_agente con l'obiettivo # obiettivo. "se licence_to_kill stampa" La licenza per uccidere è stato concesso. "else print" La licenza per uccidere non è stata concessa ". end end end
m = M.new m.assign_new_mission ('Octopussy', 'James Bond', 'trova il dispositivo nucleare', licence_to_kill: true) # => La missione Octopussy è stata assegnata a James Bond con l'obiettivo di trovare il dispositivo nucleare. La licenza per uccidere è stata concessa. "
Quando guardi questo e chiedi cosa succede quando i "parametri" della missione crescono in complessità, sei già su qualcosa. Questo è un punto dolente che puoi risolvere solo se passi in un singolo oggetto che ha tutte le informazioni di cui hai bisogno. Più spesso, questo ti aiuta anche a non cambiare il metodo se l'oggetto parametro cambia per qualche motivo.
"classe ruby Mission attr_reader: nome_azione,: nome_agente,: obiettivo,: licence_to_kill
def initialize (nome_azione: nome_azione, nome_agente: nome_agente, obiettivo: obiettivo, licence_to_kill: licence_to_kill) @mission_name = nome_azione @ nome_agente = nome_agente @objective = obiettivo @licence_to_kill = licence_to_kill end
def assegna stampa "Missione # nome_pubblica è stata assegnata a # agent_name con l'obiettivo di # obiettivo." if licence_to_kill print "La licenza per uccidere è stata concessa." altrimenti stampa "La licenza per uccidere non è stata concessa". fine fine
classe M def assign_new_mission (missione) mission.assign fine fine
m = M.new mission = Mission.new (nome_azione: 'Octopussy', nome_agente: 'James Bond', obiettivo: 'trova il dispositivo nucleare', licence_to_kill: true) m.assign_new_mission (missione) # => La missione Octopussy è stata assegnato a James Bond con l'obiettivo di trovare il dispositivo nucleare. La licenza per uccidere è stata concessa. "
Quindi abbiamo creato un nuovo oggetto, Missione
, quello è focalizzato esclusivamente sulla fornitura M
con le informazioni necessarie per assegnare una nuova missione e fornire #assign_new_mission
con un oggetto parametro singolare. Non è necessario passare da soli questi fastidiosi parametri. Invece dici all'oggetto di rivelare le informazioni necessarie all'interno del metodo stesso. Inoltre, abbiamo anche estratto alcuni comportamenti, le informazioni su come stampare, nel nuovo Missione
oggetto.
Perché dovrebbe M
bisogno di sapere come stampare incarichi di missione? Il nuovo #assegnare
ha anche beneficiato dell'estrazione perdendo un po 'di peso perché non avevamo bisogno di passare l'oggetto parametro, quindi non c'è bisogno di scrivere cose del genere mission.mission_name
, mission.agent_name
e così via. Ora usiamo solo il nostro attr_reader
(s), che è molto più pulito che senza l'estrazione. Scava?
Ciò che è utile anche a questo è quello Missione
potrebbe raccogliere tutti i tipi di metodi o stati aggiuntivi che sono ben incapsulati in un posto e pronti per l'accesso.
Con questa tecnica ti ritroverai con metodi più concisi, che tendono a leggere meglio e ad evitare di ripetere lo stesso gruppo di parametri dappertutto. Ottimo affare! Eliminare gruppi identici di parametri è anche una strategia importante per il codice DRY.
Cerca di estrarre più che i tuoi dati. Se riesci a posizionare il comportamento anche nella nuova classe, avrai degli oggetti che sono più utili, altrimenti presto cominceranno a puzzare.
Certo, la maggior parte delle volte ti imbatterai in versioni più complicate di questo e anche i tuoi test dovranno essere adattati simultaneamente durante tali refactoring, ma se hai questo semplice esempio alla cintura, sarai pronto per l'azione.
Adesso guarderò il nuovo Bond. Ho sentito che non è buono, però ...
Aggiornamento: Saw Spectre. Il mio verdetto: rispetto a Skyfall - che era MEH imho-Spectre era wawawiwa!