I metodi privati ​​con uno stile di riferimento singolo non sono corretti?


139

In genere utilizzo metodi privati ​​per incapsulare funzionalità che vengono riutilizzate in più punti della classe. Ma a volte ho un grande metodo pubblico che potrebbe essere suddiviso in passaggi più piccoli, ognuno nel suo metodo privato. Ciò renderebbe il metodo pubblico più breve, ma sono preoccupato che forzare chiunque legga il metodo a saltare a diversi metodi privati ​​danneggerà la leggibilità.

C'è un consenso su questo? È meglio avere lunghi metodi pubblici o suddividerli in pezzi più piccoli anche se ogni pezzo non è riutilizzabile?



7
Tutte le risposte sono basate sull'opinione. Gli autori originali pensano sempre che il loro codice dei ravioli sia più leggibile; i redattori successivi lo chiamano ravioli.
Frank Hileman,

5
Ti suggerisco di leggere Clean Code. Raccomanda questo stile e ha molti esempi. Ha anche una soluzione per il problema "saltare in giro".
Kat,

1
Penso che questo dipenda dal tuo team e dalle righe di codice contenute.
Speriamo utile

1
L'intero framework di STRUTS non è basato sui metodi Getter / Setter monouso, che sono tutti metodi monouso?
Zibbobz,

Risposte:


203

No, questo non è un cattivo stile. In effetti è uno stile molto buono.

Le funzioni private non devono esistere semplicemente a causa della riusabilità. Questa è certamente una buona ragione per crearle, ma ce n'è un'altra: la decomposizione.

Considera una funzione che fa troppo. È lungo un centinaio di righe e impossibile ragionare.

Se dividi questa funzione in pezzi più piccoli, "continua" a lavorare tanto come prima, ma in pezzi più piccoli. Chiama altre funzioni che dovrebbero avere nomi descrittivi. La funzione principale si legge quasi come un libro: fai A, poi fai B, poi fai C, ecc. Le funzioni che chiama possono essere chiamate solo in un posto, ma ora sono più piccole. Ogni funzione particolare è necessariamente in modalità sandbox rispetto alle altre funzioni: hanno ambiti diversi.

Quando si decompone un problema di grandi dimensioni in problemi più piccoli, anche se tali problemi (funzioni) più piccoli vengono utilizzati / risolti una sola volta, si ottengono numerosi vantaggi:

  • Leggibilità. Nessuno può leggere una funzione monolitica e capire cosa fa completamente. Puoi continuare a mentire a te stesso o dividerlo in blocchi di dimensioni ridotte che hanno un senso.

  • Località di riferimento. Ora diventa impossibile dichiarare e usare una variabile, quindi tenerla ferma e riutilizzarla dopo 100 righe. Queste funzioni hanno ambiti diversi.

  • Testing. Sebbene sia necessario testare solo i membri pubblici di una classe, può essere desiderabile testare anche determinati membri privati. Se esiste una sezione critica di una funzione lunga che potrebbe trarre vantaggio dal test, è impossibile testarla in modo indipendente senza estrarla in una funzione separata.

  • Modularità. Ora che hai funzioni private, potresti trovarne una o più che potrebbero essere estratte in una classe separata, sia che venga usata solo qui o che sia riutilizzabile. Al punto precedente, è probabile che anche questa classe separata sia più facile da testare, poiché avrà bisogno di un'interfaccia pubblica.

L'idea di dividere il codice grande in pezzi più piccoli che sono più facili da capire e testare è un punto chiave del libro Clean Code di zio Bob . Al momento della stesura di questa risposta, il libro ha nove anni, ma è rilevante oggi come lo era allora.


7
Non dimenticare di mantenere livelli coerenti di astrazione e buoni nomi che assicurano che sbirciare all'interno dei metodi non ti sorprenda. Non essere sorpreso se un intero oggetto si nasconde lì dentro.
candied_orange,

14
A volte i metodi di divisione possono essere buoni, ma anche mantenere le cose in linea può avere dei vantaggi. Se un controllo dei limiti potrebbe essere eseguito in modo ragionevole all'interno o all'esterno di un blocco di codice, ad esempio, mantenere quel blocco di codice in linea renderà più semplice vedere se il controllo dei limiti viene eseguito una volta, più di una volta o per niente . L'inserimento del blocco di codice nella propria subroutine renderebbe tali cose più difficili da accertare.
supercat

7
Il proiettile "leggibilità" potrebbe essere etichettato "Astrazione". Riguarda l'astrazione. I "pezzi di dimensioni ridotte" fanno una cosa , una cosa di basso livello che non ti interessa davvero dei dettagli chiacchieroni per quando leggi o passi attraverso il metodo pubblico. Astrattandolo su una funzione, puoi superarlo e concentrarti sul grande schema di cose / cosa sta facendo il metodo pubblico a un livello superiore.
Mathieu Guindon,

7
Il proiettile "Testing" è discutibile IMO. Se i tuoi membri privati ​​vogliono essere testati, probabilmente appartengono alla loro stessa classe, come membri pubblici. Concesso, il primo passo per estrarre una classe / interfaccia è estrarre un metodo.
Mathieu Guindon,

6
Suddividere una funzione esclusivamente per numeri di riga, blocchi di codice e ambito variabile senza alcuna preoccupazione per la funzionalità effettiva è un'idea terribile, e direi che la migliore alternativa a questa è lasciarla in una funzione. Dovresti dividere le funzioni in base alla funzionalità. Vedi la risposta di DaveGauer . Stai in qualche modo accennando a questo nella tua risposta (con menzioni di nomi e problemi), ma non posso fare a meno di pensare che questo aspetto abbia bisogno di molta più attenzione, data la sua importanza e pertinenza in relazione alla domanda.
Dukeling,

36

È probabilmente un'ottima idea!

Metto in discussione la suddivisione di lunghe sequenze lineari di azioni in funzioni separate puramente per ridurre la lunghezza media della funzione nella tua base di codice:

function step1(){
  // ...
  step2(zarb, foo, biz);
}

function step2(zarb, foo, biz){
  // ...
  step3(zarb, foo, biz, gleep);
}

function step3(zarb, foo, biz, gleep){
  // ...
}

Ora hai effettivamente aggiunto linee di origine e ridotto notevolmente la leggibilità totale. Soprattutto se stai passando molti parametri tra ciascuna funzione per tenere traccia dello stato. Yikes!

Tuttavia , se sei riuscito a estrarre una o più righe in una funzione pura che ha un unico scopo chiaro ( anche se chiamato solo una volta ), allora hai migliorato la leggibilità:

function foo(){
  f = getFrambulation();
  g = deglorbFramb(f);
  r = reglorbulate(g);
}

Questo probabilmente non sarà facile nelle situazioni del mondo reale, ma pezzi di pura funzionalità possono spesso essere presi in giro se ci pensi abbastanza a lungo.

Saprai che sei sulla strada giusta quando hai funzioni con bei nomi di verbi e quando la tua funzione genitore li chiama e il tutto praticamente si legge come un paragrafo di prosa.

Quindi quando torni settimane dopo per aggiungere più funzionalità e scopri che puoi effettivamente riutilizzare una di quelle funzioni, quindi oh, gioia estatica! Che meraviglia radiosa meravigliosa!


2
Ho sentito questa discussione per molti anni e non si svolge mai nel mondo reale. Sto parlando specificamente di una o due funzioni di linea, chiamate da un solo posto. Mentre l'autore originale pensa sempre che sia "più leggibile", i redattori successivi spesso lo chiamano ravioli. Questo dipende dalla profondità della chiamata, in generale.
Frank Hileman,

34
@FrankHileman Ad essere sinceri, non ho mai visto nessuno sviluppatore complimentarmi con il codice del suo antenato.
T. Sar,

4
@TSar lo sviluppatore precedente è la fonte di tutti i problemi!
Frank Hileman,

18
@TSar Non ho mai fatto i complimenti allo sviluppatore precedente quando ero lo sviluppatore precedente.
IllusiveBrian,

3
La cosa bella della decomposizione è che puoi quindi comporre foo = compose(reglorbulate, deglorbFramb, getFrambulation);
:;

19

La risposta è che dipende molto dalla situazione. @Snowman copre gli aspetti positivi dell'interruzione di una grande funzione pubblica, ma è importante ricordare che possono esserci anche effetti negativi, di cui sei giustamente preoccupato.

  • Molte funzioni private con effetti collaterali possono rendere il codice difficile da leggere e piuttosto fragile. Ciò è particolarmente vero quando queste funzioni private dipendono dagli effetti collaterali reciproci. Evitare di avere funzioni strettamente accoppiate.

  • Le astrazioni perdono . Mentre è bello fingere come viene eseguita un'operazione o come vengono archiviati i dati, non ci sono casi in cui lo fa, ed è importante identificarli.

  • Semantica e materia. Se non riesci a catturare chiaramente ciò che una funzione fa nel suo nome, puoi di nuovo ridurre la leggibilità e aumentare la fragilità della funzione pubblica. Ciò è particolarmente vero quando si inizia a creare funzioni private con un gran numero di parametri di input e output. E mentre dalla funzione pubblica, vedi quali funzioni private chiama, dalla funzione privata, non vedi come le funzioni pubbliche lo chiamano. Ciò può portare a una "correzione di bug" in una funzione privata che interrompe la funzione pubblica.

  • Il codice fortemente decomposto è sempre più chiaro per l'autore che per gli altri. Ciò non significa che non possa essere ancora chiaro agli altri, ma è facile dire che ha perfettamente senso che bar()deve essere chiamato prima foo()al momento della scrittura.

  • Riutilizzo pericoloso. La tua funzione pubblica probabilmente ha limitato l'input possibile per ciascuna funzione privata. Se questi presupposti di input non vengono acquisiti correttamente (documentare TUTTI i presupposti è difficile), qualcuno potrebbe riutilizzare impropriamente una delle tue funzioni private, introducendo bug nel codebase.

Decomporre le funzioni in base alla coesione interna e all'accoppiamento, non alla lunghezza assoluta.


6
Ignora la leggibilità, diamo un'occhiata all'aspetto della segnalazione dei bug: se l'utente segnala che si è verificato un bug MainMethod(abbastanza facile da ottenere, di solito sono inclusi i simboli e dovresti avere un file di dereference dei simboli) che è 2k righe (i numeri di riga non sono mai inclusi in segnalazioni di errori) ed è un NRE che può accadere a 1k di quelle righe, beh, sarò dannato se lo sto esaminando. Ma se mi dicono che è successo SomeSubMethodAed è 8 righe e l'eccezione era un NRE, allora probabilmente lo troverò e risolverò abbastanza facilmente.
410_Andato il

2

L'IMHO, il valore di estrarre blocchi di codice puramente come mezzo per spezzare la complessità, sarebbe spesso correlato alla differenza di complessità tra:

  1. Una descrizione completa e accurata del linguaggio umano di ciò che fa il codice, incluso il modo in cui gestisce i casi angolari e

  2. Il codice stesso.

Se il codice è molto più complicato della descrizione, la sostituzione del codice in linea con una chiamata di funzione può rendere più semplice la comprensione del codice circostante. D'altro canto, alcuni concetti possono essere espressi più facilmente in un linguaggio informatico che in un linguaggio umano. Considero w=x+y+z;, ad esempio, più leggibile di w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);.

Man mano che una grande funzione viene suddivisa, ci sarà sempre meno differenza tra la complessità delle sotto-funzioni rispetto alle loro descrizioni e il vantaggio di ulteriori suddivisioni diminuirà. Se le cose sono divise al punto che le descrizioni sarebbero più complicate del codice, ulteriori divisioni peggioreranno il codice.


2
Il nome della tua funzione è fuorviante. Non c'è nulla su w = x + y + z che indica qualsiasi tipo di controllo di overflow, mentre il nome del metodo da solo sembra implicare diversamente. Un nome migliore per la tua funzione sarebbe "addAll (x, y, z)", per esempio, o anche solo "Sum (x, y, z)".
T. Sar,

6
Dato che stavano parlando di metodi privati , questa domanda non si riferiva probabilmente a C. Comunque, non è questo il mio punto - potresti semplicemente chiamare la stessa funzione "somma" senza la necessità di affrontare il presupposto sulla firma del metodo. Secondo la tua logica, qualsiasi metodo ricorsivo dovrebbe essere chiamato "DoThisAssumingStackDoesntOverflow", ad esempio. Lo stesso vale per "FindSubstringAssumingStartingPointIsInsideBounds".
T. Sar,

1
In altre parole, i presupposti di base, qualunque essi siano (due numeri non traboccano, lo stack non trabocca, l'indice è stato passato correttamente) non dovrebbe essere presente sul nome del metodo. Queste cose dovrebbero essere documentate, ovviamente, se necessario, ma non sulla firma del metodo.
T. Sar,

1
@TSar: ero un po 'faceto con il nome, ma il punto chiave è che (passando alla media perché è un esempio migliore) un programmatore che vede "(x + y + z + 1) / 3" nel contesto sarà molto meglio posizionato per sapere se questo è un modo appropriato di calcolare la media dato il codice chiamante, rispetto a qualcuno che sta guardando la definizione o il sito di chiamata di int average3(int n1, int n2, int n3). Suddividere la funzione "media" significherebbe che qualcuno che voleva vedere se fosse adatto ai requisiti avrebbe dovuto cercare in due punti.
supercat

1
Se prendiamo ad esempio C #, avresti un solo metodo "Medio", che può essere fatto per accettare un numero qualsiasi di parametri. In effetti, in c #, funziona su qualsiasi raccolta di numeri - e sono sicuro che sia molto più facile da capire che affrontare la matematica grezza nel codice.
T. Sar,

2

È una sfida equilibrante.

Più fine è meglio

Il metodo privato fornisce effettivamente un nome per il codice contenuto e talvolta una firma significativa (a meno che metà dei suoi parametri non siano dati ad hoc completamente vaghi con interdipendenze non chiare e non documentate).

Dare nomi ai costrutti di codice è generalmente buono, purché i nomi suggeriscano un contratto significativo per il chiamante e il contratto del metodo privato corrisponda esattamente a ciò che suggerisce il nome.

Costringendosi a pensare a contratti significativi per parti più piccole del codice, lo sviluppatore iniziale può individuare alcuni bug ed evitarli indolore anche prima di qualsiasi test di sviluppo. Questo funziona solo se lo sviluppatore si sta impegnando per un nome conciso (dal suono semplice ma accurato) ed è disposto ad adattare i confini del metodo privato in modo che sia possibile anche un nome conciso.

Anche la manutenzione successiva è utile, poiché la denominazione aggiuntiva aiuta a rendere il codice auto-documentante .

  • Contratti su piccoli pezzi di codice
  • I metodi di livello superiore a volte si trasformano in una breve sequenza di chiamate, ognuna delle quali fa qualcosa di significativo e con un nome distinto, accompagnata da un sottile strato di gestione generale degli errori più esterni. Tale metodo può diventare una preziosa risorsa di formazione per chiunque debba rapidamente diventare un esperto sulla struttura generale del modulo.

Fino a quando non diventa troppo bello

È possibile esagerare dando nomi a piccoli blocchi di codice e finire con troppi metodi privati ​​troppo piccoli? Sicuro. Uno o più dei seguenti sintomi indicano che i metodi stanno diventando troppo piccoli per essere utili:

  • Troppi sovraccarichi che in realtà non rappresentano firme alternative per la stessa logica essenziale, ma piuttosto un singolo stack di chiamate fisse.
  • Sinonimi utilizzati solo per riferirsi ripetutamente allo stesso concetto ("nome divertente")
  • Molte decorazioni di denominazione superficiale come XxxInternalo DoXxx, specialmente se non c'è uno schema unificato nell'introdurle.
  • Nomi goffi quasi più lunghi dell'implementazione stessa, come LogDiskSpaceConsumptionUnlessNoUpdateNeeded

1

Contrariamente a quanto hanno affermato gli altri, direi che un lungo metodo pubblico è un odore di design che non si corregge decomponendosi in metodi privati.

Ma a volte ho un grande metodo pubblico che potrebbe essere suddiviso in passaggi più piccoli

In questo caso, direi che ogni passaggio dovrebbe essere il proprio cittadino di prima classe con una sola responsabilità ciascuno. In un paradigma orientato agli oggetti, suggerirei di creare un'interfaccia e un'implementazione per ogni passaggio in modo tale che ciascuno abbia una singola responsabilità facilmente identificabile e possa essere nominato in modo che la responsabilità sia chiara. Ciò consente di testare unitamente il (precedentemente) metodo pubblico di grandi dimensioni, nonché ogni singolo passaggio indipendentemente l'uno dall'altro. Dovrebbero anche essere tutti documentati.

Perché non scomporre in metodi privati? Ecco alcuni motivi:

  • Accoppiamento stretto e testabilità. Riducendo le dimensioni del tuo metodo pubblico, hai migliorato la sua leggibilità, ma tutto il codice è ancora strettamente accoppiato. È possibile testare in unità i singoli metodi privati ​​(utilizzando le funzionalità avanzate di un framework di test), ma non è possibile testare facilmente il metodo pubblico indipendentemente dai metodi privati. Ciò va contro i principi del test unitario.
  • Dimensione e complessità della classe. Hai ridotto la complessità di un metodo, ma hai aumentato la complessità della classe. Il metodo pubblico è più facile da leggere, ma ora la classe è più difficile da leggere perché ha più funzioni che ne definiscono il comportamento. La mia preferenza è per le piccole classi a responsabilità singola, quindi un metodo lungo è un segno che la classe sta facendo troppo.
  • Non può essere riutilizzato facilmente. È spesso il caso che quando un corpo di codice matura riutilizzabilità è utile. Se i tuoi passaggi sono in metodi privati, non possono essere riutilizzati altrove senza prima estrarli in qualche modo. Inoltre, può incoraggiare il copia-incolla quando è necessario un passaggio altrove.
  • La divisione in questo modo è probabilmente arbitraria. Direi che dividere un lungo metodo pubblico non tiene conto tanto del pensiero o del design quanto se dovessi suddividere le responsabilità in classi. Ogni classe deve essere giustificata con un nome, una documentazione e dei test appropriati, mentre un metodo privato non tiene in considerazione.
  • Nasconde il problema. Quindi hai deciso di dividere il tuo metodo pubblico in piccoli metodi privati. Ora non ci sono problemi! Puoi continuare ad aggiungere sempre più passaggi aggiungendo sempre più metodi privati! Al contrario, penso che questo sia un grosso problema. Stabilisce un modello per aggiungere complessità a una classe che sarà seguita da successive correzioni di bug e implementazioni di funzionalità. Ben presto i vostri metodi privati cresceranno e che dovranno essere divisi.

ma sono preoccupato che forzare chiunque legga il metodo a saltare a diversi metodi privati ​​danneggerà la leggibilità

Questa è una discussione che ho avuto di recente con uno dei miei colleghi. Sostiene che avere l'intero comportamento di un modulo nello stesso file / metodo migliora la leggibilità. Concordo sul fatto che il codice è più facile da seguire quando tutti insieme, ma il codice è meno facile da ragionare con l'aumentare della complessità. Man mano che un sistema cresce, diventa intrattabile ragionare sull'intero modulo nel suo insieme. Quando si decompone la logica complessa in più classi ognuna con una sola responsabilità, diventa molto più facile ragionare su ciascuna parte.


1
Dovrei essere in disaccordo. Un'astrazione troppo o prematura porta a un codice inutilmente complesso. Un metodo privato può essere il primo passo in un percorso che potrebbe aver bisogno di essere interfacciato e astratto, ma il tuo approccio qui suggerisce di vagare per luoghi che potrebbero non aver mai bisogno di essere visitati.
WillC

1
Capisco da dove vieni, ma penso che il codice abbia l'odore di quello che l'OP sta chiedendo è segno che è pronto per un design migliore. L'astrazione prematura dovrebbe progettare queste interfacce prima ancora di incorrere in quel problema.
Samuel,

Sono d'accordo con questo approccio. In effetti, quando segui TDD, è la progressione naturale. L'altra persona dice che è troppa astrazione prematura, ma trovo molto più facile deridere rapidamente una funzionalità di cui ho bisogno in una classe separata (dietro un'interfaccia), piuttosto che doverla effettivamente implementare in un metodo privato per far passare il test .
Eterno21,
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.