• Non ci sono risultati.

E quindi fondamentale, nel progettare un sistema, conoscere e compren-dere i principˆı ed i metodi che permettono di realizzare architetture modulari.

4.2 Moduli

Il termine modulo pu`o assumere significati diversi in contesti diversi, e in questo corso adotteremo il significato pi´u generale: una porzione di software che contiene e fornisce risorse o servizi1, e a sua volta pu`o usare risorse o servizi offerti da altri moduli. Un modulo viene quindi caratterizzato dalla sua interfaccia, cio`e dall’elenco dei servizi offerti (o esportati) e richiesti (o

importati). Queste due parti dell’interfaccia si chiamano interfaccia offer-ta e interfaccia richiesoffer-ta, ma spesso la parola “interfaccia” viene usaoffer-ta in

riferimento alla sola parte offerta.

Le risorse offerte da un modulo possono essere strutture dati, operazioni, e definizioni di tipi. L’interfaccia di un modulo pu`o specificare un protocollo, cio`e un insieme di vincoli sulle possibili sequenze di scambi di messaggi (o chiamate di operazioni) fra il modulo e i suoi clienti. Alle operazioni si pos-sono associare precondizioni e postcondizioni (v. oltre). Infine, l’interfaccia pu`o specificare le eccezioni, cio`e condizioni anomale che si possono verificare nell’uso del modulo. Riguardo alla possibilit`a che un modulo possa offrire l’accesso diretto (cio`e senza la mediazione di procedure apposite) ad alcune strutture dati, vedremo fra poco che questa pratica viene sconsigliata nei metodi attuali di progetto e di programmazione.

L’interfaccia di un modulo `e una specifica, che viene realizzata dall’implementazione del modulo. Possiamo distinguere fra implementazioni

composte, in cui l’interfaccia del modulo viene implementata per mezzo di pi´u sottomoduli, e implementazioni semplici, in cui l’implementazione (costituita da definizioni di dati e operazioni in un qualche linguaggio di programma-zione) appartiene al modulo stesso. La Fig. 4.2 riassume schematicamente la definizione di modulo qui esposta.

L’interfaccia e l’implementazione definiscono un modulo logico, cio`e un’en-tit`a astratta capace di interagire con altre enun’en-tit`a. Nella fase di codifica ven-gono prodotti dei file contenenti codice sorgente nel linguaggio di program-mazione adottato. Chiameremo moduli fisici o artefatti sia i file sorgente, sia i file collegabili ed eseguibili ottenuti per compilazione e collegamento. I moduli fisici contengono (in linguaggio sorgente o in linguaggio macchina)

1

4.2. MODULI 137 modulo        interfaccia  offerta richiesta implementazione  semplice composta

Figura 4.2: Modulo logico.

le definizioni delle entit`a che realizzano i moduli logici, cio`e strutture dati e sottoprogrammi, e la corrispondenza fra moduli logici e fisici dipende sia dal linguaggio e dall’ambiente di programmazione che da scelte fatte dagli sviluppatori. Questa corrispondenza non `e necessariamente biunivoca, in quanto un modulo fisico pu`o contenere le definizioni relative a pi´u moduli logici, oppure le definizioni appartenenti a un modulo logico possono essere suddivise fra pi´u moduli fisici. Ai fini della mantenibilit`a e della modifica-bilit`a del sistema, `e importante che la corrispondenza fra struttura logica e struttura fisica sia chiara e razionale. Una buona organizzazione dei moduli fisici permette un uso pi´u efficace di strumenti automatici di programmazio-ne (come, per esempio il programma make) e di gestioprogrammazio-ne delle configurazioni (per esempio, i sistemi SCCS, RCS e CVS).

La maggior parte dell’attivit`a di progetto `e rivolta alla definizione della struttura logica del sistema, ma nelle fasi pi´u “basse”, cio`e pi´u vicine al-l’implementazione, si pu`o definire, almeno a grandi linee, anche la struttura fisica. Nel seguito ci riferiremo essenzialmente alla struttura logica.

4.2.1 Interfaccia e implementazione

Per progettare l’interfaccia di un modulo bisogna compiere un insieme di scel-te guidascel-te dai principˆı della divisione delle responsabilit`a e dell’information

hiding (occultamento dell’informazione).

Il principio della divisione delle responsabilit`a dice che bisogna cercare di suddividere il lavoro svolto dal sistema (e ricorsivamente da ciascun sot-tosistema) fra i vari moduli, in modo che a ciascuno di essi venga affidato un compito ben definito e limitato (ricordiamo il vecchio motto del sistema operativo Unix: “do one thing well ”). I moduli progettati secondo questo criterio hanno la propriet`a della coesione, cio`e di offrire un insieme omogeneo di servizi. Per esempio, un modulo che calcola dei valori e provvede anche a scriverli sull’output `e poco coeso: poich´e la rappresentazione dei risulta-ti `e indipendente dal procedimento di calcolo (per esempio, un insieme di

risultati potrebbe essere visualizzato con una tabella oppure un grafico, o non essere visualizzato affatto, essendo destinato ad ulteriori elaborazioni), `e meglio che la funzione di produrre un output venga affidata a un modulo specializzato, in modo da avere un sistema pi´u strutturato e flessibile.

La divisione di responsabilit`a comporta anche la necessit`a di specificare gli obblighi reciproci dei moduli. Un modulo fornitore di un servizio garanti-sce ad ogni modulo cliente che in conseguenza dell’espletamento del servizio (per esempio, l’esecuzione di una procedura) saranno vere certe relazioni lo-giche (postcondizioni ) sullo stato del sistema. Il modulo fornitore, per`o, si aspetta che all’atto dell’invocazione del servizio siano vere altre relazioni

(pre-condizioni ). Per esempio, un modulo che deve calcolare la radice quadrata

di un numero x ha come precondizione un vincolo sul segno del valore di x (x ≥ 0), e come postcondizione una relazione fra x e il risultato y (x = y2). Le post-condizioni e le pre-condizioni quindi rappresentano, rispettivamente, le responsabilit`a del modulo fornitore e quelle del modulo cliente riguardo a un dato servizio. `E buona prassi scrivere le pecondizioni e postcondizioni di ciascuna operazione nella documentazione di progetto.

Un altro aspetto importante della divisione delle responsabilit`a `e la gestio-ne degli errori e delle situazioni anomale. Se si prevede che gestio-nello svolgimento di un servizio si possano verificare delle situazioni anomale, bisogna decidere se tali situazioni possono essere gestite nel modulo fornitore, nascondendone gli effetti ai moduli clienti, oppure se il modulo fornitore deve limitarsi a segnalare il problema, delegandone la gestione ai moduli clienti.

Il principio dell’information hiding afferma che bisogna rendere inacces-sibile dall’esterno tutto ci`o che non `e strettamente necessario all’interazione con gli altri moduli, in modo che vengano ridotte al minimo le dipendenze e quindi sia possibile progettare, implementare e collaudare ciascun modulo indipendentemente dagli altri.

Dopo che `e stato individuato il compito di un modulo, si vede che tale compito, per essere svolto, ha bisogno di varie strutture dati e di operazioni (o di altre risorse, magari pi´u astratte, come tipi di dati o politiche di gestione di certe risorse). Il progettista deve decidere quali di queste entit`a devono far parte dell’interfaccia, ed essere cio`e accessibili dall’esterno. Le entit`a che non sono parte dell’interfaccia servono unicamente a implementare le altre, e non devono essere accessibili. Questa scelta non `e sempre facile e immediata, poich´e spesso pu`o sembrare che certe parti dell’implementazione (per esempio, una procedura) possano essere utili ad altri moduli. Comunque, quando si `e deciso che una certa entit`a fa parte dell’implementazione bisogna che questa sia effettivamente nascosta, poich´e la dipendenza di un modulo

4.2. MODULI 139

cliente dall’implementazione di un modulo fornitore fa s´ı che quest’ultimo non possa venire modificato senza modificare il cliente. In un sistema di media complessit`a il costo di qualsiasi cambiamento pu`o diventare proibitivo, se le dipendenze reciproche costringono gli sviluppatori a propagare i cambiamenti da un modulo all’altro.

In particolare, conviene nascondere nell’implementazione le strutture da-ti, a cui si dovrebbe accedere soltanto per mezzo di sottoprogrammi. Anche le politiche di accesso a una risorsa gestita da un modulo (per esempio, or-dinamento FIFO oppure LIFO delle richieste di accesso), di norma devono essere nascoste ai moduli clienti, in modo che questi non dipendano da tali politiche.

In conclusione, nello specificare un’architettura software si cerca di otte-nere il massimo disaccoppiamento fra i moduli.

4.2.2 Relazioni fra moduli

Come gi`a detto, un’architettura software `e la specifica di un insieme di moduli e delle loro relazioni. Fra queste, hanno un’importanza particolare le relazioni di composizione e di uso.

La relazione di composizione sussiste fra due moduli quando uno `e parte dell’altro ed `e necessariamente gerarchica, cio`e viene descritta da un grafo orientato aciclico.

La relazione di uso sussiste quando il corretto funzionamento di un mo-dulo richiede la presenza e generalmente2 il corretto funzionamento di un altro modulo. Questa relazione permette l’esistenza di cicli nel grafo ad essa associato, per`o si cerca di evitare i cicli poich´e i moduli che si trovano su un ciclo non possono venire esaminati e verificati isolatamente.

Le relazioni di composizione e uso sono il minimo indispensabile per de-finire un’architettura, ma ne esistono e se ne possono concepire molte, che possono essere pi´u o meno utili a seconda del tipo di sistema progettato, della metodologia di progetto, e del livello di dettaglio richiesto in ciascuna fase della progettazione. Per esempio, nei metodi orientati agli oggetti `e par-ticolarmente importante la relazione di generalizzazione o eredit`a. Un’altra

2

Il corretto funzionamento del modulo usato non `e sempre richiesto, poich´e un modulo in certi casi contiene solo risorse “statiche”, come strutture dati o definizioni di tipi, per le quali non si pu`o parlare di “funzionamento”

relazione importante `e la comunicazione fra moduli. In generale, si mettono in evidenza vari tipi di dipendenza, di cui l’uso `e un caso particolare.

4.2.3 Tipi di moduli

Il concetto di modulo visto finora `e molto generale. In questa sezione esa-mineremo alcuni concetti relativi a diversi tipi di componenti modulari del software.

Le astrazioni procedurali, il tipo di moduli pi´u semplice, sono modu-li che non gestiscono strutture dati, ma forniscono soltanto delle funzio-ni. Un esempio tipico di astrazioni procedurali sono le tradizionali librerie matematiche.

Un modulo che gestisce una struttura dati `e chiamato oggetto astratto: la struttura dati `e astratta in quanto vi si pu`o accedere soltanto attraverso le operazioni definite dall’interfaccia, e la sua realizzazione concreta `e nascosta. Gli oggetti astratti permettono di controllare gli accessi alle informazioni contenute in una struttura dati che devono essere condivise fra pi´u moduli.

Un altra categoria di moduli `e quella dei tipi di dati astratti (TDA), che definiscono dei tipi da cui si possono istanziare oggetti. Un tipo `e quindi la definizione di un insieme di oggetti che hanno la stessa struttura e le stesse operazioni.

Se la struttura o l’algoritmo di un oggetto astratto (o tutti e due) possono essere resi parametrici rispetto a qualche caratteristica del sistema, si ottiene un oggetto generico. Generalmente la caratteristica che si rende generica `e il tipo di qualche componente dell’oggetto. Un oggetto generico `e la defini-zione di un insieme di oggetti che hanno strutture ed operazioni simili, ma specializzate secondo alcuni parametri. Per esempio, una tabella generica pu`o rappresentare una famiglia di tabelle che differiscono per il tipo degli elementi o per il loro numero, o per tutte e due le caratteristiche.

Anche un tipo di dati astratto pu`o essere reso generico, ottenendo un tipo

di dati astratto generico.

Gli oggetti e le classi sono concetti che abbiamo gi`a incontrato nell’`ambito delle metodologie di specifica orientate agli oggetti (sez. 3.7), dove vengono usati per rappresentare entit`a del dominio di applicazione. Nel progetto del software, gli oggetti sono i componenti software che implementano l’applica-zione. Alcuni di questi oggetti simulano le entit`a del dominio

implementan-4.2. MODULI 141

done direttamente attributi e operazioni, altri partecipano al funzionamento dell’applicazione fornendo i meccanismi necessari.

Ogni oggetto `e un’istanza di una classe, che `e quindi simile a un tipo di dati astratto. La differenza fra una classe ed un TDA `e sottile: i tipi di dato astratti permettono al programmatore di definire dei tipi che possano essere usati come i tipi predefiniti dei linguaggi di programmazione, mentre le classi permettono di “simulare” delle entit`a. Un classico esempio di TDA `e il tipo dei numeri complessi costruito a partire dai numeri in virgola mobile. In pratica, i linguaggi di programmazione non permettono di fare questa distinzione di natura pragmatica, che serve solo a chiarire il modo in cui si useranno i diversi tipi di moduli.

La relazione di eredit`a fra classi offre al progettista altre possibilit`a di strutturazione dell’architettura, oltre a quelle offerte dalle relazioni di uso e di composizione.

Esistono dei moduli il cui scopo `e di raggruppare altri elementi (che posso-no essere moduli a loro volta) e di presentarli come un’unit`a logica. General-mente si usa il termine package per riferirsi a un insieme di entit`a logicaGeneral-mente correlate (per esempio un gruppo di classi o di sottoprogrammi). Il concetto di “package” di solito implica quello di spazio di nomi (namespace): uno spa-zio di nomi serve a identificare un insieme di entit`a e ad evitare conflitti di nomenclatura fra tali entit`a e quelle appartenenti ad un altro spazio di nomi: due entit`a aventi lo stesso nome ma appartenenti a spazi di nomi diversi sono entit`a distinte.

Un sottosistema `e una parte di un sistema pi´u grande, relativamente autonoma e caratterizzata da una propria funzione, e quindi da un’interfaccia. I sottosistemi si possono modellare per mezzo dei package o in altri modi che vedremo pi´u oltre.

Infine, accenniamo al concetto di componente. Questo viene generalmente inteso come un elemento software interamente definito dalle interfacce offer-te e richiesoffer-te, che possa essere sostituito da un’altro componenoffer-te equivalenoffer-te anche se diversamente implementato, e che possa essere riusato in contesti diversi, analogamente ai componenti elettronici. Secondo questa definizione, qualsiasi modulo realizzato rispettando rigorosamente il principio dell’infor-mation hiding e capace di offrire servizi di utilit`a abbastanza generale si potrebbe considerare un componente, per`o spesso si usa questo termine in modo pi´u restrittivo, richiedendo che i componenti si possano sostituire a tempo di esecuzione: questo impone dei particolari requisiti sugli ambien-ti di programmazione e di esecuzione, e richiede che i componenambien-ti vengano

realizzati rispettando determinate convenzioni. Una terza definizione, inter-media fra le precedenti, richiede che i componenti seguano certe convenzioni, ma non che siano necessariamente istanziabili a tempo di esecuzione.