giovedì 29 gennaio 2009

Best Practices: Thinking in C++ di Bruce Eckel


Tra le letture che consigliamo vi è Thinking in C++ di Bruce Eckel. La prima edizione è del 2000 ma i concetti espressi sono ancor oggi molto moderni. Vi sono elencate una serie di best practices fondamentali per chi scrive codice e ciò indipendentemente dalla piattaforma tecnologica utilizzata.
Di seguito una serie di spunti di riflessione tratti e tradotti (molto!) liberamente dal testo originale... e per non prenderci troppo sul serio lasceremo la parola a Dilbert, mitico personaggio di Scott Adams che tutti i developer per la loro sopravvivenza tecnica e non solo (altra lettura consigliata è The Career Programmer: Guerilla Tactics for an Imperfect World) dovrebbero tener in gran conto... forse ancor più di Bruce Eckel!!!


1. Prima fatelo funzionare, poi rendetelo veloce! Questo è vero anche se siete certi che un qualsiasi frammento di codice sia realmente importante e che diverrà esso il collo di bottiglia principale del vostro sistema. Non toccate quel frammento per ottimizzarlo. Prima di tutto cercate di ottenere un sistema funzionante con un progetto il più semplice possibile. Soltanto dopo, se non è abbastanza veloce, lo analizzerete. Quasi sempre scoprirete che il vero problema non è il "vostro" collo di bottiglia. Risparmiate tempo per le cose veramente utili.

2. L'eleganza nella scrittura del codice paga sempre. Non è un'attività frivola. Avrete in mano un programma non solo più facile da compilare e testare, ma anche più semplice da comprendere e da manutenere: ed è qui che si ritrova il valore economico del vostro lavoro. Potrebbe servire un po' di esperienza prima di comprendere questo dato, in quanto potrebbe apparire che, mentre cercate di rendere elegante un frammento di codice, non siete in realtà produttivi. La produttività emergerà poi quando il codice si integrerà perfettamente nel vostro sistema, ed ancor più quando il codice o il sistema verrà modificato.

3. Ricordatevi del principio "divide et impera". Se il problema che state affrontando è troppo complesso, cercate di immaginare quali potrebbero essere le operazioni basilari del programma. Magari immaginando un "qualcosa" che si occupi delle parti più difficili!
Questo qualcosa è un oggetto e quindi scrivete il codice che utilizza quell'oggetto. Successivamente analizzate l'oggetto ed incapsulate le sue parti complicate all'interno di altri oggetti, e così via.

4. Se avete un grosso blocco di codice che ha bisogno di modifiche, cominciate ad isolarne le parti che non verranno modificate, eventualmente inglobandole in una"classe API" come metodi statici. Successivamente, focalizzate la vostra attenzione sul codice che verrà modificato, ristrutturandolo in classi. In tal modo renderete agevoli le modifiche man mano che la vostra attività di manutenzione procederà.

5. Tenete ben distinti il creatore della classe dal suo utilizzatore (il programmatore client). Chi utilizza la classe è il "cliente", e non ha bisogno né vuole sapere cosa accade dietro le quinte. Chi crea la classe dev'essere l'esperto di progettazione di classi e deve scriverla in modo che possa essere usata anche dal più principiante dei programmatori, continuando a comportarsi in maniera robusta nell'applicazione. L'utilizzo di una qualsiasi libreria è semplice soltanto se è trasparente.

6. Quando create una classe, usate una nomenclatura il più chiara possibile. Il vostro obiettivo è quello di rendere l'interfaccia di programmazione concettualmente semplice. Cercate di rendere i vostri nomi talmente chiari da rendere superflui i commenti. A tal fine, sfruttate l'overloading delle funzioni e gli argomenti di default per creare un'interfaccia intuitiva e facile da usare.

7. Il controllo dell'accesso consente a voi (creatori della classe) di fare in futuro estese modifiche che saranno quindi possibili senza danneggiare il codice client nel quale la classe è utilizzata. In questa prospettiva, mantenete tutto il più private possibile, e rendete public solo l'interfaccia della classe, utilizzando sempre le funzioni anziché i dati. Rendete i dati public solo quando siete costretti. Se gli utilizzatori di una classe non hanno bisogno di richiamare una funzione, dichiaratela private. Se una parte della vostra classe deve restare visibile alle classi eventualmente ereditate come protected, fornite un'interfaccia a funzioni piuttosto che esporre direttamente i dati. In questo modo, le modifiche di implementazione avranno un impatto minimo sulle classi derivate.

8. Prima di tutto e se possibile scrivete il codice di test. Ancor prima di scrivere la classe e poi tenetelo in evoluzione insieme alla classe. Rendete automatica l'esecuzione dei vostri test magari tramite un makefile oppure mediante uno strumento similare. In questo modo, qualunque modifica potrà essere controllata automaticamente lanciando il codice di test, e gli errori verranno immediatamente scoperti. Sapendo di avere la rete di sicurezza dell'ambiente di test, sarete più propensi ad effettuare modifiche consistenti quando ne sentirete il bisogno. Ricordate che i maggiori miglioramenti nei linguaggi di programmazione vengono dai controlli interni forniti da controllo del tipo, gestione delle eccezioni e così via, ma queste caratteristiche servono fino ad un certo punto. Dovete arrivare in fondo alla strada che porta alla creazione di sistemi robusti introducendo i test che verificano le caratteristiche specifiche della vostra classe o del vostro programma.

9. Ricordate una regola fondamentale dell'Ingegneria del Software: tutti i problemi di progetto del software possono essere semplificati introducendo un ulteriore livello di dereferenziamento concettuale. Quest'idea sta alla base dell'astrazione, la caratteristica primaria della programmazione orientata agli oggetti.

10. Rendete le classi le più atomiche possibile; in altri termini, date ad ogni classe un unico scopo chiaro. Se le vostre classi o il vostro progetto del sistema diventano troppo complicati, suddividete le classi complesse in classi più semplici. Il segnale più ovvio di questo fatto è proprio la stessa dimensione: se una classe è grande, c'è la possibilità che stia facendo troppo e che quindi è necessario scomporla.

11. Guardatevi dalle definizioni dei metodi lunghe. Una funzione lunga e complicata è difficile e costosa da mantenere, e probabilmente sta cercando di fare troppo da sola. Se vi trovate a maneggiare una funzione del genere, significa che, quanto meno, andrebbe suddivisa in alcune funzioni più piccole. Potrebbe anche suggerire la creazione di una nuova classe.

12. Guardatevi dalle liste di argomenti lunghe. Le chiamate di funzione diventano difficili da scrivere, leggere e manutenere. Piuttosto, cercate di spostare il metodo in una classe nella quale sia più adatto, e/o a passargli un oggetto come parametro.

13. Non ripetetevi. Se un pezzo di codice compare in molte funzioni nelle classi derivate, spostate quel codice in un'unica funzione nella classe base e chiamatelo dalle funzioni delle classi derivate. Non solo risparmierete spazio, ma consentirete un'agevole propagazione delle modifiche. Potete utilizzare una funzione inline perl'efficienza. Talvolta, la scoperta di questo codice comune porta considerevoli benefici alla funzionalità della vostra interfaccia.

14. Guardatevi dalle istruzioni switch o dagli if-else concatenati. Tipicamente, questo è un chiaro indicatore della programmazione di tipo type-check.
Programmazione type - check significa che state scegliendo quale codice eseguire in base ad un qualche genere di informazione sul tipo (il tipo esatto potrebbe non esservi immediatamente chiaro). Solitamente, potete rimpiazzare questo genere di codice sfruttando ereditarietà e polimorfismo; la chiamata ad una funzione polimorfica eseguirà il controllo di tipo per voi, e consentirà un estensibilità più affidabile e semplice. [N.d.T. - In realtà il controllo sui tipi è caratteristico del C++ che è un linguaggio fortemente tipizzato, ma il principio è comunque valido come punto di attenzione di ordine generale, nel senso che un eccesso di if-else nidificati è sicuramente indice di entropia elevata che state inserendo nel codice.]

15. Guardatevi dalle limitazioni nell'ereditarietà. I progetti più puliti aggiungono nuove funzionalità a quelle ereditate. Diffidate di un progetto che rimuove le vecchie funzionalità (mentre state ereditando) senza aggiungerne altre. Ma le regole sono fatte per essere infrante, e se state lavorando con una vecchia libreria di classi potrebbe essere più efficiente restringere una classe esistente nelle sue sottoclassi, piuttosto che ristrutturare la gerarchia in modo che la vostra nuova classe si vada ad inserire dove dovrebbe, al di sopra della vecchia classe.

16. Non estendete le funzionalità fondamentali nelle sottoclassi. Se un elemento dell'interfaccia è fondamentale per una classe si dovrebbe trovare nella classe base, e non essere aggiunto nel corso delle derivazioni. Se state aggiungendo dei metodi tramite l'ereditarietà, forse dovreste ripensare il progetto.

17. Meno è più. Iniziate da un'interfaccia minimale per la classe, semplice e piccola quanto basta per risolvere il vostro problema corrente, ma non cercate di anticipare tutti i modi nei quali la vostra classe potrebbe essere usata. Al momento del suo utilizzo, scoprirete il modo nel quale dovrete espandere l'interfaccia. Comunque, una volta che la classe è in uso, non potete modificarne l'interfaccia senza disturbare il codice client. Se avete bisogno di aggiungere più funzioni, va bene; non creerà problemi al codice, se non la necessità di una ricompilazione. Ma anche se i nuovi metodi rimpiazzano le funzionalità di quelle vecchie, lasciate da sola l'interfaccia già esistente (se volete, potete combinare le funzionalità nell'implementazione sottostante). Se avete bisogno di espandere l'interfaccia di una funzione esistente aggiungendo nuovi argomenti, lasciate gli argomenti già esistenti nel loro ordine, ed assegnate dei valori di default a tutti quelli nuovi; in questo modo, non creerete problemi a nessuna chiamata già esistente a quella funzione.

18. L'overloading degli operatori è soltanto "zucchero sintattico": un modo diverso per chiamare una funzione. Se l'overloading di un operatore non rende l'interfaccia della classe più chiara e semplice da usare, non fatelo. Per una classe create solo un operatore per la conversione automatica di tipo.

19. Non preoccupatevi di un'ottimizzazione prematura. È pura follia. In particolare, non preoccupatevi di forzare il codice ad essere efficiente quando state appena costruendo il sistema. Il vostro scopo principale è di verificare il progetto, a meno che lo stesso richieda una certa efficienza.

20. Mantenete gli scope più piccoli possibile, in modo che la visibilità e la vita dei vostri oggetti siano le più ridotte possibile. In questo modo, si diminuisce la possibilità di utilizzare un oggetto in un contesto sbagliato e di nascondere un bug difficile da trovare.

21. Evitate, evitate, evitate il più possibile... non ci stancheremo mai di ribadirlo, le variabili globali. Cercate sempre di inserire i dati all'interno delle classi. È più probabile imbattersi in funzioni globali piuttosto che in variabili globali, sebbene potreste rendervi conto in seguito che una funzione globale troverebbe una collocazione più consona come metodo static di una classe.

22. Sfruttate a vostro vantaggio il controllo degli errori effettuato dal compilatore. Compilate il vostro codice abilitando tutti i warning, e correggete il vostro codice inmodo da eliminarli tutti. Scrivete codice che utilizza gli errori ed i warning al momento della compilazione, piuttosto che codice che provochi errori di runtime. Utilizzate assert per il debug, ma a runtime usate le eccezioni.

23. Preferite gli errori di compilazione agli errori di runtime. Cercate di gestire un errore il più vicino possibile al punto in cui si è verificato. È preferibile gestire l’errore in quel punto piuttosto che lanciare un eccezione. Catturate le eccezioni nel gestore più vicino che abbia informazioni sufficienti per gestirle. Fate il possibile con l’eccezione al livello corrente; se in questo modo non risolvete il problema, rilanciatela nuovamente.

24. Non create una vostra notazione “personalizzata” per i nomi delle variabili membro (underscore, prefissi, variazioni fantasiose della notazione ungherese e così via), a meno che non abbiate una gran quantità di variabili globali preesistenti; se così non è, lasciate che le classi ed i namespace svolgano il lavoro per voi.

3 commenti:

Francesco ha detto...

18. L'overload non è "zucchero sintattico" ma un utilissimo strumento di controllo dei dati di input.

L'overloading deve essere utilizzato per implementare la stessa funzionalità con un set di parametri differente utili al contesto in cui si trova la chiamata del metodo, e non perchè si vuole fornire un metodo alternativo per la scrittura del codice a scelta dell'utente. la decisione di quale metodo in overload utilizzare deve essere imposta dal contesto non dal gusto del programmatore, normalmente i metodi in overload richiamano un metodo che prevede tutti i parametri in ingresso, questo metodo deve essere mantenuto sempre privato (o quasi), perchè non si deve dare l'opportunità al programmatore di poter scegliere fra i metodi in overload quello che prevede tutti i parametri con la possibilità di passare dei parametri non inizializzati, infatti potrebbe scegliere una combinazione di parametri non prevista che solleva eccezzioni.

Andrea ha detto...

Mah non mi trovo completamente d’accordo, con entrambi, giusto per aggiungere caciara.
L’overloading non è un abbellimento del codice, ha una sua funzione specifica che a sua volta non è solo mascherare metodi privati ai programmatori che utilizzeranno poi il codice.
L’overload di un metodo dovrebbe esser fatto per implementare diversi parametri in ingresso e non solo per filtrare tutti i parametri di una sottospecie di metodo onnisciente ed error prone in caso di uso non conscio (vedi: una combinazione di parametri non prevista).
Spesso questo sistema di filtraggio è più utilizzato per i costruttori (che sono sempre metodi) che per i metodi di per se, altrimenti per i costruttori potrebbe essere il caso di affidarsi all’uso del factory per instanziare classi, ma è un altro discorso.
Fare l’overload di un metodo solo per non cambiare nome è errato nella sostanza, nel momento in cui i due metodi fanno cose assolutamente diverse. Questo ingenera errore nell’utilizzatore che si aspetta un comportamento e un risultato omogeneo tra i due metodi.
Se si unisce a questo l’utilizzo smodato dello strict off sarebbe come spararsi sulle balle, ma de gustibus.. ;)

neteller casinos ha detto...

Matchless theme, it is very interesting to me :)

 
Extension Factory Builder