Riga aggiuntiva nel blocco vs parametro aggiuntivo nel codice pulito


33

Contesto

In Clean Code , pagina 35, si dice

Ciò implica che i blocchi all'interno delle istruzioni if, else, while, e così via dovrebbero essere lunghi una riga. Probabilmente quella linea dovrebbe essere una chiamata di funzione. Questo non solo mantiene piccola la funzione che racchiude, ma aggiunge anche valore documentale perché la funzione chiamata all'interno del blocco può avere un nome ben descrittivo.

Sono completamente d'accordo, questo ha molto senso.

Più avanti, a pagina 40, parla degli argomenti delle funzioni

Il numero ideale di argomenti per una funzione è zero (niladic). Ne segue uno (monadico), seguito da vicino da due (diadico). Tre argomenti (triadici) dovrebbero essere evitati ove possibile. Più di tre (poliadici) richiedono una giustificazione molto speciale, e quindi non dovrebbero essere usati comunque. Gli argomenti sono difficili. Prendono molto potere concettuale.

Sono completamente d'accordo, questo ha molto senso.

Problema

Tuttavia, piuttosto spesso mi ritrovo a creare un elenco da un altro elenco e dovrò vivere con uno dei due mali.

O uso due righe nel blocco , una per creare la cosa, una per aggiungerla al risultato:

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            Flurp flurp = CreateFlurp(badaBoom);
            flurps.Add(flurp);
        }
        return flurps;
    }

Oppure aggiungo un argomento alla funzione per l'elenco in cui la cosa verrà aggiunta, rendendola "un argomento peggiore".

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            CreateFlurpInList(badaBoom, flurps);
        }
        return flurps;
    }

Domanda

Ci sono (dis) vantaggi che non vedo, che rendono uno di questi preferibile in generale? O ci sono tali vantaggi in determinate situazioni; in tal caso, cosa devo cercare quando prendo una decisione?


58
Cosa c'è che non va flurps.Add(CreateFlurp(badaBoom));?
cmaster

47
No, è solo una singola affermazione. È solo un'espressione banalmente nidificata (un singolo livello nidificato). E se un semplice f(g(x))è contro la tua guida di stile, beh, non posso aggiustare la tua guida di stile. Voglio dire, non ti dividi nemmeno sqrt(x*x + y*y)in quattro righe, vero? E questo è tre (!) Sottoespressioni nidificate su due (!) Livelli di annidamento interno (sussulto!). Il tuo obiettivo dovrebbe essere la leggibilità , non le dichiarazioni di un singolo operatore. Se vuoi il dopo, beh, ho il linguaggio perfetto per te: Assembler.
cmaster

6
@cmaster Anche l'assemblaggio x86 non ha rigorosamente istruzioni per operatore singolo. Le modalità di indirizzamento della memoria includono molte operazioni complicate e possono essere utilizzate per l'aritmetica: in effetti, è possibile creare un computer completo di Turing utilizzando solo le movistruzioni x86 e una singola jmp toStartalla fine. Qualcuno ha effettivamente creato un compilatore che fa esattamente questo: D
Luaan,

5
@Luaan Per non parlare delle famigerate rlwimiistruzioni sul PPC. (Questo indica l'inserimento della maschera immediata Ruota a sinistra.) Questo comando non ha preso meno di cinque operandi (due registri e tre valori immediati) ed ha eseguito le seguenti operazioni: Il contenuto di un registro è stato ruotato da uno spostamento immediato, una maschera è stata creato con una singola corsa di 1 bit che era controllata dagli altri due operandi immediati, e i bit che corrispondevano a 1 bit in quella maschera nell'altro operando di registro venivano sostituiti con i corrispondenti bit del registro ruotato. Istruzioni molto interessanti :-)
cmaster

7
@ R.Schmitz "Sto programmando per scopi generici" - in realtà no, non lo stai facendo, stai programmando per uno scopo specifico (non so quale scopo, ma suppongo che tu lo faccia ;-). Ci sono letteralmente migliaia di scopi per la programmazione e gli stili di codifica ottimali variano per loro - quindi ciò che è appropriato per te potrebbe non essere adatto ad altri, e viceversa: spesso i consigli qui sono assoluti (" fai sempre X; Y è male "ecc.) ignorando che in alcuni settori è assolutamente impraticabile attenersi. Ecco perché i consigli in libri come Clean Code dovrebbero sempre essere presi con un pizzico di sale (pratico) :)
psmears

Risposte:


104

Queste linee guida sono una bussola, non una mappa. Ti indicano una direzione sensata . Ma non possono davvero dirti in termini assoluti quale soluzione è "migliore". Ad un certo punto, devi smettere di camminare nella direzione indicata dalla bussola, perché sei arrivato a destinazione.

Clean Code ti incoraggia a dividere il tuo codice in blocchi molto piccoli ed evidenti. Questa è generalmente una buona direzione. Ma se portato all'estremo (come suggerisce un'interpretazione letterale dei consigli citati), allora avrai suddiviso il tuo codice in pezzi inutilmente piccoli. Niente fa davvero nulla, tutto solo delegati. Questo è essenzialmente un altro tipo di offuscamento del codice.

Il tuo compito è bilanciare "più piccolo è meglio" con "troppo piccolo è inutile". Chiediti quale soluzione è più semplice. Per me, questa è chiaramente la prima soluzione in quanto ovviamente assembla un elenco. Questo è un linguaggio ben compreso. È possibile capire quel codice senza dover guardare ancora un'altra funzione.

Se è possibile fare di meglio, è notando che "trasformare tutti gli elementi da un elenco a un altro elenco" è un modello comune che può essere spesso estratto, usando un'operazione funzionale map(). In C #, penso che si chiami Select. Qualcosa come questo:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}

7
Il codice è ancora sbagliato e reinventa inutilmente la ruota. Perché chiamare CreateFlurps(someList)quando il BCL fornisce già someList.ConvertAll(CreateFlurp)?
Ben Voigt,

44
@BenVoigt Questa è una domanda a livello di design. Non mi preoccupo della sintassi esatta, soprattutto perché una lavagna non ha un compilatore (e ho scritto C # per l'ultima volta nel '09). Il mio punto non è "ho mostrato il miglior codice possibile" ma "a parte questo è uno schema comune che è già stato risolto". Linq è un modo per farlo, il ConvertTo ne citi un altro . Grazie per aver suggerito quell'alternativa.
am

1
La tua risposta è ragionevole, ma il fatto che LINQ allontani la logica e riduca l'affermazione a una riga dopo tutto sembra contraddire il tuo consiglio. Come nota a margine, BadaBoom => CreateFlurp(badaBoom)è ridondante; puoi passare CreateFlurpdirettamente come funzione ( Select(CreateFlurp)). (Per quanto ne so, è sempre stato così.)
jpmc26,

2
Si noti che ciò elimina completamente la necessità del metodo. Il nome CreateFlurpsè in realtà più fuorviante e più difficile da capire rispetto al solo vedere badaBooms.Select(CreateFlurp). Quest'ultimo è completamente dichiarativo: non vi è alcun problema a decomporsi e quindi non è necessario alcun metodo.
Carl Leth,

1
@ R.Schmitz Non è difficile da capire, ma è meno facile da capire di badaBooms.Select(CreateFlurp). Si crea un metodo in modo che il suo nome (alto livello) sostituisca la sua implementazione (basso livello). In questo caso sono allo stesso livello, quindi per scoprire esattamente cosa sta succedendo devo solo guardare il metodo (invece di vederlo in linea). CreateFlurps(badaBooms)potrebbe contenere sorprese, ma badaBooms.Select(CreateFlurp)non può. È anche fuorviante perché erroneamente chiede un Listinvece di un IEnumerable.
Carl Leth,

61

Il numero ideale di argomenti per una funzione è zero (niladic)

No! Il numero ideale di argomenti per una funzione è uno. Se è zero, allora stai garantendo che la funzione debba accedere alle informazioni esterne per poter eseguire un'azione. "Zio" Bob ha sbagliato molto.

Per quanto riguarda il tuo codice, il tuo primo esempio ha solo due righe nel blocco perché stai creando una variabile locale sulla prima riga. Rimuovi quel compito e stai rispettando queste linee guida per il codice pulito:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    List<Flurp> flurps = new List<Flurp>();
    foreach (BadaBoom badaBoom in badaBooms)
    {
        flurps.Add(CreateFlurp(badaBoom));
    }
    return flurps;
}

Ma questo è un codice molto lungo (C #). Fallo come:

IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
    from badaBoom in babaBooms select CreateFlurp(badaBoom);

14
Una funzione con zero argomenti intende implicare che l'oggetto incapsula i dati richiesti e non che le cose esistano in uno stato globale al di fuori di un oggetto.
Ryathal,

19
@Ryathal, due punti: (1) se stai parlando di metodi, quindi per la maggior parte (tutte?) Le lingue OO, quell'oggetto viene dedotto (o dichiarato esplicitamente, nel caso di Python) come primo parametro. In Java, C # ecc., Tutti i metodi sono funzioni con almeno un parametro. Il compilatore ti nasconde quel dettaglio. (2) Non ho mai menzionato "globale". Lo stato dell'oggetto è esterno ad un metodo, ad esempio.
David Arno,

17
Sono abbastanza sicuro, quando lo zio Bob ha scritto "zero" intendeva "zero (senza contare questo)".
Doc Brown,

26
@DocBrown, probabilmente perché è un grande fan della miscelazione di stato e funzionalità negli oggetti, quindi per "funzione" probabilmente si riferisce specificamente ai metodi. E non sono ancora d'accordo con lui. È molto meglio dare un metodo solo ciò di cui ha bisogno, piuttosto che lasciarlo frugare nell'oggetto per ottenere ciò che vuole (cioè, è classico "dillo, non chiedere" in azione).
David Arno,

8
@AlessandroTeruzzi, L'ideale è un parametro. Zero è troppo pochi. Questo è il motivo per cui, ad esempio, i linguaggi funzionali adottano uno come numero di parametri ai fini del curry (in effetti in alcuni linguaggi funzionali, tutte le funzioni hanno esattamente un parametro: niente di più; niente di meno). Currying con zero parametri sarebbe privo di senso. Affermare che "l'ideale è il meno possibile, ergo zero è il migliore" è un esempio di reductio ad absurdum .
David Arno,

19

Il consiglio "Clean Code" è completamente sbagliato.

Usa due o più righe nel tuo loop. Nascondere le stesse due linee in una funzione ha senso quando si tratta di alcuni calcoli casuali che richiedono una descrizione ma non fanno nulla quando le linee sono già descrittive. "Crea" e "Aggiungi"

Il secondo metodo in cui menzioni non ha davvero senso, poiché non sei obbligato ad aggiungere un secondo argomento per evitare le due righe.

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            flurps.Add(badaBoom .CreateFlurp());
            //or
            badaBoom.AddToListAsFlurp(flurps);
            //or
            flurps.Add(new Flurp(badaBoom));
            //or
            //make flurps a member of the class
            //use linq.Select()
            //etc
        }
        return flurps;
    }

o

foreach(var flurp in ConvertToFlurps(badaBooms))...

Come notato da altri, il consiglio che la migliore funzione è una senza argomenti è distorto da OOP nella migliore delle ipotesi e chiaramente un cattivo consiglio nella peggiore


Forse vuoi modificare questa risposta per renderla più chiara? La mia domanda era se una cosa è più grande di un'altra sotto Clean Code. Dici che tutto è sbagliato e poi continua descrivendo una delle opzioni che ho dato. Al momento sembra che tu stia seguendo un'agenda del codice anti-clean invece di provare effettivamente a rispondere alla domanda.
R. Schmitz,

mi dispiace di aver interpretato la tua domanda come suggerendo che il primo era il modo "normale", ma ti stavi spingendo nel secondo. Non sono un anti-clean-code in generale, ma questa citazione è ovviamente sbagliata
Ewan,

19
@ R.Schmitz Ho letto personalmente "Clean Code" e seguo la maggior parte di ciò che dice quel libro. Tuttavia, considerando che la dimensione della funzione perfetta è praticamente una singola istruzione, è semplicemente sbagliato. L'unico effetto è che trasforma il codice spaghetti in codice riso. Il lettore si perde nella moltitudine di funzioni banali che producono un significato sensibile solo se viste insieme. Gli esseri umani hanno una capacità di memoria di lavoro limitata e puoi sovraccaricarla con istruzioni o con funzioni. Devi essere in equilibrio tra i due se vuoi essere leggibile. Evita gli estremi!
cmaster

@cmaster La risposta è stata solo i primi 2 paragrafi quando ho scritto quel commento. Ormai è una risposta migliore.
R. Schmitz,

7
francamente ho preferito la mia risposta più breve. C'è troppo discorso diplomatico nella maggior parte di queste risposte. Il consiglio citato è chiaramente sbagliato, non c'è bisogno di speculare su "cosa significhi davvero" o girarsi per cercare una buona interpretazione.
Ewan,

15

Il secondo è decisamente peggio, poiché CreateFlurpInListaccetta l'elenco e modifica tale elenco, rendendo la funzione non pura e più difficile da ragionare. Nulla nel nome del metodo suggerisce che il metodo si aggiunge solo all'elenco.

E offro la terza, migliore opzione:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(CreateFlurp).ToList();
}

E diavolo, puoi incorporare immediatamente quel metodo se c'è solo un posto dove viene usato, poiché il one-liner è chiaro da solo, quindi non ha bisogno di essere incapsulato dal metodo per dargli un significato.


Non mi lamento così tanto di quel metodo "non essere puro e più difficile da ragionare" (anche se vero), ma sul fatto che sia un metodo completamente inutile per gestire un caso speciale. Cosa succede se desidero creare un Flurp autonomo, un Flurp aggiunto a un array, a un dizionario, un Flurp che viene quindi cercato in un dizionario e il Flurp corrispondente rimosso, ecc.? Con lo stesso argomento, il codice Flurp avrebbe bisogno anche di tutti questi metodi.
gnasher729,

10

La versione a un argomento è migliore, ma non principalmente a causa del numero di argomenti.

Il motivo più importante è che ha un accoppiamento inferiore , che lo rende più utile, più facile da ragionare, più facile da testare e con meno probabilità di trasformarsi in cloni copia + incollati.

Se mi fornisci con una CreateFlurp(BadaBoom), posso usare che con qualsiasi tipo di contenitore di raccolta: semplice Flurp[], List<Flurp>, LinkedList<Flurp>, Dictionary<Key, Flurp>, e così via. Ma con un CreateFlurpInList(BadaBoom, List<Flurp>), domani torno da te per chiedere in CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>)modo che il mio modello di visualizzazione possa ricevere la notifica che l'elenco è cambiato. Che schifo!

Come ulteriore vantaggio, è più probabile che la firma più semplice si adatti alle API esistenti. Dici di avere un problema ricorrente

piuttosto spesso mi ritrovo a creare un elenco da un altro elenco

Si tratta solo di utilizzare gli strumenti disponibili. La versione più breve, più efficiente e migliore è:

var Flurps = badaBooms.ConvertAll(CreateFlurp);

Questo codice non solo consente di scrivere e testare, ma è anche più veloce, poiché List<T>.ConvertAll()è abbastanza intelligente da sapere che il risultato avrà lo stesso numero di elementi dell'input e preallocare l'elenco dei risultati nella dimensione corretta. Mentre il tuo codice (entrambe le versioni) ha richiesto di ampliare l'elenco.


Non usare List.ConvertAll. Viene chiamato il modo idiomatico di mappare un enumerabile oggetto su oggetti diversi in C # Select. L'unico motivo per cui ConvertAllè disponibile anche qui è perché l'OP chiede erroneamente un Listmetodo nel - dovrebbe essere un IEnumerable.
Carl Leth,

6

Tieni a mente l'obiettivo generale: rendere il codice facile da leggere e mantenere.

Spesso, sarà possibile raggruppare più righe in un'unica funzione significativa. Fallo in questi casi. Occasionalmente, dovrai riconsiderare il tuo approccio generale.

Ad esempio, nel tuo caso, sostituendo l'intera implementazione con var

flups = badaBooms.Select(bb => new Flurp(bb));

potrebbe essere una possibilità. Oppure potresti fare qualcosa del genere

flups.Add(new Flurp(badaBoom))

A volte, la soluzione più pulita e più leggibile semplicemente non si adatta in una riga. Quindi avrai due righe. Non rendere il codice più difficile da capire, solo per soddisfare una regola arbitraria.

Il tuo secondo esempio è (secondo me) considerevolmente più difficile da capire rispetto al primo. Non è solo che hai un secondo parametro, è che il parametro viene modificato dalla funzione. Cerca ciò che Clean Code ha da dire al riguardo. (Non ho il libro a portata di mano in questo momento, ma sono abbastanza sicuro che sostanzialmente "non farlo se puoi evitarlo").

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.