=> La missione Octopussy è stata assegnata a James Bond con l'obiettivo di trovare il dispositivo nucleare. La licenza per uccidere è stata concessa.

Questa è la seconda parte di una piccola serie sugli odori del codice e i possibili refactoring. Il pubblico di riferimento che avevo in mente sono i neofiti che hanno sentito parlare di questo argomento e che forse volevano aspettare un po 'prima di entrare in queste acque avanzate. Il seguente articolo tratta "Feature Envy", "Shotgun Surgery" e "Divergent Change".

Temi

  • Caratteristica invidia
  • Chirurgia Shotgun
  • Cambiamento divergente

Quello che realizzerete rapidamente con gli odori del codice è che alcuni di loro sono cugini molto stretti. Anche i loro refactoring sono a volte correlati, per esempio, Classe Inline e Estrai Classe non sono così diversi.

Ad esempio, inserendo una classe, estrai l'intera classe mentre ti sbarazzi di quella originale. Quindi estrai la lezione con un po 'di torsione. Il punto che sto cercando di fare è che non dovresti sentirti sopraffatto dal numero di odori e di refactoring, e certamente non dovresti scoraggiarti dai loro nomi intelligenti. Cose come Shotgun Surgery, Feature Envy e Divergent Change possono sembrare fantasiose e intimidatorie per le persone che hanno appena iniziato. Forse mi sbaglio, ovviamente.

Se ti immergi un po 'in questo argomento e giochi con un paio di refactoring per gli odori del codice, vedrai che spesso finiscono nello stesso campo. Un sacco di refactoring sono semplicemente diverse strategie per arrivare a un punto in cui si hanno classi concise, ben organizzate e focalizzate su una piccola quantità di responsabilità. Penso che sia giusto dire che se riesci a ottenere questo, sarai sempre più avanti rispetto alla maggior parte del tempo, non che essere davanti agli altri sia così importante, ma tale design di classe semplicemente manca spesso nel codice delle persone prima che considerati "esperti".

Quindi, perché non iniziare presto il gioco e creare una base concreta per la progettazione del tuo codice. Non credere a quello che potrebbe essere il tuo racconto, questo è un argomento avanzato che dovresti rimandare per un po 'finché non sei pronto. Anche se sei un principiante, se fai piccoli passi puoi avvolgere la tua testa intorno agli odori e ai loro refactoring molto prima di quanto potresti pensare.

Prima di immergerci nella meccanica, voglio ripetere un punto importante del primo articolo. Non tutti gli odori sono intrinsecamente cattivi, e non ogni refactoring ne vale sempre la pena. Devi decidere sul posto-quando disponi di tutte le informazioni pertinenti a tua disposizione-se il tuo codice è più stabile dopo un refactoring e se vale la pena dedicare del tempo per sistemare l'odore.

Caratteristica invidia

Rivediamo un esempio dell'articolo precedente. Abbiamo estratto un lungo elenco di parametri per #assign_new_mission in un oggetto parametro via il Missione classe. Fin qui tutto bene.

M con invidia caratteristica

"ruby class M def assign_new_mission (missione) print" La missione # mission.mission_name è stata assegnata a # mission.agent_name con l'obiettivo di # mission.objective. "se mission.licence_to_kill stampa" La licenza per uccidere è stato concesso. "else print" La licenza per uccidere non è stata concessa ". end end end

classe 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 end

m = Nuovo

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 assegnata a James Bond con l'obiettivo di trovare il dispositivo nucleare. La licenza per uccidere è stata concessa. "

"

Ho brevemente menzionato come possiamo semplificare il M classe ancora di più spostando il metodo #assign_new_mission alla nuova classe per l'oggetto parametro. Quello che non ho affrontato è stato il fatto M aveva una forma facilmente curabile di caratteristica dell'invidia anche. M era troppo ficcanaso per gli attributi di Missione. In altre parole, ha posto troppe "domande" sull'oggetto della missione. Non è solo un brutto caso di microgestione, ma anche un odore di codice molto comune.

Lascia che ti mostri cosa intendo. Nel M # assign_new_mission, M è "invidioso" per i dati nel nuovo oggetto parametro e vuole accedervi da tutte le parti.

  • mission.mission_name
  • mission.agent_name
  • mission.objective
  • mission.licence_to_kill

Oltre a ciò, hai anche un oggetto parametro Missione questo è solo il responsabile dei dati in questo momento, che è un altro odore, a Classe di dati.

Tutta questa situazione ti dice sostanzialmente questo #assign_new_mission vuole essere da un'altra parte e M non ha bisogno di conoscere i dettagli di come vengono assegnate le missioni. Dopotutto, perché non dovrebbe essere una responsabilità della missione assegnare nuove missioni? Ricorda sempre di mettere insieme le cose che cambiano anche insieme.

M senza invidia caratteristica

"ruby class M def assign_new_mission (mission) mission.assign end end

classe 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 assign print "Missione # nome_pubblica è stato assegnato a # nome_agente con l'obiettivo # obiettivo." se licence_to_kill stampa "La licenza per uccidere è stata concessa." altrimenti stampa "La licenza per uccidere non è stata concesso. "end end end

m = M.new mission = Mission.new (mission_name: 'Octopussy', nome_agente: 'James Bond', obiettivo: 'trova il dispositivo nucleare', licence_to_kill: true) m.assign_new_mission (missione) "

Come puoi vedere, abbiamo semplificato un po 'le cose. Il metodo si riduce in modo significativo e delega il comportamento all'oggetto responsabile. M non richiede più specifiche dei dati di missione e sicuramente non si lascia coinvolgere da come vengono stampati i compiti. Ora può concentrarsi sul suo vero lavoro e non ha bisogno di essere disturbata se cambiano i dettagli degli incarichi di missione. Più tempo per i giochi mentali e la caccia agli agenti canaglia. Win-win!

L'invidia fa rizzare l'entanglement - con ciò non intendo il tipo buono, quello che fa viaggiare le informazioni più velocemente di una luce spettrale - parlo di quello che col passare del tempo potrebbe lasciare che il tuo slancio di sviluppo si riduca ad una fermata più vicina. Non bene! Perchè così? Effetti a catena in tutto il codice creeranno resistenza! Un cambiamento in un posto farfalle attraverso tutti i tipi di cose e finisci come un aquilone in un uragano. (Ok, un po 'eccessivamente drammatico, ma mi concedo un B + per il riferimento Bond in là.)

Come antidoto generale per l'invidia per le caratteristiche, vuoi puntare a progettare classi che si occupano principalmente delle loro stesse cose e possedere, se possibile, singole responsabilità. In breve, le classi dovrebbero essere qualcosa come l'otakus amichevole. Socialmente potrebbe non essere il più sano dei comportamenti, ma per progettare le classi spesso è una guida ragionevole per mantenere il tuo slancio dove dovrebbe essere: andare avanti!

Chirurgia Shotgun

Il nome è un po 'sciocco, no? Ma allo stesso tempo è una descrizione abbastanza accurata. Sembra un affare serio, ed è così! Fortunatamente non è così difficile da afferrare, ma comunque è uno dei più cattivi odori del codice. Perché? Perché genera duplicati come nessun altro, ed è facile perdere di vista tutte le modifiche che dovresti apportare per sistemare le cose. Cosa succede durante l'intervento con il fucile a pompa, apporti modifiche in una classe / file e devi anche toccare molte altre classi / file che devono essere aggiornati. Spero che non ti piaccia il momento giusto.

Ad esempio, potresti pensare di poter fare a meno di un piccolo cambiamento in un punto, e poi rendersi conto che devi passare un intero gruppo di file per fare lo stesso cambiamento o correggere qualcos'altro che è rotto a causa di esso. Non bene, niente affatto! Sembra più una buona ragione per cui le persone iniziano a odiare il codice con cui hanno a che fare.

Se si dispone di uno spettro con codice DRY su un lato, il codice che spesso richiede l'intervento del fucile a pompa è praticamente all'estremità opposta. Non essere pigro e lasciati entrare in quel territorio. Sono sicuro che preferiresti aprire un file e applicare le tue modifiche lì e averne fatto. Questo è il tipo di pigro per cui dovresti lottare!

Per evitare questo odore, ecco un breve elenco di sintomi che puoi cercare:

  • Caratteristica invidia
  • Accoppiamento stretto
  • Elenco dei parametri lunghi
  • Qualsiasi forma di duplicazione del codice

Cosa intendiamo quando parliamo di codice accoppiato? Diciamo che abbiamo oggetti UN e B. Se non sono accoppiati, puoi cambiarne uno senza influire sull'altro. In caso contrario, anche il più delle volte dovrà occuparsi anche dell'altro oggetto.

Questo è un problema, e l'intervento con il fucile a pompa è un sintomo anche per l'accoppiamento stretto. Quindi stai sempre attento a quanto facilmente puoi cambiare il tuo codice. Se è relativamente facile, significa che il tuo livello di accoppiamento è accettabilmente basso. Detto questo, mi rendo conto che le vostre aspettative sarebbero irrealistiche se vi aspettate di essere in grado di evitare di accoppiare tutto il tempo a tutti i costi. Non succederà! Troverete buoni motivi per decidere contro i condizionali sostitutivi simili a urgenza Polimorfismo. In tal caso, un po 'di accoppiamento, chirurgia shotgun e mantenimento dell'API degli oggetti in sincronia vale la pena di sbarazzarsi di un sacco di Oggetto Nullo (più su questo in un pezzo successivo).

Più comunemente è possibile applicare uno dei seguenti rifacimenti per guarire le ferite:

  • Sposta il campo
  • Classe Inline
  • Estrai Classe
  • Metodo di spostamento

Diamo un'occhiata ad un codice. Questo esempio è una parte di come un'app Specter gestisce i pagamenti tra i loro contraenti e malvagi clienti. Ho semplificato un po 'i pagamenti avendo commissioni standard sia per gli appaltatori che per i clienti. Quindi non importa se Spectre ha il compito di rapire un gatto o estorcere un intero paese: la tassa rimane la stessa. Lo stesso vale per ciò che pagano i loro appaltatori. Nel raro caso un'operazione va a sud e un'altra Nr. 2 deve letteralmente saltare lo squalo, Spectre offre un rimborso completo per mantenere felici i malvagi clienti. Spectre utilizza una gemma di pagamento proprietaria che è fondamentalmente un segnaposto per qualsiasi tipo di processore di pagamento.

Nel primo esempio qui sotto, sarebbe un dolore se Spectre decidesse di usare un'altra libreria per gestire i pagamenti. Ci sarebbero più parti in movimento coinvolte, ma per la dimostrazione di un intervento con fucili a pompa questa complessità mi farà pensare:

Esempio con odore di chirurgia shotgun:

"classe rubino 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) end

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

operazione di classe # ...

REFUND_AMOUNT = 10000000

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

Contractor di classe # ...

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 == 'Ucciso in azione' PaymentGem.transfer_funds (spectre_agent_id, BONUS_PAYOUT) else PaymentGem.transfer_funds (spectre_agent_id, STANDARD_PAYOUT) fine fine "

Quando guardi questo codice dovresti chiederti: "Dovrebbe il EvilClients la classe è davvero preoccupata di come il processore di pagamenti accetti nuovi malvagi clienti e di come sono addebitati per le operazioni? "Certo che no! È una buona idea distribuire le varie somme per pagare tutto il posto? I dettagli di implementazione del processore pagamenti dovrebbero essere visualizzati in una di queste classi? Sicuramente no!

Guardalo da quella parte. Se vuoi cambiare qualcosa sul modo in cui gestisci i pagamenti, perché dovresti aprire il EvilClient classe? In altri casi potrebbe essere un utente o un cliente. Se ci pensi, non ha senso familiarizzare con questo processo.

In questo esempio, dovrebbe essere facile vedere che le modifiche al modo in cui accetti e trasferisci i pagamenti creano effetti a catena lungo tutto il codice. Inoltre, se si desidera modificare l'importo da addebitare o trasferire ai propri appaltatori, occorrono ulteriori modifiche in tutto il luogo. Esempi primari di chirurgia shotgun. E in questo caso abbiamo a che fare solo con tre classi. Immagina il tuo dolore se è coinvolta una complessità un po 'più realistica. Sì, questa è la roba di cui sono fatti gli incubi. Diamo un'occhiata ad un esempio che è un po 'più sano di mente:

Esempio senza intervento chirurgico con fucile a pompa e classe estratta:

"classe ruby ​​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) fine

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

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

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

def contractor_payout (appaltatore) spectre_agent_id = @ payment_handler.find_contractor (contractor.email) .payments_id if operation.enemy_agent == 'James Bond' && operation.enemy_agent_status == 'Ucciso in azione' @ payment_handler.transfer_funds (spectre_agent_id, BONUS_CONTRACTOR_PAYOUT) else @ payment_handler.transfer_funds (spectre_agent_id, STANDARD_CONTRACTOR_PAYOUT) end end end

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

operazione di classe # ...

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

Contractor di classe # ...

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

Quello che abbiamo fatto qui è avvolgere il PaymentGem API nella nostra stessa classe. Ora abbiamo un posto centrale in cui applichiamo le nostre modifiche se decidiamo che, ad esempio, a SpectrePaymentGem funzionerebbe meglio per noi. Non è più necessario toccare i file non associati interni di pagamento multiplo se è necessario adattarsi ai cambiamenti. Nelle classi che trattano i pagamenti, abbiamo semplicemente istanziato il PaymentHandler e delegare la funzionalità necessaria. Facile, stabile e senza motivo per cambiare.

E non solo abbiamo contenuto tutto in un singolo file. All'interno del PaymentsHandler classe, c'è solo un posto che dobbiamo scambiare e fare riferimento a un possibile nuovo processore di pagamento inizializzare. Questo è rad nel mio libro. Certo, se il nuovo servizio di pagamento ha un'API completamente diversa, è necessario modificare i corpi di un paio di metodi in PaymentHandler. È un piccolo prezzo da pagare rispetto alla chirurgia full shot: è più come un intervento chirurgico per una piccola scheggia nel dito. Buon affare!

Se non si presta attenzione quando si scrivono test per un processore di pagamenti come questo o qualsiasi servizio esterno su cui si deve fare affidamento, è possibile che si verifichino gravi mal di testa quando modificano la propria API. Anche loro "soffrono di cambiamenti", ovviamente. E la domanda non è che cambieranno le loro API, solo quando.

Attraverso la nostra incapsulazione siamo in una posizione molto migliore per bloccare i nostri metodi per il processore di pagamento. Perché? Perché i metodi che abbiamo mozzato sono nostri e cambiano solo quando li vogliamo. Questa è una grande vittoria. Se sei nuovo ai test e questo non ti è completamente chiaro, non preoccuparti. Prenditi il ​​tuo tempo; questo argomento può essere difficile all'inizio. Perché è un tale vantaggio, volevo solo menzionarlo per motivi di completezza.

Come puoi vedere, ho semplificato l'elaborazione dei pagamenti in questo stupido esempio. Avrei potuto anche ripulire il risultato finale, ma il punto era dimostrare chiaramente l'odore e come liberarsene attraverso l'astrazione.

Se non sei completamente soddisfatto di questa lezione e vedi le opportunità per il refactoring, ti saluto e sono felice di prenderne il merito. Ti raccomando di buttarti fuori! Un buon inizio potrebbe riguardare il modo in cui lo trovi payments_idS. Anche la classe si è un po 'affollata ...

Cambiamento divergente

Il cambiamento divergente è, in un certo senso, l'opposto della chirurgia del fucile da caccia: dove vuoi cambiare una cosa e devi far saltare quel cambiamento attraverso una serie di file diversi. Qui una singola classe viene spesso cambiata per ragioni diverse e in modi diversi. La mia raccomandazione è di identificare parti che cambiano insieme ed estrarle in una classe separata che può concentrarsi su quella singola responsabilità. Queste classi, a loro volta, non dovrebbero avere più di una ragione per cambiare, in caso contrario, un altro odore di cambiamento divergente è molto probabilmente in attesa di morderti.

Le classi che soffrono di cambiamenti divergenti sono quelle che vengono cambiate molto. Con strumenti come Churn, puoi misurare la frequenza con cui particolari parti del tuo codice dovevano cambiare in passato. Più punti trovi in ​​una classe, maggiore è la probabilità che il cambiamento divergente possa essere al lavoro. Inoltre non sarei sorpreso se esattamente queste classi fossero quelle che causano il maggior numero di bug in generale.

Non fraintendetemi: cambiarsi spesso non è direttamente l'odore. È un sintomo utile, però. Un altro sintomo molto comune e più esplicito è che questo oggetto deve destreggiarsi tra più responsabilità. Il principio di responsabilità unica SRP è un'eccellente linea guida per prevenire questo odore di codice e per scrivere codice più stabile in generale. Può essere difficile da seguire, ma comunque vale la pena.

Diamo un'occhiata a questo brutto esempio qui sotto. Ho modificato un po 'l'esempio di chirurgia shotgun. Blofeld, il capo di Spectre, potrebbe essere conosciuto per il materiale del microgaming, ma dubito che sarebbe interessato a metà della roba con cui questa classe è coinvolta.

"Spettrale di classe rubino

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) end

def contractor_payout (appaltatore) spectre_agent_id = PaymentGem.find_contractor (contractor.email) .payments_id PaymentGem.transfer_funds (spectre_agent_id, STANDARD_PAYOUT) end

def assign_new_operation (operation) operation.contractor = 'Some evil dude' operation.objective = 'Ruba un carico di oggetti preziosi' operation.deadline = 'Midnight, November 18th' end

def print_operation_assignment (operation) print "# operation.contractor è assegnato a # operation.objective. La scadenza della missione termina con # operation.deadline. "Fine

def dispose_of_agent (spectre_agent) mette "Hai deluso questa organizzazione. Sai come Spectre gestisce il fallimento. Arrivederci # spectre_agent.code_name! "End end"

Il Spettro la classe ha troppe cose diverse su cui è preoccupata:

  • Assegnazione di nuove operazioni
  • Ricarica per il loro lavoro sporco
  • Stampa degli incarichi di missione
  • Uccidere agenti spettrali senza successo
  • Trattare con gli interni di PaymentGem
  • Pagare i loro agenti Spectre / appaltatori
  • Conosce anche la quantità di denaro per l'addebito e il pagamento

Sette diverse responsabilità in una singola classe. Non bene! Hai bisogno di cambiare il modo in cui gli agenti vengono smaltiti? Un vettore per cambiare il Spettro classe. Vuoi gestire i pagamenti in modo diverso? Un altro vettore Hai la deriva.

Sebbene questo esempio sia lontano dall'essere realistico, continua a raccontare la storia di quanto sia facile accumulare inutilmente comportamenti che devono cambiare frequentemente in un unico luogo. Possiamo fare di meglio!

"ruby class Spectre # ...

def dispose_of_agent (spectre_agent) mette "Hai deluso questa organizzazione. Sai come Spectre gestisce il fallimento. Arrivederci # spectre_agent.code_name! End-end

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 (male_client_id, STANDARD_CHARGE) fine

def contractor_payout (appaltatore) spectre_agent_id = @ payment_handler.find_contractor (contractor.email) .payments_id @ payment_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

Contractor di classe # ...

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

classe Operazione attr_accessor: appaltatore,: obiettivo,: scadenza

def initialize (attrs = ) @contractor = attrs [: appaltatore] @objective = attrs [: obiettivo] @deadline = attrs [: scadenza] fine

def print_operation_assignment print "# contractor è assegnato a # obiettivo. La scadenza della missione termina alle # deadline. "End end"

Qui abbiamo estratto un sacco di classi e abbiamo dato loro le loro uniche responsabilità - e quindi la loro ragione contenuta per cambiare.

Vuoi gestire i pagamenti in modo diverso? Ora non avrai bisogno di toccare il Spettro classe. Devi pagare o pagare in modo diverso? Ancora una volta, non c'è bisogno di aprire il file per Spettro. Le assegnazioni delle operazioni di stampa sono ora l'attività operativa, a cui appartiene. Questo è tutto. Non troppo complicato, penso, ma sicuramente uno degli odori più comuni che dovresti imparare a gestire presto.

Pensieri finali

Spero che tu sia arrivato al punto in cui ti senti pronto per provare questi refactoring nel tuo codice e avere un tempo più facile per identificare gli odori del codice intorno a te. Attenzione, abbiamo appena iniziato, ma che hai già affrontato un paio di grandi. Scommetto che non era così complicato come una volta avresti potuto pensare!

Certo, gli esempi del mondo reale saranno molto più impegnativi, ma se hai compreso i meccanismi e i modelli per individuare gli odori, sarai sicuramente in grado di adattarti rapidamente a complessità realistiche.