Il mio capo mi chiede di smettere di scrivere piccole funzioni e fare tutto nello stesso ciclo


209

Ho letto un libro chiamato Clean Code di Robert C. Martin. In questo libro ho visto molti metodi per ripulire il codice come scrivere piccole funzioni, scegliere con cura i nomi, ecc. Sembra di gran lunga il libro più interessante sul codice pulito che ho letto. Tuttavia, oggi al mio capo non è piaciuto il modo in cui ho scritto il codice dopo aver letto questo libro.

I suoi argomenti erano

  • Scrivere piccole funzioni è una seccatura perché ti costringe a spostarti in ogni piccola funzione per vedere cosa sta facendo il codice.
  • Metti tutto in un grande loop principale anche se il loop principale è più di 300 righe, è più veloce da leggere.
  • Scrivi solo piccole funzioni se devi duplicare il codice.
  • Non scrivere una funzione con il nome del commento, inserisci la tua complessa riga di codice (3-4 righe) con un commento sopra; allo stesso modo è possibile modificare direttamente il codice in errore

Questo è contro tutto ciò che ho letto. Come si scrive di solito il codice? Un grande loop principale, non piccole funzioni?

La lingua che uso è principalmente Javascript. Ho davvero difficoltà a leggere ora da quando ho eliminato tutte le mie piccole funzioni con un nome chiaro e ho messo tutto in un grande ciclo. Tuttavia, al mio capo piace così.

Un esempio è stato:

// The way I would write it
if (isApplicationInProduction(headers)) {
  phoneNumber = headers.resourceId;
} else {
  phoneNumber = DEV_PHONE_NUMBER;
}

function isApplicationInProduction(headers) {
   return _.has(headers, 'resourceId');
}

// The way he would write it
// Take the right resourceId if application is in production
phoneNumber = headers.resourceId ? headers.resourceId : DEV_PHONE_NUMBER;

Nel libro che ho letto, ad esempio, i commenti sono considerati come incapacità di scrivere codice pulito perché sono obsoleti se si scrivono piccole funzioni e spesso portano a commenti non aggiornati (si modifica il codice e non il commento). Tuttavia, ciò che faccio è eliminare il commento e scrivere una funzione con il nome del commento.

Bene, vorrei un consiglio, in che modo / pratica è meglio scrivere codice pulito?



4
phoneNumber = headers.resourceId?: DEV_PHONE_NUMBER;
Giosuè,

10
Convalida, che vuoi lavorare sul posto, dove la direzione ti ha detto COME fare il tuo lavoro, invece di COSA devi risolvere.
Konstantin Petrukhnov,

8
@rjmunro A meno che non ti piaccia davvero il tuo lavoro, tieni presente che ci sono meno sviluppatori che lavori. Quindi, per citare Martin Fowler: "Se non puoi cambiare la tua organizzazione, cambia la tua organizzazione!" e i Boss che mi dicono come programmare è qualcosa che consiglio di voler cambiare.
Niels van Reijmersdal,

10
NON MAI avere una isApplicationInProduction()funzione! È necessario disporre di test e i test sono inutili se il codice si comporta in modo diverso da quando è in produzione. È come avere intenzionalmente codice non testato / scoperto in produzione: non ha senso.
Ronan Paixão,

Risposte:


215

Prendendo prima gli esempi di codice. Tu favorisci:

if (isApplicationInProduction(headers)) {
  phoneNumber = headers.resourceId;
} else {
  phoneNumber = DEV_PHONE_NUMBER;
}

function isApplicationInProduction(headers) {
   return _.has(headers, 'resourceId');
}

E il tuo capo lo scriverebbe come:

// Take the right resourceId if application is in production
phoneNumber = headers.resourceId ? headers.resourceId : DEV_PHONE_NUMBER;

A mio avviso, entrambi hanno problemi. Mentre leggevo il tuo codice, il mio pensiero immediato fu "puoi sostituirlo ifcon un'espressione ternaria". Poi ho letto il codice del tuo capo e ho pensato "perché ha sostituito la tua funzione con un commento?".

Suggerirei che il codice ottimale sia tra i due:

phoneNumber = isApplicationInProduction(headers) ? headers.resourceId : DEV_PHONE_NUMBER;

function isApplicationInProduction(headers) {
   return _.has(headers, 'resourceId');
}

Questo ti dà il meglio di entrambi i mondi: un'espressione di test semplificata e il commento viene sostituito con un codice verificabile.

Per quanto riguarda le opinioni del tuo capo sulla progettazione del codice, però:

Scrivere piccole funzioni è una seccatura perché ti costringe a spostarti in ogni piccola funzione per vedere cosa sta facendo il codice.

Se la funzione è ben denominata, non è così. isApplicationInProductionè evidente e non dovrebbe essere necessario esaminare il codice per vedere cosa fa. In realtà è vero il contrario: esaminare il codice rivela meno l'intenzione rispetto al nome della funzione (motivo per cui il tuo capo deve ricorrere ai commenti).

Metti tutto in un grande loop principale anche se il loop principale è più di 300 righe, è più veloce da leggere

Potrebbe essere più veloce scansionare, ma per "leggere" veramente il codice, devi essere in grado di eseguirlo efficacemente nella tua testa. È facile con piccole funzioni ed è molto, molto difficile con metodi lunghi 100 linee.

Scrivi solo piccole funzioni se devi duplicare il codice

Non sono d'accordo. Come mostra il tuo esempio di codice, le piccole funzioni ben definite migliorano la leggibilità del codice e dovrebbero essere utilizzate ogni volta che non sei interessato al "come", solo al "cosa" di una funzionalità.

Non scrivere una funzione con il nome del commento, inserisci la tua complessa riga di codice (3-4 righe) con un commento sopra. In questo modo è possibile modificare direttamente il codice in errore

Non riesco davvero a capire il ragionamento alla base di questo, supponendo che sia davvero serio. È il genere di cose che mi aspetto di vedere scritto in parodia dall'account twitter di The Expert Beginner . I commenti hanno un difetto fondamentale: non sono compilati / interpretati e quindi non possono essere testati in unità. Il codice viene modificato e il commento viene lasciato solo e si finisce per non sapere quale sia giusto.

Scrivere un codice di auto-documentazione è difficile e talvolta sono necessari documenti supplementari (anche sotto forma di commenti). Ma il punto di vista di "Zio Bob" secondo cui i commenti sono un errore di programmazione vale troppo spesso.

Chiedi al tuo capo di leggere il libro Clean Code e cerca di resistere a rendere il tuo codice meno leggibile solo per soddisfarlo. Alla fine, però, se non riesci a convincerlo a cambiare, devi metterti in fila o trovare un nuovo capo in grado di programmare meglio.


26
Le fucntions di piccole dimensioni sono
facilmente

13
Quoth @ ExpertBeginner1 : "Sono stanco di vedere tonnellate di piccoli metodi dappertutto nel nostro codice, quindi da qui in poi c'è un minimo di 15 LOC su tutti i metodi."
Greg Bacon

34
"I commenti hanno un difetto fondamentale: non sono compilati / interpretati e quindi non possono essere testati in unità" Giocando qui l'avvocato del diavolo, questo vale anche se si sostituiscono i "commenti" con "nomi di funzioni".
Mattecapu,

11
@mattecapu, prenderò il tuo patrocinio e te lo raddoppierò. Qualsiasi vecchio sviluppatore di immondizia può confondersi in un commento cercando di descrivere cosa fa un pezzo di codice. Descrivere in modo succinto quel pezzo di codice con un buon nome di funzione richiede un abile comunicatore. I migliori sviluppatori sono abili comunicatori poiché la scrittura del codice riguarda principalmente la comunicazione con altri sviluppatori e il compilatore come preoccupazione secondaria. Ergo, un buon sviluppatore utilizzerà funzioni ben denominate e nessun commento; i poveri sviluppatori nascondono le loro scarse competenze dietro scuse per l'utilizzo dei commenti.
David Arno,

4
@DavidArno Tutte le funzioni hanno pre e postcondizioni, la domanda è se le documenti o meno. Se la mia funzione accetta un parametro, che è una distanza in piedi misurati, è necessario fornire in piedi, non chilometri. Questa è una condizione preliminare.
Jørgen Fogh,

223

Ci sono altri problemi

Nessuno dei due codici è buono, perché entrambi sostanzialmente gonfiano il codice con un caso di test di debug . Cosa succede se si desidera testare più cose per qualsiasi motivo?

phoneNumber = DEV_PHONE_NUMBER_WHICH_CAUSED_PROBLEMS_FOR_CUSTOMERS;

o

phoneNumber = DEV_PHONE_NUMBER_FROM_OTHER_COUNTRY;

Vuoi aggiungere ancora più rami?

Il problema significativo è che praticamente duplicate parte del codice e quindi non state testando il codice reale. Si scrive il codice di debug per testare il codice di debug, ma non il codice di produzione. È come creare parzialmente una base di codice parallela.

Stai discutendo con il tuo capo su come scrivere in modo più intelligente codice errato. Invece, dovresti risolvere il problema intrinseco del codice stesso.

Iniezione di dipendenza

Ecco come dovrebbe apparire il tuo codice:

phoneNumber = headers.resourceId;

Non ci sono ramificazioni qui, perché la logica qui non ha ramificazioni. Il programma dovrebbe estrarre il numero di telefono dall'intestazione. Periodo.

Se vuoi avere DEV_PHONE_NUMBER_FROM_OTHER_COUNTRYcome risultato, dovresti metterlo headers.resourceId. Un modo per farlo è semplicemente iniettare un headersoggetto diverso per i casi di test (scusate se questo non è un codice corretto, le mie abilità JavaScript sono un po 'arrugginite):

function foo(headers){
    phoneNumber = headers.resourceId;
}

// Creating the test case
foo({resourceId: DEV_PHONE_NUMBER_FROM_OTHER_COUNTRY});

Supponendo che faccia headersparte di una risposta che ricevi da un server: idealmente hai un intero server di test che offre headersvari tipi a scopo di test. In questo modo testate il codice di produzione reale così com'è e non un codice parzialmente duplicato che potrebbe funzionare o meno come il codice di produzione.


11
Ho preso in considerazione di affrontare proprio questo argomento nella mia risposta, ma ho ritenuto che fosse già abbastanza lungo. Quindi fai +1 per te :)
David Arno,

5
@DavidArno Stavo per aggiungerlo come commento alla tua risposta, perché la domanda era ancora bloccata quando l'ho letta per la prima volta, ho scoperto con mia sorpresa che era di nuovo aperta e quindi l'ho aggiunto come risposta. Forse si dovrebbe aggiungere che ci sono dozzine di framework / strumenti per eseguire test automatici. Soprattutto in JS sembra che ce ne sia uno nuovo che esce ogni giorno. Potrebbe essere difficile venderlo al capo però.
null,

56
@DavidArno Forse avresti dovuto dividere la tua risposta in risposte più piccole. ;)
Krillgar,

2
@ user949300 Usare un OR bit per bit non sarebbe saggio;)
curiousdannii

1
@curiousdannii Sì, ho notato che è troppo tardi per modificarlo ...
user949300

59

Non esiste una risposta "giusta" o "sbagliata" a questo. Tuttavia, offrirò la mia opinione sulla base di 36 anni di esperienza professionale nella progettazione e nello sviluppo di sistemi software ...

  1. Non esiste un "codice auto-documentante". Perché? Perché questa affermazione è completamente soggettiva.
  2. I commenti non sono mai errori. Ciò che è un errore è il codice che non può essere compreso affatto senza commenti.
  3. 300 righe di codice ininterrotte in un blocco di codice sono un incubo per la manutenzione e altamente soggette a errori. Tale blocco è fortemente indicativo di cattiva progettazione e pianificazione.

Parlando direttamente all'esempio che hai fornito ... Inserire isApplicationInProduction()la propria routine è la cosa intelligente da fare. Oggi quel test è semplicemente un controllo delle "intestazioni" e può essere gestito da un ?:operatore ternario ( ). Domani il test potrebbe essere molto più complesso. Inoltre, "headers.resourceId" non ha una relazione chiara con "nello stato di produzione dell'applicazione"; Direi che un test per tale status deve essere disaccoppiato dai dati sottostanti; una subroutine lo farà e un ternario no. Inoltre, un utile commento potrebbe essere il motivo per cui resourceId è un test per "in produzione".

Fai attenzione a non esagerare con "piccole funzioni ben definite". Una routine dovrebbe incapsulare un'idea più del "solo codice". Appoggio il suggerimento phoneNumber = getPhoneNumber(headers)e l'aggiunta di Amon che getPhoneNumber()dovrebbe fare il test dello "stato di produzione"isApplicationInProduction()


25
C'è una cosa come buoni commenti che non sono un fallimento. Tuttavia, i commenti che sono quasi alla lettera del codice che presumibilmente spiegano o sono solo blocchi di commenti vuoti che precedono un metodo / classe / ecc. sono sicuramente un fallimento.
jpmc26,

3
È possibile avere un codice più piccolo e più facile da leggere rispetto a qualsiasi descrizione in lingua inglese di ciò che fa e dei casi angolari che fa e non gestisce. Inoltre, se una funzione viene estratta nel proprio metodo, qualcuno che legge la funzione non saprà quali casi angolari sono o non sono gestiti dai suoi chiamanti e, a meno che il nome della funzione non sia molto prolisso, qualcuno che esamina i chiamanti potrebbe non sapere quale angolo i casi sono gestiti dalla funzione.
supercat

7
I commenti non sono mai intrinsecamente fallimenti. I commenti possono essere errori e lo sono quando sono inaccurati. Il codice errato può essere rilevato a più livelli, incluso un comportamento errato nella modalità scatola nera. I commenti errati possono essere rilevati solo dalla comprensione umana in modalità white box, attraverso il riconoscimento del fatto che due modelli sono descritti e che uno di essi è errato.
Timbo,

7
@ Timbo Intendi ", almeno uno di questi non è corretto." ;)
jpmc26,

5
@immibis Se non riesci a capire cosa fa il codice senza commenti, probabilmente il codice non è abbastanza chiaro. Lo scopo principale dei commenti è chiarire perché il codice sta facendo ciò che sta facendo. È il programmatore che spiega il suo design ai futuri manutentori. Il codice non può mai fornire quel tipo di spiegazione, quindi i commenti colmano quelle lacune nella comprensione.
Graham,

47

"Le entità non devono essere moltiplicate oltre la necessità".

- Rasoio di Occam

Il codice deve essere il più semplice possibile. Ai bug piace nascondersi tra la complessità, perché sono difficili da individuare lì. Quindi cosa rende semplice il codice?

Le piccole unità (file, funzioni, classi) sono una buona idea . Le piccole unità sono facili da capire perché ci sono meno cose che devi capire contemporaneamente. Gli esseri umani normali possono destreggiarsi tra ~ 7 concetti alla volta. Ma la dimensione non è solo misurata in righe di codice . Posso scrivere il minor codice possibile "giocando a golf" il codice (scegliendo nomi di variabili brevi, prendendo scorciatoie "intelligenti", frantumando più codice possibile su una sola riga), ma il risultato finale non è semplice. Cercare di capire questo codice è più simile al reverse engineering che alla lettura.

Un modo per abbreviare una funzione è estrarre varie funzioni di supporto. Questa può essere una buona idea quando estrae un pezzo di complessità autonomo . In isolamento, quel pezzo di complessità è molto più semplice da gestire (e testare!) Rispetto a quando incorporato in un problema non correlato.

Ma ogni chiamata di funzione ha un sovraccarico cognitivo : non devo solo capire il codice all'interno del mio attuale pezzo di codice, ma devo anche capire come interagisce con il codice esterno . Penso che sia giusto dire che la funzione che hai estratto introduce una maggiore complessità nella funzione di quella che estrae . Questo è ciò che il tuo capo intendeva per " piccole funzioni [sono] una seccatura perché ti costringe a spostarti in ogni piccola funzione per vedere cosa sta facendo il codice.

A volte, le lunghe funzioni noiose possono essere incredibilmente semplici da capire, anche quando sono lunghe centinaia di righe. Ciò tende a verificarsi nell'inizializzazione e nel codice di configurazione, ad esempio quando si crea una GUI manualmente senza un editor di trascinamento della selezione. Non esiste un pezzo di complessità autonomo che potresti ragionevolmente estrarre. Ma se la formattazione è leggibile e ci sono alcuni commenti, non è davvero difficile seguire ciò che sta accadendo.

Esistono molte altre metriche di complessità: il numero di variabili in un ambito deve essere il più piccolo possibile. Ciò non significa che dovremmo evitare le variabili. Significa che dovremmo limitare ogni variabile al più piccolo ambito possibile dove è necessario. Le variabili diventano anche più semplici se non cambiamo mai il valore in esse contenuto.

Una metrica molto importante è la complessità ciclomatica (complessità di McCabe). Misura il numero di percorsi indipendenti attraverso un pezzo di codice. Questo numero cresce esponenzialmente con ogni condizione. Ogni condizionale o ciclo raddoppia il numero di percorsi. Ci sono prove che suggeriscono che un punteggio di oltre 10 punti sia troppo complesso. Ciò significa che una funzione molto lunga che ha forse un punteggio di 5 è forse migliore di una funzione molto breve e densa con un punteggio di 25. Possiamo ridurre la complessità estraendo il flusso di controllo in funzioni separate.

Il tuo condizionale è un esempio di un pezzo di complessità che potrebbe essere estratto interamente:

function bigFatFunction(...) {
  ...
  phoneNumber = getPhoneNumber(headers);
  ...
}

...

function getPhoneNumber(headers) {
  return headers.resourceId ? headers.resourceId : DEV_PHONE_NUMBER;
}

Questo è ancora al limite dell'essere utile. Non sono sicuro che ciò diminuisca sostanzialmente la complessità perché questo condizionale non è molto condizionale . In produzione, prenderà sempre la stessa strada.


La complessità non può mai scomparire. Può essere mischiato solo in giro. Molte piccole cose sono più semplici di alcune grandi cose? Dipende molto dalle circostanze. Di solito, c'è una combinazione che sembra giusta. Trovare quel compromesso tra diversi fattori di complessità richiede intuizione ed esperienza e un po 'di fortuna.

Saper scrivere funzioni molto piccole e funzioni molto semplici è un'abilità utile, perché non puoi fare una scelta senza conoscere le alternative. Seguire ciecamente le regole o le migliori pratiche senza pensare a come si applicano alla situazione attuale porta nel migliore dei casi a risultati medi, nella peggiore delle ipotesi di programmazione cargo-cult .

Ecco dove non sono d'accordo con il tuo capo. I suoi argomenti non sono invalidi, ma neanche il libro di Clean Code è sbagliato. Probabilmente è meglio seguire le linee guida del tuo capo, ma il fatto stesso che stai pensando a questi problemi, cercando di trovare un modo migliore, è molto promettente. Man mano che acquisisci esperienza, troverai più facile trovare un buon factoring per il tuo codice.

(Nota: questa risposta si basa in parte su pensieri tratti dal post del blog Reasonable Code su The Whiteboard di Jimmy Hoffa , che fornisce una visione di alto livello su ciò che rende semplice il codice.)


Sono generale, mi è piaciuta la tua risposta. Tuttavia, metto in discussione la misura della complessità ciclomatica di mcabes. Da quello che ne ho visto, non presenta una vera misura di complessità.
Robert Baron,

27

Lo stile di programmazione di Robert Martin è polarizzante. Troverai molti programmatori, anche esperti, che trovano molte scuse perché dividere "così tanto" è troppo e perché mantenere le funzioni un po 'più grandi è "il modo migliore". Tuttavia, la maggior parte di questi "argomenti" sono spesso espressione di riluttanza a cambiare vecchie abitudini e ad apprendere qualcosa di nuovo.

Non ascoltarli!

Ogni volta che puoi salvare un commento refactoring di un pezzo di codice in una funzione separata con un nome espressivo, fallo - molto probabilmente migliorerà il tuo codice. Non sei andato così lontano come Bob Martin nel suo libro di codice pulito, ma la stragrande maggioranza del codice che ho visto in passato che ha causato problemi di manutenzione conteneva funzioni troppo grandi, non troppo piccole. Quindi, provare a scrivere funzioni più piccole con nomi che si descrivono da soli è ciò che dovresti provare.

Gli strumenti di refactoring automatico rendono facile, semplice e sicuro estrarre metodi. E per favore, non prendere sul serio le persone che raccomandano di scrivere funzioni con> 300 righe - tali persone sicuramente non sono qualificate per dirti come dovresti programmare.


53
"Non ascoltarli!" : dato che al suo capo viene chiesto all'OP di smettere di dividere il codice, l'OP dovrebbe probabilmente evitare il vostro consiglio. Anche se il capo non è disposto a cambiare le sue vecchie abitudini. Inoltre, come evidenziato dalle risposte precedenti, sia il codice dell'OP che il codice del suo capo sono scritti male e tu (intenzionalmente o no) non lo dici nella tua risposta.
Arseni Mourzenko,

10
@ArseniMourzenko: non ognuno di noi deve allacciarsi davanti al suo capo. Spero che l'OP sia abbastanza grande da sapere quando deve fare la cosa giusta o quando deve fare ciò che dice il suo capo. E sì, non stavo entrando intenzionalmente nei dettagli dell'esempio, ci sono già abbastanza altre risposte che discutono di quei dettagli.
Doc Brown,

8
@DocBrown Concordato. 300 righe sono discutibili per un'intera classe. Una funzione di 300 linee è oscena.
JimmyJames,

30
Ho visto molte classi lunghe più di 300 righe che sono classi perfettamente buone. Java è così prolisso che quasi non puoi fare nulla di significativo in una classe senza quel codice. Quindi "il numero di righe di codice in una classe", di per sé, non è una metrica significativa, più di quanto considereremmo SLOC una metrica significativa per la produttività del programmatore.
Robert Harvey,

9
Inoltre, ho visto il saggio consiglio dello zio Bob frainteso e maltrattato così tanto che ho i miei dubbi sul fatto che sia utile a chiunque, tranne che ai programmatori esperti .
Robert Harvey,

23

Nel tuo caso: vuoi un numero di telefono. O è ovvio come otterresti un numero di telefono, quindi scrivi il codice ovvio. Oppure non è ovvio come otterresti un numero di telefono, quindi scrivi un metodo per farlo.

Nel tuo caso, non è ovvio come ottenere il numero di telefono, quindi scrivi un metodo per farlo. L'implementazione non è ovvia, ma è per questo che la stai inserendo in un metodo separato, quindi devi affrontarla una sola volta. Un commento sarebbe utile perché l'implementazione non è ovvia.

Il metodo "isApplicationInProduction" è abbastanza inutile. Chiamarlo dal metodo getPhonenumber non rende l'implementazione più ovvia e rende solo più difficile capire cosa sta succedendo.

Non scrivere piccole funzioni. Scrivi funzioni che hanno uno scopo ben definito e soddisfano quello scopo ben definito.

PS. Non mi piace affatto l'implementazione. Presuppone che l'assenza del numero di telefono significhi che è una versione di sviluppo. Quindi, se il numero di telefono è assente in produzione, non solo non lo gestisci, ma sostituisci un numero di telefono casuale. Immagina di avere 10.000 clienti e 17 non hanno un numero di telefono e che hai problemi di produzione. Se si è in produzione o sviluppo dovrebbe essere verificato direttamente, non derivato da qualcos'altro.


1
"Non scrivere piccole funzioni. Scrivi funzioni che hanno uno scopo ben definito e soddisfano quello scopo ben definito." questo è il criterio corretto per la divisione del codice. se una funzione non troppi (come più disparate) funzioni , poi dividerlo. Il principio di responsabilità unica è il principio guida.
robert bristow-johnson,

16

Anche ignorando il fatto che nessuna delle due implementazioni è così buona, noterei che questa è essenzialmente una questione di gusti almeno a livello di astrazione delle funzioni banali monouso.

Il numero di righe non è una metrica utile nella maggior parte dei casi.

300 (o addirittura 3000) righe di codice puramente sequenziale assolutamente banale (Setup, o qualcosa del genere) raramente sono un problema (ma potrebbero essere generate automaticamente meglio o come una tabella di dati o qualcosa del genere), 100 righe di loop nidificati con molti complicati condizioni di uscita e matematica che potresti trovare nell'eliminazione gaussiana o nell'inversione della matrice o cose del genere potrebbero essere troppe da seguire facilmente.

Per me, non scriverei una funzione monouso a meno che la quantità di codice richiesta per dichiarare la cosa fosse molto più piccola della quantità di codice che forma l'implementazione (A meno che non avessi motivo come dire che volevo essere in grado di fare facilmente l'iniezione di errori). Un singolo condizionale raramente si adatta a questo disegno di legge.

Ora vengo da un piccolo mondo embedded centrale, in cui dobbiamo anche considerare cose come la profondità dello stack e le spese generali di call / return (che ancora una volta discute contro il tipo di minuscole funzioni che sembrano essere sostenute qui), e questo potrebbe influenzare il mio design decisioni, ma se vedessi quella funzione originale in una revisione del codice, otterrei una fiamma usenet vecchio stile in risposta.

Il gusto è che il design è difficile da insegnare e arriva davvero solo con l'esperienza, non sono sicuro che possa essere ridotto a regole sulle lunghezze delle funzioni, e persino la complessità ciclomatica ha i suoi limiti come metrica (a volte le cose sono solo complicate, comunque le affronti).
Questo non vuol dire che il codice pulito non discute alcune cose buone, lo fa, e queste cose dovrebbero essere pensate, ma anche le abitudini locali e ciò che fa la base di codice esistente dovrebbero essere ponderati.

Questo particolare esempio mi sembra essere molto dettagliato, sarei più preoccupato per le cose di livello molto più alto, poiché ciò conta molto di più per la capacità di comprendere e eseguire il debug di un sistema facilmente.


1
Sono pienamente d'accordo - ci vorrebbe un one-liner molto complesso per considerare l'idea di inserirlo in una funzione ... Certamente non avvolgerei una linea di valore ternaria / predefinita. Ho spostato una riga, ma di solito è quello che shell script in cui sono dieci pipe per analizzare qualcosa e il codice è incomprensibile senza eseguirlo.
TemporalWolf

15

Non mettere tutto in un unico grande ciclo, ma non farlo neanche troppo spesso:

function isApplicationInProduction(headers) {
   return _.has(headers, 'resourceId');
}

Il problema con il grande loop è che è davvero difficile vedere la sua struttura generale quando si estende su molti schermi. Quindi cerca di eliminare grossi pezzi, idealmente pezzi che hanno una sola responsabilità e sono riutilizzabili.

Il problema con la minuscola funzione sopra, è che mentre l'atomicità e la modularità sono generalmente buone, ciò può essere portato troppo lontano. Se non hai intenzione di riutilizzare la funzione sopra menzionata, ciò toglie la leggibilità e la manutenibilità del codice. Per eseguire il drill-down nei dettagli, è necessario passare alla funzione anziché essere in grado di leggere i dettagli in linea e la chiamata della funzione occupa pochissimo spazio rispetto al dettaglio stesso.

Chiaramente c'è un equilibrio tra metodi che fanno troppo e metodi che fanno troppo poco . Non vorrei mai interrompere una minuscola funzione come sopra a meno che non fosse chiamata da più di un posto, e anche allora ci penserei due volte, perché la funzione non è poi così sostanziale in termini di introduzione di nuova logica e come a malapena garantisce di avere la propria esistenza.


2
Capisco che una sola riga booleana è facile da leggere ma che da sola spiega davvero "Cosa" sta succedendo. Scrivo ancora funzioni che racchiudono semplici espressioni ternarie perché il nome della funzione aiuta a spiegare il motivo "Perché" sto eseguendo questo controllo delle condizioni. Ciò è particolarmente utile quando qualcuno di nuovo (o te stesso in 6 mesi) deve capire la logica aziendale.
AJ X.

14

Sembra che quello che vuoi davvero sia questo:

phoneNumber = headers.resourceId || DEV_PHONE_NUMBER

Questo dovrebbe essere autoesplicativo per chiunque lo legga: impostare phoneNumbersu resourceIdse è disponibile o predefinito su DEV_PHONE_NUMBERse non lo è.

Se vuoi veramente impostare quella variabile solo in produzione, dovresti avere qualche altro metodo di utilità più canonico a livello di app (che non richiede parametri) per determinare da dove stai eseguendo. Leggere le intestazioni per tali informazioni non ha senso.


È autoesplicativo cosa fa (con un po 'di indovinare quale lingua stai usando), ma non è affatto ovvio cosa stia succedendo. Apparentemente lo sviluppatore presuppone che il numero di telefono sia archiviato sotto "ID risorsa" nella versione di produzione e che risorsa ID non sia presente nella versione di sviluppo e che voglia utilizzare DEV_PHONE_NUMBER nella versione di sviluppo. Ciò significa che il numero di telefono è archiviato in uno strano titolo, e significa che le cose andranno molto male se manca un numero di telefono nella versione di produzione
gnasher729,

14

Consentitemi di essere schietto: mi sembra che il vostro ambiente (linguaggio / framework / design di classe ecc.) Non sia adatto al codice "pulito". Stai mescolando tutti i possibili tipi di cose in poche righe di codice che non dovrebbero davvero essere vicine tra loro. Quale attività ha una singola funzione con la consapevolezza che ciò resourceId==undefsignifica che non si è in produzione, che si sta utilizzando un numero di telefono predefinito in sistemi non di produzione, che l'ID risorsa viene salvato in alcune "intestazioni" e così via? Presumo che headerssiano intestazioni HTTP, quindi lasci persino la decisione su quale ambiente ti trovi all'utente finale?

Factoring singoli pezzi di questo in funzioni non ti aiuterà molto con quel problema di fondo.

Alcune parole chiave da cercare:

  • disaccoppiamento
  • coesione
  • iniezione di dipendenza

È possibile ottenere ciò che si desidera (in altri contesti) con zero righe di codice, spostando le responsabilità del codice e utilizzando quadri moderni (che possono esistere o meno per l'ambiente / il linguaggio di programmazione).

Dalla tua descrizione ("300 righe di codice in una funzione 'principale'"), anche la parola "funzione" (invece del metodo) mi fa supporre che non abbia senso ciò che stai cercando di ottenere. In quell'ambiente di programmazione della vecchia scuola (cioè, programmazione imperativa di base con poca struttura, certamente senza classi significative, senza schemi di classi come MVC o simili), non ha davvero senso fare qualcosa . Non uscirai mai dalla coppa senza cambiamenti fondamentali. Almeno il tuo capo sembra permetterti di creare funzioni per evitare la duplicazione del codice, questo è un buon primo passo!

Conosco sia il tipo di codice che il tipo di programmatore che stai descrivendo abbastanza bene. Francamente, se fosse un collega, il mio consiglio sarebbe diverso. Ma poiché è il tuo capo, è inutile che tu combatti per questo. Non solo il tuo capo può sovrascriverti, ma le tue aggiunte di codice porteranno a un codice peggiore se solo tu "fai la tua cosa" parzialmente, e il tuo capo (e probabilmente altre persone) continueranno come prima. Potrebbe essere meglio per il risultato finale se ti adegui al loro stile di programmazione (solo mentre lavori su questa particolare base di codice, ovviamente) e cerchi, in questo contesto, di trarne il meglio.


1
Sono d'accordo al 100% sul fatto che ci siano componenti implicite che dovrebbero essere separate, ma senza sapere di più sul linguaggio / quadro, è difficile sapere se un approccio OO ha senso. Il disaccoppiamento e il Principio della singola responsabilità sono importanti in qualsiasi lingua, dal puro funzionale (ad esempio Haskell) al puro imperativo (ad esempio C.) Il mio primo passo - se il capo lo permettesse - sarebbe quello di convertire la funzione principale in una funzione dispatcher ( come una struttura o un sommario) che legge in uno stile dichiarativo (descrivendo le politiche, non gli algoritmi) e trasferisce il lavoro ad altre funzioni.
David Leppik,

JavaScript è un prototipo, con funzioni di prima classe. È intrinsecamente OO, ma non nel senso classico, quindi i tuoi presupposti potrebbero non essere corretti. Indovina ore di guardare i video di Crockford su YouTube ...
Kevin_Kinsey

13

"Clean" è un obiettivo nella scrittura del codice. Non è l'unico obiettivo. Un altro obiettivo degno è la colocalità . Detto in modo informale, la colocalità significa che le persone che cercano di capire il tuo codice non devono saltare dappertutto per vedere cosa stai facendo. L'uso di una funzione ben definita anziché di un'espressione ternaria può sembrare una buona cosa, ma a seconda di quante funzioni hai e dove si trovano, questa pratica può trasformarsi in un fastidio. Non posso dirti se hai oltrepassato questo limite, se non per dire che se le persone si lamentano, dovresti ascoltare, in particolare se quelle persone hanno voce in capitolo sul tuo stato lavorativo.


2
"... tranne per dire che se le persone si lamentano, dovresti ascoltare, in particolare se quelle persone hanno voce in capitolo sul tuo status professionale". IMO questo è davvero un cattivo consiglio. A meno che tu non sia uno sviluppatore seriamente povero che deve apprezzare qualsiasi lavoro tu possa ottenere, quindi applica sempre il principio "se non puoi cambiare lavoro, cambia lavoro". Non sentirti mai legato a un'azienda; hanno bisogno di te più di quanto tu abbia bisogno di loro, quindi vai in un posto migliore se non offrono quello che vuoi.
David Arno,

4
Mi sono spostato un po 'durante la mia carriera. Non credo di aver mai avuto un lavoro in cui ho visto il mio capo al 100% su come programmare. Siamo esseri umani con il nostro background e le nostre filosofie. Quindi personalmente non avrei lasciato un lavoro solo perché c'erano alcuni standard di codifica che non mi piacevano. (Le convenzioni sulla denominazione che si piegano con le dita sembrano particolarmente contrarie al modo in cui codificherei se lasciato ai miei dispositivi.) Ma hai ragione, un programmatore decente non dovrebbe preoccuparsi troppo di rimanere semplicemente impiegato .
user1172763,

6

L'uso di piccole funzioni è, in generale, una buona pratica. Ma idealmente credo che l'introduzione di una funzione dovrebbe separare grossi blocchi logici o ridurre la dimensione complessiva del codice ESSICCANDO. L'esempio che hai fornito allunga il codice e richiede più tempo per la lettura da parte di uno sviluppatore, mentre l'alternativa breve non spiega che il "resourceId"valore sia presente solo in produzione. Qualcosa di semplice è facile da dimenticare e da confondere quando si cerca di lavorare con esso, in particolare se si è ancora nuovi alla base di codice.

Non dirò che dovresti assolutamente usare un ternario, alcune persone con cui ho lavorato preferiscono un po 'più a lungo if () {...} else {...}, è principalmente una scelta personale. Tendo a preferire un "approccio a una riga per una cosa", ma sostanzialmente mi atterrerei a qualunque cosa la base di codice usi normalmente.

Quando si utilizza ternario se il controllo logico rende la linea troppo lunga o complicata, prendere in considerazione la creazione di una / e variabile / e ben denominata per contenere il valore.

// NOTE "resourceId" not present in dev build, use test data
let isProduction = 'resourceId' in headers;
let phoneNumber = isProduction ? headers.resourceId : DEV_PHONE_NUMBER;

Vorrei anche dire che se la base di codice si estende verso 300 funzioni di linea, allora ha bisogno di una suddivisione. Ma consiglierei l'uso di tratti leggermente più ampi.


5

Nell'esempio di codice che hai fornito, il tuo capo È CORRETTO. Una sola linea chiara è migliore in quel caso.

In generale, spezzare la logica complessa in pezzi più piccoli è meglio per leggibilità, manutenzione del codice e possibilità che le sottoclassi abbiano un comportamento diverso (anche se solo leggermente).

Non ignorare gli svantaggi: sovraccarico di funzioni, oscuramento (la funzione non fa ciò che implicano commenti e nome della funzione), logica spaghetti complessa, il potenziale per funzioni morte (un tempo sono state create per uno scopo che non viene più chiamato).


1
"overhead di funzione": dipende dal compilatore. "oscuramento": OP non ha indicato se è l'unico o il modo migliore per controllare quella proprietà; neanche tu puoi saperlo con certezza. "complessa logica degli spaghetti": dove? "il potenziale per funzioni morte": quel tipo di analisi del codice morto è un frutto basso e le toolchain di sviluppo che mancano sono immature.
Rhymoid,

Le risposte erano più incentrate sui vantaggi, volevo solo sottolineare anche gli svantaggi. Chiamare una funzione come sum (a, b) sarà sempre più costoso di "a + b" (a meno che la funzione non sia delineata dal compilatore). Gli altri svantaggi dimostrano che l'eccessiva complessità può portare a una propria serie di problemi. Il codice errato è un codice errato, e solo perché è suddiviso in byte più piccoli (o mantenuto in un ciclo di 300 righe) non significa che sia più facile da ingoiare.
Phil M,

2

Mi vengono in mente almeno due argomenti a favore di lunghe funzioni:

  • Significa che hai un sacco di contesto attorno ad ogni riga. Un modo per formalizzare questo: disegna il grafico del flusso di controllo del tuo codice. A un vertice (~ = linea) tra l'ingresso e l'uscita della funzione, si conoscono tutti i bordi in entrata. Più lunga è la funzione, più ci sono tali vertici.

  • Molte piccole funzioni indicano che esiste un grafico delle chiamate più ampio e complesso. Scegli una linea casuale in una funzione casuale e rispondi alla domanda "in quale contesto (i) viene eseguita questa linea?" Questo diventa più difficile quanto più grande e complesso è il grafico della chiamata, perché devi guardare più vertici in quel grafico.

Ci sono anche argomenti contro le lunghe funzioni: mi viene in mente la testabilità dell'unità. Usa t̶h whene̶ ̶f̶o̶r̶c̶e̶ la tua esperienza quando scegli tra l'una e l'altra.

Nota: non sto dicendo che il tuo capo ha ragione, solo che la sua prospettiva potrebbe non essere completamente priva di valore.


Penso che il mio punto di vista sia che il buon parametro di ottimizzazione non sia la lunghezza della funzione. Penso che un desiderato più utile da pensare in termini di sia il seguente: a parità di tutti gli altri, è preferibile poter leggere dal codice una descrizione di alto livello sia della logica aziendale sia dell'implementazione. (I dettagli di implementazione di basso livello possono sempre essere letti se riesci a trovare il bit di codice rilevante.)


Commentando la risposta di David Arno :

Scrivere piccole funzioni è una seccatura perché ti costringe a spostarti in ogni piccola funzione per vedere cosa sta facendo il codice.

Se la funzione è ben denominata, non è così. isApplicationInProduction è evidente e non dovrebbe essere necessario esaminare il codice per vedere cosa fa. In realtà è vero il contrario: esaminare il codice rivela meno l'intenzione rispetto al nome della funzione (motivo per cui il tuo capo deve ricorrere ai commenti).

Il nome rende evidente quale sia il valore di ritorno means , ma non dice nulla circa gli effetti di esecuzione del codice (= ciò che il codice fa ). Nomi (solo) trasmettono informazioni relative intenti , codice trasmette informazioni circa il comportamento (da quali parti l'intento possono talvolta essere dedotti).

A volte ne vuoi uno, a volte l'altro, quindi questa osservazione non crea una regola di decisione universalmente valida unilaterale.

Metti tutto in un grande loop principale anche se il loop principale è più di 300 righe, è più veloce da leggere

Potrebbe essere più veloce scansionare, ma per "leggere" veramente il codice, devi essere in grado di eseguirlo efficacemente nella tua testa. È facile con piccole funzioni ed è molto, molto difficile con metodi lunghi 100 linee.

Sono d'accordo che devi eseguirlo nella tua testa. Se hai 500 linee di funzionalità in una grande funzione rispetto a molte piccole funzioni, non mi è chiaro perché questo diventa più facile.

Supponiamo che il caso estremo di 500 righe di codice a linea retta che abbia un forte effetto collaterale e che si desideri sapere se l'effetto A si verifica prima o dopo l'effetto B. Nel caso della grande funzione, utilizzare Pagina su / giù per individuare due linee e poi confrontare numeri di riga. Nel caso di molte piccole funzioni, devi ricordare dove si verificano gli effetti nell'albero delle chiamate e, se ti sei dimenticato, devi dedicare un tempo non banale alla riscoperta della struttura di questo albero.

Quando si attraversa l'albero delle chiamate delle funzioni di supporto, si deve anche affrontare la sfida di determinare quando si è passati dalla logica aziendale ai dettagli di implementazione. Sostengo senza prove * che più semplice è il grafico della chiamata, più facile è fare questa distinzione.

(*) Almeno ne sono onesto ;-)

Ancora una volta, penso che entrambi gli approcci abbiano punti di forza e di debolezza.

Scrivi solo piccole funzioni se devi duplicare il codice

Non sono d'accordo. Come mostra il tuo esempio di codice, piccole funzioni ben definite migliorano la leggibilità del codice e dovrebbero essere utilizzate ogni volta che [ad esempio] non sei interessato al "come", solo al "cosa" di una funzionalità.

Se sei interessato al "come" o al "cosa" è una funzione dello scopo per cui stai leggendo il codice (ad es. Ottenere un'idea generale o rintracciare un bug). Lo scopo per cui stai leggendo il codice non è disponibile durante la scrittura del programma e molto probabilmente leggerai il codice per scopi diversi; decisioni diverse ottimizzeranno per scopi diversi.

Detto questo, questa è la parte del punto di vista del capo con cui probabilmente non sono più d'accordo.

Non scrivere una funzione con il nome del commento, inserisci la tua complessa riga di codice (3-4 righe) con un commento sopra. In questo modo è possibile modificare direttamente il codice in errore

Non riesco davvero a capire il ragionamento alla base di questo, supponendo che sia davvero serio. [...] I commenti hanno un difetto fondamentale: non sono compilati / interpretati e quindi non possono essere testati in unità. Il codice viene modificato e il commento viene lasciato solo e si finisce per non sapere quale sia giusto.

I compilatori confrontano i nomi solo per uguaglianza, non ti danno mai un errore fuorviante. Inoltre, poiché diversi siti di chiamata possono invocare una determinata funzione in base al nome, a volte è più arduo e soggetto a errori cambiare un nome. I commenti non hanno questo problema. Tuttavia, questo è in qualche modo speculativo; per risolverlo davvero, probabilmente avremmo bisogno di dati sul fatto se i programmatori hanno maggiori probabilità di aggiornare commenti fuorvianti rispetto a nomi fuorvianti, e io non ce l'ho.


-1

A mio avviso, il codice corretto per la funzionalità richiesta è:

phoneNumber = headers.resourceId || DEV_PHONE_NUMBER;

O se vuoi dividerlo in una funzione, probabilmente qualcosa del tipo:

phoneNumber = getPhoneNumber(headers);

function getPhoneNumber(headers) {
  return headers.resourceId || DEV_PHONE_NUMBER
}

Ma penso che tu abbia un problema più fondamentale con il concetto di "in produzione". Il problema con la tua funzione isApplicationInProductionè che sembra strano che questo sia l'unico posto nel sistema in cui è importante essere in "produzione" e che puoi sempre fare affidamento sulla presenza o l'assenza dell'intestazione resourceId per dirti. Dovrebbe esserci un isApplicationInProductionmetodo generale o un getEnvironmentmetodo che controlli direttamente l'ambiente. Il codice dovrebbe apparire come:

function isApplicationInProduction() {
  process.env.NODE_ENV === 'production';
}

Quindi puoi ottenere il numero di telefono con:

phoneNumber = isApplicationInProduction() ? headers.resourceId : DEV_PHONE_NUMBER;

-2

Solo un commento su due dei punti elenco

  • Scrivere piccole funzioni è una seccatura perché ti costringe a spostarti in ogni piccola funzione per vedere cosa sta facendo il codice.
  • Metti tutto in un grande loop principale anche se il loop principale è più di 300 righe, è più veloce da leggere.

Molti editor (ad esempio IntelliJ) ti permetteranno di passare a una funzione / classe semplicemente premendo Ctrl-clic sull'uso. Inoltre, spesso non è necessario conoscere i dettagli di implementazione di una funzione per leggere il codice, rendendo così più veloce la lettura del codice.

Ti consiglio di dirlo al tuo capo; gli piacerà il tuo patrocinio e lo vedrà come una guida. Sii educato.

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.