Codice pulito: conseguenze di metodi brevi con pochi parametri


15

Di recente, durante una revisione del codice, mi sono imbattuto in un codice, scritto da un nuovo collega, che contiene uno schema con un odore. Sospetto che le decisioni del mio collega si basino su regole proposte dal famoso libro Clean Code (e forse anche da altri libri simili).

Comprendo che il costruttore della classe è interamente responsabile della creazione di un oggetto valido e che il suo compito principale è l'assegnazione delle proprietà (private) di un oggetto. Naturalmente potrebbe accadere che valori di proprietà opzionali possano essere impostati con metodi diversi dal costruttore della classe, ma tali situazioni sono piuttosto rare (sebbene non necessariamente errate, a condizione che il resto della classe tenga conto dell'opzionalità di tale proprietà). Questo è importante perché consente di assicurarsi che l'oggetto sia sempre in uno stato valido.

Tuttavia, nel codice che ho riscontrato, la maggior parte dei valori di proprietà sono effettivamente impostati con metodi diversi dal costruttore. I valori risultanti dai calcoli sono assegnati a proprietà da utilizzare all'interno di diversi metodi privati ​​in tutta la classe. Apparentemente l'autore usa le proprietà della classe come se fossero variabili globali che dovrebbero essere accessibili in tutta la classe, invece di parametrizzare questi valori alle funzioni che ne hanno bisogno. Inoltre, i metodi della classe dovrebbero essere chiamati in un ordine specifico, perché la classe non farà molto diversamente.

Sospetto che questo codice sia stato ispirato dal consiglio di mantenere i metodi brevi (<= 5 righe di codice), per evitare grandi elenchi di parametri (<3 parametri) e che i costruttori non devono funzionare (come eseguire un calcolo di qualche tipo questo è essenziale per la validità dell'oggetto).

Ora, naturalmente, potrei presentare un ricorso contro questo modello se posso dimostrare che tutti i tipi di errori indefiniti sorgono potenzialmente quando i metodi non vengono chiamati in un ordine specifico. Tuttavia, prevedo che la risposta a questa sarà l'aggiunta di convalide che verificano che le proprietà debbano essere impostate una volta chiamati i metodi che richiedono l'impostazione di tali proprietà.

Preferirei piuttosto proporre di modificare completamente il codice, in modo che la classe diventi una stampa blu su un oggetto reale, piuttosto che una serie di metodi che dovrebbero essere chiamati (proceduralmente) in un ordine specifico.

Sento che il codice che ho incontrato ha un odore. In effetti, credo che esista una distinzione piuttosto chiara su quando salvare un valore in una proprietà di classe e quando inserirlo in un parametro per un metodo diverso da usare - Non credo davvero che possano essere alternative l'una all'altra . Sto cercando le parole per questa distinzione.


6
1. Interpretare l'avvocato del diavolo per un momento ... Il codice funziona davvero? Perché i Data Transfer Objects sono una tecnica perfettamente valida, e se è tutto questo è ...
Robert Harvey,

7
2. Se ti mancano le parole per descrivere il problema, non hai abbastanza esperienza per confutare la posizione del tuo collega.
Robert Harvey,

4
3. Se disponi di un codice funzionante che puoi pubblicare, pubblicalo in Revisione del codice e fagli dare un'occhiata. Altrimenti, questa è solo una generalità errante.
Robert Harvey,

5
@RobertHarvey "I valori che risultano dai calcoli sono assegnati a proprietà da utilizzare all'interno di diversi metodi privati ​​in tutta la classe" non mi sembra un DTO che si rispetti. Sono d'accordo che un po 'più di specificità sarebbe utile.
topo Ripristina Monica il

4
A parte: sembra che qualcuno non abbia letto il codice pulito prima di colpirlo. L'ho appena scansionato di nuovo, e non sono riuscito a trovare alcun posto in cui suggerisca che "i costruttori non dovrebbero funzionare" (alcuni esempi in effetti eseguono il lavoro) e la soluzione suggerita per evitare troppi parametri è quella di creare un oggetto parametro che si sta consolidando gruppi di parametri, non bastardizzare le tue funzioni. E il libro non suggerisce il refactoring del codice per evitare le dipendenze temporali tra metodi. Penso che il tuo pregiudizio nei confronti di alcuni dei suoi stili di codice preferiti abbia colorato la tua percezione del libro.
Eric King,

Risposte:


13

Come qualcuno che ha letto Clean Code e ha guardato più volte la serie Clean Coders, e spesso insegna e istruisce altre persone a scrivere codice più pulito, posso davvero garantire che le tue osservazioni sono corrette - le metriche che fai notare sono tutte menzionate nel libro .

Tuttavia, il libro prosegue sottolineando altri punti che dovrebbero essere applicati insieme alle linee guida che hai sottolineato. Questi sono stati apparentemente ignorati nel codice con cui hai a che fare. Questo può essere successo perché il tuo collega è ancora in fase di apprendimento, nel qual caso, per quanto sia necessario sottolineare gli odori del loro codice, è bene ricordare che lo stanno facendo in buona volontà, imparando e provando per scrivere un codice migliore.

Clean Code propone che i metodi dovrebbero essere brevi, con il minor numero possibile di argomenti. Ma seguendo queste linee guida, propone di seguire i principi S OLID, aumentare la coesione e ridurre l' accoppiamento .

La S in SOLID sta per Single Responsibility Principle, che afferma che un oggetto dovrebbe essere responsabile di una sola cosa. "Cosa" non è un termine molto preciso, quindi le descrizioni di questo principio variano selvaggiamente. Tuttavia, lo zio Bob, l'autore di Clean Code, è anche la persona che ha coniato questo principio, descrivendolo come: "Riunire le cose che cambiano per le stesse ragioni. Separare quelle cose che cambiano per ragioni diverse". Continua dicendo cosa intende con ragioni per cambiare qui e qui(una spiegazione più lunga qui sarebbe troppo). Se questo principio fosse applicato alla classe con cui hai a che fare, è molto probabile che i pezzi che trattano i calcoli sarebbero separati da quelli che trattano lo stato di tenuta, suddividendo la classe in due o più, a seconda di quante ragioni per cambiare quei calcoli hanno.

Inoltre, le classi Clean dovrebbero essere coerenti , nel senso che la maggior parte dei suoi metodi usa la maggior parte dei suoi attributi. Come tale, una classe al massimo coesiva è quella in cui tutti i metodi usano tutto il suo attributo; ad esempio, in un'app grafica potresti avere una Vectorclasse con attributi Point ae Point b, dove gli unici metodi sono scaleBy(double factor)e printTo(Canvas canvas), entrambi operanti su entrambi gli attributi. Al contrario, una classe minimamente coesa è quella in cui ogni attributo viene utilizzato in un solo metodo e mai più di un attributo viene utilizzato da ciascun metodo. In media, una classe presenta incoerenti "gruppi" di parti coesive - cioè alcuni metodi utilizzano attributi a, be c, mentre l'uso di riposo ced - significa che se dividiamo la classe in due, finiamo con due oggetti coesivi.

Infine, le classi Clean dovrebbero ridurre il più possibile l' accoppiamento . Mentre ci sono molti tipi di accoppiamento che vale la pena discutere qui, sembra che il codice a portata di mano soffra principalmente di accoppiamento temporale , dove, come hai sottolineato, i metodi dell'oggetto funzioneranno solo come previsto quando vengono chiamati nell'ordine corretto. E come le due linee guida sopra menzionate, le soluzioni a questo comportano di solito la suddivisione della classe in due o più oggetti coesivi . La strategia di scissione in questo caso di solito coinvolge modelli come Builder o Factory e, in casi molto complessi, macchine a stati.

TL; DR: le linee guida del codice pulito seguite dal collega sono buone, ma solo quando seguono anche i restanti principi, pratiche e schemi citati nel libro. La versione pulita della "classe" che stai vedendo sarebbe suddivisa in più classi, ognuna con una sola responsabilità, metodi coerenti e nessun accoppiamento temporale. Questo è il contesto in cui hanno senso piccoli metodi e argomenti poco o niente.


1
Sia tu che topo morto avete scritto una buona risposta, ma posso accettarne solo una. Mi piace che tu abbia affrontato SRP, coesione e accoppiamento. Questi sono termini utili che posso usare nella revisione del codice. Dividere l'oggetto in oggetti più piccoli con le proprie responsabilità è ovviamente la strada da percorrere. Un metodo (non costruttore) che inizializza i valori su un gruppo di proprietà della classe è un omaggio morto che un nuovo oggetto dovrebbe essere restituito. Avrei dovuto vederlo.
user2180613,

1
SRP è la linea guida più importante; uno squillo per dominarli tutti e tutto il resto. Ben fatto SRP si traduce naturalmente in metodi più brevi. Esempio: ho una classe frontale con solo 2 metodi pubblici e circa 8 metodi non pubblici. Nessuno è più di ~ 3 righe; l'intera classe è di circa 35 LOC. Ma ho scritto questa lezione l'ultima volta! Quando tutto il codice sottostante è stato scritto, questa classe essenzialmente ha scritto se stessa e non ho dovuto, anzi, non potrei ingrandire i metodi. In nessun momento ho detto "Scriverò questi metodi in 5 righe se mi uccide". Ogni volta che si applica SRP succede e basta.
radarbob,

11

Comprendo che il costruttore della classe è interamente responsabile della creazione di un oggetto valido e che il suo compito principale è l'assegnazione delle proprietà (private) di un oggetto.

Di solito è responsabile di mettere l'oggetto in uno stato iniziale valido, sì; altre proprietà o metodi possono quindi cambiare lo stato in un altro stato valido.

Tuttavia, nel codice che ho riscontrato, la maggior parte dei valori di proprietà sono effettivamente impostati con metodi diversi dal costruttore. I valori risultanti dai calcoli sono assegnati a proprietà da utilizzare all'interno di diversi metodi privati ​​in tutta la classe. Apparentemente l'autore usa le proprietà della classe come se fossero variabili globali che dovrebbero essere accessibili in tutta la classe, invece di parametrizzare questi valori alle funzioni che ne hanno bisogno. Inoltre, i metodi della classe dovrebbero essere chiamati in un ordine specifico, perché la classe non farà molto diversamente.

Oltre ai problemi di leggibilità e manutenibilità a cui alludi, sembra che ci siano più fasi del flusso / trasformazione dei dati in corso all'interno della classe stessa, il che potrebbe indicare che la classe sta fallendo nel principio della responsabilità singola.

sospetto che questo codice sia stato ispirato dal consiglio di mantenere i metodi brevi (<= 5 righe di codice), per evitare elenchi di parametri di grandi dimensioni (<3 parametri) e che i costruttori non devono funzionare (come eseguire un calcolo di qualche tipo che è essenziale per la validità dell'oggetto).

Seguire alcune linee guida per la codifica mentre ignorarne altre spesso porta a codici stupidi. Se vogliamo evitare che un costruttore faccia un lavoro, ad esempio, il modo ragionevole sarebbe di solito fare il lavoro prima della costruzione e passare il risultato di quel lavoro al costruttore. (Un argomento per questo approccio potrebbe essere che stai evitando di affidare alla tua classe due responsabilità: il lavoro della sua inizializzazione e il suo "lavoro principale", qualunque esso sia.)

Nella mia esperienza, rendere le classi e i metodi piccoli è raramente qualcosa che devo tenere a mente come considerazione separata - piuttosto, deriva in qualche modo naturalmente dalla singola responsabilità.

Preferirei piuttosto proporre di modificare completamente il codice, in modo che la classe diventi una stampa blu su un oggetto reale, piuttosto che una serie di metodi che dovrebbero essere chiamati (proceduralmente) in un ordine specifico.

Probabilmente avresti ragione a farlo. Non c'è niente di sbagliato nello scrivere un codice procedurale semplice; c'è un problema nell'abusare il paradigma OO per scrivere codice procedurale offuscato.

Credo che esista una distinzione piuttosto chiara su quando salvare un valore in una proprietà di classe e quando inserirlo in un parametro per un metodo diverso da utilizzare - Non credo davvero che possano essere alternative l'una all'altra. Sto cercando le parole per questa distinzione.

Di solito , non dovresti mettere un valore in un campo come modo per passarlo da un metodo all'altro; un valore in un campo dovrebbe essere una parte significativa dello stato di un oggetto in un determinato momento. (Posso pensare ad alcune valide eccezioni, ma non a quelle in cui tali metodi sono pubblici o in cui esiste una dipendenza dell'ordine di cui un utente della classe dovrebbe essere a conoscenza)


2
votato perché: 1. Enfatizzare SRP. "... metodi piccoli ... seguono naturalmente" 2. Scopo del costruttore - stato valido. 3. "Seguire alcune linee guida di codifica ignorandone altre." Questo è il Walking Dead della programmazione.
radarbob,

6

È molto probabile che tu ti sbagli contro lo schema sbagliato qui. Le piccole funzioni e la bassa arità sono raramente problematiche da sole. Il vero problema qui è l'accoppiamento che causa la dipendenza dell'ordine tra le funzioni, quindi cerca modi per affrontarlo senza buttare via i benefici delle piccole funzioni.

Il codice parla più forte delle parole. Le persone ricevono questo tipo di correzioni molto meglio se puoi effettivamente fare parte del refactoring e mostrare il miglioramento, forse come un esercizio di programmazione di coppia. Quando lo faccio, trovo spesso più difficile di quanto pensassi per ottenere il design giusto, bilanciando tutti i criteri.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.