Leggibilità contro manutenibilità, caso speciale di scrittura di chiamate di funzione nidificate


57

Il mio stile di codifica per le chiamate di funzione nidificate è il seguente:

var result_h1 = H1(b1);
var result_h2 = H2(b2);
var result_g1 = G1(result_h1, result_h2);
var result_g2 = G2(c1);
var a = F(result_g1, result_g2);

Di recente sono passato a un dipartimento in cui è molto utilizzato il seguente stile di codifica:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Il risultato del mio modo di scrivere codice è che, nel caso di una funzione di arresto anomalo, Visual Studio può aprire il dump corrispondente e indicare la riga in cui si verifica il problema (sono particolarmente preoccupato per le violazioni dell'accesso).

Temo che, in caso di un incidente dovuto allo stesso problema programmato nel primo modo, non sarò in grado di sapere quale funzione ha causato l'incidente.

D'altra parte, maggiore è l'elaborazione che metti su una linea, maggiore è la logica che ottieni su una pagina, il che migliora la leggibilità.

La mia paura è corretta o mi manca qualcosa, e in generale, che è preferito in un ambiente commerciale? Leggibilità o manutenibilità?

Non so se sia rilevante, ma stiamo lavorando in C ++ (STL) / C #.


17
@gnat: fai riferimento a una domanda generale, mentre io sono particolarmente interessato al caso citato di chiamate di funzione nidificate e alle conseguenze in caso di analisi di crash dump, ma grazie per il link contiene alcune informazioni piuttosto interessanti.
Dominique,

9
Nota che se questo esempio dovesse essere applicato al C ++ (come dici che questo è usato nel tuo progetto) allora questa non è solo una questione di stile, poiché l' ordine di valutazione delle invocazioni HXe GXpuò cambiare nel one-liner, come l'ordine di valutazione degli argomenti delle funzioni non è specificato. Se per qualche motivo dipendete dall'ordine degli effetti collaterali (consapevolmente o inconsapevolmente) nelle invocazioni, questo "refactoring di stile" potrebbe finire per influire più della semplice leggibilità / manutenzione.
venerdì

4
Il nome della variabile è result_g1quello che avresti effettivamente utilizzato o questo valore rappresenta effettivamente qualcosa con un nome sensibile? es percentageIncreasePerSecond. Sarebbe davvero il mio test per decidere tra i due
Richard Tingle,

3
Indipendentemente dai tuoi sentimenti sullo stile di codifica, dovresti seguire la convenzione che è già in atto a meno che non sia chiaramente sbagliata (non sembra che sia in questo caso).
n00b,

4
@ t3chb0t Sei libero di votare come preferisci, ma tieni presente nell'interesse di incoraggiare domande utili, utili e su questo argomento (e scoraggiare quelle cattive), che lo scopo di votare in alto o in basso una domanda è indicare se una domanda è utile e chiara, quindi votare per altri motivi come l'uso di un voto come mezzo per fornire critiche su alcuni esempi di codice pubblicati per aiutare il contesto della domanda in generale non è utile per mantenere la qualità del sito : softwareengineering.stackexchange.com/help/privileges/vote-down
Ben Cottrell

Risposte:


111

Se ti sentissi in dovere di espandere una fodera come

 a = F(G1(H1(b1), H2(b2)), G2(c1));

Non ti biasimerei. Non è solo difficile da leggere, è difficile eseguire il debug.

Perché?

  1. È denso
  2. Alcuni debugger metteranno in evidenza tutto in una volta
  3. È privo di nomi descrittivi

Se lo espandi con risultati intermedi, otterrai

 var result_h1 = H1(b1);
 var result_h2 = H2(b2);
 var result_g1 = G1(result_h1, result_h2);
 var result_g2 = G2(c1);
 var a = F(result_g1, result_g2);

ed è ancora difficile da leggere. Perché? Risolve due dei problemi e introduce un quarto:

  1. È denso
  2. Alcuni debugger metteranno in evidenza tutto in una volta
  3. È privo di nomi descrittivi
  4. È ingombra di nomi non descrittivi

Se lo espandi con nomi che aggiungono un nuovo, buon significato semantico, ancora meglio! Un buon nome mi aiuta a capire.

 var temperature = H1(b1);
 var humidity = H2(b2);
 var precipitation = G1(temperature, humidity);
 var dewPoint = G2(c1);
 var forecast = F(precipitation, dewPoint);

Ora almeno questo racconta una storia. Risolve i problemi ed è chiaramente migliore di qualsiasi altra cosa offerta qui, ma richiede di trovare i nomi.

Se lo fai con nomi insignificanti come result_thise result_thatperché semplicemente non riesci a pensare a buoni nomi, preferirei davvero che ci risparmiassi il disordine di nomi insignificanti ed espanderlo usando qualche buon vecchio spazio bianco:

int a = 
    F(
        G1(
            H1(b1), 
            H2(b2)
        ), 
        G2(c1)
    )
;

È altrettanto leggibile, se non di più, di quello con i nomi dei risultati insignificanti (non che questi nomi di funzioni siano fantastici).

  1. È denso
  2. Alcuni debugger metteranno in evidenza tutto in una volta
  3. È privo di nomi descrittivi
  4. È ingombra di nomi non descrittivi

Quando non riesci a pensare a buoni nomi, va bene così.

Per qualche ragione i debugger adorano le nuove linee, quindi dovresti scoprire che il debug non è difficile:

inserisci qui la descrizione dell'immagine

Se ciò non bastasse, immagina di essere G2()stato chiamato in più di un posto e quindi è successo:

Exception in thread "main" java.lang.NullPointerException
    at composition.Example.G2(Example.java:34)
    at composition.Example.main(Example.java:18)

Penso che sia bello che dato che ogni G2()chiamata sarebbe sulla propria linea, questo stile ti porta direttamente alla chiamata offensiva in linea di massima.

Quindi, per favore, non usare i problemi 1 e 2 come scusa per attaccarci al problema 4. Usa dei bei nomi quando riesci a pensarci. Evita i nomi insignificanti quando non puoi.

Lightness Races nel commento di Orbit sottolinea correttamente che queste funzioni sono artificiali e hanno nomi morti poveri. Quindi, ecco un esempio di applicazione di questo stile ad un codice dal selvaggio:

var user = db.t_ST_User.Where(_user => string.Compare(domain,  
_user.domainName.Trim(), StringComparison.OrdinalIgnoreCase) == 0)
.Where(_user => string.Compare(samAccountName, _user.samAccountName.Trim(), 
StringComparison.OrdinalIgnoreCase) == 0).Where(_user => _user.deleted == false)
.FirstOrDefault();

Odio guardare quel flusso di rumore, anche quando non è necessario lo scambio di parole. Ecco come appare sotto questo stile:

var user = db
    .t_ST_User
    .Where(
        _user => string.Compare(
            domain, 
            _user.domainName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(
        _user => string.Compare(
            samAccountName, 
            _user.samAccountName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(_user => _user.deleted == false)
    .FirstOrDefault()
;

Come puoi vedere, ho scoperto che questo stile funziona bene con il codice funzionale che si sposta nello spazio orientato agli oggetti. Se riesci a trovare buoni nomi per farlo in stile intermedio, allora più potere per te. Fino ad allora lo sto usando. Ma in ogni caso, per favore, trova un modo per evitare nomi di risultati insignificanti. Mi fanno male agli occhi.


20
@Steve e non ti sto dicendo di non farlo. Sto chiedendo un nome significativo. Tutto a volte ho visto lo stile intermedio fatto senza pensarci. I nomi cattivi mi bruciano il cervello molto più del codice sparso per riga. Non permetto che considerazioni su larghezza o lunghezza mi motivino a rendere il mio codice denso o i miei nomi brevi. Lascio che mi motivino a scomporre di più. Se non succederanno dei bei nomi, considera questo lavoro in giro per evitare rumori insignificanti.
candied_orange,

6
Aggiungo al tuo post: ho una piccola regola empirica: se non riesci a nominarlo, potrebbe essere un segno che non è ben definito. Lo uso su entità, proprietà, variabili, moduli, menu, classi di supporto, metodi, ecc. In numerose situazioni questa minuscola regola ha rivelato un grave difetto nel design. Quindi, in un certo senso, una buona denominazione non solo contribuisce alla leggibilità e alla manutenibilità, ma aiuta anche a verificare il design. Naturalmente ci sono eccezioni ad ogni semplice regola.
Alireza,

4
La versione estesa sembra brutta. C'è troppo spazio bianco lì che riduce l'effetto di se stesso perché tutto ciò che è graduale, con ciò significa nulla.
Mateen Ulhaq,

5
@MateenUlhaq L'unico spazio extra in più ci sono un paio di nuove righe e un po 'di rientro, ed è tutto accuratamente posto ai confini significativi . Il tuo commento posiziona invece gli spazi bianchi ai confini non significativi. Ti suggerisco di dare uno sguardo leggermente più vicino e più aperto.
jpmc26,

3
A differenza di @MateenUlhaq, sono in questo recinto per lo spazio bianco in questo particolare esempio con tali nomi di funzioni, ma con nomi di funzioni reali (che sono più di due caratteri di lunghezza, giusto?) Potrebbe essere quello che farei.
Corse di leggerezza con Monica il

50

D'altra parte, maggiore è l'elaborazione che metti su una linea, maggiore è la logica che ottieni su una pagina, il che migliora la leggibilità.

Non sono assolutamente d'accordo con questo. Basta guardare i tuoi due esempi di codice per definirlo errato:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

si sente leggere. "Leggibilità" non significa densità di informazioni; significa "facile da leggere, comprendere e mantenere".

A volte, il codice è semplice ed ha senso usare una sola riga. Altre volte, farlo rende più difficile la lettura, senza ovvi benefici oltre a stipare di più su una riga.

Tuttavia, ti esorto anche a sostenere che "la diagnosi facile degli arresti anomali" significa che il codice è facile da mantenere. Il codice che non si arresta in modo anomalo è molto più semplice da mantenere. "Facile da mantenere" si ottiene principalmente tramite il codice è stato facile da leggere e comprendere, supportato da una buona serie di test automatizzati.

Quindi, se stai trasformando una singola espressione in una multilinea con molte variabili solo perché il tuo codice spesso si arresta in modo anomalo e hai bisogno di migliori informazioni di debug, allora smetti di farlo e rendi il codice più robusto. Dovresti preferire la scrittura di codice che non necessita di debug su codice facile da eseguire il debug.


37
Mentre sono d'accordo che F(G1(H1(b1), H2(b2)), G2(c1))è difficile da leggere, questo non ha nulla a che fare con l'essere stipato troppo denso. (Non sono sicuro di voler dire questo, ma potrebbe essere interpretato in questo modo.) L'annidamento di tre o quattro funzioni in una singola riga può essere perfettamente leggibile, in particolare se alcune delle funzioni sono semplici operatori di infix. Sono i nomi non descrittivi che sono il problema qui, ma quel problema è ancora peggio nella versione multilinea, dove vengono introdotti ancora più nomi non descrittivi. L'aggiunta di solo boilerplate non aiuta quasi mai la leggibilità.
lasciato il

23
@leftaroundabout: Per me, la difficoltà è che non è ovvio se G1accetta 3 parametri o solo 2 ed G2è un altro parametro F. Devo socchiudere gli occhi e contare le parentesi.
Matthieu M.,

4
@MatthieuM. questo può essere un problema, anche se se le funzioni sono ben note è spesso ovvio che accetta quanti argomenti. Nello specifico, come ho detto, per le funzioni infix è immediatamente chiaro che prendono due argomenti. (Inoltre, la sintassi delle tuple tra parentesi che la maggior parte delle lingue usa aggrava questo problema; in una lingua che preferisce Currying è automaticamente più chiaro:. F (G1 (H1 b1) (H2 b2)) (G2 c1))
leftaroundabout

5
Personalmente preferisco la forma più compatta, purché ci sia uno stile attorno come nel mio precedente commento, perché garantisce meno stato di cui tenere traccia mentalmente - result_h1non può essere riutilizzato se non esiste, e l'impianto idraulico tra le 4 variabili è ovvio.
Izkata,

8
Ho scoperto che il codice che è facile da eseguire il debug in genere è codice che non necessita di debug.
Rob K,

25

Il tuo primo esempio, il modulo a assegnazione singola, è illeggibile perché i nomi scelti sono assolutamente privi di significato. Potrebbe essere un artefatto nel cercare di non divulgare informazioni interne da parte tua, il vero codice potrebbe andare bene al riguardo, non possiamo dire. Comunque, è prolisso a causa della densità di informazioni estremamente bassa, che generalmente non si presta a una facile comprensione.

Il tuo secondo esempio è condensato ad un livello assurdo. Se le funzioni avessero nomi utili, ciò potrebbe andare bene e ben leggibile perché non ce n'è troppo , ma com'è è confuso nell'altra direzione.

Dopo aver introdotto nomi significativi, potresti vedere se una delle forme sembra naturale o se c'è un centro dorato per cui sparare.

Ora che hai un codice leggibile, la maggior parte dei bug sarà ovvia e gli altri avranno almeno più difficoltà a nasconderti.


17

Come sempre, quando si tratta di leggibilità, il fallimento è agli estremi . Puoi prendere qualsiasi buon consiglio di programmazione, trasformarlo in una regola religiosa e usarlo per produrre codice assolutamente illeggibile. (Se non mi credete a questo, dai un'occhiata a queste due IOCCC vincitori Borsányi e Goren e dare un'occhiata a come diverso usano le funzioni per rendere il codice completamente illeggibile. Suggerimento: Borsányi utilizza esattamente una funzione, Goren molto, molto di più ...)

Nel tuo caso, i due estremi sono 1) usando solo le espressioni a espressione singola e 2) unendo tutto in istruzioni grandi, concise e complesse. L'uno o l'altro approccio estremo rende il tuo codice illeggibile.

Il tuo compito, come programmatore, è quello di trovare un equilibrio . Per ogni affermazione che scrivi, è tuo compito rispondere alla domanda: "Questa affermazione è facile da comprendere e serve a rendere leggibile la mia funzione?"


Il punto è che non esiste una singola complessità misurabile che può decidere, cosa è buono da includere in una singola dichiarazione. Prendi ad esempio la linea:

double d = sqrt(square(x1 - x0) + square(y1 - y0));

Questa è un'affermazione piuttosto complessa, ma qualsiasi programmatore degno di nota dovrebbe essere in grado di cogliere immediatamente ciò che fa. È un modello abbastanza noto. Come tale, è molto più leggibile dell'equivalente

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

che spezza il modello ben noto in un numero apparentemente insignificante di semplici passaggi. Tuttavia, la dichiarazione dalla tua domanda

var a = F(G1(H1(b1), H2(b2)), G2(c1));

mi sembra eccessivamente complicato, anche se si tratta di un'operazione in meno del calcolo della distanza . Naturalmente, questa è una conseguenza diretta di me non sapere nulla F(), G1(), G2(), H1(), o H2(). Potrei decidere diversamente se ne sapessi di più. Ma questo è esattamente il problema: la complessità consigliabile di un'affermazione dipende fortemente dal contesto e dalle operazioni coinvolte. E tu, come programmatore, sei tu che devi dare un'occhiata a questo contesto e decidere cosa includere in una singola affermazione. Se ti interessa la leggibilità, non puoi scaricare questa responsabilità su una regola statica.


14

@Dominique, penso nell'analisi della tua domanda, stai commettendo l'errore che "leggibilità" e "manutenibilità" sono due cose separate.

È possibile avere un codice mantenibile ma illeggibile? Al contrario, se il codice è estremamente leggibile, perché dovrebbe diventare non raggiungibile a causa della sua leggibilità? Non ho mai sentito parlare di un programmatore che ha giocato questi fattori uno contro l'altro, dovendo scegliere l'uno o l'altro!

In termini di decidere se utilizzare le variabili intermedie per le chiamate di funzione nidificate, nel caso di 3 variabili fornite, le chiamate a 5 funzioni separate e alcune chiamate nidificate in profondità 3, tenderei ad utilizzare almeno alcune variabili intermedie per scomporla, come hai fatto.

Ma certamente non spingo fino a dire che le chiamate di funzione non debbano mai essere nidificate. È una questione di giudizio nelle circostanze.

Direi che i seguenti punti riguardano la sentenza:

  1. Se le funzioni chiamate rappresentano operazioni matematiche standard, sono più in grado di essere nidificate rispetto a funzioni che rappresentano qualche oscura logica di dominio i cui risultati sono imprevedibili e non possono necessariamente essere valutati mentalmente dal lettore.

  2. Una funzione con un singolo parametro è più in grado di partecipare a un nido (come funzione interna o esterna) rispetto a una funzione con più parametri. Miscelare funzioni di diverse arità a diversi livelli di nidificazione è incline a lasciare il codice simile all'orecchio di un maiale.

  3. Un insieme di funzioni che i programmatori sono abituati a vedere espresse in un modo particolare - forse perché rappresenta una tecnica o un'equazione matematica standard, che ha un'implementazione standard - può essere più difficile da leggere e verificare se è suddiviso in variabili intermedie.

  4. Un piccolo nido di funzioni chiama che esegue funzionalità semplici ed è già chiaro da leggere, e quindi è suddiviso eccessivamente e atomizzato, è in grado di essere più difficile da leggere di uno che non è stato affatto scomposto.


3
Da +1 a "È possibile avere un codice mantenibile ma illeggibile?". È stato anche il mio primo pensiero.
RonJohn,

4

Entrambi non sono ottimali. Considera i commenti.

// Calculating torque according to Newton/Dominique, 4th ed. pg 235
var a = F(G1(H1(b1), H2(b2)), G2(c1));

O funzioni specifiche piuttosto che generali:

var a = Torque_NewtonDominique(b1,b2,c1);

Quando si decide quali risultati precisare, tenere presente il costo (copia vs riferimento, valore l vs valore r), leggibilità e rischio, individualmente per ogni istruzione.

Ad esempio, non vi è alcun valore aggiunto dallo spostamento di conversioni di unità / tipo semplici sulle proprie righe, poiché sono facili da leggere ed estremamente improbabili:

var radians = ExtractAngle(c1.Normalize())
var a = Torque(b1.ToNewton(),b2.ToMeters(),radians);

Per quanto riguarda la tua preoccupazione di analizzare i dump dell'arresto anomalo, la convalida dell'input è di solito molto più importante: è molto probabile che l'effettivo arresto anomalo avvenga all'interno di queste funzioni piuttosto che sulla linea che le chiama, e anche in caso contrario, di solito non è necessario dire esattamente dove le cose sono esplose. È molto più importante sapere dove le cose hanno iniziato a crollare, piuttosto che sapere dove sono finalmente esplose, che è ciò che cattura la convalida dell'input.


Per quanto riguarda il costo del superamento di un argomento: esistono due regole di ottimizzazione. 1) Non farlo. 2) (solo per esperti) Non ancora .
RubberDuck

1

La leggibilità è la parte principale della manutenibilità. Dubito di me? Scegli un progetto di grandi dimensioni in una lingua che non conosci (probabilmente sia il linguaggio di programmazione che il linguaggio dei programmatori) e scopri come procedere per riformattarlo ...

Vorrei mettere la leggibilità tra 80 e 90 di manutenibilità. L'altro 10-20 percento è quanto sia possibile il refactoring.

Detto questo, si passa effettivamente in 2 variabili alla funzione finale (F). Queste 2 variabili vengono create utilizzando altre 3 variabili. Faresti meglio a passare b1, b2 e c1 in F, se F esiste già, quindi crea D che fa la composizione per F e restituisce il risultato. A quel punto si tratta solo di dare a D un buon nome e non importa quale stile usi.

Su un non correlato, dici che più logica nella pagina aiuta la leggibilità. Ciò è errato, la metrica non è la pagina, è il metodo e la logica MENO contiene un metodo, più è leggibile.

Leggibile significa che il programmatore può tenere la logica (input, output e algoritmo) nella propria testa. Più lo fa, MENO un programmatore può capirlo. Leggi sulla complessità ciclomatica.


1
Sono d'accordo con tutto ciò che dici sulla leggibilità. Ma io non sono d'accordo che di cracking un'operazione logica in metodi separati, necessariamente lo rende più leggibile rispetto fessurazione in linee separate (entrambe le tecniche che possono , se abusato, rendere semplice logica meno leggibile, e rendere l'intero programma più ingombra) - se dividi le cose troppo a fondo nei metodi, finisci per emulare le macro del linguaggio assembly e perdi di vista il modo in cui si integrano nel loro insieme. Inoltre, in questo metodo separato, dovresti ancora affrontare lo stesso problema: annidare le chiamate o spezzarle in variabili intermedie.
Steve,

@Steve: non ho detto di farlo sempre, ma se stai pensando di usare 5 righe per ottenere un singolo valore, ci sono buone probabilità che una funzione sia migliore. Per quanto riguarda le linee multiple vs la linea complessa: se si tratta di una funzione con un buon nome entrambi funzioneranno ugualmente bene.
jmoreno,

1

Indipendentemente dal fatto che tu sia in C # o C ++, fintanto che sei in una build di debug, una possibile soluzione è il wrapping delle funzioni

var a = F(G1(H1(b1), H2(b2)), G2(c1));

È possibile scrivere un'espressione on-line e ottenere ancora il punto in cui si trova il problema semplicemente osservando la traccia dello stack.

returnType F( params)
{
    returnType RealF( params);
}

Naturalmente, se si chiama la stessa funzione più volte nella stessa linea non è possibile sapere quale funzione, tuttavia è ancora possibile identificarla:

  • Guardando i parametri delle funzioni
  • Se i parametri sono identici e la funzione non ha effetti collaterali, allora due chiamate identiche diventano 2 chiamate identiche ecc.

Questo non è un proiettile d'argento, ma non è poi così male a metà strada.

Per non parlare del fatto che il wrapping di un gruppo di funzioni può essere ancora più vantaggioso per la leggibilità del codice:

type CallingGBecauseFTheorem( T b1, C b2)
{
     return G1( H1( b1), H2( b2));
}

var a = F( CallingGBecauseFTheorem( b1,b2), G2( c1));

1

A mio avviso, il codice di auto-documentazione è migliore sia per la manutenibilità che per la leggibilità, indipendentemente dalla lingua.

L'affermazione di cui sopra è densa, ma "auto-documentante":

double d = sqrt(square(x1 - x0) + square(y1 - y0));

Se suddiviso in più fasi (più facile da testare, sicuramente) perde tutto il contesto come indicato sopra:

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

E ovviamente usare nomi di variabili e funzioni che dichiarano chiaramente il loro scopo è inestimabile.

Anche i blocchi "if" possono essere buoni o cattivi nell'auto-documentazione. Questo è negativo perché non è possibile forzare facilmente le prime 2 condizioni per testare la terza ... tutte non sono correlate:

if (Bill is the boss) && (i == 3) && (the carnival is next weekend)

Questo ha più senso "collettivo" ed è più facile creare condizioni di prova:

if (iRowCount == 2) || (iRowCount == 50) || (iRowCount > 100)

E questa affermazione è solo una stringa casuale di caratteri, vista da una prospettiva auto-documentante:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Osservando la precedente affermazione, la manutenibilità è ancora una grande sfida se le funzioni H1 e H2 modificano entrambe le stesse "variabili dello stato del sistema" invece di essere unificate in una singola funzione "H", perché qualcuno alla fine altererà H1 senza nemmeno pensare che ci sia un Funzione H2 da guardare e potrebbe interrompere H2.

Credo che una buona progettazione del codice sia molto impegnativa perché non esistono regole rigide che possano essere rilevate e applicate sistematicamente.

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.