Conoscenze preliminari
Prima di proseguire è bene richiamare concetti e termini fondamentali presumibilmente visti durante il corso di Programmazione II.
Object orientation
Per essere definito object oriented, un linguaggio di programmazione deve soddisfare tre proprietà:
- Ereditarietà: ovvero la possibilità di poter definire una classe ereditando proprietà e comportamenti di un’altra classe.
- Polimorfismo: quando una classe può assumere diverse forme in base alle interfacce che implementa. Il prof fa l’esempio del tennista scacchista: in un torneo di tennis è poco utile sostituire una persona che gioca a tennis ed è brava con gli scacchi (quindi una classe che implementa entrambe le interfacce) con una che gioca sia a tennis che a scacchi, basta che sappia giocare tennis. Il collegamento tra capacità e oggetto è fatto a tempo di compilazione: non è importante quindi se la capacità non è ancora definita;
- Collegamento dinamico: in Java il tipo concreto degli oggetti può non essere specificato staticamente e quindi il problema di stabilire quale metodo chiamare viene risolto durante l’esecuzione.
In C++ occorre esplicitare questo comportamento utilizzando la keyword
virtual
.
SOLID principles
Ci sono 5 parti che compongono questo principio:
- SINGLE RESPONSIBILITY: una classe, un solo scopo. Così facendo, le classi rimangono semplici e si agevola la riusabilità.
- OPEN-CLOSE PRINCIPLE: le classi devono essere aperte ai cambiamenti (opened) ma senza modificare le parti già consegnate e in produzione (closed). Il refactoring è comunque possibile, ma deve essere preferibile estendere la classe attuale.
- LISKOV SUBSTITUTION PRINCIPLE: c’è la garanzia che le caratteristiche eredidate dalla classe padre continuinino ad esistere nelle classi figlie. Questo concetto si collega all’aspetto contract-based del metodo Agile: le precondizioni di un metodo di una classe figlia devono essere ugualmente o meno restrittive del metodo della classe padre. Al contrario, le postcondizioni di un metodo della classe figlia non possono garantire più di quello che garantiva il metodo nella classe padre. Fare casting bypassa queste regole.
- INTERFACE SEGREGATION: più le capacità e competenze di una classe sono frammentate in tante interfacce più è facile utilizzarla in contesti differenti. In questo modo un client non dipende da metodi che non usa. Meglio quindi avere tante interfacce specifiche e piccole (composte da pochi metodi), piuttosto che poche, grandi e generali.
- DEPENDENCY INVERSION: il codice dal quale una classe dipende non deve essere più concreto di tale classe. Per esempio, se il telaio della FIAT 500 dipende da uno specifico motore, è possibile utilizzarlo solo per quel specifico motore. Se invece il telaio dipende da un concetto di motore, non c’è questa limitazione. In conclusione, le classi concrete devono tendenzialmente dipendere da classi astratte e non da altre classi concrete.
Reference escaping
Il reference escaping è una violazione dell’incapsulamento (compiere questo errore equivale ad una bocciatura diretta all’esame).
Basandoci sull’esempio del mazzo di carte, vogliamo che la sua implementazione rimanga segreta, quindi ecco i possibili errori per non rispettare questa condizione:
- quando un getter ritorna un riferimento a un segreto;
public Deck {
private List<Card> cards;
public List<Card> getCards() {
return this.cards;
}
}
- quando un setter assegna a un segreto un riferimento che gli viene passato;
public Deck {
private List<Card> cards;
public setCards(List<Card> cards) {
this.cards = cards;
}
}
- quando il costruttore assegna al segreto un riferimento che gli viene passato;
public Deck {
private List<Card> cards;
public Deck(List<Card> cards) {
this.cards = cards;
}
}
Encapsulation e information hiding
Legge di Parnas (L8).
Solo ciò che è nascosto può essere cambiato liberamente e senza pericoli.
Lo stato mostrato all’esterno non può essere modificato, mentre quello nascosto sì.
Questo principio serve per facilitare la comprensione del codice e renderne più facile la modifica parziale senza fare danni. Dovrà essere quindi chiarito prima dell’implementazione ciò che sarà pubblico e ciò che invece sarà privato.
Legacy o Deprecated
Una classe o una funzionalità, dopo diverse modifiche nel tempo, può arrivare un punto di non ritorno, dove l’evoluzione si ferma per diversi motivi, come un design iniziale troppo limitante o l’arrivo di un innovazione tecnologica.
In questi casi la funzione può essere chiamata:
- Legacy: Una classe di questo genere continuerà a funzionare e sarà supportata, però verrà consigliato l’utilizzo di un altra classe più recente.
- Deprecated: In questo caso la classe resterà comunque funzionante ma non sarà più supportata. Il suo utilizzo sarà fortemente sconsigliato e si spingerà il programmatore a fare un refactoring del codice laddove è presente la funzione deprecata. Essa deve essere sostituita con la nuova classe standard, poichè dopo un certo lasso di tempo verrà rimossa o la sua funzionalità non sarà più garantita.
Immutabilità
Una classe è immutabile quando non c’è modo di modificare lo stato di ogni suo oggetto dopo la creazione. Questo ci garantisce grandi vantaggi, come ad esempio condividere oggetti senza il rischio che il suo stato venga modificato (in questo modo l’encapsulation potrebbe non essere rispettata), quindi sarà fondamentale massimizzare l’utilizzo di questo tipo di classi.
Per assicurare tale proprietà è necessario:
- non fornire metodi di modifica allo stato;
- avere tutti gli attributi privati per i tipi potenzialmente mutabili (come
List<T>
) e fornire solo il valore tramite i getter e non la referenza; - avere tutti gli attributi final se non già privati;
- assicurare l’accesso esclusivo a tutte le parti non mutabili, ovvero non avere reference escaping.
Code smell
I code smell sono dei segnali, che suggeriscono problemi nella progettazione del codice, mantenere questi problemi nel codice significa aumentare il debito tecnico. Di seguito ne sono elencati alcuni:
- codice duplicato: si può fare per arrivare velocemente al verde quando si usa la tecnica TDD, ma è da rimuovere con il refactoring. Rischia di portarsi dietro degli errori o particolarità legate al applicazione originale di questo codice. È dunque importante cercare di fattorizzare il più possibile.
- metodi troppo lunghi: non è un vincolo “hard” dato che dipende dai casi ma solitamente sono poco leggibili e poco riusabili;
- troppi livelli di indentazione: scarsa leggibilità e riusabilità, è bene fattorizzare il codice invece che avere una serie di if e for innestati che lo rendono confusionario, quindi è meglio creare dei metodi con nomi chiari per evitare ciò.
- troppi attributi: suggerisce che la classe non rispetta la single responsability, ovvero fa troppe cose;
- lunghe sequenze di if-else o switch: possono essere sostituiti da strutture basate su polimorfismo e collegamento dinamico;
- classe troppo grande;
- lista parametri troppo lunga: se proprio ne ho bisogno meglio raggrupparli in una struttura e passarli come unico parametro;
- numeri magici: è importante assegnare alle costanti numeriche all’interno del codice un nome per comprendere meglio il loro scopo, infatti dei semplici numeri possono avere significati diversi in base al loro contesto, ad esempio uno zero può indicare il suo valore numerico, l’assenza di valori o NULL;
- commenti che spiegano cosa fa il codice: indica/ammette che il codice non è abbastanza chiaro;
- nomi oscuri o inconsistenti;
- codice morto: nel programma non deve essere presente del codice irraggiungibile, commentato o non testato. Questo appesantisce il progetto o porta a possibili rischi, è quindi preferibile eliminarlo. Nel caso in cui dovesse tornare utile è possibile recuperarlo utilizzando strumenti di versioning, accedendo a commit precedenti alla sua cancellazione.
- getter e setter: Questi metodi causano la perdita dell’incapsulation e dell’information hiding, perchè esportano esternamente il segreto contenuto nella classe. Sono utili nella fase preliminare della stesura del codice, è importante rimuoverli per far spazio a dei metodi che permettano all’utente di eseguire una specifica operazione da lui richiesta, piuttosto che fornirgli il dato e permettergli di elaborarlo come meglio crede (vedi principio di tell don’t ask nella prossima sezione).
Ecco alucni link utili per approfondire i code smell: