Funzioni su una riga chiamate una sola volta


120

Considera una funzione senza parametri ( modifica: non necessariamente) che esegue una singola riga di codice e viene chiamata una sola volta nel programma (anche se non è impossibile che sia necessario di nuovo in futuro).

Potrebbe eseguire una query, controllare alcuni valori, fare qualcosa che coinvolge regex ... qualcosa di oscuro o "hacky".

La logica alla base di ciò sarebbe quella di evitare valutazioni difficilmente leggibili:

if (getCondition()) {
    // do stuff
}

dov'è getCondition()la funzione di una riga.

La mia domanda è semplicemente: è una buona pratica? Mi sembra a posto ma non so a lungo termine ...


9
A meno che il tuo frammento di codice non provenga da un linguaggio OOP con ricevitore implicito (ad esempio questo), questa sarebbe sicuramente una cattiva pratica, poiché getCondition () molto probabilmente si basa sullo stato globale ...
Ingo,

2
Potrebbe aver inteso implicare che getCondition () potrebbe avere argomenti?
user606723,

24
@Ingo - alcune cose hanno davvero lo stato globale. L'ora corrente, i nomi host, i numeri di porta ecc. Sono tutti "Globali" validi. L'errore di progettazione sta trasformando un globale in qualcosa che non è intrinsecamente globale.
James Anderson,

1
Perché non sei solo in linea getCondition? Se è piccolo e usato di rado come dici, dargli un nome non significa ottenere nulla.
davidk01,

12
davidk01: leggibilità del codice.
wjl,

Risposte:


240

Dipende da quella riga. Se la riga è leggibile e concisa da sola, la funzione potrebbe non essere necessaria. Esempio semplicistico:

void printNewLine() {
  System.out.println();
}

OTOH, se la funzione dà un buon nome a una riga di codice contenente ad esempio un'espressione complessa e difficile da leggere, è perfettamente giustificata (per me). Esempio contrastato (suddiviso in più righe per leggibilità qui):

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

99
+1. La parola magica qui è "Codice auto-documentante".
Konamiman,

8
Un buon esempio di ciò che lo zio Bob chiamerebbe "incapsulanti condizionali".
Anthony Pegram,

12
@Aditya: nulla dice che taxPayersia globale in questo scenario. Forse questa classe è TaxReturned taxPayerè un attributo.
Adam Robinson,

2
Inoltre, consente di documentare la funzione in un modo che può essere raccolto da javadoc ed essere pubblicamente visibile.

2
@dallin, il Clean Code di Bob Martin mostra molti esempi di codici di vita semi-reali. Se hai troppe funzioni in una singola classe, forse la tua classe è troppo grande?
Péter Török,

67

Sì, questo può essere usato per soddisfare le migliori pratiche. Ad esempio, è meglio avere una funzione con un nome chiaro che faccia un po 'di lavoro, anche se è lunga solo una riga, piuttosto che avere quella riga di codice all'interno di una funzione più grande e avere bisogno di un commento di una riga che spieghi cosa fa. Inoltre, le righe di codice vicine dovrebbero eseguire attività allo stesso livello di astrazione. Un controesempio sarebbe qualcosa di simile

startIgnition();
petrolFlag |= 0x006A;
engageChoke();

In questo caso è sicuramente meglio spostare la linea mediana in una funzione chiamata con un nome ragionevole.


15
Che 0x006A sia adorabile: costante ben nota di DA di combustibile carbonizzato con additivi a, bec.
Coder

2
+1 Sono tutto per il codice di auto-documentazione :) a proposito, penso di essere d'accordo nel mantenere un livello di astrazione costante attraverso i blocchi, ma non sono riuscito a spiegare il perché. Ti dispiacerebbe espandere quello?
Vemv,

3
Se stai solo dicendo che petrolFlag |= 0x006A;senza alcun tipo di processo decisionale, sarebbe meglio dire semplicemente petrolFlag |= A_B_C; senza una funzione aggiuntiva. Presumibilmente, engageChoke()dovrebbe essere chiamato solo se petrolFlagsoddisfa determinati criteri e ciò dovrebbe chiaramente dire "Ho bisogno di una funzione qui". Solo un piccolo sospiro, questa risposta è praticamente perfetta altrimenti :)
Tim Post

9
+1 per indicare che il codice all'interno di un metodo dovrebbe essere nello stesso livello di astrazione. @vemv, è una buona pratica perché rende il codice più facile da capire, evitando la necessità che la tua mente salti su e giù tra i diversi livelli di astrazione durante la lettura del codice. Legare il livello di astrazione passa a chiamate / ritorni di metodo (ad es. "Salti" strutturali) è un buon modo per rendere il codice più fluido e pulito.
Péter Török,

21
No, è un valore completamente inventato. Ma il fatto che tu te ne chieda sottolinea solo il punto: se il codice avesse detto mixLeadedGasoline(), non dovresti!
Kilian Foth,

37

Penso che in molti casi tale funzione sia di buon stile, ma potresti considerare una variabile booleana locale come alternativa nei casi in cui non è necessario utilizzare questa condizione da qualche parte in altri luoghi, ad esempio:

bool someConditionSatisfied = [complex expression];

Questo darà un suggerimento al lettore di codice e salverà dall'introduzione di una nuova funzione.


1
Il bool è particolarmente utile se il nome della funzione condizione sarebbe difficile o eventualmente fuorviante (ad es IsEngineReadyUnlessItIsOffOrBusyOrOutOfService.).
dbkk,

2
Ho avuto una cattiva idea di questo consiglio: il bool e la condizione distolgono l'attenzione dal core business della funzione. Inoltre, uno stile che preferisce le variabili locali rende più difficile il refactoring.
Lupo,

1
@Wolf OTOH, preferisco questo a una chiamata di funzione per ridurre la "profondità di astrazione" del codice. IMO, saltare in una funzione è un cambio di contesto più ampio di una coppia esplicita e diretta di righe di codice, specialmente se restituisce solo un valore booleano.
Kache,

@Kache Penso che dipenda dal fatto che il codice sia orientato agli oggetti o meno, nel caso OOP, l'uso della funzione membro mantiene il design molto più flessibile. Dipende molto dal contesto ...
Wolf,

23

Oltre alla risposta di Peter , se in futuro potrebbe essere necessario aggiornare tale condizione, facendola incapsulare nel modo in cui suggerisci che avresti un solo punto di modifica.

Seguendo l'esempio di Pietro, se questo

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

diventa questo

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isMutant() 
        && (taxPayer.getNumberOfThumbs() > 2 
        || (taxPayer.getAge() > 123 && taxPayer.getEmployer().isXMan()));
}

Fai una singola modifica e viene aggiornata universalmente. Manutenibilità, questo è un vantaggio.

Per quanto riguarda le prestazioni, i compilatori più ottimizzanti rimuoveranno la chiamata di funzione e incorporeranno comunque il piccolo blocco di codice. L'ottimizzazione di qualcosa del genere può effettivamente ridurre la dimensione del blocco (eliminando le istruzioni necessarie per la chiamata di funzione, ritorno, ecc.), Pertanto è normalmente sicuro farlo anche in condizioni che potrebbero altrimenti impedire l'allineamento.


2
Sono già consapevole dei vantaggi di mantenere un singolo punto di modifica - ecco perché la domanda dice "... chiamato una volta". Tuttavia è bello conoscere queste ottimizzazioni del compilatore, ho sempre pensato che seguissero letteralmente questo tipo di istruzioni.
Vemv,

La suddivisione di un metodo per testare una condizione semplice tende a implicare che una condizione può essere modificata cambiando il metodo. Tale implicazione è utile quando è vera, ma può essere pericolosa quando non lo è. Ad esempio, supponiamo che il codice debba dividere le cose in oggetti leggeri, oggetti verdi pesanti e oggetti non verdi pesanti. Gli oggetti hanno una rapida proprietà "seemGreen" che è affidabile per oggetti pesanti, ma può restituire falsi positivi con oggetti leggeri; hanno anche una funzione "measureSpectralResponse" che è lenta, ma funzionerà in modo affidabile per tutti gli oggetti.
supercat

Il fatto che seemsGreennon sia affidabile con oggetti leggeri sarà irrilevante se al codice non importa mai se gli oggetti leggeri sono verdi. Se la definizione di "leggero" cambia, tuttavia, in modo tale che alcuni oggetti non verdi che restituiscono true per seemsGreennon vengano riportati come "leggero", tale modifica alla definizione di "leggero" potrebbe violare il codice che verifica gli oggetti essere verde". In alcuni casi, testare il verde e il peso insieme nel codice può rendere più chiara la relazione tra i test rispetto a metodi separati.
supercat

5

Oltre alla leggibilità (o in complemento di ciò), ciò consente di scrivere funzioni al giusto livello di astrazione.


1
Temo di non capire cosa intendevi ...
vemv,

1
@vemv: Penso che per "corretto livello di astrazione" intendiamo che non si finisce con un codice che ha diversi livelli di astrazione mescolati (cioè ciò che Kilian Foth ha già detto). Non vuoi che il tuo codice gestisca dettagli apparentemente insignificanti (ad esempio la formazione di tutte le obbligazioni nel carburante) quando il codice circostante sta prendendo in considerazione una visione di più alto livello della situazione a portata di mano (ad esempio, facendo funzionare un motore). Il primo ingombra il secondo e rende il codice meno facile da leggere e mantenere poiché dovrai considerare tutti i livelli di astrazione contemporaneamente in qualsiasi momento.
Egon,

4

Dipende. A volte è meglio incapsulare un'espressione in una funzione / metodo, anche se è solo una riga. Se è complicato da leggere o ne hai bisogno in più punti, allora lo considero una buona pratica. A lungo termine è più facile da mantenere, poiché hai introdotto un unico punto di cambiamento e una migliore leggibilità .

Tuttavia, a volte è solo qualcosa che non ti serve. Quando l'espressione è comunque facile da leggere e / o appare solo in un punto, non avvolgerla.


3

Penso che se ne hai solo alcuni, allora va bene, ma il problema si presenta quando ce ne sono molti nel tuo codice. E quando il compilatore viene eseguito o l'interpitor (a seconda della lingua utilizzata), andrà a quella funzione in memoria. Quindi supponiamo che tu ne abbia 3 che non credo che il computer noterà, ma se inizi a avere centinaia di quelle piccole cose, il sistema deve registrare le funzioni in memoria che vengono chiamate solo una volta e quindi non distrutte.


Secondo la risposta di Stephen questo potrebbe non succedere sempre (anche se fidarsi ciecamente della magia dei compilatori non può essere comunque buono)
vemv,

1
sì, dovrebbe essere cancellato a seconda di molti fatti. Se si tratta di un linguaggio interpolato, il sistema cadrà sempre per le funzioni a linea singola, a meno che non si installi qualcosa per la cache che potrebbe comunque non funzionare. Ora, quando si tratta di compilatori, importa solo nel giorno dei deboli e del collocamento dei planati se il compilatore sarà tuo amico e risolverà il tuo piccolo pasticcio o se penserà che ne hai davvero bisogno. Ricordo che se si conosce l'esatta quantità di volte in cui un ciclo verrà eseguito alcune volte meglio è sufficiente copiarlo e incollarlo più volte, quindi eseguire il ciclo.
WojonsTech,

+1 per l'allineamento dei pianeti :) ma la tua ultima frase mi sembra del tutto fuori di testa, lo fai davvero?
Vemv,

Dipende davvero Il più delle volte no, a meno che non stia ottenendo l'importo corretto del pagamento per verificare se accelera la velocità e altre cose del genere. Ma nei compilatori più vecchi era meglio copiarlo e incollarlo, quindi lasciare il compilere per capirlo 9/10.
WojonsTech,

3

Ho fatto esattamente questa cosa di recente in un'applicazione che ho eseguito il refactoring, per rendere esplicito il significato reale del codice senza commenti:

protected void PaymentButton_Click(object sender, EventArgs e)
    Func<bool> HaveError = () => lblCreditCardError.Text == string.Empty && lblDisclaimer.Text == string.Empty;

    CheckInputs();

    if(HaveError())
        return;

    ...
}

Solo curioso, che lingua è?
Vemv,

5
@vemv: A me sembra C #.
Scott Mitchell,

Preferisco anche identificatori extra rispetto ai commenti. Ma questo differisce davvero dall'introduzione di una variabile locale per farla ifbreve? Questo approccio locale (lambda) rende la PaymentButton_Clickfunzione (nel suo insieme) più difficile da leggere. Nel lblCreditCardErrortuo esempio sembra essere un membro, quindi HaveErrorè anche un predicato (privato) valido per l'oggetto. Tenderei a sottovalutare questo, ma non sono un programmatore C #, quindi resisto.
Lupo,

@ Lupo Ehi amico, sì. L'ho scritto un po 'di tempo fa :) Sicuramente faccio cose in modo molto diverso oggi. In effetti, guardare il contenuto dell'etichetta per vedere se c'è stato un errore mi fa rabbrividire ... Perché non ho appena restituito un bool da CheckInputs()???
joshperry,

0

Lo spostamento di quella riga in un metodo ben definito semplifica la lettura del codice. Molti altri lo hanno già menzionato ("codice auto-documentante"). L'altro vantaggio di spostarlo in un metodo è che semplifica il test unitario. Quando è isolato nel suo metodo e testato l'unità, puoi essere sicuro che se / quando viene trovato un bug, non sarà in questo metodo.


0

Ci sono già molte buone risposte, ma c'è un caso speciale degno di nota .

Se la tua dichiarazione di una riga necessita di un commento e sei in grado di identificare chiaramente (il che significa: nome) il suo scopo, considera di estrarre una funzione migliorando il commento nel documento API. In questo modo si rende la chiamata di funzione più semplice e veloce da capire.

È interessante notare che lo stesso può essere fatto se al momento non c'è nulla da fare, ma un commento che ricorda le espansioni necessarie (nel prossimo futuro 1) ), quindi questo,

def sophisticatedHello():
    # todo set up
    say("hello")
    # todo tear down

potrebbe anche essere cambiato in questo

def sophisticatedHello():
    setUp()
    say("hello")
    tearDown()

1) dovresti esserne veramente sicuro (vedi il principio YAGNI )


0

Se la lingua lo supporta, di solito uso funzioni anonime etichettate per farlo.

someCondition = lambda p: True if [complicated expression involving p] else False
#I explicitly write the function with a ternary to make it clear this is a a predicate
if (someCondition(p)):
    #do stuff...

IMHO questo è un buon compromesso perché ti dà il vantaggio della leggibilità di non avere l'espressione complicata che ingombra la ifcondizione evitando allo stesso tempo di ingombrare lo spazio dei nomi globale / pacchetto con piccole etichette usa e getta. Ha l'ulteriore vantaggio che la funzione "definizione" è proprio dove viene utilizzata, facilitando la modifica e la lettura della definizione.

Non devono essere solo funzioni predicate. Mi piace racchiudere il ripetuto boiler-plate anche in piccole funzioni come questa (Funziona particolarmente bene per la generazione di liste pythonic senza ingombrare la sintassi della parentesi). Ad esempio, il seguente esempio semplificato quando si lavora con PIL in Python

#goal - I have a list of PIL Image objects and I want them all as grayscale (uint8) numpy arrays
im_2_arr = lambda im: array(im.convert('L')) 
arr_list = [im_2_arr(image) for image in image_list]

Perché "lambda p: True se [espressione complicata che coinvolge p] else False" invece di "lambda p: [espressione complicata che coinvolge p]"? 8-)
Hans-Peter Storr

@hstoerr è nel commento proprio sotto quella riga. Mi piace far sapere esplicitamente che someCondition è un predicato. Sebbene sia strettamente necessario, scrivo molti script scientifici letti da persone che non scrivono codice così tanto, personalmente ritengo sia più appropriato avere la terseness in più piuttosto che confondere i miei colleghi perché non lo sanno [] == Falseo altri equivalente equivalenza pitonica che non è "sempre" intuitiva. È fondamentalmente un modo per segnalare che SomeCondition è, in effetti, un predicato.
crasic,

Giusto per chiarire il mio ovvio errore, [] != Falsema []è falso come quando viene lanciato su un bool
crasico

@crasic: quando non mi aspetto che i miei lettori sappiano che [] valuta Falso, preferisco fare len([complicated expression producing a list]) == 0, piuttosto che usare, il True if [blah] else Falseche richiede comunque al lettore di sapere che [] valuta Falso.
Lie Ryan,
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.