5 - MODULARIZZAZIONE
versione 15/01/2013
 
MODULARIZZAZIONE

Quando un progetto diviene troppo complesso allora, per poter essere gestito, é necessario modularizzarlo ovvero:

- il progetto viene strutturato in parti separate
- si stabiliscono relazioni precise tra le parti

La modularizzazione è quindi la suddivisione in parti (moduli) di un progetto, in modo che sia più semplice da comprendere e manipolare. Questo processo richiede che ciascuna parte del progetto realizzi un particolare aspetto o un comportamento all’interno dell'intero sistema. Occorre quindi effettuare un processo di astrazione. L'astrazione porta ad individuare e considerare le proprietà rilevanti di un’entità, ignorando i dettagli non essenziali.
Le proprietà prese in considerazione definiscono una particolare "vista" dell’entità. Ad esempio una entità "Persona" può essere analizzata dal punto di vista "anagrafico" o "clinico".

vista "anagrafica": Nome, cognome, data di nascita, luogo di nascita, residenza ...
vista "clinica": Temperatura corporea, peso, pressione arteriosa, ...
I meccanismi di astrazione più diffusi sono:
 
- astrazione sul controllo (tipica dei linguaggi procedurali);
- astrazione sui dati (tipica nei linguaggi ad oggetti).

L’astrazione sul controllo consiste nell’astrarre una data funzione partendo dai dettagli della sua implementazione.
L’astrazione sui dati consiste nell’astrarre gli oggetti costituenti il sistema, descrivendoli in termini di struttura dati e di operazioni possibili su di essi.

Chiamiamo "modulo utente" quella sezione di un programma che usufruisce dei servizi/dati offerti dal modulo.
Il modulo, in un progetto software, è la componente che realizza una particolare astrazione. Un modulo è dotato di due elementi separati:

- interfaccia;
- corpo.

L’interfaccia specifica "cosa" fa il modulo (l’astrazione realizzata) e "come si utilizza" (in c++ corrisponde al file header). Deve essere visibile all’esterno del modulo per poter essere utilizzata dal "modulo utente" al fine di usufruire dei servizi/dati offerti dal modulo.
Il corpo descrive "come è realizzata" l’astrazione. Contiene il corpo di tutte le funzioni implementate dal modulo. Il "modulo utente" può accedere ad esse solo attraverso l’interfaccia.

Nel linguaggio C un modulo è rappresentato da files sorgente. L’uso disciplinato di alcuni meccanismi del linguaggio C++ consente una corretta strutturazione di un programma in moduli. Tra i principali meccanismi vi sono:

- la compilazione separata,
- l’inclusione testuale (direttiva #include),
- le dichiarazioni extern,
- l’uso dei prototipi di funzioni.

Un modulo dunque è composto da un file sorgente contenente una interfaccia (specifica) ed un corpo (implementazione).
L’interfaccia deve fornire all’utente indicazione di cosa il modulo esporta e come usare le funzionalità esportate, ad esempio le dichiarazioni dei tipi definiti dal modulo e i prototipi delle funzioni. Poiché l’utente del modulo deve conoscerne l’interfaccia ma ignorarne l’implementazione, è buona norma tenere separate la specifica del modulo dalla sua implementazione.
L’interfaccia è pertanto realizzata mediante un HEADER file ed utilizzata dagli utenti del modulo includendola mediante i meccanismi di inclusione testuale forniti dal linguaggio (direttiva #include).

Esempio di modularizzazione

Immaginiamo di voler implementare un modulo che realizzi alcune funzioni nell'ambito dei numeri complessi: ad esempio la stampa e la lettura di un numero complesso, la somma e la differenza tra due numeri complessi. Seguendo le osservazioni fatte la modularizzazione del nostro progetto potrebbe essere la seguente:

Main.c Complex.h Complex.c
#include <stdio.h>
#include "complex.h"
int main()
{
    tComplex a,b,s,d;
    // INPUT
    a=Leggi();
    b=Leggi();
    // ALGORITMO
    s=Somma(a,b);
    d=Differenza(a,b);
    // OUTPUT
    Stampa("Somma      = ",s);
    Stampa("Differenza = ",d);

	fflush(stdin);
	getchar();
	return(0);
}
#ifndef COMPLEX_H
#define COMPLEX_H

    // Strutture dati
    typedef struct 
    {
       float r;  /* parte reale       */
       float i;  /* parte immaginaria */
    } tComplex;

    // Prototipi
    tComplex Leggi();
    tComplex Differenza(tComplex, tComplex);
    tComplex Somma(tComplex, tComplex);
    void Stampa(char *, tComplex);

#endif
#include <stdio.h>
#include "complex.h"
tComplex Leggi() 
{
    tComplex x;
    printf("Digita un complesso (a+bi):");
    scanf("%f%fi", &x.r, &x.i);
    return(x);
}

tComplex Somma(tComplex a, tComplex b) 
{
    tComplex x;
    x.r=a.r+b.r;
    x.i=a.i+b.i;
    return(x);
}

tComplex Differenza(tComplex a, tComplex b) 
{
    tComplex x;
    x.r=a.r-b.r;
    x.i=a.i-b.i;
    return(x);
}

void Stampa(char *p, tComplex a)
{
     printf("%s",p);
     if (a.i==0)
         printf("%.2f",a.r);
     else if (a.i>0) 
         printf("%.2f+%.2fi\n",a.r,a.i);
     else if (a.i<0)
         printf("%.2f-%.2fi\n",a.r,-a.i);
}

Nell’esempio in figura il modulo COMPLEX contiene le funzionalità che devono essere utilizzate dal "modulo utente" Main (in Main.c). L'esportazione di tali funzionalità viene realizzata dal file di intestazione o header file (complex.h) che risulta separato dall’implementazione delle funzionalità di COMPLEX. Tale header contiene le dichiarazioni che costituiscono l’interfaccia di COMPLEX.
Siccome ogni modulo deve essere autoconsistente, ovvero deve contenere tutte le informazioni necessarie per la compilazione (in questo modo diventa usabile in più progetti senza apportare alcuna modifica), l’header file deve essere incluso (mediante la direttiva al preprocessore #include) sia nel "modulo utente" main.c che nell'implementazione del modulo complex.c.



Si osservi che, fintanto che l’interfaccia resta inalterata, l’implementazione complex.c può essere modificata senza dover ricompilare il modulo principale main.c Naturalmente occorre ricollegare i moduli oggetto. La compilazione separata consente dunque di compilare i moduli singolarmente rimandando alla fase di collegamento la risoluzione dei riferimenti esterni.




Quando più programmatori lavorano simultaneamente ad un progetto di grandi dimensioni, una volta accordatisi sulla specifica (interfaccia) dei vari moduli, possono procedere all’implementazione dei rispettivi moduli indipendentemente l’uno dagli altri.

Creare un progetto

Vediamo come costruire l’applicazione COMPLEX utilizzando l’ambiente DEV_C++ attraverso la costruzione di un progetto. Si opera nel modo seguente:

- Lanciamo il programma Dev-C++
- Dal menù file scegliere Nuovo->progetto



- Definire il tipo di file eseguibile che si vuole ottenere: nel nostro caso un "Empty Project". Indicare il linguaggio (ad esempio C). Dare un nome al progetto (che sarà il nome dell’eseguibile), confermare le scelte e salvare i file del progetto nella cartella desiderata.



- Utilizzando il tasto destro sulla cartella del progetto richiamiamo il menu "Nuova unità"

 

- Ripetiamo l'operazione in modo da ottenere i files necessari. A questo punto l’applicazione è costituita da tre file e due moduli (main e complex), si può iniziare a scrivere il codice. Al termine dovremo avere questa situazione:

 

Organizzazione dei moduli

L’organizzazione in moduli di un sistema software è un problema trattato dall’ingegneria del software.

E’ possibile organizzare in moduli secondo diverse metodologie di sviluppo:

-
Top down: si parte dall’alto, considerando il problema nella sua interezza e si procede verso il basso per raffinamenti successivi fino a ridurlo ad un insieme di sottoproblemi elementari.
- Bottom up: si risolvono singole parti del problema, senza averne necessariamente una visione d’insieme, per poi risalire procedendo per aggiustamenti successivi fino ad ottenere la soluzione globale.

Nella modularizzazione possono essere adottati diversi criteri che tengano conto di alcuni elementi: Information hiding - Coesione - Accoppiamento.

INFORMATION HIDING

Consiste nel nascondere e proteggere (incapsulare) alcune informazioni di una entità all’interno del modulo stesso. L’accesso alle informazioni protette è fornito in maniera controllata solo attraverso l’interfaccia.

Esempio di Information hiding:

Supponiamo che un modulo realizzi l’astrazione "Persona" fornendo una struttura dati di tipo "Persona". Una persona, nel livello di astrazione scelto, è caratterizzato da: Codice Fiscale, Nome, Cognome, Data e Luogo di nascita, Residenza, ... Il modulo esporta attraverso la sua interfaccia il tipo "persona" ed solo alcune operazionicon le quali è possibile effettuare delle modifiche sulle variabili "nascoste" di quel modulo: ad esempio inizializzazione dei dati della persona, stampa dei dati etc…
Non è possibile per i "moduli clienti" del modulo modificare o leggere i dati di una variabile persona se non utilizzando le funzionalità esportate da tale modulo.

Nella definizione dei criteri relativi all'Information hiding valutare con attenzione cosa esportare e cosa nascondere. Le interfacce devono essere:

- minimali, cioè devono esporre solo ciò che è strettamente necessario;
- stabili, cioè possibilmente non devono subire cambiamenti nel tempo:

Se l’interfaccia non cambia, le informazioni nascoste possono essere modificate senza che questo influisca sulle altre parti del sistema di cui l’entità fa parte.

ACCOPPIAMENTO

Due moduli si dicono debolmente accoppiati se le dipendenze tra di essi sono minimizzate. In altre parole un modulo risulta debolmente accoppiato con il "modulo utente" se può essere usato con altri "moduli utente" senza apportate alcuna modifica allo stesso. Per minimizzare l'accoppiamento occorre limitare al massimo l’uso di variabili globali (visibili e utilizzabili da più moduli) poiché queste creano dipendenze non facilmente controllabili.

COESIONE

Un modulo è fortemente coeso se incapsula un insieme di caratteristiche omogenee, sufficientemente indipendenti da altri moduli. Un esempio di modulo fortemente coeso è un modulo che realizza un’ unica astrazione {Persona, Ordinamento} (l'implementazione relativa all'ordinamento è legata all'entità Persona e non può essere applicato ad altre entità).

Nello sviluppo software la Coesione e l'Accoppiamento suggeriscono criteri contrastanti, tra i quali è opportuno valutare un punto di equilibrio (tradeoff).
L’ideale sarebbe riuscire ad ottenere una alta coesione tra i moduli ed un basso accoppiamento, ma:

- un livello molto alto di coesione dei moduli genera in generale una eccessiva crescita delle dipendenze tra di essi.
- un livello molto basso di accoppiamento può determinare uno scadere della qualità delle astrazioni realizzate dai moduli.


Link utili

http://www.math.unipd.it/~sperduti/CORSO-C%2B%2B/Strutture.htmm