Rubrica / Rails Code Smell Basics 03

Questo articolo per principianti copre un altro giro di odori e refactoring che dovresti familiarizzare con i primi anni della tua carriera. Copriamo dichiarazioni di caso, polimorfismo, oggetti nulli e classi di dati.

Temi

  • Dichiarazioni del caso
  • Polimorfismo
  • Oggetti Null
  • Classe di dati

Dichiarazioni del caso

Questo potrebbe anche essere chiamato "odore della lista di controllo" o qualcosa del genere. Le affermazioni di casi sono un odore perché causano la duplicazione - sono spesso ineleganti. Possono anche condurre a classi inutilmente grandi perché tutti questi metodi che rispondono ai vari scenari (e potenzialmente in crescita) spesso finiscono nella stessa classe, che quindi ha tutti i tipi di responsabilità miste. Non è un caso raro che tu abbia molti metodi privati ​​che sarebbero meglio nelle loro classi.

Un grosso problema con le dichiarazioni del caso si verifica se si desidera espanderle. Quindi devi cambiare quel particolare metodo, possibilmente ancora e ancora. E non solo lì, perché spesso hanno ripetuti gemelli dappertutto che ora necessitano anche di un aggiornamento. Un ottimo modo per allevare bug di sicuro. Come potresti ricordare, vogliamo essere aperti per estensione ma chiusi per modifiche. Qui, la modifica è inevitabile e solo una questione di tempo.

Rendi più difficile per te estrarre e riutilizzare il codice, in più è una bomba ingombrante. Spesso il codice di visualizzazione dipende da tali dichiarazioni del caso, che poi duplicano l'odore e aprono il cancello in modo aperto per un giro di fucile da caccia in futuro. Ahia! Inoltre, interrogare un oggetto prima di trovare il metodo giusto da eseguire è una brutta violazione del principio "Tell-Don't-Ask".

Polimorfismo

C'è una buona tecnica per gestire la necessità di dichiarazioni di casi. Parola fantasia in arrivo! Polimorfismo. Ciò consente di creare la stessa interfaccia per oggetti diversi e utilizzare qualunque oggetto sia necessario per diversi scenari. Puoi semplicemente scambiare l'oggetto appropriato e si adatta alle tue esigenze perché ha gli stessi metodi su di esso. Il loro comportamento al di sotto di questi metodi è diverso, ma finché gli oggetti rispondono alla stessa interfaccia, a Ruby non importa. Per esempio, VegetarianDish.new.order e VeganDish.new.order comportarsi in modo diverso, ma entrambi rispondono #ordine allo stesso modo. Vuoi solo ordinare e non rispondere a tonnellate di domande come se mangi uova o no.

Il polimorfismo viene implementato estraendo una classe per il ramo dell'istruzione case e spostando quella logica in un nuovo metodo su quella classe. Continui a farlo per ogni ramo nell'albero condizionale e dai loro tutti lo stesso nome del metodo. In questo modo incapsuli quel comportamento su un oggetto che è più adatto per prendere quel tipo di decisione e non ha motivo di cambiare ulteriormente. Vedi, in questo modo puoi evitare tutte queste fastidiose domande su un oggetto: devi solo dirgli cosa dovrebbe fare. Quando si presenta la necessità di ulteriori casi condizionali, basta creare un'altra classe che si prende cura di quella singola responsabilità con lo stesso nome di metodo.

Logica della dichiarazione del caso

class Operazione prezzo limite @mission_tpe quando: counter_intelligence @standard_fee + @counter_intelligence_fee when: terrorism @standard_fee + @terrorism_fee when: revenge @standard_fee + @revenge_fee when: extortion @standard_fee + @extortion_fee end end end counter_intel_op = Operation.new (mission_type: : counter_intelligence) counter_intel_op.price terror_op = Operation.new (mission_type:: terrorism) terror_op.price revenge_op = Operation.new (mission_type:: revenge) revenge_op.price extortion_op = Operation.new (mission_type:: extortion) extortion_op.price 

Nel nostro esempio, abbiamo un operazione classe che ha bisogno di chiedere in giro su di essa mission_type prima che possa dirti il ​​suo prezzo. È facile vederlo prezzo il metodo attende solo di cambiare quando viene aggiunto un nuovo tipo di operazione. Quando vuoi mostrarlo anche alla tua vista, dovrai applicare anche quella modifica. (A proposito, per le viste è possibile utilizzare partials polimorfi in Rails per evitare di avere queste dichiarazioni case fatte esplodere su tutta la tua vista.)

Classi polimorfici

class CounterIntelligenceOperation def price @standard_fee + @counter_intelligence_fee end end class TerrorismOperation def price @standard_fee + @terrorism_fee end end class RevengeOperation def price @standard_fee + @revenge_fee end end class ExtortionOperation def price @standard_fee + @extortion_fee end end counter_intel_op = CounterIntelligenceOperation.new counter_intel_op .price terror_op = CounterIntelligenceOperation.new terror_op.price revenge_op = CounterIntelligenceOperation.new revenge_op.price extortion_op = CounterIntelligenceOperation.new extortion_op.price 

Quindi, invece di andare in quella tana del coniglio, abbiamo creato un gruppo di classi operative che hanno la loro conoscenza di quanto le loro tasse si sommano al prezzo finale. Possiamo semplicemente dire loro di darci il loro prezzo. Non è sempre necessario liberarsi dell'originale (operazione) di classe solo quando scopri di aver estratto tutta la conoscenza che aveva.

Penso che la logica dietro le dichiarazioni del caso sia inevitabile. Gli scenari in cui devi passare attraverso una sorta di checklist prima di trovare l'oggetto o il comportamento che ottiene il lavoro sono fin troppo comuni. La domanda è semplicemente come sono gestiti meglio. Sono favorevole a non ripeterli e usando gli strumenti di programmazione orientata agli oggetti mi offre la possibilità di progettare classi discrete che possono essere facilmente scambiate tramite la loro interfaccia.

Oggetti Null

Il controllo di zero in tutto il luogo è un tipo speciale di odore di dichiarazione del caso. Chiedere un oggetto su zero è spesso una sorta di dichiarazione di un caso nascosto. Gestire nil condizionatamente può assumere la forma di object.nil?, object.present?, object.try, e poi una sorta di azione nel caso zero si presenta alla tua festa.

Un'altra domanda più subdola è chiedere un oggetto sulla sua veridicità - ergo se esiste o se è zero - e poi agire. Sembra innocuo, ma è solo un travestimento. Non farti ingannare: operatori ternari o || anche gli operatori rientrano in questa categoria. Metti diversamente, i condizionali non solo appaiono chiaramente identificabili come salvo che, se altro o Astuccio dichiarazioni. Hanno modi più sottili per mandare in crash la tua festa. Non chiedere agli oggetti la loro nullità, ma di dire loro se l'oggetto per il tuo percorso felice è assente che un oggetto nullo sia ora in grado di rispondere ai tuoi messaggi.

Gli oggetti nulli sono classi ordinarie. Non c'è niente di speciale in loro: solo un "nome di fantasia". Si estrae una logica condizionale relativa a zero e poi te ne occupi polimorficamente. Contiene questo comportamento, controlli il flusso della tua app attraverso queste classi e hai anche oggetti aperti per altre estensioni adatte a loro. Pensa a come a Prova (NullSubscription) classe potrebbe crescere nel tempo. Non solo è più ASCIUTTO e yadda-yadda-yadda, è anche molto più descrittivo e stabile.

Un grande vantaggio nell'usare oggetti nulli è che le cose non possono esplodere facilmente. Questi oggetti rispondono agli stessi messaggi degli oggetti emulati, non è sempre necessario copiare l'intera API su oggetti nulli, il che dà alla tua app pochi motivi per impazzire. Tuttavia, si noti che gli oggetti nulli incapsulano la logica condizionale senza rimuoverla completamente. Hai appena trovato una casa migliore per questo.

Dato che avere un sacco di azioni relative alla tua app nil è abbastanza contagioso e malsano per la tua app, mi piace pensare a oggetti nulli come "Pattern Nil Containment" (Per favore non farmi causa!). Perché contagioso? Perché se passi zero intorno, qualche altro posto nella tua gerarchia, un altro metodo è prima o poi anche costretto a chiedere se zero è in città, che poi porta a un altro giro di contromisure per affrontare un caso del genere.

Metti diversamente, nil non è bello da frequentare perché chiedere la sua presenza diventa contagioso. Chiedere oggetti per zero è molto probabile che sia sempre un sintomo di un design scadente, senza offesa e che non si senta male! Non voglio andare "volenti o nolenti" su quanto poco amichevole possa essere, ma alcune cose devono essere citate:

  • Nil è un guastafeste (mi dispiace, doveva essere detto).
  • Nil non è utile perché manca di significato.
  • Nil non risponde a nulla e viola l'idea di "Duck Typing".
  • Nil messaggi di errore sono spesso un dolore da affrontare.
  • Nil ti morderà, prima o poi.

Nel complesso, lo scenario in cui manca un oggetto qualcosa viene visualizzato molto frequentemente. Un esempio spesso citato è un'app che ha registrato Utente e a NilUser. Ma dal momento che gli utenti che non esistono sono un concetto stupido se quella persona sta chiaramente navigando nella tua app, è probabilmente più interessante avere un ospite che non si è ancora registrato. Un abbonamento mancante potrebbe essere un Prova, una carica zero potrebbe essere a Freebie, e così via.

Denominare i propri oggetti nulli è a volte ovvio e facile, a volte super difficile. Ma cerca di stare lontano dal nominare tutti gli oggetti nulli con un "Null" o "No". Puoi fare meglio! Fornire un po 'di contesto fa molto, penso. Scegli un nome che sia più specifico e significativo, qualcosa che rispecchia il caso d'uso reale. In questo modo comunichi più chiaramente agli altri membri del team e al tuo io futuro, naturalmente.

Ci sono un paio di modi per implementare questa tecnica. Ti mostrerò uno che penso sia semplice e adatto ai principianti. Spero che sia una buona base per capire gli approcci più coinvolti. Quando incontri ripetutamente una sorta di condizionale che si occupa di zero, sai che è ora di creare semplicemente una nuova classe e spostare quel comportamento. Dopodiché, fai sapere alla classe originale che questo nuovo giocatore ha a che fare ora con il nulla.

Nell'esempio qui sotto, puoi vedere che il Spettro la classe chiede un po 'troppo zero e ingombra il codice inutilmente. Vuole essere sicuro di avere un evil_operation prima che decida di caricare. Riesci a vedere la violazione di "Tell-Don't-Ask"?

Un'altra parte problematica è il motivo per cui Spectre dovrebbe preoccuparsi dell'implementazione di un prezzo pari a zero. Il provare il metodo chiede anche se il evil_operation ha un prezzo gestire il nulla tramite il o (||dichiarazione. evil_operation.present? fa lo stesso errore. Possiamo semplificarlo:

class Spectre include ActiveModel :: Model attr_accessor: credit_card,: bad_operation def charge a meno che evil_operation.nil? evil_operation.charge (credit_card) end end def has_discount? evil_operation.present? && evil_operation.has_discount? end def price evil_operation.try (: price) || 0 end end class EvilOperation include ActiveModel :: Model attr_accessor: discount,: price def has_discount? sconto fine addebito (credit_card) credit_card.charge (prezzo) fine fine 
class NoOperation def charge (creditcard) "Nessuna operazione malvagia registrata" end def has_discount? false end def price 0 end end class Spectre include ActiveModel :: Model attr_accessor: credit_card,: evil_operation charge charge evil_operation.charge (credit_card) end def has_discount? evil_operation.has_discount? end def price evil_operation.price end private def evil_operation @evil_operation || NoOperation.new end-end class EvilOperation include ActiveModel :: Model attr_accessor: discount,: price def has_discount? sconto fine addebito (credit_card) credit_card.charge (prezzo) fine fine 

Immagino che questo esempio sia abbastanza semplice da vedere subito come possono essere eleganti oggetti null. Nel nostro caso, abbiamo un NoOperation classe null che sa come gestire un'operazione malvagia non esistente tramite:

  • Spese di gestione di 0 quantità.
  • Sapendo che anche le operazioni non esistenti non hanno sconti.
  • Restituzione di una piccola stringa di errore informativo quando la carta viene caricata.

Abbiamo creato questa nuova classe che si occupa del nulla, un oggetto che gestisce l'assenza di cose, e lo scambia nella vecchia classe se zero si presenta. L'API è la chiave qui perché se corrisponde a quella della classe originale è possibile scambiare senza problemi l'oggetto nullo con i valori di ritorno di cui abbiamo bisogno. Questo è esattamente ciò che abbiamo fatto nel metodo privato Spectre # evil_operation. Se abbiamo il evil_operation oggetto, abbiamo bisogno di usarlo; se no, usiamo il nostro zero camaleonte che sa come gestire questi messaggi, quindi evil_operation non tornerà mai più. Duck digitando al meglio.

La nostra logica condizionale problematica è ora racchiusa in un unico punto e il nostro oggetto nullo è responsabile del comportamento Spettro sta cercando. ASCIUTTO! Abbiamo ricostruito la stessa interfaccia dall'oggetto originale che nil non poteva gestire. Ricorda, niente fa un cattivo lavoro nel ricevere messaggi, nessuno a casa, sempre! D'ora in poi, sta solo dicendo agli oggetti cosa fare senza chiedere prima loro il loro "permesso". La cosa interessante è che non c'era bisogno di toccare il EvilOperation classe a tutti.

Ultimo ma non meno importante, mi sono sbarazzato dell'assegno se è presente un'operazione malvagia Spectre # has_discount?. Non è necessario assicurarsi che esista un'operazione per ottenere lo sconto. Come risultato dell'oggetto null, il Spettro la classe è molto più snella e non condivide tanto le responsabilità delle altre classi.

Una buona regola da seguire è quella di non controllare gli oggetti se sono inclini a fare qualcosa. Comandoli come un sergente istruttore. Come spesso accade, probabilmente non è bello nella vita reale, ma è un buon consiglio per la programmazione orientata agli oggetti. Adesso dammi 20!

In generale, tutti i vantaggi dell'uso del polimorfismo invece delle istruzioni caso valgono anche per gli oggetti null. Dopotutto. è solo un caso speciale di dichiarazioni di un caso. Lo stesso vale per gli svantaggi:

  • Capire l'implementazione e il comportamento può diventare complicato perché il codice è diffuso e gli oggetti null non sono super-espliciti sulla loro esistenza.
  • Potrebbe essere necessario mantenere sincronizzato l'aggiunta di nuovi comportamenti tra oggetti nulli e le loro controparti.

Classe di dati

Chiudiamo questo articolo con qualcosa di leggero. Questo è un odore perché è una classe che non ha alcun comportamento se non quello di ottenere e impostare i suoi dati: fondamentalmente non è altro che un contenitore di dati senza metodi aggiuntivi che fanno qualcosa con quei dati. Questo li rende di natura passiva perché questi dati sono tenuti lì per essere usati da altre classi. E sappiamo già che questo non è uno scenario ideale perché urla caratteristica dell'invidia. In generale, vogliamo avere classi che abbiano dati di significato statistico di cui si occupano, oltre a comportamenti tramite metodi che possono agire su tali dati senza molto ostacolo o ficcare il naso in altre classi.

Puoi avviare il refactoring di classi come queste estraendo probabilmente il comportamento da altre classi che agiscono sui dati nella tua classe di dati. Facendo ciò, potresti lentamente attirare un comportamento utile per quei dati e dargli una casa adeguata. Va bene se queste classi crescono nel tempo. A volte è possibile spostare facilmente un intero metodo e talvolta è necessario estrarre parti di un metodo più grande e quindi estrarlo nella classe dati. In caso di dubbi, se è possibile ridurre l'accesso che altre classi hanno sulla classe di dati, si dovrebbe assolutamente andare avanti e spostare quel comportamento. Il tuo obiettivo dovrebbe essere quello di ritirarti a un certo punto dai getter e setter che hanno dato ad altri oggetti l'accesso a quei dati. Di conseguenza, avrai un accoppiamento più basso tra le classi, che è sempre una vittoria!

Pensieri di chiusura

Vedi, non è stato tutto così complicato! C'è un sacco di linguaggi fantasiosi e complicate tecniche di suono attorno agli odori del codice, ma si spera che tu abbia capito che gli odori e i loro refactoring condividono anche un paio di caratteristiche che sono limitate nel numero. Gli odori del codice non scompariranno mai o diventeranno irrilevanti, almeno non finché i nostri corpi non saranno potenziati dalle IA che ci permettono di scrivere il tipo di codice di qualità che siamo lontani anni luce dal momento.

Nel corso degli ultimi tre articoli, ti ho presentato una buona parte dei più importanti e comuni scenari di odore di codice che ti imbatterai durante la tua carriera di programmazione orientata agli oggetti. Penso che questa sia stata una buona introduzione per i neofiti a questo argomento, e spero che gli esempi di codice siano stati sufficientemente prolissi da seguire facilmente la discussione.

Una volta che hai capito questi principi per la progettazione del codice di qualità, raccoglierai nuovi odori e i loro refactoring in pochissimo tempo. Se sei arrivato così lontano e senti di non aver perso il quadro generale, penso che tu sia pronto ad avvicinarti allo status di OOP a livello di capo.