Riga aggiuntiva in blocco rispetto a parametro aggiuntivo in Codice pulito

33

Contesto

In Pulisci codice , pagina 35, si dice

This implies that the blocks within if statements, else statements, while statements, and so on should be one line long. Probably that line should be a function call. Not only does this keep the enclosing function small, but it also adds documentary value because the function called within the block can have a nicely descriptive name.

Sono completamente d'accordo, questo ha molto senso.

Più avanti, a pagina 40, si parla degli argomenti della funzione

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn’t be used anyway. Arguments are hard. They take a lot of conceptual power.

Sono completamente d'accordo, questo ha molto senso.

Problema

Tuttavia, piuttosto spesso mi trovo a creare una lista da un'altra lista e dovrò vivere con uno dei due mali.

O io 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 I aggiungi un argomento alla funzione per la lista a cui verrà aggiunta la cosa, 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 preferibile uno di essi in generale? O ci sono tali vantaggi in determinate situazioni; in tal caso, cosa dovrei cercare quando prendo una decisione?

    
posta R. Schmitz 16.02.2018 - 13:19
fonte

6 risposte

104

Queste linee guida sono una bussola, non una mappa. Ti indirizzano in una direzione sensibile. Ma non possono davvero dirti in termini assoluti quale soluzione è "migliore". Ad un certo punto, devi smettere di camminare nella direzione in cui punta la tua bussola, perché sei arrivato a destinazione.

Clean Code ti incoraggia a dividere il tuo codice in blocchi molto piccoli e ovvi. Questa è una direzione generalmente buona. Ma una volta portato all'estremo (come suggerisce un'interpretazione letterale del consiglio citato), allora avrai suddiviso il tuo codice in pezzi inutilmente piccoli. Niente fa veramente niente, tutto solo delegato. Questo è essenzialmente un altro tipo di offuscamento del codice.

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

Se è possibile fare di meglio, è notare che "trasformare tutti gli elementi da una lista in un'altra lista" è un modello comune che può essere spesso sottratto, usando un'operazione funzionale map() . In C #, penso che si chiami Select . Qualcosa del genere:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}
    
risposta data 16.02.2018 - 13:41
fonte
61

The ideal number of arguments for a function is zero (niladic)

No! Il numero ideale di argomenti per una funzione è uno. Se è zero, si garantisce che la funzione deve accedere a informazioni esterne per poter eseguire un'azione. "Zio" Bob ha sbagliato molto questo.

Riguardo al tuo codice, il tuo primo esempio ha solo due righe nel blocco perché stai creando una variabile locale sulla prima riga. Rimuovi questo incarico e segui 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 è un codice molto lungo con codice (C #). Fallo come:

IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
    from badaBoom in babaBooms select CreateFlurp(badaBoom);
    
risposta data 16.02.2018 - 13:39
fonte
19

Il consiglio "Codice pulito" è completamente sbagliato.

Utilizza due o più linee nel tuo loop. Nascondere le stesse due linee in una funzione ha senso quando sono una matematica casuale che ha bisogno di una descrizione, ma non fa nulla quando le linee sono già descrittive. "Crea" e "Aggiungi"

Il secondo metodo che hai citato in realtà non ha alcun senso, dato che non sei obbligato ad aggiungere un secondo argomento per evitare le due linee.

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 funzione migliore sia quella senza argomenti è distorta a OOP nella migliore delle ipotesi e semplice cattivo consiglio nel peggiore dei casi

    
risposta data 16.02.2018 - 14:35
fonte
15

Il secondo è decisamente peggio, dato che CreateFlurpInList accetta la lista e modifica quella lista, rendendo la funzione non pura e più difficile da ragionare. Nulla nel nome del metodo suggerisce che il metodo venga aggiunto alla lista.

E offro la terza opzione, la migliore:

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

E diavolo, è possibile inline quel metodo immediatamente se c'è un solo posto in cui viene utilizzato, in quanto l'one-liner è chiaro da solo, quindi non ha bisogno di essere incapsulato dal metodo per dargli un significato.

    
risposta data 16.02.2018 - 13:36
fonte
10

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

La ragione più importante è che ha lower coupling , che lo rende più utile, più facile da ragionare, più facile da testare e meno probabilità di trasformarsi in cloni copiati e incollati.

Se mi fornisci un CreateFlurp(BadaBoom) , posso utilizzarlo con qualsiasi tipo di contenitore di raccolta: semplice Flurp[] , List<Flurp> , LinkedList<Flurp> , Dictionary<Key, Flurp> e così via. Ma con CreateFlurpInList(BadaBoom, List<Flurp>) , ti ricontatteremo domani per chiedere CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>) in modo che il mio viewmodel possa ricevere la notifica che l'elenco è cambiato. Yuck!

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

rather often I find myself creating a list from another list

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

var Flurps = badaBooms.ConvertAll(CreateFlurp);

Non solo questo è meno codice da scrivere e testare, ma è anche più veloce, perché List<T>.ConvertAll() è abbastanza intelligente da sapere che il risultato avrà lo stesso numero di elementi dell'input e preallocato l'elenco dei risultati sul dimensione corretta Mentre il tuo codice (entrambe le versioni) richiedeva di far crescere la lista.

    
risposta data 16.02.2018 - 16:54
fonte
6

Tieni presente l'obiettivo generale: semplificare la lettura e la manutenzione del codice.

Spesso, sarà possibile raggruppare più linee 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à. O potresti fare qualcosa di simile

flups.Add(new Flurp(badaBoom))

A volte, la soluzione più pulita e più leggibile semplicemente non si adatta a una riga. Quindi avrai due linee. Non rendere il codice più difficile da capire, solo per soddisfare alcune regole arbitrarie.

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

    
risposta data 16.02.2018 - 13:48
fonte

Leggi altre domande sui tag