Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Porfirio Tramontana » 13.Principi di progettazione


Principi per una buona progettazione

Obiettivi finali di un buon progetto:

  • Aumentare il profitto riducendo i costi e aumentando i guadagni;
  • Assicurarsi della conformità con i requisiti;
  • Accelerare lo sviluppo;
  • Migliorare la qualità, (per ridurre i costi e aumentare i guadagni).

Principio di progetto 1: Decomposizione

Cercare di risolvere un problema in una volta sola è in genere più difficile che risolverlo per parti.

VANTAGGI:

  1. I requisiti da implementare possono essere divisi fra diversi sottosistemi;
  2. Persone diverse possono lavorare su parti diverse;
  3. Ogni ingegnere del software può specializzarsi su parti diverse;
  4. Ogni singolo componente sarà più piccolo e dunque più semplice da capire;
  5. Le varie parti potranno essere sostituite senza dover sostituire o cambiare molto le altre parti.

Decomposizione

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.


Principio di progetto 2: aumentare la coesione

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à.

Coesione Funzionale

Un modulo che svolge un’unica elaborazione e restituisce un risultato, senza produrre effetti collaterali (side-effects):

  • Lascia il sistema nello stesso stato precedente l’elaborazione.
  • Fornendogli gli stessi input, restituirà gli stessi output
    • Es. Moduli che eseguono calcoli matematici, risolvono equazioni, statistiche di dati, …

Benefici per il sistema:

  • Più facile da comprendere;
  • Più riusabile;
  • Più facile da sostituire, modificare.

Moduli che aggiornano un database, creano un nuovo file o interagiscono con l’utente non sono a coesione funzionale.

Coesione di Livello (Layer)

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

  1. Livelli più alti possono accedere a servizi dei livelli più bassi;
  2. Livelli bassi non accedono ai livelli più alti.

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.

Coesione Comunicazionale

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

  • Se tutte le funzioni del sistema che memorizzano e manipolanoi i suoi dati sono contenute esclusivamente in questa classe;
  • Se la classe non fa altro che gestire i propri dati.

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 e Coesione Procedurale

Coesione Sequenziale
Una serie di procedure in cui una fornisce input alla successiva sono tenute insieme- e tutto il resto è fuori.

  • Può semplificare il progetto, in particolare evita la necessità di serializzare/deserializzare i dati privati per poter consentire l’accesso ad altre classi.

Coesione Procedurale
Tenere insieme in un solo modulo varie procedure che sono usate l’una dopo l’altra.

  • Le procedure non forniscono necessariamente input alle successive.
  • Es. un modulo che svolge in sequenza tutte le azioni necessarie a registrare uno studente in un corso.
  • É una coesione più debole di quella sequenziale (perchè manca lo scambio dei dati).
  • Semplifica la gestione della visibilità dei metodi delle classi: tutti quelli coinvolti nella procedura possono essere visibili a livello di package.

Coesione Temporale e di Utilità

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.

Esempi di coesione

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.

Principio di Progetto 3: Ridurre l’accoppiamento

L’accoppiamento (Coupling) fra moduli esiste quando ci sono interdipendenze tra un modulo e l’altro

  • Se ci sono interdipendenze, le modifiche in un punto richiederanno modifiche anche altrove.
  • Una rete di interdipendenze rende difficile capire come un componente lavora effettivamente.
  • Non si può riusare un modulo senza tutti gli altri.
  • Ridurre l’accoppiamento diminuendo il numero e la forza delle connessioni.

Tipi di accoppiamento


Accoppiamento sul Contenuto

Si verifica quando un componente riesce ‘clandestinamente’ a modificare dati interni ad un altro componente.

  • Va evitato perchè ogni modifica ai dati dovrebbe essere localizzata facilmente.
  • Per ridurre questo accoppiamento si dovrebbero incapsulare tutte le variabili d’istanza
    • dichiarandole private;
    • Fornendo i metodi di get e set.
  • Una peggiore forma di accoppiamento per contenuto è quella in cui si può modificare una variabile di istanza dichiarata all’interno di un’altra variabile di istanza.

Utilizzo di aritmetica dei puntatori

  • Es. Viene passato un puntatore ad un attributo di un oggetto, ma tramite l’aritmetica dei puntatori si riesce ad accedere ad altri attributi privati (o addirittura ad istanze di altri oggetti!)

Accoppiamento sul Contenuto (segue)

Utilizzo di GOTO

  • il goto (salto incondizionato a riga di programma) può essere visto come un utilizzo non vincolato di puntatori a funzione.

Modifica di attributi tramite oggetti

  • Possibile quando un passaggio per riferimento è utilizzato al posto di un passaggio per valore, nonostante i metodi get e set possano essere dichiarati privati.

Accoppiamento per dati comuni

É tipico dell’uso di variabili globali

  • Tutti i componenti che usano la variabile globale diventano accoppiati tra di loro;
  • Una forma più lieve di accoppiamento comune si ha quando una variabile può essere acceduta solo da un sottoinsieme di classi del sistema
    • es. un package Java;
  • Può essere accettabile per creare variabili globali che rappresentano valori di default costanti usati in tutto il sistema;
  • Le variabili globali creano dependency allo stesso modo delle variabili esplicitamente passate per parametro.

Accoppiamento sul Controllo

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

  • int semctl (int semid, int semnum, int cmd, args);
    • cmd: operazione da eseguire:
      • IPC_STAT per leggere dati del gruppo di semafori
      • IPC_SET per variare alcune caratteristiche (es.:permessi)
      • IPC_RMID per rilasciare la risorsa
      • GETVAL restituisce il valore di ’semval’ del semaforo specificato
      • SETVAL setta ad un intero il ’semval’ del semaforo
      • GETALL legge i valori di ’semval’ di tutto il gruppo
      • SETALL setta il valore dei semafori di un gruppo

Accoppiamento sul Controllo (segue)

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!

Stamp coupling

Si ha quando un oggetto è dichiarato come il tipo dell’argomento di un metodo.

  • Il metodo a cui viene passata l’istanza dell’altra classe può eseguire tutti i metodi pubblici della classe, anche se non ce ne sarebbe bisogno.
  • Siccome una classe usa l’altra, è più difficile cambiare il sistema
    • Riusare una classe richiede il riuso anche dell’altra.
  • Un modo per ridurre lo stamp coupling
    • Passando variabili semplici.

Accoppiamento sui Dati

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.

Accoppiamento per Chiamata di Routine

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.


Accoppiamento per uso di Tipo

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.

  • Es. usare il tipo List dall’interfaccia java.util.List e poi istanziare la specifica lista

Accoppiamento per Inclusione e Esterno

Accoppiamento per Inclusione o Importazione

  • Si verifica quando un componente importa un package (come in Java)
  • O quando un componente ne include un altro (come in C++).
    • Il componente che include o importa componenti dipende da tutto ciò che si trova nel componente incluso o importato.
    • Se il componente incluso o importato modifica o aggiunge qualcosa:
      • Si può generare un conflitto con qualcosa contenuto nell’includente, obbligando quest’ultimo a cambiare.
    • Un elemento in un componente importato potrebbe avere lo stesso nome di qualcosa già definito.

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.

Astrazione

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

  • Le migliori astrazioni si ottengono dando ai metodi pochi parametri.

Principio di progetto 5: aumentare la riusabilità

Progettare i vari aspetti del sistema in modo che possano essere riusati anche in altri contesti.

  • Generalizzare il progetto il più possibile
  • Semplificare il progetto il più possibile:
    • Il riuso di un componente piccolo è più frequente
      • Un componente piccolo e semplice, però, risolve un problema altrettanto semplice!

Principio di progetto 6: Riusa altri progetti e codice

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:

  • Il codice riusato può essere assunto come corretto;
  • Il codice clonato, invece, deve essere testato ogni volta che lo si modifica.

Principio di progetto 7: Progettare per la flessibilità

Anticipare concretamente le modifiche cui un progetto potrà essere sottoposto in futuro e prepararsi per gestirle.

  • Ridurre l’accoppiamento ed aumentare la coesione
  • Creare astrazioni
  • Non prestabilire limitazioni (es. costanti) nel codice
  • Lasciarsi aperte varie alternative
    • Meglio associare ad ogni evento la chiamata ad un metodo che lo gestisce, piuttosto che gestirlo direttamente
    • Facilita il lavoro di chi dovrà modificare il sistema
  • Usare codice riusabile e produrre codice riusabile.

Principio di progetto 8: Anticipare l’obsolescenza

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.

Principio di progetto 9: Progettare per la Portabilità

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.

Principio di progetto 10: Progettare per la Testabilità

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.

Principio di progetto 11: Progettare in modo difensivo

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.

Design by contract

Una tenica per progettare in modo difensivo in maniera efficiente e sistematica.

  • Idea di base
    • Ciascun metodo ha un contratto con i suoi chiamanti
  • Il contratto ha una serie di asserzioni che stabiliscono:
    • Quali precondizioni il metodo chiamato richiede che siano soddisfatte quando comincia l’esecuzione;
    • Quali postcondizioni metodo chiamato garantisce essere vere al termine della sua esecuzione;
    • Quali invarianti metodo chiamato garantisce di non modificare durante l’esecuzione.

I materiali di supporto della lezione

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/).

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion