Sono metodi privati con un unico stile di riferimento non valido?

139

Generalmente utilizzo metodi privati per incapsulare funzionalità riutilizzate in più punti della classe. Ma a volte ho un grande metodo pubblico che può essere suddiviso in passi più piccoli, ciascuno nel proprio 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 disporre di metodi pubblici lunghi o suddividerli in parti più piccole anche se ogni pezzo non è riutilizzabile?

    
posta Jordak 05.06.2017 - 16:46
fonte

6 risposte

202

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

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

Considera una funzione che fa troppo. È lungo un centinaio di righe, ed è impossibile ragionare.

Se dividi questa funzione in parti più piccole, continua a "fare" più lavoro di prima, ma in parti più piccole. 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. Qualsiasi funzione particolare è necessariamente in modalità sandbox dalle altre funzioni: hanno ambiti diversi.

Quando decomponi un grosso problema in problemi più piccoli, anche se quei problemi più piccoli (funzioni) sono solo usati / risolti una volta, ottieni diversi vantaggi:

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

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

  • Test. Mentre è solo necessario testare i membri pubblici di una classe, potrebbe essere opportuno testare anche alcuni 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.

  • La modularità. Ora che hai funzioni private, puoi trovare una o più di esse che possono essere estratte in una classe separata, sia che sia usata solo qui sia che sia riutilizzabile. Al punto precedente, questa classe separata è probabilmente più facile da testare, poiché avrà bisogno di un'interfaccia pubblica.

L'idea di suddividere il grande codice in parti più piccole che sono più facili da comprendere e testare è un punto chiave di il libro di Uncle Bob's Clean Codice . Al momento di scrivere questa risposta il libro ha nove anni, ma è altrettanto attuale oggi come lo era allora.

    
risposta data 05.06.2017 - 17:04
fonte
36

Probabilmente è una grande idea!

Prendo il problema dividendo le lunghe sequenze di azioni lineari in funzioni separate puramente per ridurre la lunghezza media della funzione nella 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 aggiunto effettivamente le righe e della sorgente riducendo notevolmente la leggibilità totale. Soprattutto se stai passando molti parametri tra una funzione e l'altra per tenere traccia dello stato. Yikes!

Tuttavia , se sei riuscito a estrarre una o più linee in una funzione pura che serve un singolo, chiaro scopo ( anche se chiamato una sola volta ), quindi 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 pensate abbastanza a lungo.

Saprai di essere sulla strada giusta quando hai delle funzioni con nomi di verbo di qualità e quando la tua funzione genitrice li chiama e l'intera cosa si legge praticamente come un paragrafo di prosa.

Poi, quando ritorni settimane dopo per aggiungere più funzionalità e scopri che puoi riutilizzare una di quelle funzioni, allora oh, gioia estatica! Che meraviglia meravigliosa!

    
risposta data 05.06.2017 - 18:41
fonte
19

La risposta è altamente dipende dalla situazione. @ Snownow copre gli aspetti positivi della rottura di una grande funzione pubblica, ma è importante ricordare che possono esserci anche degli effetti negativi, come giustamente ti preoccupi.

  • Molte funzioni private con effetti collaterali possono rendere il codice difficile da leggere e molto fragile. Ciò è particolarmente vero quando queste funzioni private dipendono dagli effetti collaterali l'uno dell'altro. Evita di avere funzioni strettamente accoppiate.

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

  • Semantica e contesto. 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 numero elevato di parametri di input e output. E mentre dalla funzione pubblica, si vedono le funzioni private che chiama, dalla funzione privata, non si vede ciò che le funzioni pubbliche chiamano. Questo può portare a una "correzione dei bug" in una funzione privata che interrompe la funzione pubblica.

  • Il codice strongmente decomposto è sempre più chiaro per l'autore che per gli altri. Ciò non significa che non sia ancora chiaro agli altri, ma è facile dire che è perfettamente logico che bar() debba essere chiamato prima di foo() al momento della scrittura.

  • Riutilizzo pericoloso. La tua funzione pubblica probabilmente limita l'input possibile a ciascuna funzione privata. Se queste ipotesi di input non sono correttamente acquisite (documentando che TUTTE le ipotesi sono difficili), qualcuno potrebbe riutilizzare in modo improprio una delle tue funzioni private, introducendo bug nella base di codice.

Decompone le funzioni in base alla loro coesione e accoppiamento interni, non alla lunghezza assoluta.

    
risposta data 05.06.2017 - 19:05
fonte
2

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

  1. Una descrizione completa e accurata in lingua umana di ciò che fa il codice, compreso 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ù comprensibile il codice circostante. D'altra parte, alcuni concetti possono essere espressi più facilmente in un linguaggio informatico che in quello umano. Per esempio, considererei w=x+y+z; più leggibile di w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z); .

Quando una grande funzione viene suddivisa, ci sarà sempre meno differenza tra la complessità delle sotto-funzioni e le 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 suddivisioni potrebbero peggiorare il codice.

    
risposta data 05.06.2017 - 18:45
fonte
2

È una sfida equilibrata.

Finer è migliore

Il metodo privato fornisce effettivamente un nome per il codice contenuto, e talvolta una firma significativa (a meno che metà dei suoi parametri siano completamente ad-hoc tramp data con interdipendenze non chiare e prive di documenti) .

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

Forzandosi a pensare a contratti significativi per porzioni più piccole del codice, lo sviluppatore iniziale può individuare alcuni bug ed evitarli senza dolore, anche prima di qualsiasi test di sviluppo. Funziona solo se lo sviluppatore cerca la denominazione concisa (suono semplice ma accurato) ed è disposto ad adattare i limiti del metodo privato in modo tale che persino la denominazione concisa sia possibile.

Anche la manutenzione successiva è utile, perché la denominazione aggiuntiva contribuisce a rendere il codice autodocumentato .

  • Contratti su piccoli pezzi di codice
  • I metodi di livello più elevato a volte si trasformano in una breve sequenza di chiamate, ognuna delle quali fa qualcosa di significativo e chiaramente chiamato - accompagnato dal sottile strato di generale, gestione degli errori più esterna. Tale metodo può diventare una preziosa risorsa di formazione per chiunque debba diventare rapidamente un esperto sulla struttura complessiva del modulo.

Fino a quando non diventa troppo fine

È possibile esagerare nel dare nomi a piccoli pezzi di codice e finire con troppi, troppo piccoli metodi privati? 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 unico stack di chiamate fisse.
  • Sinonimi usato solo per riferirsi ripetutamente allo stesso concetto ("intrattenere i nomi")
  • Un sacco di decorazioni di nomi superficiali come XxxInternal o DoXxx , specialmente se non c'è uno schema unificato nell'introdurle.
  • Nomi maldestri quasi più lunghi dell'implementazione stessa, come LogDiskSpaceConsumptionUnlessNoUpdateNeeded
risposta data 08.06.2017 - 09:26
fonte
1

Contrariamente a quanto hanno detto gli altri, direi che un lungo metodo pubblico è un odore di design che è non rettificato mediante la scomposizione in metodi privati.

But sometimes I have a large public method that could be broken up into smaller steps

Se questo è il caso, direi che ogni passo 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 fase in modo che ognuno abbia una singola responsabilità facilmente identificabile e possa essere nominato in modo che la responsabilità sia chiara. Ciò consente di testare in unità il (precedente) metodo pubblico di grandi dimensioni, nonché ogni singolo passo indipendentemente l'uno dall'altro. Dovrebbero essere tutti documentati.

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

  • Accoppiamento stretto e testabilità. Riducendo la dimensione del tuo metodo pubblico, hai migliorato la sua leggibilità, ma tutto il codice è ancora strettamente accoppiato. È possibile testare l'unità dei singoli metodi privati (utilizzando le funzionalità avanzate di un framework di test), ma non è possibile testare facilmente il metodo pubblico indipendentemente dai metodi privati. Questo va contro i principi del test unitario.
  • Dimensioni 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 la classe è ora più difficile da leggere perché ha più funzioni che ne definiscono il comportamento. La mia preferenza è per le piccole classi di responsabilità singola, quindi un metodo lungo è un segno che la classe sta facendo troppo.
  • Non può essere riutilizzato facilmente. È spesso il caso che, come corpo di codice, la riusabilità sia utile. Se i tuoi passaggi sono in metodi privati, non possono essere riutilizzati altrove senza prima averli estratti in qualche modo. Inoltre può incoraggiare la copia-incolla quando è necessario un passaggio altrove.
  • È probabile che suddividere in questo modo sia arbitrario. Direi che la suddivisione di un lungo metodo pubblico non prende in considerazione il pensiero o il pensiero del design come se si dovessero suddividere le responsabilità in classi. Ogni classe deve essere giustificata con un nome, una documentazione e dei test corretti, mentre un metodo privato non prende in considerazione l'importanza.
  • Nasconde il problema. Quindi hai deciso di dividere il tuo metodo pubblico in piccoli metodi privati. Ora non c'è nessun problema! 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à. Presto i tuoi metodi privati cresceranno e loro dovranno essere divisi.

but I'm worried that forcing anyone who reads the method to jump around to different private methods will damage readability

Questa è una discussione che ho avuto recentemente con uno dei miei colleghi. Sostiene che avere l'intero comportamento di un modulo nello stesso file / metodo migliora la leggibilità. Sono d'accordo che il codice è più facile da seguire quando tutti insieme, ma il codice è meno facile da motivo per man mano che la complessità aumenta. Man mano che un sistema cresce, diventa intrattabile ragionare sull'intero modulo nel suo insieme. Quando decomponi la logica complessa in più classi ciascuna con una singola responsabilità, allora diventa molto più facile ragionare su ciascuna parte.

    
risposta data 07.06.2017 - 05:40
fonte

Leggi altre domande sui tag