risorse | clean code
A Handbook of Agile Software Craftsmanship
Una raccolta di alcuni interessanti spunti trovati nel libro, raggruppati per capitolo.
Il primo capitolo contiene la definizione di clean code secondo l'esperienza dei maggiori esperti del settore; alcune di esse sono riportate qui di seguito:
I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide…– Bjarne Stroustrup, inventor of C++;
Clean Code always looks like it was written by someone who cares.– Michael Feathers, author of Working Effectively with Legacy Code.
You know you are working on clean code when each routine you read turns out to be pretty much what you expect…– Ward Cunningham, inventor of Wiki;
Si argomenta sul paradosso che il codice sporco, risultato di un'attività frenetica di sviluppo volta ad accelarare i tempi di consegna, si rivela essere in realtà una – se non la principale – causa di ritardo. Viene quindi presentata la metafora della finestra rotta, la cui morale dovrebbe spronare a ripulire il codice ogni qualvolta se ne presenti la possibilità, in particolare durante le fasi di refactoring: anche la ripulitura del codice quindi, può essere effettuata in maniera incrementale.
Viene infine proposta la regola del boyscout adattata al contesto del libro: lascia il codice più pulito di come l'hai trovato
.
I consigli proposti in questo capitolo sono piuttosto noti, e spesso si ritrovano in pubblicazioni analoghe (cfr. Pescio, Manuale di Stile C++, Fowler, Refactoring, …):
Anche le indicazioni riportate in questo capitolo, riguardante la scrittura delle funzioni, si trovano spesso citati in pubblicazioni simili; interessante l'idea della stratificazione delle funzioni in analogia con i livelli di astrazione delle operazioni effettuate.
separare funzioni operative e interrogazioni, si generano forme ambigue:
if (set("username", "pippo")) { // ??? }
isolare i blocchi try/catch in funzioni esterne; la funzione:
void doSeveralThings() { doOneThing(); try { tryToDoSomethingCritical(); } catch (...) { } doAnotherThing(); }
è consigliabile riscriverla in questa forma:
void doSeveralThings() { doOneThing(); doSomethingCritical(); doAnotherThing(); } void doSomethingCritical() { try { tryToDoSomethingCritical(); } catch (...) { } }
esplicitare le dipendenze temporali: se due funzioni vanno chiamate secondo un ordine determinato, fare in modo che la seconda dipenda dal risultato della prima:
class A { B b; C c; public: void f() { first(); // calcola b second(); // usa b per calcolare c // usa c } }
diventa:
class A { public: void f() { B b = first(); C c = second(b); // usa c } }
Le avvertenze sono le solite:
Anche in questo caso non ci si discosta da ciò che si trova in letteratura:
evitare gli allineamenti orizzontali:
int current_page; std::vector<int> pages; bool is_published; std::string header;
questo tipo di formattazione induce una suddivisione tra i tipi (a sinistra) e le variabili (a destra) anziché l'associazione di ogni variabile col suo tipo.
Il sesto capitolo affronta la dicotomia oggetti/strutture, affermando che se l'interfaccia dei primi deve astrarre dall'implementazione, per le seconde ciò non vale, essendo dei semplici contenitori di dati. Seguono due considerazioni:
La tesi viene suffragata considerando che l'introduzione di una nuova entità all'interno di una gerarchia di classi si ottiene derivando la nuova entità da una esistente ed implementandone le funzionalità specifiche; nel caso di una gerarchia di strutture, è necessario intervenire su tutte le funzioni operanti sulle strutture esistenti, dovendo esse ora trattare anche la nuova casistica. Viceversa, l'introduzione di una nuova funzionalità in una gerarchia di classi spesso si traduce nell'aggiornamento dell'interfaccia di gran parte delle classi esistenti; nel caso di strutture, normalmente è sufficiente implementare la nuova funzionalità in un metodo globale.
Limitatamente al dominio degli oggetti, viene citata la legge di Demeter, e viene proposto un metodo per soddisfarla, che consiste nell'esporre parte della funzionalità del contenuto nell'interfaccia del contenitore:
final String outputDir = ctxt.options.scratchDir.absolutePath; ... String outFile = outputDir + "/" + className.replace('.', '/') + ".class"; FileOutputStream fout = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fout);
si può rendere meglio così:
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
L'ottavo capitolo è dedicato alla gestione dei confini del proprio codice, ovvero come rendere il codice di nostra competenza robusto rispetto alla variabilità e alla disponibilità di librerie di terze parti o di codice sviluppato da altri colleghi. Il suggerimento principale è il ricorso al wrapping.
Librerie di terze parti:
Relativamente al codice sviluppato da altri colleghi, qualora questo non sia ancora disponibile, si raccomanda l'uso di mock/stubs per emulare le funzionalità necessarie; questi potranno in seguito essere riutilizzati come adapter dei componenti reali, quando diventeranno disponibili.
Il capitolo inizia enunciando le tre leggi del TDD:
Il TDD costringe lo sviluppatore a passare continuamente dallo scrivere codice di test – che fallisce – alla scrittura di codice di produzione – che fa passare il test. Il codice progredisce così per piccoli passi, essendo la durata tipica di un ciclo di qualche minuto. Risulta perciò quasi impossibile perdersi nel codice dato che, per definizione, se a un dato momento il test non passa la causa è da ricercare nelle modifiche introdotte nell'ultima iterazione. Ciò regala una notevole serenità al programmatore, che ha la certezza che, qualunque cosa sia successa, pochi undo lo ricondurranno ad una situazione stabile.
Il codice di test va trattato come il codice di produzione. Cosa fa invece di un test case un buon test case?
La copertura del codice di produzione deve essere totale: deve essere testato tutto ciò che in linea di principio potrebbe fallire; non si devono trascurare nemmeno le funzionalità più banali (se non altro, è documentazione).
Oggetto di questo capitolo sono le classi; vengono rispolverati i più importanti principi della progettazione object-oriented:
Single Responsibility Principle: per una classe deve esistere un unica ragione perché debba essere modificata;
Non ci si deve spaventare di fronte ad una moltitudine di piccole (super-focalizzate) classi rispetto a poche, vaste classi: essendo le funzionalità implementate le medesime, medesima è la quantità di concetti da gestire e comprendere.
Una classe deve essere coesa:
Un'osservazione interessante è la seguente: favorire la coesione porta all'aumento del numero di classi
.
Il capitolo è molto java-oriented, essendo particolarmente focalizzato sull'AOP.
Alcune osservazioni di carattere generale sono dedicate all'inizializzazione del sistema software, inteso come istanziazione degli oggetti principali. Vengono presentate tre differenti tecniche di start-up del sistema software:
Vengono descritte e commentate le regole del simple design, riportate qui sotto nella loro forma originale:
Una volta ottemperato alla prima regola, il refactoring consente di realizzare le successive.
Martin, Robert C. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008.
Pagina modificata l'8/11/2011