Come si giustifica la scrittura di più codice seguendo le pratiche di codice pulito?


106

Nota del moderatore
Questa domanda ha già ricevuto diciassette risposte . Prima di pubblicare una nuova risposta, leggi le risposte esistenti e assicurati che il tuo punto di vista non sia già adeguatamente coperto.

Ho seguito alcune delle pratiche raccomandate nel libro "Clean Code" di Robert Martin, in particolare quelle che si applicano al tipo di software con cui lavoro e quelle che hanno senso per me (non lo seguo come dogma) .

Un effetto collaterale che ho notato, tuttavia, è che il codice "pulito" che scrivo è più codice che se non seguissi alcune pratiche. Le pratiche specifiche che portano a questo sono:

  • Condizionali incapsulanti

Quindi invece di

if(contact.email != null && contact.emails.contains('@')

Potrei scrivere un piccolo metodo come questo

private Boolean isEmailValid(String email){...}
  • Sostituzione di un commento incorporato con un altro metodo privato, in modo che il nome del metodo si descriva piuttosto che avere un commento incorporato sopra di esso
  • Una classe dovrebbe avere solo un motivo per cambiare

E pochi altri Il punto è che quello che potrebbe essere un metodo di 30 righe, finisce per essere una classe, a causa dei minuscoli metodi che sostituiscono i commenti e incapsulano i condizionali, ecc. Quando ti rendi conto di avere così tanti metodi, allora "ha senso" mettere tutte le funzionalità in una classe, quando in realtà avrebbe dovuto essere un metodo.

Sono consapevole che qualsiasi pratica portata all'estremo può essere dannosa.

La domanda concreta a cui cerco una risposta è:

È un sottoprodotto accettabile della scrittura di codice pulito? In tal caso, quali sono alcuni argomenti che posso usare per giustificare il fatto che sono stati scritti più LOC?

L'organizzazione non si preoccupa in modo specifico di più LOC, ma più LOC può portare a classi molto grandi (che, di nuovo, potrebbero essere sostituite con un metodo lungo senza un mucchio di funzioni di supporto una volta utilizzate per motivi di leggibilità).

Quando vedi una classe abbastanza grande, ti dà l'impressione che la classe sia abbastanza occupata e che la sua responsabilità sia stata conclusa. Potresti, quindi, finire per creare più classi per ottenere altre funzionalità. Il risultato è quindi un sacco di classi, tutte facendo "una cosa" con l'aiuto di molti piccoli metodi di supporto.

QUESTA è la preoccupazione specifica ... quelle classi potrebbero essere una singola classe che raggiunge ancora "una cosa", senza l'aiuto di molti piccoli metodi. Potrebbe essere una singola classe con forse 3 o 4 metodi e alcuni commenti.


98
Se la tua organizzazione utilizza solo LOC come metrica per le tue basi di codice, giustificando il codice pulito non c'è speranza di iniziare.
Kilian Foth,

24
Se la manutenibilità è il tuo obiettivo, LOC non è la migliore metrica da giudicare: è una di queste, ma c'è molto di più da considerare che non limitarla.
Zibbobz,

29
Non è una risposta, ma un punto da sottolineare: esiste un'intera sottounità nella scrittura di codice con il minor numero di righe / simboli possibile. codegolf.stackexchange.com Si può sostenere che la maggior parte delle risposte non sono così leggibili come potrebbero essere.
Antitheos il

14
Scopri le ragioni dietro ogni migliore pratica non solo le regole stesse. Seguire le regole senza ragioni è il culto del carico. Ogni singola regola ha una sua ragione.
Gherman,

9
A parte questo, e usando il tuo esempio, a volte spingere le cose verso i metodi ti farà pensare: "Forse c'è una funzione di libreria che può fare questo". Ad esempio, per convalidare un indirizzo e-mail, è possibile creare un indirizzo System.Net.Mail.Mail che lo convaliderà per voi. Puoi quindi (si spera) fidarti dell'autore di quella biblioteca per farlo bene. Questo significa che la tua base di codice avrà più astrazioni e diminuirà di dimensioni.
Gregory Currie,

Risposte:


130

... siamo un team molto piccolo che supporta una base di codice relativamente grande e non documentata (che abbiamo ereditato), quindi alcuni sviluppatori / gestori vedono valore nello scrivere meno codice per fare le cose in modo da avere meno codice da mantenere

Queste persone hanno identificato correttamente qualcosa: vogliono che il codice sia più facile da mantenere. Laddove si sono sbagliati, tuttavia, si presuppone che quanto meno codice sia presente, tanto più facile da mantenere.

Affinché il codice sia facile da mantenere, deve essere facile da modificare. Di gran lunga il modo più semplice per ottenere un codice facile da cambiare è quello di avere una serie completa di test automatici che falliranno se la tua modifica è in avaria. I test sono in codice, quindi scrivere quei test aumenterà la tua base di codice. E questa è una buona cosa.

In secondo luogo, per capire cosa deve essere modificato, il codice deve essere sia facile da leggere sia da ragionare. È molto improbabile che il codice sia molto conciso, ridotto nelle dimensioni solo per mantenere il conto alla rovescia della linea. C'è ovviamente un compromesso da raggiungere poiché il codice più lungo richiederà più tempo per la lettura. Ma se è più veloce da capire, allora ne vale la pena. Se non offre quel vantaggio, allora la verbosità smette di essere un vantaggio. Ma se un codice più lungo migliora la leggibilità, anche questa è una buona cosa.


27
"Di gran lunga il modo più semplice per ottenere un codice facile da cambiare è quello di avere una serie completa di test automatici che falliranno se la tua modifica è in avaria". Questo semplicemente non è vero. I test richiedono un lavoro aggiuntivo per ogni cambiamento comportamentale perché anche i test devono essere modificati , questo è in base alla progettazione e molti sostengono che rendere il cambiamento più sicuro, ma rende anche i cambiamenti più difficili.
Jack Aidley il

63
Certo, ma il tempo perso nel mantenimento di questi test è ridotto dal tempo in cui avresti perso la diagnosi e la correzione di bug che i test impediscono.
MetaFight il

29
@JackAidley, dover cambiare i test insieme al codice potrebbe dare l'apparenza di più lavoro, ma solo se si ignorano i bug difficili da trovare che si introducono nel codice non testato e che spesso non saranno trovati fino a dopo la spedizione . Quest'ultimo offre semplicemente l'illusione di meno lavoro.
David Arno,

31
@JackAidley, non sono completamente d'accordo con te. I test semplificano la modifica del codice. Concedo però che quel codice mal progettato che sia troppo strettamente accoppiato e quindi strettamente legato ai test può essere difficile da cambiare, ma un codice ben strutturato e ben testato è semplice da cambiare nella mia esperienza.
David Arno,

22
@JackAidley Puoi eseguire il refactoring molto senza cambiare API o interfaccia. Significa che puoi impazzire mentre modifichi il codice senza dover cambiare una singola riga in unità o test funzionali. Cioè, se i test non verificano un'implementazione specifica.
Eric Duminil,

155

Sì, è un sottoprodotto accettabile e la giustificazione è che ora è strutturato in modo tale da non dover leggere la maggior parte del codice per la maggior parte del tempo. Invece di leggere una funzione a 30 righe ogni volta che stai apportando una modifica, stai leggendo una funzione a 5 righe per ottenere il flusso complessivo, e forse un paio di funzioni di supporto se la modifica tocca quell'area. Se viene chiamata la tua nuova classe "extra" EmailValidatore sai che il tuo problema non riguarda la convalida della posta elettronica, puoi saltare la lettura del tutto.

È anche più facile riutilizzare pezzi più piccoli, il che tende a ridurre il conteggio delle linee per il programma generale. Un EmailValidatorpuò essere utilizzato ovunque. Alcune righe di codice che eseguono la convalida della posta elettronica ma sono scricchiolate insieme al codice di accesso al database non possono essere riutilizzate.

E poi considera cosa deve essere fatto se le regole di convalida della posta elettronica dovessero mai essere modificate, cosa che preferiresti: una posizione nota; o molte posizioni, forse ne manchi qualche?


10
risposta molto migliore quindi la stancante "unit test risolve tutti i tuoi problemi"
Dirk Boer

13
Questa risposta colpisce un punto chiave che zio Bob e gli amici sembrano sempre mancare: il refactoring in piccoli metodi aiuta solo se non devi andare a leggere tutti i piccoli metodi per capire cosa sta facendo il tuo codice. La creazione di una classe separata per convalidare gli indirizzi e-mail è saggia. Tirare il codice iterations < _maxIterationsin un metodo chiamato ShouldContinueToIterateè stupido .
BJ Myers,

4
@DavidArno: "to be helpful"! = "Risolve tutti i tuoi problemi"
Christian Hackl

2
@DavidArno: Quando ci si lamenta delle persone che implicano che i test unitari "risolvono tutti i tuoi problemi", ovviamente si intendono le persone che implicano che i test unitari risolvono, o almeno contribuiscono alla soluzione di, quasi tutti i problemi nell'ingegneria del software. Penso che nessuno accusa nessuno di aver suggerito test unitari come un modo per porre fine alla guerra, alla povertà e alle malattie. Un altro modo per dirlo è che l'estremo superamento del test unitario in molte risposte, non solo a questa domanda, ma su SE in generale, è (giustamente) criticato.
Christian Hackl,

2
Ciao @DavidArno, il mio commento è stato chiaramente un'iperbole non un pagliaccio;) Per me è così: sto chiedendo come riparare la mia macchina e le persone religiose passano e mi dicono che dovrei vivere una vita meno peccaminosa. In teoria qualcosa vale la pena discutere, ma non mi aiuta davvero a migliorare le riparazioni delle auto.
Dirk Boer,

34

Bill Gates è stato notoriamente attribuito a dire: "Misurare il progresso della programmazione mediante righe di codice è come misurare il progresso della costruzione di aeromobili in base al peso".

Concordo umilmente con questo sentimento. Questo non vuol dire che un programma dovrebbe cercare più o meno righe di codice, ma che alla fine non è ciò che conta per creare un programma funzionante e funzionante. Aiuta a ricordare che, in definitiva, il motivo dietro l'aggiunta di ulteriori righe di codice è che in teoria è più leggibile in quel modo.

Si possono avere divergenze sul fatto che una modifica specifica sia più o meno leggibile, ma non credo che sbaglieresti a apportare una modifica al tuo programma perché pensi così facendo lo stai rendendo più leggibile. Ad esempio, fare un isEmailValidpotrebbe essere ritenuto superfluo e non necessario, specialmente se viene chiamato esattamente una volta dalla classe che lo definisce. Tuttavia preferirei di gran lunga vedere un isEmailValidin una condizione piuttosto che una serie di condizioni ANDed per cui devo determinare cosa controlla ogni singola condizione e perché viene controllata.

Il punto in cui ti trovi nei guai è quando crei un isEmailValidmetodo che ha effetti collaterali o controlla cose diverse dall'e-mail, perché è peggio che scrivere semplicemente tutto. È peggio perché è fuorviante e potrei perdere un bug a causa sua.

Anche se chiaramente non lo stai facendo in questo caso, ti incoraggio a continuare come stai. Dovresti sempre chiederti se apportando la modifica, è più facile da leggere, e se è il tuo caso, allora fallo!


1
Il peso del velivolo è tuttavia una metrica importante. E durante la progettazione il peso atteso viene monitorato attentamente. Non come un segno di progresso, ma come un vincolo. Le linee di monitoraggio del codice suggeriscono che più è meglio, mentre nella progettazione degli aeromobili è minore il peso. Quindi penso che il signor Gates avrebbe potuto scegliere un'illustrazione migliore per il suo punto.
jos

21
@jos su quel particolare team con cui OP sta lavorando, sembra che un minor numero di LOC sia considerato "migliore". Il punto che Bill Gates stava affermando è che LOC non è correlato ai progressi in alcun modo significativo, proprio come nel peso della costruzione di aeromobili non è correlato ai progressi in modo significativo. Un velivolo in costruzione può avere il 95% del suo peso finale relativamente rapidamente, ma sarebbe solo un guscio vuoto senza sistemi di controllo, non è completo al 95%. Lo stesso nel software, se un programma ha 100k righe di codice, ciò non significa che ogni 1000 righe fornisca l'1% della funzionalità.
Mr.Mindor

7
Il monitoraggio dei progressi è un lavoro duro, no? Manager poveri.
jos

@jos: anche nel codice è meglio avere meno righe per la stessa funzionalità, se tutto il resto è uguale.
RemcoGerlich

@jos Leggi attentamente. Gates non dice nulla sul fatto che il peso sia una misura importante per un aereo stesso. Dice che il peso è una misura terribile per i progressi nella costruzione di un aereo. Dopotutto, con quella misura non appena l'intero scafo viene gettato a terra, in pratica hai finito, dato che presumibilmente ammonta al 9x% del peso dell'intero piano.
Voo

23

così alcuni sviluppatori / gestori vedono il valore nello scrivere meno codice per fare le cose in modo da avere meno codice da mantenere

Si tratta di perdere di vista l'obiettivo reale.

Ciò che conta è ridurre le ore dedicate allo sviluppo . Ciò è misurato nel tempo (o sforzo equivalente), non in righe di codice.
Questo è come dire che i produttori di automobili dovrebbero costruire le loro auto con meno viti, perché ci vuole un tempo diverso da zero per inserire ogni vite. Mentre questo è pedanticamente corretto, il valore di mercato di un'auto non è definito da quante viti fa o non ha. Soprattutto, un'auto deve essere performante, sicura e di facile manutenzione.

Il resto della risposta sono esempi di come il codice pulito può portare a guadagni di tempo.


Registrazione

Prendi un'applicazione (A) che non ha registrazione. Ora crea l'applicazione B, che è la stessa applicazione A ma con la registrazione. B avrà sempre più righe di codice, quindi è necessario scrivere più codice.

Ma molto tempo sprofonderà nell'indagare su problemi e bug e nel capire cosa è andato storto.

Per l'applicazione A, gli sviluppatori rimarranno bloccati nella lettura del codice e dovranno continuamente riprodurre il problema e scorrere il codice per trovare l'origine del problema. Ciò significa che lo sviluppatore deve testare dall'inizio dell'esecuzione fino alla fine, in ogni livello utilizzato, e deve osservare ogni logica utilizzata.
Forse è fortunato a trovarlo immediatamente, ma forse la risposta sarà nell'ultimo posto in cui pensa di guardare.

Per l'applicazione B, presupponendo una registrazione perfetta, uno sviluppatore osserva i registri, può identificare immediatamente il componente difettoso e ora sa dove cercare.

Questo può essere una questione di minuti, ore o giorni risparmiati; a seconda delle dimensioni e della complessità della base di codice.


regressioni

Prendi l'applicazione A, che non è affatto DRY-friendly.
Prendi l'applicazione B, che è ASCIUTTA, ma ha finito per aver bisogno di più righe a causa delle astrazioni aggiuntive.

Viene archiviata una richiesta di modifica, che richiede una modifica alla logica.

Per l'applicazione B, lo sviluppatore modifica la logica (unica, condivisa) in base alla richiesta di modifica.

Per l'applicazione A, lo sviluppatore deve modificare tutte le istanze di questa logica in cui si ricorda che viene utilizzata.

  • Se riesce a ricordare tutte le istanze, dovrà comunque implementare la stessa modifica più volte.
  • Se non riesce a ricordare tutti i casi, ora hai a che fare con una base di codice incoerente che si contraddice. Se lo sviluppatore ha dimenticato un pezzo di codice usato raramente, questo errore potrebbe non essere evidente agli utenti finali fino a molto tempo fa. A quel tempo, gli utenti finali identificheranno qual è l'origine del problema? Anche in questo caso, lo sviluppatore potrebbe non ricordare ciò che il cambiamento ha comportato e dovrà capire come cambiare questo pezzo di logica dimenticato. Forse lo sviluppatore non ha nemmeno lavorato in azienda per allora, e quindi qualcun altro ora deve capire tutto da zero.

Questo può portare a enormi perdite di tempo. Non solo nello sviluppo, ma nella caccia e nella ricerca del bug. L'applicazione può iniziare a comportarsi in modo irregolare in un modo che gli sviluppatori non possono comprendere facilmente. E questo porterà a lunghe sessioni di debug.


Intercambiabilità degli sviluppatori

Sviluppatore Un'applicazione creata A. Il codice non è pulito né leggibile, ma funziona come un fascino ed è stato eseguito in produzione. Non sorprende che non ci sia nemmeno documentazione.

Lo sviluppatore A è assente per un mese a causa di festività. Viene presentata una richiesta di modifica di emergenza. Non possono aspettare altre tre settimane per far tornare Dev A.

Lo sviluppatore B deve eseguire questa modifica. Ora ha bisogno di leggere l'intera base di codice, capire come funziona tutto, perché funziona e cosa cerca di ottenere. Questo richiede secoli, ma diciamo che può farlo tra tre settimane.

Allo stesso tempo, l'applicazione B (creata dallo sviluppatore B) presenta un'emergenza. Dev B è occupato, ma Dev C è disponibile, anche se non conosce la base di codice. Cosa facciamo?

  • Se continuiamo a lavorare con B su A e mettiamo C su B, allora abbiamo due sviluppatori che non sanno cosa stanno facendo e il lavoro di bering viene eseguito in modo non ottimale.
  • Se allontaniamo B da A e gli facciamo fare B, e ora mettiamo C su A, allora tutto il lavoro dello sviluppatore B (o una parte significativa di esso) potrebbe finire per essere scartato. Questo è potenzialmente giorni / settimane di sforzo sprecato.

Lo sviluppatore A ritorna dalle sue vacanze e vede che B non ha capito il codice e quindi lo ha implementato male. Non è colpa di B, poiché ha usato tutte le risorse disponibili, il codice sorgente non era adeguatamente leggibile. A deve ora dedicare tempo a correggere la leggibilità del codice?


Tutti questi problemi, e molti altri, finiscono per perdere tempo . Sì, a breve termine, il codice pulito richiede ora maggiori sforzi , ma finirà per pagare dividendi in futuro quando dovranno essere affrontati inevitabili bug / modifiche.

Il management deve capire che una breve attività ora ti farà risparmiare diverse attività lunghe in futuro. Non riuscire a pianificare sta pianificando di fallire.

In tal caso, quali sono alcuni argomenti che posso usare per giustificare il fatto che sono stati scritti più LOC?

La mia spiegazione goto è chiedere al management cosa preferirebbero: un'applicazione con una base di codice 100KLOC che può essere sviluppata in tre mesi o una base di codice 50KLOC che può essere sviluppata in sei mesi.

Ovviamente sceglieranno i tempi di sviluppo più brevi, perché alla direzione non interessa KLOC . I manager che si concentrano su KLOC sono microgestione mentre non sono informati su ciò che stanno cercando di gestire.


23

Penso che dovresti stare molto attento ad applicare pratiche di "codice pulito" nel caso in cui portino a una maggiore complessità generale. Il refactoring precoce è la radice di molte cose cattive.

L'estrazione di un condizionale in una funzione porta a un codice più semplice nel punto in cui il condizionale è stato estratto , ma porta a una maggiore complessità complessiva perché ora hai una funzione che è visibile da più punti nel programma. Aggiungete un leggero onere di complessità a tutte le altre funzioni in cui questa nuova funzione è ora visibile.

Non sto dicendo che non dovresti estrarre il condizionale, solo che dovresti considerare attentamente se necessario.

  • Se si desidera testare in modo specifico la logica di convalida dell'e-mail. Quindi è necessario estrarre quella logica in una funzione separata, probabilmente anche nella classe.
  • Se la stessa logica viene utilizzata da più posizioni nel codice, ovviamente è necessario estrarla in una singola funzione. Non ripeterti!
  • Se la logica è ovviamente una responsabilità separata, ad esempio la convalida dell'email avviene nel mezzo di un algoritmo di ordinamento. La convalida dell'e-mail cambierà indipendentemente dall'algoritmo di ordinamento, quindi dovrebbero essere in classi separate.

In tutto quanto sopra è una ragione per l'estrazione al di là del fatto che è semplicemente "codice pulito". Inoltre, probabilmente non saresti nemmeno in dubbio se fosse la cosa giusta da fare.

Direi, in caso di dubbio, scegliere sempre il codice più semplice e diretto.


7
Devo essere d'accordo, trasformare ogni condizionale in un metodo di validazione può introdurre complessità più indesiderate quando si tratta di manutenzione e revisioni del codice. Ora devi passare avanti e indietro nel codice solo per assicurarti che i tuoi metodi condizionali siano corretti. E cosa succede quando si hanno condizioni diverse per lo stesso valore? Ora potresti avere un incubo di denominazione con diversi piccoli metodi che vengono chiamati solo una volta e sembrano quasi uguali.
pboss3010,

7
Facilmente la migliore risposta qui. Soprattutto l'osservazione (nel terzo paragrafo) che la complessità non è semplicemente una proprietà dell'intero codice nel suo insieme, ma qualcosa che esiste e differisce su più livelli di astrazione contemporaneamente.
Christian Hackl

2
Penso che un modo per dirlo sia che, in generale, l'estrazione di una condizione dovrebbe essere fatta solo se esiste un nome significativo e non offuscato per quella condizione. Questa è una condizione necessaria ma non sufficiente.
JimmyJames,

Ri "... perché ora hai una funzione che è visibile da più punti nel programma" : in Pascal è possibile avere funzioni locali - "... Ogni procedura o funzione può avere le proprie dichiarazioni di goto etichette, costanti , tipi, variabili e altre procedure e funzioni, ... "
Peter Mortensen,

2
@PeterMortensen: è anche possibile in C # e JavaScript. E questo è fantastico! Ma il punto rimane, una funzione, anche una funzione locale, è visibile in un ambito più ampio di un frammento di codice in linea.
Jacques

9

Vorrei sottolineare che non c'è nulla di intrinsecamente sbagliato in questo:

if(contact.email != null && contact.email.contains('@')

Almeno supponendo che sia usato questa volta.

Potrei avere problemi con questo molto facilmente:

private Boolean isEmailValid(String email){
   return email != null && email.contains('@');
}

Alcune cose che vorrei cercare:

  1. Perché è privato? Sembra uno stub potenzialmente utile. È abbastanza utile essere un metodo privato e nessuna possibilità che venga utilizzato più ampiamente?
  2. Non vorrei nominare personalmente il metodo IsValidEmail, possibilmente ContainsAtSign o LooksVaguelyLikeEmailAddress perché non ha quasi alcuna vera convalida, il che è forse buono, forse non ciò che viene previsto.
  3. Viene utilizzato più di una volta?

Se viene utilizzato una volta, è semplice da analizzare e richiede meno di una riga, indovinerei la decisione. Probabilmente non è qualcosa che chiamerei se non fosse un problema particolare di una squadra.

D'altra parte ho visto i metodi fare qualcosa del genere:

if (contact.email != null && contact.email.contains('@')) { ... }
else if (contact.email != null && contact.email.contains('@') && contact.email.contains("@mydomain.com")) { //headquarters email }
else if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") ) { //internal contract teams }

Questo esempio non è ovviamente ASCIUTTO.

O anche solo l'ultima affermazione può dare un altro esempio:

if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") )

L'obiettivo dovrebbe essere quello di rendere il codice più leggibile:

if (LooksSortaLikeAnEmail(contact.Email)) { ... }
else if (LooksLikeFromHeadquarters(contact.Email)) { ... }
else if (LooksLikeInternalEmail(contact.Email)) { ... }

Un altro scenario:

Potresti avere un metodo come:

public void SaveContact(Contact contact){
   if (contact.email != null && contact.email.contains('@'))
   {
       contacts.Add(contact);
       contacts.Save();
   }
}

Se questo si adatta alla tua logica aziendale e non viene riutilizzato, qui non c'è un problema.

Ma quando qualcuno chiede "Perché viene salvato" @ ", perché non è giusto!" e decidi di aggiungere una convalida effettiva di qualche tipo, quindi estrarla!

Sarai contento di averlo fatto anche quando hai bisogno di rendere conto del secondo account di posta elettronica dei presidenti Pr3 $ sid3nt @ h0m3! @ Miodominio.com e decidere di uscire e provare a supportare RFC 2822.

Sulla leggibilità:

// If there is an email property and it contains an @ sign then process
if (contact.email != null && contact.email.contains('@'))

Se il tuo codice è così chiaro, non hai bisogno di commenti qui. In effetti, non hai bisogno di commenti per dire cosa sta facendo il codice la maggior parte delle volte, ma piuttosto perché lo sta facendo:

// The UI passes '@' by default, the DBA's made this column non-nullable but 
// marketing is currently more concerned with other fields and '@' default is OK
if (contact.email != null && contact.email.contains('@'))

Se i commenti sopra un'istruzione if o all'interno di un metodo minuscolo sono per me pedanti. Potrei anche discutere il contrario dell'utile con buoni commenti all'interno di un altro metodo perché ora dovresti passare a un altro metodo per vedere come e perché fa quello che fa.

In sintesi: non misurare queste cose; Concentrati sui principi da cui è stato costruito il testo (DRY, SOLID, KISS).

// A valid class that does nothing
public class Nothing 
{

}

3
Whether the comments above an if statement or inside a tiny method is to me, pedantic.Questo è un problema di "paglia che ha rotto la schiena del cammello". Hai ragione sul fatto che questa cosa non è particolarmente difficile da leggere apertamente. Ma se si dispone di un grande metodo (ad esempio, un grande import), che ha decine di questi piccoli valutazioni, avendo questi incapsulato nei nomi dei metodi leggibili ( IsUserActive, GetAverageIncome, MustBeDeleted, ...) diventerà un notevole miglioramento sulla lettura del codice. Il problema con l'esempio è che osserva solo una goccia, non l'intero fascio che rompe la schiena del cammello.
Flater

@Flater e spero che questo sia lo spirito che il lettore prende da quello.
AthomSfere

1
Questo "incapsulamento" è un anti-modello e la risposta lo dimostra in realtà. Torniamo a leggere il codice per scopi di debug e per estensione del codice. In entrambi i casi è fondamentale capire cosa fa effettivamente il codice. L'inizio del blocco di codice if (contact.email != null && contact.email.contains('@'))è errato. Se if è falso, nessuna altra riga if può essere vera. Questo non è affatto visibile nel LooksSortaLikeAnEmailblocco. Una funzione che contiene una singola riga di codice non è molto meglio di un commento che spiega come funziona la riga.
Quirk

1
Nella migliore delle ipotesi, un altro livello di riferimento indiretto oscura la meccanica reale e rende più difficile il debug. Nel peggiore dei casi, il nome della funzione è diventato una bugia nello stesso modo in cui i commenti diventano bugie: i contenuti vengono aggiornati ma il nome no. Questo non è uno sciopero contro l'incapsulamento in generale, ma questo particolare idioma è sintomatico del grande problema moderno con l'ingegneria del software "aziendale" - strati e strati di astrazione e colla che seppelliscono la logica pertinente.
Quirk

@quirk Penso che tu sia d'accordo con il mio punto complessivo? E con la colla, stai scendendo un problema completamente diverso. In realtà utilizzo le mappe dei codici quando guardo un nuovo codice per i team. È terribile quello che ho visto fare per alcuni metodi di grandi dimensioni chiamando una serie di metodi di grandi dimensioni anche a livello di modello mvc.
AthomSfere

6

Clean Code è un libro eccellente, e vale la pena leggerlo, ma non è l'autorità finale su tali questioni.

La suddivisione del codice in funzioni logiche è di solito una buona idea, ma pochi programmatori lo fanno nella misura in cui lo fa Martin: ad un certo punto si ottengono rendimenti decrescenti dalla trasformazione di tutto in funzioni e può essere difficile da seguire quando tutto il codice è minuscolo pezzi.

Un'opzione quando non vale la pena creare una funzione completamente nuova è semplicemente usare una variabile intermedia:

boolean isEmailValid = (contact.email != null && contact.emails.contains('@');

if (isEmailValid) {
...

Questo aiuta a mantenere il codice facile da seguire senza dover saltare molto nel file.

Un altro problema è che Clean Code sta diventando piuttosto vecchio come un libro ora. Molta ingegneria del software si è spostata nella direzione della programmazione funzionale, mentre Martin fa di tutto per aggiungere stato alle cose e creare oggetti. Sospetto che avrebbe scritto un libro del tutto diverso se l'avesse scritto oggi.


Alcuni sono preoccupati per la riga aggiuntiva di codice vicino alla condizione (non lo sono affatto), ma forse lo affrontano nella tua risposta.
Peter Mortensen,

5

Considerando il fatto che la condizione "è valida per la posta elettronica" attualmente accetterebbe un indirizzo email molto valido " @", penso che tu abbia tutti i motivi per sottrarre una classe EmailValidator. Ancora meglio, utilizzare una libreria ben collaudata per convalidare gli indirizzi e-mail.

Le righe di codice come metrica non hanno senso. Le domande importanti in ingegneria del software non sono:

  • Hai troppo codice?
  • Hai troppo poco codice?

Le domande importanti sono:

  • L'applicazione nel suo insieme è stata progettata correttamente?
  • Il codice è implementato correttamente?
  • Il codice è mantenibile?
  • Il codice è testabile?
  • Il codice è stato testato adeguatamente?

Non ho mai pensato a LoC quando scrivevo codice per qualsiasi scopo tranne Code Golf. Mi sono chiesto "Potrei scrivere questo in modo più succinto?", Ma ai fini della leggibilità, della manutenibilità e dell'efficienza, non semplicemente della lunghezza.

Certo, forse potrei usare una lunga catena di operazioni booleane invece di un metodo di utilità, ma dovrei?

La tua domanda in realtà mi fa ripensare ad alcune lunghe catene di booleani che ho scritto e mi rendo conto che probabilmente avrei dovuto scrivere uno o più metodi di utilità.


3

A un livello, hanno ragione: meno codice è meglio. Un'altra risposta citata Gate, preferisco:

"Se il debug è il processo di rimozione dei bug del software, la programmazione deve essere il processo per inserirli." - Edsger Dijkstra

“Durante il debug, i principianti inseriscono un codice correttivo; gli esperti rimuovono il codice difettoso. ”- Richard Pattis

I componenti più economici, veloci e affidabili sono quelli che non ci sono. - Gordon Bell

In breve, meno codice hai, meno può andare storto. Se qualcosa non è necessario, taglialo.
Se esiste un codice troppo complicato, semplificalo fino a quando non rimangono gli elementi funzionali effettivi.

Ciò che è importante qui, è che tutti si riferiscono alla funzionalità e hanno solo il minimo richiesto per farlo. Non dice nulla su come si esprime.

Quello che stai facendo cercando di avere un codice pulito non è contro quanto sopra. Stai aggiungendo a LOC ma non aggiungi funzionalità inutilizzate.

L'obiettivo finale è avere un codice leggibile ma nessun extra superfluo. I due principi non dovrebbero agire l'uno contro l'altro.

Una metafora avrebbe costruito un'auto. La parte funzionale del codice è il telaio, il motore, le ruote ... ciò che fa funzionare l'auto. Il modo in cui lo spezzi è più simile alle sospensioni, al servosterzo e così via, facilita la gestione. Vuoi che i tuoi meccanici siano il più semplice possibile mentre svolgi il loro lavoro, per ridurre al minimo la possibilità che qualcosa vada storto, ma ciò non ti impedisce di avere dei bei posti.


2

C'è molta saggezza nelle risposte esistenti, ma vorrei aggiungere un altro fattore: la lingua .

Alcune lingue richiedono più codice di altre per ottenere lo stesso effetto. In particolare, mentre Java (che sospetto sia il linguaggio in questione) è estremamente noto e generalmente molto solido, chiaro e diretto, alcuni linguaggi più moderni sono molto più concisi ed espressivi.

Ad esempio, in Java potrebbero essere facilmente necessarie 50 righe per scrivere una nuova classe con tre proprietà, ognuna con un getter e setter e uno o più costruttori, mentre puoi ottenere esattamente lo stesso in una singola riga di Kotlin * o Scala. (Anche maggior risparmio se si voleva anche adatti equals(), hashCode()e toString()metodi.)

Il risultato è che in Java, il lavoro extra significa che hai maggiori probabilità di riutilizzare un oggetto generale che non si adatta davvero, di comprimere proprietà in oggetti esistenti o di passare un gruppo di proprietà "nude" in giro individualmente; mentre in un linguaggio conciso ed espressivo, è più probabile che tu scriva un codice migliore.

(Ciò evidenzia la differenza tra la complessità 'superficiale' del codice e la complessità delle idee / modelli / elaborazione implementate. Le righe di codice non sono una cattiva misura della prima, ma hanno molto meno a che fare con la seconda .)

Quindi il "costo" di fare le cose nel modo giusto dipende dalla lingua. Forse un segno di una buona lingua è quello che non ti fa scegliere tra fare bene le cose e farle semplicemente!

(* Questo non è davvero il posto per una spina, ma Kotlin vale la pena dare un'occhiata IMHO.)


1

Supponiamo che tu stia Contactattualmente lavorando con la classe . Il fatto che tu stia scrivendo un altro metodo per la convalida dell'indirizzo e-mail è la prova del fatto che la classe Contactnon gestisce una singola responsabilità.

Gestisce anche alcune responsabilità via e-mail, che idealmente dovrebbe essere la sua classe.


Un'ulteriore prova del fatto che il tuo codice è una fusione Contacte la Emailclasse è che non sarai in grado di testare facilmente il codice di convalida dell'email. Richiederà molte manovre per raggiungere il codice di convalida della posta elettronica in un grande metodo con i giusti valori. Vedi il metodo cioè sotto.

private void LargeMethod() {
    //A lot of code which modifies a lot of values. You do all sorts of tricks here.
    //Code.
    //Code..
    //Code...

    //Email validation code becoming very difficult to test as it will be difficult to ensure 
    //that you have the right data till you reach here in the method
    ValidateEmail();

    //Another whole lot of code that modifies all sorts of values.
    //Extra work to preserve the result of ValidateEmail() for your asserts later.
}

D'altra parte, se avessi una classe di posta elettronica separata con un metodo per la convalida della posta elettronica, per testare l'unità del tuo codice di convalida dovrai semplicemente effettuare una chiamata Email.Validation()con i tuoi dati di prova.


Contenuto bonus: i discorsi di MFeather sulla profonda sinergia tra testabilità e buon design.


1

La riduzione della LOC è stata trovata correlata con difetti ridotti, nient'altro. Supponendo quindi che ogni volta che riduci LOC, hai ridotto la possibilità di difetti essenzialmente cade nella trappola di credere che la correlazione sia uguale alla causalità. Il LOC ridotto è il risultato di buone pratiche di sviluppo e non di ciò che rende buono il codice.

Nella mia esperienza, le persone che possono risolvere un problema con meno codice (a livello macro) tendono ad essere più qualificate di quelle che scrivono più codice per fare la stessa cosa. Quello che fanno questi abili sviluppatori per ridurre le righe di codice è usare / creare astrazioni e soluzioni riutilizzabili per risolvere problemi comuni. Non passano il tempo a contare le righe di codice e ad agonizzarsi se possono tagliare una riga qui o là. Spesso il codice che scrivono è più dettagliato di quanto sia necessario, ne scrivono solo meno.

Lasciate che vi faccia un esempio. Ho dovuto fare i conti con la logica intorno ai periodi di tempo e al modo in cui si sovrappongono, se sono adiacenti e quali lacune esistono tra di loro. Quando ho iniziato a lavorare su questi problemi, avrei avuto blocchi di codice che eseguivano i calcoli ovunque. Alla fine, ho creato delle classi per rappresentare i periodi di tempo e le operazioni che calcolavano sovrapposizioni, complementi, ecc. Ciò ha immediatamente rimosso grandi parti di codice e le ha trasformate in alcune chiamate di metodo. Ma quelle stesse classi non sono state scritte per nulla.

Dichiarato chiaramente: se stai cercando di ridurre LOC cercando di tagliare una riga di codice qui o là con più terse, stai sbagliando. È come cercare di perdere peso riducendo la quantità di verdure che mangi. Scrivi codice di facile comprensione, manutenzione e debug e riduci LOC attraverso il riutilizzo e l'astrazione.


1

Hai identificato un compromesso valido

Quindi c'è davvero un compromesso qui ed è inerente all'astrazione nel suo insieme. Ogni volta che qualcuno cerca di inserire N righe di codice nella propria funzione per nominarlo e isolarlo, allo stesso tempo facilita la lettura del sito chiamante (facendo riferimento a un nome piuttosto che a tutti i dettagli cruenti che stanno alla base di quel nome) e più complesso (ora hai un significato che è impigliato in due diverse parti della base di codice). "Facile" è l'opposto di "difficile", ma non è sinonimo di "semplice" che è l'opposto di "complesso". I due non sono opposti e l'astrazione aumenta sempre la complessità al fine di inserire una forma o l'altra di facilità.

Possiamo vedere direttamente la complessità aggiunta quando alcuni cambiamenti nei requisiti aziendali fanno sì che l'astrazione inizi a perdere. Forse alcune nuove logiche sarebbero andate più naturalmente nel mezzo del codice pre-estratto, ad esempio se il codice estratto attraversa un albero e ti piacerebbe davvero raccogliere (e forse agire su) una sorta di informazione mentre sei attraversando l'albero. Nel frattempo, se hai estratto questo codice, potrebbero esserci altri siti di chiamata e l'aggiunta della logica richiesta nel mezzo del metodo potrebbe interrompere tali altri siti di chiamata. Vedi, ogni volta che cambiamo una riga di codice dobbiamo solo guardare al contesto immediato di quella riga di codice; quando cambiamo un metodo dobbiamo Cmd-F il nostro intero codice sorgente alla ricerca di qualcosa che possa rompersi a seguito della modifica del contratto di quel metodo,

L'algoritmo goloso può fallire in questi casi

La complessità ha anche reso il codice in un certo senso meno leggibile piuttosto che più. In un lavoro precedente avevo trattato di un'API HTTP che era strutturata in modo molto accurato e preciso in più livelli, ogni endpoint è specificato da un controller che convalida la forma del messaggio in arrivo e poi lo passa a un manager "livello di logica aziendale" , che ha quindi richiesto alcuni "livelli di dati" che erano responsabili della creazione di numerose query su alcuni livelli di "oggetti di accesso ai dati", responsabili della creazione di numerosi delegati SQL che avrebbero effettivamente risposto alla domanda. La prima cosa che posso dire al riguardo è stata che qualcosa come il 90% del codice era copia e incolla boilerplate, in altre parole era no-ops. Quindi in molti casi la lettura di un determinato passaggio di codice è stata molto "facile", perché "oh questo gestore inoltra la richiesta a quell'oggetto di accesso ai dati".un sacco di cambio di contesto e ricerca di file e tentativo di tracciare informazioni che non avresti mai dovuto tenere traccia ", questo si chiama X in questo livello, diventa chiamato X 'in questo altro livello, quindi si chiama X" in questo altro altro livello ".

Penso che quando ho lasciato fuori, questa semplice API CRUD era nella fase in cui se la stampassi a 30 righe per pagina, occuperebbe 10-20 libri di cinquecento pagine su uno scaffale: era un'intera enciclopedia di ripetitivi codice. In termini di complessità essenziale, non sono sicuro che ci fosse anche metà di un libro di testo di complessità essenziale; forse avevamo solo 5-6 diagrammi di database per gestirlo. Apportare qualche piccola modifica ad esso è stata un'impresa mastodontica, apprendendo che era un'impresa mastodontica, l'aggiunta di nuove funzionalità è diventata così dolorosa che in realtà avevamo dei file template di tipo plateplate che avremmo usato per aggiungere nuove funzionalità.

Quindi ho visto in prima persona come rendere ogni parte molto leggibile e ovvia può rendere il tutto molto illeggibile e non ovvio. Ciò significa che l'algoritmo avido può fallire. Conosci l'algoritmo goloso, sì? "Farò qualunque passo a livello locale per migliorare maggiormente la situazione, e poi mi fiderò di trovarmi in una situazione globalmente migliorata". Spesso è un bellissimo primo tentativo ma può anche mancare in contesti complessi. Ad esempio nel settore manifatturiero potresti provare ad aumentare l'efficienza di ogni particolare passaggio in un processo di fabbricazione complesso - fare lotti più grandi, urlare contro le persone sul pavimento che sembrano non fare nulla per impegnare le mani con qualcos'altro - e questo può spesso distruggere l'efficienza globale del sistema.

Best practice: utilizzare DRY e lunghezze per effettuare la chiamata

(Nota: il titolo di questa sezione è in qualche modo uno scherzo; spesso dico ai miei amici che quando qualcuno dice "dovremmo fare X perché le migliori pratiche lo dicono " sono il 90% delle volte che non parlano di qualcosa come iniezione SQL o hashing della password o qualsiasi altra cosa - le migliori pratiche unilaterali - e quindi la dichiarazione può essere tradotta in quel 90% delle volte in "dovremmo fare X perché lo dico io ." Come se potessero avere qualche articolo sul blog di qualche azienda che ha fatto un lavoro migliore con X anziché X ', ma in genere non esiste alcuna garanzia che la tua attività sia simile a quella aziendale, e in genere c'è qualche altro articolo di qualche altra attività che ha fatto un lavoro migliore con X' piuttosto che X. Quindi, per favore, non prendere anche il titolo sul serio.)

Quello che consiglierei è basato su un discorso di Jack Diederich chiamato Stop Writing Classes (youtube.com) . Parla di alcuni grandi punti in quel discorso: per esempio, sapere che una classe è in realtà solo una funzione quando ha solo due metodi pubblici e uno di questi è il costruttore / inizializzatore. Ma in un caso sta parlando di come un'ipotetica libreria che ha sostituito da stringa per il discorso come "Muffin" abbia dichiarato la propria classe "MuffinHash" che era una sottoclasse del dicttipo incorporato che Python ha. L'implementazione era completamente vuota: qualcuno aveva appena pensato: "potremmo aver bisogno di aggiungere funzionalità personalizzate ai dizionari Python in seguito, introduciamo un'astrazione proprio ora, per ogni evenienza".

E la sua risposta ribelle era semplicemente "possiamo sempre farlo più tardi, se necessario".

Penso che a volte facciamo finta di essere programmatori peggiori in futuro di quanto lo siamo ora, quindi potremmo voler inserire una sorta di piccola cosa che potrebbe renderci felici in futuro. Anticipiamo le esigenze del futuro-noi. "Se il traffico è 100 volte più grande di quanto pensiamo che sarà, quell'approccio non si ridimensionerà, quindi dobbiamo investire gli investimenti iniziali in questo approccio più difficile che si ridimensionerà". Molto sospettoso.

Se prendiamo sul serio quel consiglio, allora dobbiamo identificare quando è arrivato "dopo". Probabilmente la cosa più ovvia sarebbe stabilire un limite superiore alla lunghezza delle cose per motivi di stile. E penso che il miglior consiglio rimanente sarebbe quello di usare DRY - non ripeterti - con queste euristiche sulle lunghezze delle linee per riparare un buco nei principi SOLIDI. Basato sull'euristica di 30 righe che sono una "pagina" di testo e un'analogia con la prosa,

  1. Rifattorizzare un controllo in una funzione / metodo quando si desidera copiarlo e incollarlo. Come se ci fossero ragioni valide occasionali per copiare e incollare, ma dovresti sempre sentirti sporco. Autori reali non ti fanno rileggere una lunga frase lunga 50 volte in tutta la narrativa a meno che non stiano davvero cercando di evidenziare un tema.
  2. Una funzione / metodo dovrebbe idealmente essere un "paragrafo". La maggior parte delle funzioni dovrebbe avere una lunghezza di circa mezza pagina o 1-15 righe di codice e solo il 10% delle funzioni dovrebbe avere la possibilità di estendersi a una pagina e mezza, 45 righe o più. Una volta che hai più di 120 righe di codice e commenti, la cosa deve essere suddivisa in parti.
  3. Un file dovrebbe idealmente essere un "capitolo". La maggior parte dei file dovrebbe essere lunga o meno di 12 pagine, quindi 360 righe di codice e commenti. Solo forse il 10% dei tuoi file dovrebbe avere una lunghezza di 50 pagine o 1500 righe di codice e commenti.
  4. Idealmente, la maggior parte del codice dovrebbe essere indentata con la base della funzione o profonda un livello. Sulla base di alcune euristiche sull'albero dei sorgenti di Linux, se sei religioso al riguardo, solo il 10% del tuo codice dovrebbe essere rientrato di 2 o più livelli all'interno della baseline, meno del 5% di 3 livelli o più rientrati. Ciò significa in particolare che le cose che devono "avvolgere" qualche altra preoccupazione, come la gestione degli errori in un grande tentativo / cattura, dovrebbero essere estratte dalla logica reale.

Come ho detto lassù, ho testato queste statistiche sull'albero dei sorgenti di Linux corrente per trovare quelle percentuali approssimative, ma sono anche in qualche modo ragionevoli nell'analogia letteraria.

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.