Obiettivi finali di un buon progetto:
Cercare di risolvere un problema in una volta sola è in genere più difficile che risolverlo per parti.
VANTAGGI:
Decomporre il Sistema in sottosistemi, gestibili separatamente, per dominarne la complessità.
P= problema, C=complessità di P, E=sforzo per la risoluzione di P
Dati 2 problemi P1 e P2
se C(P1) > C(P2), allora E(P1)> E(P2)
ma è stato dimostrato empiricamente che C(P1+P2)> C(P1) + C(P2),
e quindi si ha che E(P1+P2)> E(P1) + E(P2)
MA….
La scomposizione introduce il problema della comunicazione fra le varie parti, il che aggiunge complessità per l’interfacciamento.
Intuitivamente, la complessità dell’interfacciamento aumenta con il quadrato del numero dei moduli.
La coesione ci dice con quale criterio suddividere in parti.
Un sottosistema o un modulo avrà elevata coesione se contiene al suo interno elementi correlati fra di loro e lascia fuori il resto.
La coesione permette di comprendere e modificare meglio ogni singolo elemento.
Diversi tipi di coesione (dalla più alta alla più bassa):
Funzionale, Layer, Comunicazionale, Sequenziale, Procedurale, Temporale, di Utilità.
Un modulo che svolge un’unica elaborazione e restituisce un risultato, senza produrre effetti collaterali (side-effects):
Benefici per il sistema:
Moduli che aggiornano un database, creano un nuovo file o interagiscono con l’utente non sono a coesione funzionale.
Tutte le componenti che forniscono o danno l’accesso ad un insieme di servizi correlati sono tenute insieme in un livello, tutto il resto ne è fuori.
I livelli dovrebbero formare una gerarchia
Esempi di servizi: servizi necessari per elaborazioni, memorizzazione di dati, per gestire la sicurezza, per interagire con gli utenti, accedere al sistema operativo, interagire con l’hardware.
L’insieme di procedure o metodi attraverso i quali un livello fornisce i suoi servizi costituisce la application programming interface (API).
Si può sostituire un livello con un livello equivalente, senza alcun impatto su altri livelli.
In pratica basta che l’API sia replicata nel nuovo livello.
Tutti i moduli che accedono o manipolano certi dati sono tenuti insieme (e.g. nella stessa classe) – e tutto il resto sta fuori.
Una classe può avere una buona coesione comunicazionale
Un modulo che aggiorna un database e un modulo che aggiorna un file di log delle modifiche al database dovrebbero stare insieme in uno stesso modulo (a coesione comunicazionale).
Un modulo a coesione comunicazionale potrebbe stare in un layer.
Vantaggio: quando bisogna modificare i dati, tutto il codice relativo ai dati si troverà in un solo posto.
Coesione Sequenziale
Una serie di procedure in cui una fornisce input alla successiva sono tenute insieme- e tutto il resto è fuori.
Coesione Procedurale
Tenere insieme in un solo modulo varie procedure che sono usate l’una dopo l’altra.
Coesione Temporale
Operazioni svolte durante la stessa fase di esecuzione del programma sono tenute insieme.
Ad esempio, tutto il codice usato durante la fase di avvio del sistema, o di inizializzazione, di terminazione, o in casi particolari.
Ovviamente, un modulo che inizializza variabili non deve farlo direttamente, ma invocando le apposite procedure di inizializzazione di altri moduli.
É più debole della coesione procedurale.
È molto utile in presenza di regioni critiche (durante le quali l’accesso a certi dati/servizi deve avvenire in mutua esclusione).
Coesione di utilità
Quando utilità correlate che non possono essere logicamente collocate in altri moduli coesivi sono contenute nello stesso modulo.
Un’utilità è una procedura o una classe con ampia applicabilità a vari sottosistemi e che è stata progettata per essere riusabile.
Per esempio, la java.lang.Math class.
Coesione comunicazionale
Tutte le informazioni relative ad una prenotazione sono contenute in un’unica classe.
Coesione funzionale
Un modulo che converte un’immagine bitmap in Jpeg.
Coesione procedurale
Un sottosistema che ogni notte genera statistiche sulle vendite del giorno precedente.
Coesione sequenziale
Un’operazione di data processing che riceve input da diverse sorgenti, ordina gli input, fa una sintesi dei dati, li riordina in base alla sorgente che ne fa prodotti di più, e qundi restituisce i risultati.
L’accoppiamento (Coupling) fra moduli esiste quando ci sono interdipendenze tra un modulo e l’altro
Si verifica quando un componente riesce ‘clandestinamente’ a modificare dati interni ad un altro componente.
private
;get e set
.Utilizzo di aritmetica dei puntatori
Utilizzo di GOTO
Modifica di attributi tramite oggetti
É tipico dell’uso di variabili globali
Si verifica quando una procedura ne chiama un’altra passandole un ‘flag’ o un ‘comando’ che controlla esplicitamente il comportamento della seconda procedura.
Per fare una modifica, bisogna cambiare sia il chiamante che il chiamato.
L’uso di metodi polimorfi è in genere il modo migliore per evitare questo accoppiamento.
Esempio: Primitiva semctl di accesso ai semafori
In alcuni linguaggi (Java, Smalltalk, etc.) esiste la “riflessione”.
Dato un oggetto, è possibile conoscere dettagli della classe cui appartiene
Method m=oggetto.getClass().getMethod("nomeMetodo",args)
;
Ad esempio, si potrebbe passare come parametro di una chiamata di metodo un metodo di un oggetto di un’altra classe!
Si ha quando un oggetto è dichiarato come il tipo dell’argomento di un metodo.
Si verifica quando i tipi degli argomenti di un metodo o sono primitivi, o semplici classi di libreria.
Più argomenti un metodo possiede, maggiore è l’accoppiamento.
Tutti i metodi che usano quel metodo gli devono passare tutti gli argomenti.
Si può ridurre questo accoppiamento, evitando di definire in ogni metodo argomenti non necessari.
Necessità di un compromesso fra data coupling e stamp coupling.
Aumentando l’uno, in genere diminuisce l’altro.
Si verifica quando una routine (o un metodo in un sistema object oriented) ne chiama un’altra.
Le routine sono accoppiate perchè la prima dipende dal comportamento (e dall’interfaccia) dell’altra;
É una forma di accoppiamento inevitabile in ogni sistema.
Se viene invocata ripetitivamente una sequenza di due o più metodi per calcolare qualcosa;
Si può ridurre questo accoppiamento scrivendo una sola routine che incapsula la sequenza;
Le eventuali modifiche delle routine impatteranno solo sulla routine che incapsula le altre.
Si verifica quando un modulo usa un tipo di dato definito in un altro modulo.
Si verifica ogni volta che una classe dichiara una variabile di istanza o una variabile locale del tipo di un’altra classe.
La conseguenza è che se cambia la definizione del tipo, anche gli utenti del tipo possono dover cambiare.
É bene dichiarare il tipo di una variabile della classe più generale possibile che contiene le operazioni richieste, per poi specializzare la classe, se occorre.
Accoppiamento per Inclusione o Importazione
Accoppiamento esterno
Quando un modulo dipende da cose quali il sistema operativo, librerie condivise, o dall’hardware.
É bene ridurre il numero di punti nel codice in cui esistono queste dipendenze.
Assicurarsi che il progetto permetta di nascondere o ritardare gli aspetti di dettaglio, riducendo la complessità.
Varie forme di astrazione: Procedurale, sui dati, sul controllo;
Una buona astrazione realizza information hiding (occultamento dei dettagli realizzativi);
L’astrazione permette di comprendere l’essenza di un sottosistema senza richiedere la conoscenza di dettagli insignificanti.
Le classi sono astrazioni sui dati che contengono a loro volta astrazioni procedurali.
Si può aumentare l’astrazione definendo tutte le variabili come private;
Meno metodi pubblici ha una classe, migliore è l’astrazione;
Superclassi ed interfacce aumentano il livello di astrazione;
Attributi e associazioni sono ulteriori astrazioni sui dati;
I metodi sono astrazioni procedurali
Progettare i vari aspetti del sistema in modo che possano essere riusati anche in altri contesti.
Progettare facendo riuso è complementare al progettare per la riusabilità.
Riusare il progetto ed il codice altui permette di avvalersi di altri investimenti fatti per ottenere componenti riusabili.
Componenti riusabili: Librerie di funzioni, di classi, frameworks, design patterns …
Clonare il codice (ossia copiarlo e riportarlo in più punti) non è una forma di riuso da attuare:
Anticipare concretamente le modifiche cui un progetto potrà essere sottoposto in futuro e prepararsi per gestirle.
Prevedere i cambiamenti delle tecnologie e degli ambienti operativi in modo che il software possa continuare a funzionare o possa essere cambiato facilmente.
Evitare l’uso delle prime release di una tecnologia;
Evitare librerie software specifiche di un dato ambiente di programmazione (meglio gli standard);
Evitare di usare caratteristiche non documentate o poco usate delle librerie software;
Evitare di usare software o hardware speciale di compagnie che non forniranno supporto a lungo;
Usare linguaggi standard e tecnologie supportate da più venditori.
Permettere al software di funzionare su più piattaforme.
Evitare l’uso di risorse che sono specifiche di un particolare ambiente;
Vincolerebbero il sistema all’ambiente per sempre!
Es. una libreria disponibile solo per Microsoft Windows.
Incapsulare all’interno di singoli moduli tutto il codice specifico di una certa tecnologia.
Eseguire le azioni necessarie a semplificare il testing.
Progettare un programma che possa eseguire automaticamente i test.
Assicurarsi che tutte le funzionalità del codice possano essere guidate da un programma esterno, bypassando l’interfaccia grafica utente.
In pratica, occorre fornire anche una versione a linea di comando per le varie funzionalità.
In Java, si può creare un metodo main() in ogni classe che esercita tutti gli altri metodi.
Framework come XUnit supportano la scrittura di classi di test e la loro esecuzione.
Non fidarsi mai di come gli altri useranno un componente che state progettando.
Gestire tutti i casi in cui altro codice potrebbe tentare di usare il tuo componente inappropriatamente.
É necessario validare tutti gli input di un componente: oppure controllare le precondizioni.
Sfortunamente, una validazione molto accurata può significare controlli ripetitivi.
Una tenica per progettare in modo difensivo in maniera efficiente e sistematica.
1. Introduzione
4. Casi d'uso
6. Class Diagram – parte prima
7. Class diagram – parte seconda
8. Class diagram – parte terza
9. Modellazione architetturale
10. Sequence Diagram
14. Progettazione Architetturale
15. Design Patterns – Parte prima
16. Design Patterns – Parte seconda
17. Progettazione dell'interfaccia utente
I. Sommerville – Ingegneria del Software – 8a edizione – Cap. 11, 13, 14
T. Lethbridge, R. Laganière - Object-Oriented Software Engineering: Practical Software Development using UML and Java – Capitolo 9 (http://www.site.uottawa.ca/school/research/lloseng/).