Alcuni principi di progettazione

Alcuni principi di progettazione
Principio Open-Closed
Un modulo dovrebbe essere aperto alle estensioni, ma
chiuso alle modifiche
Derivato dal lavoro di Bertrand Meyer, in sostanza, dice
Dovremmo sempre scrivere le nostre classi in modo che
siano estendibili, senza che debbano essere modificate
Vogliamo essere capaci di cambiare il comportamento
delle senza cambiarne il codice sorgente
Il vantaggio in manutenzione è ovvio: se non modifico,
non faccio errori
Principi base: ereditarietà, overriding, polimorfismo e
binding dinamico
Esempio ben noto: C
Figura
Figura figure[100];
Cerchio
Rettangolo
Triangolo
Trapezio
figure[1] = rettangolo ;
figure[2] = triangolo ;
figure[3] = cerchio ;
void disegnaTutto(Figura figure[]) {
for (i= 0; i<100; i++) {
if (figure[i] è rettangolo ) disegna rettangolo
if (figure[i] è triangolo ) disegna triangolo
if (figure[i] è cerchio ) disegna cerchio
if (figure[i] è trapezio ) disegna trapezio
}
}
Il codice già definito per disegnaTutto deve cambiare per potere
supportare il nuovo tipo!!!
Open/closed?
Quindi la progettazione tradizionale non
verifica open-closed
Il modulo disegnaTutto è aperto a estensioni, ma
solo tramite modifica: non è chiuso rispetto alle
modifiche
Come si può fare a rispettare open/closed?
Aggiungiamo la classe Trapezio
Figura[] figure = new Figura[100];
figure[1] = new Rettangolo();
figure[2] = new Triangolo();
figure[3] = new Cerchio();
figure[4] = new Trapezio();
Figura
Cerchio
Rettangolo
Triangolo
Trapezio
disegnaTutto(figure);
public static void disegnaTutto(Figura[] figure) {
for (i= 0; i<100;i++)
figure[i].disegna();
}
Il Codice definito per
Figura non cambia!!!
Estendibilità tramite ereditarietà è garantita solo
se possiamo usare oggetti di una classe che sono
sostituibili a quelli della sopraclasse
Ad esempio, la disegna() deve contenere codice che
disegna l oggetto this sullo schermo
Ogni sottoclasse di Figura deve definire una disegna()
che abbia lo stesso effetto!
Ma se si definisse disegna() in modo diverso?
Ad esempio, se disegna() di Trapezi fosse definita in modo
che disegna this solo se l area è maggiore di 20? Va ancora
bene?
6
Liskov Substitution Principle
Gli oggetti della sottoclasse devono rispettare il
contratto (ossia la specifica) della superclasse
Significa che il comportamento della sottoclasse deve
essere compatibile con la specifica della sopraclasse
Ma la classe può essere estesa con nuovi metodi, o anche
un metodo ereditatato può essere esteso per coprire
ulteriori casi
Moduli che usano oggetti di un tipo devono potere
della differenza
Il contratto della superclasse deve essere onorato da tutte
le sottoclassi
Ci ritorneremo in seguito per formalizzare meglio
Esempi
Quadrato e Rettangolo
Ortaggio e OrtaggioStagionale
Persona e Studente
Dependency Inversion Principle
Dipendere dalle astrazioni, non dagli elementi
concreti
«Inversione» nel senso che le «in alto» nelle
gerachia di ereditarietà non devono dipendere da
classi «in basso», ma al contrario.
Quindi dipendere da interfacce e classi astratte,
non da metodi e classi concrete
Principio fondante del concetto di progettazione
per componenti (component design)
CORBA, EJB, etc.
Conseguenze:
«Coding to an interface»
Ogni classe C che si ritiene possa essere estesa in futuro,
dovrebbe essere definite come sottotipo di un'interfaccia o di
una classe astratta A
Tutte le volte che non è strettamente indispensabile riferirsi ad
oggetti della classe concreta C, è meglio riferirsi invece a oggetti
il cui tipo statico è la classe astratta A, non C
In questo modo, sarà più facile in seguito modificare il codice
client per utilizzare invece di C altre classi concrete sottotipi di A
Es. la disegnaTutto() era definita in termini dei metodi della classe
astratta Figura, non delle singole classi concrete
Così continua a funzionare correttamente anche aggiungendo nuovi
tipi di figura.
Ereditarietà e delega:
«favour composition over inheritance»
List
List
+ add()
+ remove()
+ add()
+ remove()
Stack
Stack
+ push()
+ pop()
+ top()
+ push()
+ pop()
+ top()
Si vuole implementare Stack con una List: sbagliato ereditare!
Ulteriori principi
The Interface Segregation Principle: Many client specific interfaces
are better than one general purpose interface
The Reuse/Release Equivalency Principle: The granule of reuse is
the same as the granule of release. Only components that are
released through a tracking system can be effectively reused
The Acyclic Dependencies Principle: The dependency structure for
released components must be a directed acyclic graph. There can
be no cycles
The Stable Dependencies Principle: Dependencies between
released categories must run in the direction of stability. The
dependee must be more stable than the depender
The Stable Abstractions Principle: The more stable a class category
is, the more it must consist of abstract classes. A completely stable
category should consist of nothing but abstract classes
Valutazione di qualità di un Progetto
fondamentale
un progetto è
ma possiamo anche valutarlo rispetto a requisiti
Ma anche rispetto a requisiti
modificabilità e stabilità.
Possiamo valutare gli aspetti strutturali un
progetto usando
Accoppiamento, coesione e principio open-closed
Accoppiamento (coupling)
Cattura il grado di interconnessione tra classi
Un alto grado di accoppiamento significa
Alta interdipendenza tra le classi
Difficoltà di modifica della classe singola
Un basso accoppiamento è requisito
fondamentale per creare un sistema
comprensibile e modificabile
Nel mondo ad oggetti abbiamo tre tipi di
coupling: interaction, component e inheritance
Interaction coupling
Si ha quando metodi di una classe chiamano
metodi di altre classi
Le forme da evitare si hanno quando un metodo
Manipola direttamente le variabili di stato di altre
classi
Scambia informazioni attraverso variabili temporanee
Altre forme accettabili: quando i metodi
comunicano direttamente via parametri
passare la minor quantità di informazione possibile
attraverso il minor numero di parametri
Component coupling
Una classe A è accoppiata
possiede:
classe se
Attributi di tipo C, oppure
Parametri di tipo C, oppure
Metodi con variabili locali di tipo C
Quando A è accoppiata a C, è accoppiata
anche a tutte le sue sottoclassi
Component coupling solitamente implica
anche la presenza di interaction coupling
Inheritance coupling
La forma da evitare è quando una sottoclasse
cambia la segnatura di un metodo ereditato
Impossibile in java
Meglio se la segnatura resta la stessa, ma
Ancor meglio se la sottoclasse aggiunge solo
metodi e attributi, ma non non fa override dei
metodi modifica nulla
Coesione
Concetto intra-modulo
Si concentra sul perché gli elementi stanno nello
stesso modulo
Solo gli elementi fortemente correlati dovrebbero
stare nello stesso modulo
Il modulo rappresenterebbe una chiara astrazione e
sarebbe più semplice da capire
Alta coesione solitamente porta a basso
accoppiamento
Tre tipi di coesione: method, class e inheritance
Method cohesion
La forma migliore (massima coesione) si ha
quando un metodo implementa una funzione
singola e chiaramente definita
Si dovrebbe poter descrivere il compito del
metodo con una sola (semplice) frase
Class cohesion
Una classe dovrebbe rappresentare un solo
concetto e tutte le proprietà dovrebbero
contribuire alla sua rappresentazione
Se si incapsulano concetti diversi (nella stessa
classe), la coesione diminuisce
Gruppi di metodi diversi che accedono a
gruppi di attributi disgiunti rappresentano un
chiaro sintomo di bassa coesione
Inheritance cohesion
necessità di
Generalizzazione/specializzazione
Riuso
La coesione è maggiore se la gerarchia viene
usata per gestire la
generalizzazione/specializzazione
Metriche del software
possibile definire delle misure (dette metriche) di queste
caratteristiche
Weighted Methods per Class (WMC)
Numero dei metodi pesati per la loro complessità
Depth of Inheritance Tree (DIT)
Number of Children (NOC)
Coupling Between Classes (CBC)
Response for a Class (RFC)
Il numero totale di metodi che possono essere invocati da un oggetto della
classe
Lack of Cohesion in Methods (LCOM)
Dato un programma, è possibile calcolare questi valori
Gli studi condotti su sw professionale
dicono che (I)
Weighted Methods per Class (WMC)
La maggior parte delle classi ha un numero limitato di metodi
Le classi sono semplici e forniscono astrazioni e operazioni specifiche
Solo un numero limitato di classi ha molti metodi
Valore elevato ha correlazione significativa con propensione agli errori
Depth of Inheritance (DIT)
DIT massimo vicino a 10
La maggior parte delle classi ha DIT uguale a 0 (sono la radice)
quindi sacrificano la riusabilità a favore della comprensibilità
Number of Children (NOC)
Le classi spesso hanno numero limitato di figli; in molti casi NOC vale 0
Gli studi dicono che (II)
Coupling Between Classes (CBC)
La maggior parte delle classi sono auto-contenute (CBC =
0), ovvero non sono accoppiate con nessuno
Gli oggetti di interfaccia tendono ad avere un CBC più
grande
Response for a Class (RFC)
La maggior parte delle classi usa/invoca pochi metodi di
altre classi
Le classi degli oggetti di interfaccia hanno un RFC maggiore
Lack of Cohesion in Methods (LCOM)
Non molto utile per predire la propensione alla difettosità
Sintomi di un progetto
fatto
Rigidità: la tendenza del software ad essere difficile da cambiare,
anche in modo semplice
Ogni cambiamento provoca una cascata di cambiamenti in sequenza
nei moduli dipendenti
Fragilità:
volta che viene cambiato
Spesso il problema si verifica in parti del programma non logicamente
correlate al cambiamento
Immobilità
altre parti dello stesso progetto
Viscosità:
metodi che rispettano il progetto
hack
È facile fare la cosa sbagliata, difficile fare quella giusta
inefficiente
Gestione delle dipendenze
I quattro sintomi elencati nel lucido precedente
sono causati, direttamente o indirettamente, da
dipendenze improprie tra i moduli del software
Quando si ha un degrado nella struttura (architettura)
del software, si ha anche un degrado della sua
manutenibilità
Le dipendenze tra moduli devono essere gestite
attraverso appositi firewall (ossia classi ad hoc)
Le dipendenze non devono propagarsi oltre i firewall
Suggerimenti
Minimizzazione dell interfaccia
Se non esiste una ragione forte per dire che un
metodo è pubblico, lo dichiariamo privato
Definiamo solo metodo getter (e non setter)
per i campi della classe se possibile
Non dobbiamo replicare completamente la
struttura dati contenuta in una classe nella sua
interfaccia
Di solito è assurdo fornire un getter per ogni
attributo
Metodi con pochi parametri
Oltre i tre o quattro si rischia che i
programmatori non se li ricordino
Nonostante l assistenza fornita dagli IDE, i metodi
con troppi parametri rimangono incomprensibili
Particolarmente pericolose sono le liste di
parametri dello stesso tipo
Un inversione non genera errori di compilazione
ma provoca malfunzionamenti difficilmente
diagnosticabili
Metodi con pochi parametri
Un metodo che richiede molti parametri può
generalmente essere spezzato in metodi che
richiedono ciascuno meno parametri
Possiamo creare classi ausiliarie che
contengano gli aggregati dei parametri
Questa tecnica è raccomandabile quando si hanno
sequenze di parametri ricorrenti che
rappresentano sempre la stessa entità
Esempio
Class Cerchio {
public void Cerchio(double x, double y, double z, double raggio)
}
public void Quadrato(double x, double y, double z, double lato)
}
Meglio:
Class Punto {
private double x; private double y, private double z)
}
Class Cerchio
public void Cerchio(Punto centro, double raggio)
}
public void Quadrato(Punto vertice, double lato)
}
Anti-pattern
Un anti-pattern è una soluzione usata spesso,
ma che dovrebbe essere evitata
È un errore comune
Dovrebbe fornire suggerimenti su come migliorare
il codice o evitare errori noti
Gli anti-pattern non si applicano solo alla
progettazione a oggetti
Esempio
Classe Blob
Una sola classe enorme che contiene la maggior
parte della logica applicativa
La classe opera su oggetti di classi che contengono
solo dati, non operazioni.
procedurale, stile C, in un linguaggio a oggetti.
Problemi noti (I)
Codice duplicato
Estrarlo, parametrizzarlo e farlo diventare un
metodo di servizio
Codice simile in sottoclassi correlate
Spostare il codice comune come metodo della
superclasse
Metodi lunghi
Se il codice di un metodo diventa troppo lungo per
poterlo capire facilmente, bisogna estrarne delle
parti come metodi di servizio
Metodi di piccole dimensioni
Metodi di dimensioni limitate possono essere
letti e compresi in modo abbastanza agevole
Metodi brevi e chiari richiedono pochi commenti o
possono non richiederne affatto
Di solito si riesce a mantenere la maggior parte dei
metodi al di sotto delle 20 righe di codice
Raramente è necessario scrivere metodi più lunghi
di 40 righe, salvo il caso di algoritmi particolari.
Es. Non mischiare livelli
public void doTheDomesticThings() {
takeOutTheTrash();
walkTheDog();
for (Dish dish : dirtyDishStack) {
sink.washDish(dish);
teaTowel.dryDish(dish);
}
}
public void doTheDomesticThings() {
takeOutTheTrash();
walkTheDog();
doTheDishes();
}
Es: Ogni metodo una cosa sola
public bool isEdible() {
if (this.ExpirationDate > Date.Now &&
this.ApprovedForConsumption == true &&
this.InspectorId != null) {
return true;
} else {
return false;
}
}
1. Controlla la scadenza
2.
3.
spezione
4. Risponde alla richiesta
Meglio così
public bool isEdible() {
return isFresh() && isApproved() && isInspected();
}
Ora il metodo fa una cosa sola
Un cambiamento nella specifica richiederebbe
un solo cambiamento nel codice
Problemi noti (II)
Funzionalità e dati
classe, bisogna spostare il metodo nella classe in
cui sono definiti i dati
Uso del costrutto switch
Spesso lo switch indica un tipo di oggetti
tipi richiede la modifica
degli switch relativi
Bisognerebbe usare ereditarità, enum, pattern
(State) piuttosto dei switch
Es. Switch e polimorfismo
public Money calculatePay(Employee e) throws InvalidEmployeeType{
switch(e.type){
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
public abstract class Employee {
public abstract Money calculatePay();
}
Problemi noti (III)
Blocchi di dati
Dati che sono solitamente usati insieme
Potrebbero portare a una lunga lista di parametri
(altro problema)
Bisognerebbe introdurre una classe per contenere
i dati correlati
Commenti
Parti di codice con commenti significativi
potrebbero diventare metodi con un nome autoesplicativo
Es. Blocchi di dati (con tipi primitivi)
public Class Car{
private int red, green, blue;
public void paint(int red, int green, int blue){
this.red = red;
this.green = green;
this.blue = blue;
}
}
public Class Car{
private Color color;
public void paint(Color color){
this.color = color;
}
}
Altri errori comuni
Crescita del codice
Metodi lunghi e classi grandi
Violazione del principio della responsabilità singola
Ossessione per i tipi primitivi e troppi parametri
Sinonimo di cattiva progettazione
Statement switch su oggetti
Meglio usare il polimorfismo
Classi alternative con interfacce diverse
Sinonimo di cattiva progettazione della gerarchia delle classi
Es. Cattivo uso
Alcune sottoclassi non usano i metodi e gli attributi della superclasse
Es: Quota non riguarda Engineer
Dovrebbe essere solo parte di Salesman
public abstract class Employee{
private int quota;
public int getQuota();
...
}
public class Salesman extends Employee{ ... }
public class Engineer extends Employee{
...
public int getQuota(){
throw new NotSupportedException();
}
}
Ancora errori comuni
Alcune classi sono troppo accoppiate
Intimità non appropriata
Una classe dovrebbe conoscere il meno possibile delle
altre classi
Abuso di proprietà altrui
Responsabilità mal distribuite
Catene di invocazioni
Accesso ai dati troppo complesso
Le eccezioni sono usate poco
Es. Abuso di proprietà altrui
public class Customer{
private PhoneNumber mobilePhone;
...
public String getMobilePhoneNumber(){
return ( +
mobilePhone.getAreaCode() + ) +
mobilePhone.getPrefix() + - +
mobilePhone.getNumber();
}
}
public String getMobilePhoneNumber(){
return mobilePhone.toFormattedString();
}
Es. Catene di invocazioni
a.getB().getC().getD().getTheNeededData()
a.getTheNeededData()
Es. Non uso eccezioni
Gli errori devono essere codificati
I controlli richiedono codice
Es: Codice difficile da estendere
public int foo(){
...
}
public void bar(){
if(foo() == OK)
...
else
// error handling
}
Es. Con le eccezioni
Non si mischiano valori restituiti e di controllo
Sintassi più chiara
Codice facile da estendere
public void foo() throws FooException{
...
}
public void bar(){
try{
foo();
...
} catch(FooException){
// error handling
}}
Refactoring
È abbastanza difficile fare la cosa giusta al
primo colpo
Spesso le soluzioni trovate richiedono refactoring
Ovvero il miglioramento del progetto del codice
esistente senza cambiarne il comportamento
Miglioramenti al codice/architettura
Piccoli miglioramenti
Test di regressione continuo
Framework Junit
Refactoring
Il tempo e la manutenzione potrebbero rendere
disordinato il codice
Codice disordinato riduce le possibilità di
manutenzione
Il refactoring rinfresca/pulisce il codice senza
cambiarne le funzionalità
Applica i pattern
Rimuove gli anti-pattern
Ottimizza le prestazioni (velocità e memoria)
Aggiunge commenti (magari attraverso JavaDocs)
Quando fare refactoring
Non esiste regola precisa
Quando si intravedono problemi
Parti di codice particolarmente difficili
Parti troppo complesse
Parti con anti-pattern evidenti
Meglio fare refactoring prima di aggiungere
nuove funzionalità o cambiare le esistenti
Stile di Programmazione e
Commenti
Codice riusabile, manutenibile
ed estendibile
Pattern stilistici
Sono pratiche riguardanti la scrittura del
codice, non la progettazione
Es. convenzioni come identazione, scelta dei nomi
delle variabili, ecc.
Spesso, sono supportati dall IDE
Spesso, sono verificabili
È bene prendere le buone abitudini subito
È giusto concordare alcune pratiche nel
gruppo di lavoro
Commenti
Il codice è spesso scritto a un livello per essere una
buona descrizione
I commenti possono essere usati per chiarire il
significato del codice o per renderlo ancora più oscuro!
I commenti che ripetono il codice sono inutili
I commenti che contraddicono il codice indicano che
probabilmente sia codice che commenti sono scorretti
I commenti possono indicare il significato del codice in
modo parzialmente indipendente da cambiamenti
I commenti devono indicare cosa il codice si propone di
fare
Esempi
Commento inutile: i++; // Increment i
Commento utile: i++; // Increment the card counter.
Qui non c è bisogno di commento: cardCounter++;
Una buona scelta dei nomi delle variabili, dei metodi,
ecc. rende il codice largamente autocommentato
Dove/cosa commentare
Cosa commentare dipende da molteplici fattori,
in particolare dall espressività del linguaggio
In Java generalmente non è il caso di avere un
commento per ogni riga di codice
Nei linguaggi OO è generalmente una buona idea
fornire un commento all inizio di ogni metodo,
che dettagli il significato del metodo, eventuali
vincoli, requisiti ecc.
Può essere utile riportare anche riferimenti a
documentazione esterna
Al solito, dare un nome significativo ai metodi può
semplificare il commento
Quando commentare
Occorre commentare quando è necessario dire
qualcosa con maggiore chiarezza di quanto non
possa fare il codice
Se si può rendere il codice abbastanza chiaro da
poterlo considerare autocommentato, questo è
ciò che va fatto!
Valutare sempre la conoscenza di chi leggerà il codice
Se l informazione per comprendere il codice si trova
vicino allora il codice può essere considerato
comprensibile
Nomi e maiuscole
Si può fare in modo che il nome di un
elemento indichi la natura dell elemento
Questo effetto si ottiene mediante delle
convenzioni di programmazione , solitamente
stabilite a livello aziendale
In modo più artigianale, si possono usare le
maiuscole in modo convenzionale per
comunicare informazioni
Maiuscole e Java
Nomi di classi e interfacce hanno l iniziale maiuscola:
Stack
Nomi di variabili e metodi iniziano sempre con una
minuscola: push(...).
I nomi delle costanti sono completamente maiuscoli:
java.lang.Math.PI;
I nomi di package sono scritti in minuscolo: java.lang
Se il nome è composto da più parole, ciascuna di
queste è scritta con l iniziale maiuscola:
mustCopyToRunstack(...)
L uso di underscore (must_copy_to_runstack) non è
consigliato
Uso consistente dei nomi
I nomi delle classi dovrebbero essere singolari: Stack
I metodi void dovrebbero avere per nome dei predicati
verbali che descrivono cosa fanno: openFiles()
I metodi e le variabili boolean dovrebbero avere nomi
che iniziano con una declinazione di essere :
isFinished
contains» nelle Collections di java!)
Gli altri metodi non void dovrebbero avere nomi che
suggeriscono cosa restituiscono: sizeOfFigure()
Variabili non boolean dovrebbero avere nomi costituiti
da sostantivi (eventualmente con aggettivo): age
Codice comprensibile
Codice semplice, ma cosa fa?
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
Meglio?
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
Così?
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
Abbiamo
Usato nomi significativi per spiegare le
intenzioni
flaggedCells invece di list1
Sostituito numeri magici con costanti
cell[STATUS_VALUE] invece di x[0]
Creato un ADT opportuno
Cell cell invece di int[] cell