Versione per la stampa       Invia     
Valuta il contenuto e lascia un commento
MSDN
MSDN Library
Articoli tecnici
 Disegno orientato agli oggetti: ass...

  Attiva vista per larghezza di banda ridotta
Disegno orientato agli oggetti: assegnare le responsabilità

Di Riccardo Golia

Il disegno di un sistema software è l’insieme delle attività svolte da una o più persone con competenze di progettazione allo scopo di individuare tra le possibili soluzioni che soddisfano i requisiti funzionali evidenziati nell’analisi, quella che meglio soddisfa la qualità attesa (espressa anche dai requisiti non funzionali), rispettando i vincoli esistenti. L’attività principale consiste nella scomposizione del sistema in una serie di moduli a minore complessità (sottosistemi, componenti software, ecc.) per ciascuno dei quali vengono individuate e definite relazioni, collaborazioni e responsabilità.

Nel caso della progettazione di applicazioni basate sul paradigma ad oggetti si parla di Object Oriented Design (OOD), ovvero disegno orientato agli oggetti. In questo caso l’operazione di scomposizione produce una struttura di classi collegate tra loro da legami di diversa natura (gerarchica, di collaborazione, di dipendenza) e organizzate logicamente (per esempio, in namespace) e fisicamente (per esempio, in assembly) in funzione delle scelte architetturali effettuate.

Il risultato prodotto dal disegno è destinato a guidare tutte le attività che caratterizzano la realizzazione e il rilascio di un sistema software (sviluppo, integrazione, test). Le attività di disegno infatti non necessariamente si esauriscono durante la fase preliminare di un progetto software. A tal proposito possiamo identificare due diversi approcci, non necessariamente tra loro discordanti:

  • disegno up-front: le attività di progettazione vengono completate prima di iniziare lo sviluppo. In genere il disegno viene svolto da una figura dedicata (l’architetto) con competenze di progettazione e, nei casi peggiori, senza competenze di sviluppo;

  • disegno continuo o incrementale: le attività di progettazione sono parallele allo sviluppo. Il disegno viene svolto da tutto il team di sviluppo durante la stesura del codice mediante una serie di azioni di rifattorizzazione e ottimizzazione (refactoring).

Come già anticipato, i due diversi approcci possono coesistere: il disegno up-front permette di effettuare le scelte iniziali utili ad avviare le attività di sviluppo, mentre il refactoring consente di adattare il disegno al cambimento delle specifiche, alle nuove informazioni che emergono durante lo svolgimento del progetto e all'esperienza maturata dal team nella realizzazione del sistema.

In questa pagina

L’analisi dei requisiti e il disegno L’analisi dei requisiti e il disegno
Metriche Metriche
Accoppiamento Accoppiamento
Coesione Coesione
Criteri di ripartizione delle responsabilità Criteri di ripartizione delle responsabilità
Conclusioni Conclusioni
Riferimenti utili Riferimenti utili

L’analisi dei requisiti e il disegno

Uno dei fraintendimenti più comuni consiste nel considerare l’analisi dei requisiti come parte integrante del disegno. In realtà queste due attività, seppur tra loro collegate e dipendenti, differiscono fra loro dal momento che si focalizzano su aspetti diversi.

L'analisi descrive che cosa un sistema software deve fare, ovvero il suo comportamento esterno in relazione all’uso che ne viene fatto dagli utilizzatori finali. Per questo motivo il linguaggio usato nell’analisi è quello dell’utente: le specifiche, raccolte dall’analista con la collaborazione del committente, sono spesso espresse in linguaggio naturale e si focalizzano sugli aspetti funzionali, sui casi d’uso e sulle modalità di interazione con il mondo esterno. Il risultato è un insieme organico, più o meno formale, di requisiti utili ai progettisti per capire il campo d’azione (scope) e per poter procedere con il disegno.

Diversamente dall’analisi, il disegno si concentra anche sugli aspetti non funzionali che concorrono al raggiungimento del livello qualitativo atteso in termini di manutenibilità, affidabilità, sicurezza, usabilità ed estendibilità. Di fatto il disegno descrive il comportamento interno del sistema, viene svolto dall’architetto con una maggiore o minore partecipazione degli sviluppatori a seconda dell’approccio usato (up-front, refactoring, misto) ed è mirato a fornire indicazioni che riguardano l’implementazione vera e propria.

Allo scopo di fare scelte di disegno sensate in funzione della tecnologia utilizzata per lo sviluppo, l’architetto è chiamato a conoscere la piattaforma su cui si trova ad operare. Determinate valutazioni possono essere fatte unicamente conoscendo i vincoli imposti dall’ambito tecnologico scelto. Pertanto l’architetto, a differenza dell’analista che ha una visione vicina a quella del committente e dell’utente finale, condivide il punto di vista degli sviluppatori e valuta le sue scelte in funzione di come la tecnologia può influenzare e caratterizzare lo sviluppo.

Metriche

Per poter valutare l’efficacia del disegno occorre poter misurare in qualche modo il suo andamento e i risultati ottenuti. Innanzitutto si rivela necessario capire “quanto disegno” occorre fare di volta in volta. Il disegno è sufficiente quando ha raggiunto due scopi principali:

  • dominare la complessità attraverso il processo di scomposizione di cui abbiamo parlato precedentemente;

  • permettere l’evoluzione e la manutenibilità del sistema.

Inizialmente un progetto di sviluppo può sembrare piuttosto complesso e presentare punti oscuri di non immediata soluzione. Nel momento in cui abbiamo la sensazione che la complessità e le incertezze siano state in qualche modo affrontate e gestite opportunamente, il disegno ha raggiunto il suo scopo e non serve farne altro. Quando il committente richiede variazioni alle funzionalità esistenti o necessita di nuove funzionalità, possiamo avere difficoltà ad operare modifiche nel codice e nella struttura ad oggetti evidenziata durante il disegno. Se, apportando le modifiche nel codice, non avvertiamo impedimenti che frenano il cambiamento e l'evoluzione del sistema, il disegno ha raggiunto il suo scopo e non serve farne altro.

Pertanto un sistema software è ben progettato nel momento cui presenta le tre seguenti caratteristiche: flessibilità, robustezza e riusabilità. Un sistema è flessibile nel momento in cui è possibile operare modifiche intervenendo in sezioni isolate e delimitate del codice e i cambiamenti non influenzano troppe parti del sistema stesso. Un sistema è robusto quando le modifiche non portano alla regressione: ogni cambiamento incide solamente sulle parti logicamente correlate e non è causa di rotture in parti non direttamente correlate. Un sistema è riusabile nel momento in cui diventa immediato e semplice estrarre da esso le funzionalità per riutilizzarle.

Le tre caratteristiche evidenziate sono spesso correlate tra loro, in quanto le cause delle situazioni di cattivo disegno dipendono da aspetti di valenza generale quali l’alto accoppiamento e la bassa coesione. Possiamo misurare il buon disegno in funzione di queste due grandezze, individuando a tal scopo due metriche fondamentali:

  • basso accoppiamento (low coupling): la dipendenza di un componente software dagli altri componenti è bassa;

  • alta coesione (high cohesion): l’omogeneità funzionale di ciascun componente software è alta e i diversi componenti collaborano tra loro per ottenere nuove funzionalità ad un livello di complessità maggiore.

In un’ottica di favorire l’evoluzione e il riuso del codice, i progettisti spesso ricorrono a tecniche e soluzioni collaudate e di provata efficacia. È il caso per esempio dei design pattern, che forniscono soluzioni di natura generale a problematiche di disegno note. I design pattern peraltro si basano su una serie di principi di base, la cui applicazione è mirata a favorire la coesione e a limitare l’accoppiamento. Molte delle soluzioni suggerite dai design pattern infatti distribuiscono tra più oggetti le diverse responsabilità, favorendo la presenza di moduli software fortemente specializzati e ad alta coesione. L’accoppiamento viene limitato attraverso un ampio uso dell’astrazione allo scopo di non creare dipendenze dirette tra le diverse implementazioni concrete utilizzate, favorendo di conseguenza l’estendibilità.

Accoppiamento

Prima di procedere nell’articolo, è bene approfondire i concetti introdotti nel paragrafo precedente, ovvero l’accoppiamento e la coesione. Partiamo innanzitutto dal primo.

L'accoppiamento (coupling) è la misura di quanto fortemente un elemento (per esempio, una classe) è connesso, conosce o dipende da altri elementi. Un elemento caratterizzato da un basso accoppiamento non dipende da tanti altri elementi (la quantità è relativa e dipende dal contesto). Viceversa un alto accoppiamento presuppone molte dipendenze che possono comportare non pochi problemi in fatto di estendibilità e manutenibilità.

Le classi fortemente accoppiate possono presentare i seguenti problemi:

  • i cambiamenti nelle classi correlate obbligano a cambiamenti locali a discapito della flessibilità;

  • le classi correlate sono più difficili da isolare, il che è un sintomo di fragilità;

  • le classi correlate sono più difficili da riusare.

Ma in che modo si manifesta l’accoppiamento? Dati due tipi A e B, le forme più comuni di accoppiamento possono essere le seguenti:

  • la classe A ha un membro di tipo B o referenzia una istanza di B;

  • A richiama e usa servizi di B;

  • A ha un metodo che include uno o più parametri di tipo B;

  • A è una classe derivata direttamente o indirettamente dalla classe base B;

  • la classe A implementa un’interfaccia di tipo B.

Coesione

La coesione (cohesion) è la misura di quanto fortemente siano correlate e concentrate le responsabilità di un elemento quale una classe o un sottosistema. Un elemento caratterizzato da un’alta coesione possiede responsabilità correlate senza eseguire una quantità di lavoro eccessiva.

Una classe con bassa coesione fa tante cose insieme, svolgendo molto lavoro "sparso" e non correlato (ha troppe responsabilità). Questo tipo di situazione sarebbe da evitare in quanto queste classi risultano:

  • difficili da isolare (bassa robustezza);

  • complesse da riutilizzare (bassa riusabilità);

  • complicate da manutenere (scarsa manutenibiltà);

  • delicate e critiche in quanto soggette a continui cambiamenti (bassa flessibilità).

Una forma comune di bassa coesione si ha in quelle classi che presentano un grandissimo numero di metodi (pubblici o privati). In questi casi si assiste ad una concentrazione di responsabilità in un unico elemento, il che favorisce la dipendenza da un numero considerevole di classi. La scarsa specializzazione infatti si traduce nella necessità di dover interagire e comunicare con un numero elevato di altre classi e con servizi applicativi eterogenei. In queste situazioni la bassa coesione si accompagna inevitabilmente all’alto accoppiamento.

Criteri di ripartizione delle responsabilità

Anche se il concetto è già stato usato nel corso dell’articolo, prima di addentrarci nel discorso, è assolutamente indispensabile chiarire il significato di responsabilità nell'ambito del disegno object-oriented. Una responsabilità rappresenta un servizio o un gruppo di servizi forniti da un elemento (inteso come classe o sottosistema) per il conseguimento di uno o più scopi. Le responsabilità si riferiscono quindi agli obblighi e ai comportamenti che un elemento ha in funzione del suo ruolo. Di seguito, nel corso dell’articolo, per semplicità come elementi dotati di responsabilità considereremo unicamente le classi.

Possiamo sostanzialmente classificare le responsabilità in due categorie fondamentali:

  • responsabilità di fare:

  1. una classe può direttamente fare qualcosa (esempio: eseguire elaborazioni sui dati, creare istanze di altre classi, ecc.);

  2. una classe può dare inizio all'azione in un altro oggetto, richiamandone un comportamento;

  3. una classe può fungere da tramite o da contesto nell’ambito del quale i comportamenti di altre classi vengono gestiti, controllati e coordinati;

  • responsabilità di conoscere:

  1. una classe può conoscere il suo stato interno incapsulato in campi privati;

  2. una classe può conoscere le sue proprietà pubbliche accessibili direttamente dall’esterno;

  3. una classe può conoscere classi correlate con cui instaura una relazione di dipendenza;

  4. una classe può conoscere informazioni che è in grado di derivare da elaborazioni e calcoli.

Durante il disegno è fondamentale distribuire in modo corretto e appropriato le diverse responsabilità per creare strutture ad oggetti caratterizzate da una buona estendibilità e flessibilità, limitando l’accoppiamento e favorendo la coesione.

È bene tenere a mente che le responsabilità possono essere pensate secondo diversi ordini di grandezza, a seconda della complessità che le caratterizza. La traduzione delle responsabilità in classi piuttosto che in metodi dipende dal loro livello di granularità: le responsabilità più grandi coinvolgono in genere una o più classi, le responsabilità più piccole, che in genere corrispondono a comportamenti specifici, coinvolgono solitamente un unico metodo. Sia classi che metodi possono tra loro collaborare allo scopo di assolvere al loro compito.

Quali sono i criteri in base ai quali è opportuno assegnare le responsabilità alle classi? In prima approssimazione possiamo dire che in genere è opportuno assegnare una responsabilità alla classe che possiede le informazioni utili e necessarie per poterla soddisfare. In questo modo l’accoppiamento viene mantenuto basso, in quanto ciascuna classe utilizza le proprie informazioni per assolvere ai propri compiti. Il comportamento viene distribuito tra le classi che possiedono le informazioni richieste, favorendo la coesione.

Tuttavia, in taluni casi, questa regola si può rivelare non appropriata. Per chiarire il concetto facciamo un esempio. In un ipotetico sistema gestionale, quale oggetto deve essere responsabile del salvataggio di una fattura sul database? Probabilmente si potrebbe pensare di considerare una classe di tipo Invoice contenente le informazioni di una fattura e, in base a quanto affermato, si potrebbe dire che la classe in questione ha la responsabilità di salvare i dati sul database. Quindi, generalizzando, in base a questo ragionamento, ogni classe avrebbe la responsabilità di scrivere sul database, duplicando la logica di accesso ai dati. Risulta evidente che in uno scenario come quello descritto sorgerebbero problemi di coesione (ciascuna classe farebbe troppe cose, anche non di sua pertinenza), accoppiamento e duplicazione (lo stesso codice verrebbe replicato in molte classi).

Peraltro quanto ipotizzato indica una violazione di uno dei principi architetturali di base: progettare per una separazione dei principali interessi del sistema che generalmente si ottiene tramite la suddivisione logica del sistema stesso in layer applicativi. In generale sostenere la separazione degli interessi migliora l'accoppiamento e la coesione. Quindi la distribuzione delle responsabilità va gestita anche in questa ottica.

I casi come quello descritto nell’esempio dimostrano che nel processo di scomposizione e distribuzione delle responsabilità non è possibile considerare sempre e solo classi corrispondenti agli elementi reali appartenenti al dominio applicativo. In molti casi, per promuovere la coesione e favorire un basso accoppiamento, si rivela opportuno assegnare un insieme coeso di responsabilità ad una classe artificiale o di convenienza che non rappresenta un concetto di dominio, ma che è una pura invenzione (per esempio, riprendendo il caso citato, una classe che permetta di eseguire query sul database). In questo modo le responsabilità vengono distribuite tra più oggetti con compiti specifici e definiti. Questo aumento della specializzazione di ciascun oggetto facilita il riuso e la manutenibilità.

Un altro aspetto importante da tenere in considerazione è l’individuazione dei punti critici in cui vi possono essere possibili variazioni e instabilità nel codice. Occorre assegnare le responsabilità in modo tale da creare attorno ad essi un’interfaccia stabile, introducendo meccanismi di protezione e isolamento sfruttando indirezioni e astrazioni. Così facendo viene favorito il basso accoppiamento con la conseguente possibilità di estendere funzionalmente le parti del sistema in modo indipendente, riducendo l'impatto e il costo dei cambiamenti. Il sistema acquista flessibilità e si presta quindi ad essere modificato, limitando i rischi di regressione ed eventuali effetti collaterali.

Il costo di robustezza che è necessario “pagare” nel disegno può non essere compatibile con il livello di costi che è possibile sostenere per la realizzazione di un determinato sistema software. In questi casi una scelta più "fragile", purchè consapevole, può essere preferibile se il costo per la realizzazione dei meccanismi di protezione e isolamento del codice nei punti di variazione ed evoluzione sono maggiori al costo da sostenere per un eventuale rifacimento.

Conclusioni

La scelta del giusto grado di complessità di disegno dipende molto dall'esperienza e dal buon senso di chi progetta: non occorre esagerare quando non serve, anzi è sbagliato, anche se poi l'applicazione funziona! Produrre over-engineering quando questo può essere evitato non è mai una buona scelta, soprattutto in un’ottica di manutenibilità. Meglio allora ricorrere ad un approccio del tipo simpler, but not the simplest (più semplice, ma non il più semplice), anche se poi le scelte di disegno dipendono di volta in volta dalla tipologia dell'applicazione e dai suoi requisiti non funzionali.

L'incapsulamento, l'uso di interfacce e tipi astratti, il polimorfismo, le indirezioni sono motivati dalla necessità di garantire il giusto grado di flessibilità e robustezza nel codice. L’accoppiamento va limitato dove serve, ma senza esagerare. Del resto la maturità di un architetto si esprime non solo nella conoscenza maggiore o minore dei meccanismi orientati a gestire opportunamente il cambiamento e l’evoluzione del codice, ma anche nella capacità di saper scegliere quando ricorrere ad essi e quale di questi debba essere applicato nelle diverse situazioni.

Riferimenti utili

  1. Craig Larman – Applying UML and Patterns: an introduction to object-oriented analysis and design and iterative development – Prentice-Hall, 2001.

  2. Sezione dedicata al disegno object-oriented a cura di Luca Minudel e Riccardo Golia su UGIdotNETWiki.

  3. Riccardo Golia – Webcast: Design Principles – Febbraio 2007.


© 2009 Microsoft Corporation. Tutti i diritti riservati. Condizioni per l'utilizzo  |  Marchi  |  Informativa sulla privacy
Page view tracker