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
© Copyright 2024 ExpyDoc