In quali casi meno codice non è migliore? [chiuso]


55

Ultimamente ho modificato il codice sul lavoro e ho pensato di fare un buon lavoro. Ho lasciato cadere 980 righe di codice a 450 e ho dimezzato il numero di classi.

Nel mostrare questo ai miei colleghi alcuni non erano d'accordo sul fatto che si trattasse di un miglioramento.

Dissero: "meno righe di codice non sono necessariamente migliori"

Vedo che potrebbero esserci casi estremi in cui le persone scrivono righe molto lunghe e / o mettono tutto in un unico metodo per salvare alcune righe, ma non è quello che ho fatto. A mio avviso, il codice è ben strutturato e più semplice da comprendere / mantenere perché è la metà delle dimensioni.

Faccio fatica a capire perché qualcuno vorrebbe lavorare con il doppio del codice necessario per ottenere un lavoro, e mi chiedo se qualcuno si senta uguale ai miei colleghi e possa fare dei buoni casi per avere più codice anziché meno ?


145
La dimensione del codice viene misurata nel tempo necessario per leggerla e comprenderla, non per linee o numero di caratteri.
Bergi,

13
La tua domanda scritta è categoricamente troppo ampia. Consiglia invece di scriverne uno nuovo sulle modifiche specifiche che hai apportato.
jpmc26,

8
Prendi in considerazione l' algoritmo di radice quadrata inversa veloce . L'implementazione del metodo Newton completo con una corretta denominazione delle variabili sarebbe molto più chiara e molto più facile da leggere anche se probabilmente conterrebbe più righe di codice. (Si noti che in questo caso particolare l'utilizzo del codice intelligente era giustificabile da preoccupazioni perf).
Maciej Piechotka,

65
C'è un intero sito di scambio di stack dedicato a rispondere alla tua domanda: codegolf.stackexchange.com . :)
Federico Poloni,

Risposte:


123

Una persona magra non è necessariamente più sana di una persona in sovrappeso.

Una storia per bambini di 980 linee è più facile da leggere di una tesi di fisica di 450 linee.

Esistono molti attributi che determinano la qualità del tuo codice. Alcuni sono semplicemente calcolati, come la complessità ciclomatica e la complessità di Halstead . Altri sono più vagamente definiti, come coesione , leggibilità, comprensibilità, estensibilità, robustezza, correttezza, autocertificazione, pulizia, testabilità e molti altri.

Ad esempio, potrebbe essere che, pur riducendo la lunghezza complessiva del codice, sia stata introdotta ulteriore complessità ingiustificata e reso il codice più criptico.

Dividere un lungo pezzo di codice in piccoli metodi potrebbe essere tanto dannoso quanto benefico .

Chiedi ai tuoi colleghi di fornirti un feedback specifico sul perché pensano che i tuoi sforzi di refactoring abbiano prodotto un risultato indesiderabile.


1
@PiersyP solo un FYI, una delle linee guida che mi hanno insegnato su un buon refactoring è che dovremmo vedere la complessità ciclomatica ridotta a una radice quadrata di quello che era originariamente.
MA Hanin,

4
@PiersyP, inoltre, non sto dicendo che il tuo codice sia peggiore o migliore di quello che era. Come estraneo non posso davvero dirlo. Potrebbe anche essere che i tuoi colleghi siano troppo conservatori e abbiano paura del tuo cambiamento semplicemente perché non hanno fatto lo sforzo necessario per esaminarlo e convalidarlo. Ecco perché ti ho suggerito di chiedere loro un feedback aggiuntivo.
MA Hanin,

6
Bel lavoro, ragazzi - avete stabilito che c'è un peso "giusto" da qualche parte (il numero esatto può variare). Anche i post originali di @Neil dicono "Sovrappeso" al contrario di "più pesante è una persona", e questo perché c'è un punto debole, proprio come nella programmazione. L'aggiunta di codice oltre quella "giusta dimensione" è solo un disordine, e rimuovere le righe sotto quel punto sacrifica semplicemente la comprensione per ragioni di brevità. Sapere dove si trova esattamente quel punto ... QUESTO è il punto difficile.
AC

1
Solo perché non è necessario non significa che non abbia valore.
Chris Wohlert,

1
@Neil In genere hai ragione, ma il sempre inafferrabile "equilibrio" a cui alludi è qualcosa di un mito, parlando obiettivamente . Ognuno ha un'idea diversa di cosa sia un "buon equilibrio". Chiaramente, OP ha pensato di aver fatto qualcosa di buono, e i suoi colleghi no, ma sono sicuro che tutti hanno pensato a se stessi di avere il "giusto equilibrio" quando hanno scritto il codice.
code_dredd

35

È interessante notare che un collega e io siamo attualmente nel mezzo di un refactor che aumenterà il numero di classi e funzioni di un po 'meno del doppio, anche se le righe di codice rimarranno intorno allo stesso. Quindi mi capita di avere un buon esempio.

Nel nostro caso, avevamo uno strato di astrazione che in realtà avrebbe dovuto essere due. Tutto era stipato nello strato dell'interfaccia utente. Dividendolo in due strati, tutto diventa più coeso e testare e mantenere i singoli pezzi diventa molto più semplice.

Non è la dimensione del codice che disturba i tuoi colleghi, è qualcos'altro. Se non riescono a articolarlo, prova a guardare tu stesso il codice come se non avessi mai visto la vecchia implementazione, e valutalo in base ai suoi meriti piuttosto che al confronto. A volte, quando faccio un lungo rifrattore, in un certo senso perdo di vista l'obiettivo originale e porto le cose troppo lontano. Dai un'occhiata critica a una "visione d'insieme" e rimettila in carreggiata, magari con l'aiuto di un programmatore di coppia di cui apprezzi il consiglio.


1
Sì, sicuramente separare l'interfaccia utente dalle altre cose, questo è sempre utile. Sul tuo punto di perdere di vista l'obiettivo originale, sono in qualche modo d'accordo, ma potresti anche riprogettare qualcosa di meglio o sulla strada del meglio. Come la vecchia argomentazione sull'evoluzione ("a che serve un'ala?") Le cose non migliorano se non ti prendi mai del tempo per migliorarle. Non sempre sai dove stai andando fino a quando non sei sulla buona strada. Sono d'accordo con il tentativo di scoprire perché i colleghi sono a disagio, ma forse è davvero "il loro problema", non il tuo.

17

Mi viene in mente una citazione, spesso attribuita ad Albert Einstein:

Rendi tutto il più semplice possibile, ma non più semplice.

Quando esagerate nel tagliare le cose, può rendere il codice più difficile da leggere. Poiché "facile / difficile da leggere" può essere un termine molto soggettivo, spiegherò esattamente cosa intendo con questo: una misura del grado di difficoltà che uno sviluppatore esperto avrà nel determinare "cosa fa questo codice?" semplicemente guardando la fonte, senza l'assistenza di strumenti specializzati.

Lingue come Java e Pascal sono famose per la loro verbosità. Le persone spesso indicano alcuni elementi sintattici e affermano in modo derisorio che "sono lì solo per facilitare il lavoro del compilatore". Questo è più o meno vero, ad eccezione della parte "giusta". Più informazioni esplicite ci sono, più facile è la lettura e la comprensione del codice, non solo da parte di un compilatore, ma anche da parte di un essere umano.

Se lo dico var x = 2 + 2;, è immediatamente ovvio che xdovrebbe essere un numero intero. Ma se dico var foo = value.Response;, è molto meno chiaro cosa foorappresenti o quali siano le sue proprietà e capacità. Anche se il compilatore può facilmente dedurlo, mette molto più sforzo cognitivo su una persona.

Ricorda che i programmi devono essere scritti per essere letti dalle persone e solo per inciso per l'esecuzione delle macchine. (Ironia della sorte, questa citazione proviene da un libro di testo dedicato a una lingua famosa per essere estremamente difficile da leggere!) È una buona idea rimuovere le cose che sono ridondanti, ma non togliere il codice che rende più facile ai tuoi simili capire cosa sta succedendo, anche se non è strettamente necessario per il programma in fase di scrittura.


7
l' varesempio non è particolarmente utile in termini di semplificazione perché la maggior parte delle volte la lettura e la comprensione del codice comporta la determinazione del comportamento a un certo livello di astrazione, quindi conoscere i tipi effettivi di variabili specifiche in genere non cambia nulla (solo ti aiuta a capire le astrazioni più basse). Un esempio migliore sarebbe costituito da più righe di codice semplice suddivise in una singola istruzione contorta - ad esempio, if ((x = Foo()) != (y = Bar()) && CheckResult(x, y)) impiega tempo per grok e conoscere i tipi di xo ynon aiuta minimamente.
Ben Cottrell,

15

Un codice più lungo può essere probabilmente più facile da leggere. Di solito è il contrario, ma ci sono molte eccezioni, alcune delle quali descritte in altre risposte.

Ma guardiamo da una prospettiva diversa. Partiamo dal presupposto che il nuovo codice sarà visto come superiore dalla maggior parte dei programmatori esperti che vedono i 2 pezzi di codice senza avere una conoscenza aggiuntiva della cultura dell'azienda, base di codice o tabella di marcia. Anche allora, ci sono molte ragioni per obiettare al nuovo codice. Per brevità chiamerò "Le persone che criticano il nuovo codice" Pecritenc :

  • Stabilità. Se il vecchio codice era noto per essere stabile, la stabilità del nuovo codice è sconosciuta. Prima di poter utilizzare il nuovo codice, deve ancora essere testato. Se per qualche motivo non sono disponibili test adeguati, la modifica è un problema piuttosto grande. Anche se il test è disponibile, Pecritenc potrebbe pensare che lo sforzo non valga la pena (minore) di miglioramento del codice.
  • Performance / ridimensionamento. Il vecchio codice potrebbe essere ridimensionato meglio e Pecritenc presume che le prestazioni diventeranno un problema lungo la strada man mano che i client e le funzionalità presto * si accumuleranno.
  • Estensibilità. Il vecchio codice avrebbe potuto consentire una facile introduzione di alcune funzionalità che Pecritenc prevede di aggiungere presto *.
  • Familiarità. Il vecchio codice potrebbe avere schemi riutilizzati utilizzati in altri 5 posti della base di codice dell'azienda. Allo stesso tempo, il nuovo codice utilizza un modello elaborato di cui solo metà dell'azienda ha mai sentito parlare a questo punto.
  • Rossetto su un maiale. Pecritenc può pensare che sia il vecchio che il nuovo codice siano spazzatura, o irrilevanti, rendendo così inutile qualsiasi confronto tra loro.
  • Orgoglio. Pecritenc potrebbe essere stato l'autore originale del codice e non gli piace che le persone apportino enormi modifiche al suo codice. Potrebbe persino vedere i miglioramenti come un leggero insulto, perché implicano che avrebbe dovuto fare di meglio.

4
+1 per "Pecritenc" e un bel riassunto delle obiezioni ragionevoli che dovrebbero essere prese in considerazione prima del prefactoring.

1
E +1 per "estensibilità" - Pensavo che il codice originale potesse avere funzioni o classi che erano destinate all'uso in un progetto futuro, quindi le astrazioni potevano sembrare ridondanti o inutili, ma solo nel contesto di un singolo programma.
Darren Ringer,

Inoltre, il codice in questione potrebbe non essere un codice critico, quindi considerato uno spreco di risorse di ingegneria per ripulirlo.
Erik Eidt,

@nocomprende Qual è il motivo per cui hai usato ragionevole, preconsiderato e prefactoring? Un metodo simile a Pecritenc forse?
Milind R

@MilindR Probabilmente un preconcetto, una predilezione o forse una preferenza personale? O, forse, nessuna ragione, una confluenza cosmica di cofattori, che confonde le condizioni cospirative. Nessuna idea, davvero. E tu?

1

Quale tipo di codice è migliore può dipendere dall'esperienza dei programmatori e anche dagli strumenti che usano. Ad esempio, ecco perché ciò che normalmente verrebbe considerato un codice scritto male potrebbe essere più efficace in alcune situazioni rispetto a un codice orientato agli oggetti ben scritto che faccia pieno uso dell'eredità:

(1) Alcuni programmatori non hanno una comprensione intuitiva della programmazione orientata agli oggetti. Se la tua metafora per un progetto software è un circuito elettrico, allora ti aspetterai un sacco di duplicazione del codice. Ti piacerebbe vedere più o meno gli stessi metodi in molte classi. Ti faranno sentire come a casa. E un progetto in cui devi cercare metodi nelle classi genitore o anche nelle classi dei nonni per vedere cosa sta succedendo può sembrare ostile. Non vuoi capire come funziona la classe genitore e quindi capire come differisce la classe corrente. Volete capire direttamente come funziona la classe corrente e trovate confuso il fatto che le informazioni siano distribuite su più file.

Inoltre, quando desideri semplicemente risolvere un problema specifico in una classe specifica, potresti non dover pensare a se risolvere il problema direttamente nella classe base o sovrascrivere il metodo nella tua attuale classe di interesse. (Senza ereditarietà non dovresti prendere una decisione consapevole. L'impostazione predefinita è semplicemente ignorare problemi simili in classi simili fino a quando non vengono segnalati come bug.) Quest'ultimo aspetto non è in realtà un argomento valido, anche se potrebbe spiegare alcuni dei opposizione.

(2) Alcuni programmatori usano molto il debugger. Anche se in generale sono me stesso fermamente dalla parte dell'ereditarietà del codice e dalla prevenzione della duplicazione, condivido parte della frustrazione che ho descritto in (1) durante il debug del codice orientato agli oggetti. Quando segui l'esecuzione del codice, a volte continua a saltare tra le classi (antenate) anche se rimane nello stesso oggetto. Inoltre, quando si imposta un punto di interruzione in un codice ben scritto, è più probabile che si attivi quando non è utile, quindi potrebbe essere necessario impegnarsi per renderlo condizionale (dove pratico) o anche per continuare manualmente molte volte prima del trigger rilevante.


3
"lezioni dei nonni"! haw haw! Fai attenzione alle lezioni di Adamo ed Eva. (E la classe di Dio ovviamente) Prima di allora, era senza forme e vuoto.

1

Dipende totalmente. Ho lavorato su un progetto che non consente le variabili booleane come parametri di funzione, ma invece richiede un dedicato enumper ogni opzione.

Così,

enum OPTION1 { OPTION1_OFF, OPTION1_ON };
enum OPTION2 { OPTION2_OFF, OPTION2_ON };

void doSomething(OPTION1, OPTION2);

è molto più dettagliato di

void doSomething(bool, bool);

Però,

doSomething(OPTION1_ON, OPTION2_OFF);

è molto più leggibile di

doSomething(true, false);

Il compilatore dovrebbe generare lo stesso codice per entrambi, quindi non c'è niente da guadagnare usando la forma più breve.


0

Direi che la coesione potrebbe essere un problema.

Ad esempio in un'applicazione Web, diciamo che hai una pagina di amministrazione in cui indicizzi tutti i prodotti, che è essenzialmente lo stesso codice (indice) che utilizzeresti in una situazione di homepage, per ... semplicemente indicizzare i prodotti.

Se decidi di parzializzare tutto in modo da poter rimanere ASCIUTTO ed elegante, dovresti aggiungere molte condizioni per quanto riguarda se la navigazione dell'utente è un amministratore o meno e ingombra il codice con cose non necessarie che lo renderanno altamente illeggibile, diciamo un designer!

Quindi in una situazione come questa anche se il codice è praticamente lo stesso, solo perché potrebbe ridimensionarsi a qualcos'altro e i casi d'uso potrebbero cambiare leggermente, sarebbe male seguirli tutti aggiungendo condizioni e se. Quindi una buona strategia sarebbe quella di abbandonare il concetto DRY e spezzare il codice in parti mantenibili.


0
  • Quando meno codice non fa lo stesso lavoro di più codice. Rifattorizzare per semplicità è buono, ma è necessario fare attenzione a non semplificare eccessivamente lo spazio problematico che questa soluzione incontra. 980 righe di codice potrebbero gestire più casi d'angolo di 450.
  • Quando meno codice non fallisce con garbo come più codice. Ho visto un paio di lavori di "ref *** toring" eseguiti su codice per rimuovere "inutili" try-catch e altri casi di gestione degli errori. Il risultato inevitabile è stato invece di mostrare una finestra di dialogo con un bel messaggio sull'errore e su ciò che l'utente poteva fare, l'app si è bloccata o YSODed.
  • Quando meno codice è meno gestibile / estensibile di più codice. Il refactoring per la concisione del codice spesso rimuove costrutti di codice "non necessari" nell'interesse di LoC. Il problema è che quei costrutti di codice, come dichiarazioni di interfacce parallele, metodi / sottoclassi estratti ecc. Sono necessari nel caso in cui questo codice debba mai fare più di quello che fa attualmente, o farlo diversamente. All'estremo, alcune soluzioni su misura per il problema specifico potrebbero non funzionare affatto se la definizione del problema cambia solo un po '.

    Un esempio; hai un elenco di numeri interi. Ognuno di questi numeri interi ha un valore duplicato nell'elenco, tranne uno. L'algoritmo deve trovare quel valore non accoppiato. La soluzione del caso generale è quella di confrontare ogni numero con ogni altro numero fino a trovare un numero che non ha duplicati nell'elenco, che è un'operazione N ^ 2 volte. Puoi anche creare un istogramma usando una tabella hash, ma è molto inefficiente nello spazio. Tuttavia, è possibile renderlo lineare-tempo e spazio costante utilizzando un'operazione XOR bit a bit; XOR ogni numero intero rispetto a un "totale" in esecuzione (a partire da zero) e alla fine, la somma corrente sarà il valore dell'intero non accoppiato. Molto elegante. Fino a quando i requisiti non cambiano e più di un numero nell'elenco potrebbe non essere accoppiato o gli interi includono zero. Ora il tuo programma restituisce immondizia o risultati ambigui (se restituisce zero, significa che tutti gli elementi sono accoppiati o che l'elemento non accoppiato è zero?). Tale è il problema delle implementazioni "intelligenti" nella programmazione del mondo reale.

  • Quando meno codice è meno auto-documentante di più codice. Essere in grado di leggere il codice stesso e determinare cosa sta facendo è fondamentale per lo sviluppo del team. Dare un algoritmo brain-f *** che hai scritto che funziona magnificamente a uno sviluppatore junior e chiedergli di modificarlo per modificare leggermente l'output non ti porterà molto lontano. Molti sviluppatori senior avrebbero problemi anche con quella situazione. Essere in grado di capire in qualsiasi momento cosa sta facendo il codice e cosa potrebbe andare storto con esso, è la chiave per l'ambiente di sviluppo di un gruppo di lavoro (e anche da solo; ti garantisco quel lampo di genio che hai avuto quando hai scritto un 5 il metodo in linea per curare il cancro sparirà da tempo quando torni a quella funzione cercando di farlo curare anche il Parkinson.)

0

Il codice del computer deve fare una serie di cose. Un codice "minimalista" che non fa queste cose non è un buon codice.

Ad esempio, un programma per computer dovrebbe coprire tutto il possibile (o al minimo, tutti i casi probabili). Se un pezzo di codice copre solo un "caso base" e ne ignora altri, non è un buon codice, anche se è breve.

Il codice del computer dovrebbe essere "scalabile". Un codice criptico può funzionare solo per un'applicazione specializzata, mentre un programma più lungo, ma più aperto può rendere più semplice l'aggiunta di nuove applicazioni.

Il codice del computer dovrebbe essere chiaro. Come ha dimostrato un altro risponditore, è possibile che un programmatore hard-core produca una funzione di tipo "algoritmico" su una riga che fa il lavoro. Ma il one-liner doveva essere suddiviso in cinque "frasi" diverse prima che fosse chiaro al programmatore medio.


Il dovere è negli occhi di chi guarda.

-2

Prestazioni computazionali. Quando si ottimizza il rivestimento del tubo o si eseguono parti del codice in parallelo, potrebbe essere utile, ad esempio, non eseguire il loop da 1 a 400, ma da 1 a 50 e inserire 8 istanze di codice simile in ciascun loop. Non presumo che questo fosse il caso nella tua situazione, ma è un esempio in cui più linee sono migliori (dal punto di vista delle prestazioni).


4
Un buon compilatore dovrebbe sapere meglio di un programmatore medio come srotolare i loop per una specifica architettura di computer, ma il punto generale è valido. Una volta ho esaminato il codice sorgente per una routine di moltiplicazione della matrice da una libreria Cray ad alte prestazioni. La matrice si moltiplica per tre cicli annidati e circa 6 righe di codice in totale, giusto? Sbagliato: quella routine di libreria si estendeva a circa 1100 righe di codice, più un numero simile di righe di commento che spiegavano perché fosse così lungo!
alephzero,

1
@alephzero wow, mi piacerebbe vedere quel codice, deve essere solo Cray Cray.

@alephzero, i bravi compilatori possono fare molto, ma purtroppo non tutto. Il lato positivo è che quelle sono le cose che mantengono la programmazione interessante!
Hans Janssen,

2
@alephzero In effetti, un buon codice di moltiplicazione della matrice non si limita a radere un po 'di tempo libero (cioè ridurlo di un fattore costante), utilizza un algoritmo totalmente diverso con diversa complessità asintotica, ad esempio l'algoritmo di Strassen è all'incirca O (n ^ 2.8) piuttosto che O (n ^ 3).
Arthur Tacca,
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.