Sei sulla pagina 1di 658

Carlo Ghezzi

Mehdi Jazayeri
Dino Mandrioli

Ingegneria del software


Fondamenti e principi
21' edizione

PEARSON

Prefazione alla seconda edizione

xi

Il ruolo dell'orientamento agli oggetti


Lo scopo dei casi di studio
Risorse per i docenti

xii
xiii
xiii

Prefazione alla prima edizione

xv

A chi rivolto
Prerequisiti
Organizzazione e contenuti
Esercizi
Casi di studio
Laboratorio
Percorsi di lettura
Ringraziamenti

Ingegneria del software: visione d'insieme

II ruolo dell'ingegneria del software nel progetto di un sistema


Breve storia dell'ingegneria del software
II ruolo dell'ingegnere del software
II ciclo di vita del software
Rapporto tra l'ingegneria del software e altri campi dell'informatica

2
3
6
6
9

Capitolo 1
1.1
1.2
1.3
1.4
1.5

xvi
xvi
xvi
xvii
xviii
xviii
xviii
xix

1.5.1
1.5.2
1.5.3
1.5.4
1.5.5

Linguaggi di p r o g r a m m a z i o n e
Sistemi operativi
Basi di dati
Intelligenza artificiale
Modelli teorici

1.6 Relazioni tra l'ingegneria del software e altre discipline


1.6.1
1.6.2

Scienze organizzative
Ingegneria dei sistemi

1.7 Osservazioni conclusive


Note bibliografiche

Capitolo 2

II software: natura e qualit

2.1 Classificazione delle qualit del software


2.1.1
2.1.2

Qualit interne ed esterne


Qualit del processo e qualit del p r o d o t t o

9
10
11
12
12

13
13
14

14
15

17
18
18
19

2.2 Principali qualit del software


2.2.1
2.2.2
2.2.3
2.2.4
2.2.5
2.2.6
2.2.7
2.2.8
2.2.9
2.2.10
2.2.11
2.2.12

Correttezza, affidabilit e robustezza


Prestazioni
Usabilit
Verificabilit
Manutenibilit
Riusabilit
Portabilit
Comprensibilt
Interoperabilit
Produttivit
Tempestivit
Visibilit

2.3 Requisiti di qualit in diverse aree applicative


2.3.1
2.3.2
2.3.3
2.3.4

Sistemi
Sistemi
Sistemi
Sistemi

informativi
in t e m p o reale
distribuiti
embedded

2.4 Misura della qualit


2.5 Osservazioni conclusive
Note bibliografiche

Capitolo 3
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8

Principi dell'ingegneria del software

Rigore e formalit
Separazione degli interessi
Modularit
Astrazione
Anticipazione del cambiamento
Generalit
Incrementalit
Illustrazione dei principi dell'ingegneria del software attraverso
due casi di studio
3.8.1
3.8.2

Caso di studio nella costruzione di u n compilatore


Caso di studio nell'ingegneria dei sistemi

3.9 Osservazioni conclusive


Note bibliografiche

Capitolo 4

Progettazione e architetture software

4.1 Attivit di progettazione del software e suoi obiettivi


4.1.1
4.1.2

Progettazione in vista del c a m b i a m e n t o


Famiglie di prodotti

4.2 Tecniche di modularizzazione


4.2.1
4.2.2
4.2.3
4.2.4
4.2.5

La struttura m o d u l a r e e la sua rappresentazione


Interfaccia, implementazione e i n f o r m a t i o n h i d i n g
Notazioni per la progettazione
Categorie di m o d u l i
Tecniche specifiche per la progettazione in vista del c a m b i a m e n t o

19
19
13
24
25
26
29
31
31
32
33
34
36

37
37
38
40
41

42
42
43

45
47
49
51
55
56
58
59
60
61
66

71
73

75
78
80
85

87
87
95
103
1 11
119

4.2.6
4.2.7

R a f f i n a m e n t o per passi successivi


Progettazione t o p - d o w n e b o t t o m - u p

122
129

4.3 Gestione delle anomalie


4.4 Un esempio di progettazione
4.5 Software concorrente
4.5.1
4.5.2
4.5.3

130
134
137

I dati condivisi
Software real-time
Software distribuito

138
146
148

4.6 Progettazione orientata agli oggetti


4.6.1
4.6.2
4.6.3
4.6.4

154

Generalizzazione e specializzazione
Associazioni
Aggregazione
Ulteriori nozioni sui d i a g r a m m i delle classi U M L

4.7 Architettura e componenti


4.7.1
4.7.2
4.7.3
4.7.4

161

Architetture standard
C o m p o n e n t i software
L'architettura c o m e p i a t t a f o r m a per l'integrazione dei c o m p o n e n t i
Architetture per sistemi distribuiti

4.8 Osservazioni conclusive


Note bibliografiche

Capitolo 5
5.1
5.2
5.3
5.4
5.5

5-5-2
5.5.3
5.5.4

Specifica

177
178
181
184
187
188

D i a g r a m m i di flusso di dati: la specifica delle f u n z i o n i


dei sistemi informativi
D i a g r a m m i U M L per c o m p o r t a m e n t i specifici
M a c c h i n e a stati finiti: descrizione del flusso di controllo
Le reti di Petri: specifica di sistemi asincroni

5.6 Specifiche descrittive


5.6.1
5.6.2
5.6.3

Requisiti per le notazioni di specifica


Costruzione di specifiche modulari
Specifiche per l'utente

230
233
251

259

finale

5.8 Osservazioni conclusive


Note bibliografiche

Capitolo 6

Verifica

6.1 Obiettivi e requisiti della verifica


6.1.1
6.1.2

188
194
196
204

230

D i a g r a m m i entit-relazione
Specifiche logiche
Specifiche algebriche

5.7 Stesura e uso delle specifiche nella pratica


5.7.1
5.7.2
5.7.3

162
165
167
169

170
175

I possibili usi delle specifiche


Qualit delle specifiche
Classificazione degli stili di specifica
Verifica delle specifiche
Specifiche operazionali
5.5.1

155
158
160
160

Verificare t u t t o
I risultati della verifica possono n o n essere binari

259
263
282

283
291

295
296
296
297

6.2
6.3

6.4

6.5

6.6
6.7
6.8
6.9

6.10

6.1.3 La verifica pu essere oggettiva o soggettiva


6.1.4 Verificare anche le qualit implicite
Approcci alla verifica
Test
6.3.1 Obiettivi del test
6.3.2 Fondamenti teorici del test
6.3.3 Principi empirici di test
6.3.4 Test in piccolo
6.3.5 Test in grande
6.3.6 Separazione degli interessi nell'attivit di test
6.3.7 Test di software concorrente e real-time
Analisi
6.4.1 Tecniche di analisi informale
6.4.2 Prove di correttezza
Esecuzione simbolica
6.5.1 Concetti fondamentali dell'esecuzione simbolica
6.5.2 Programmi con array
6.5.3 Uso dell'esecuzione simbolica nel test
Model checking
Integrazione delle tecniche di verifica
Debugging
Verifica di altre propriet del software
6.9.1 Verifica delle prestazioni
6.9.2 Verifica dell'affidabilit
6.9.3 Verifica di qualit soggettive
Osservazioni conclusive
Note bibliografiche

Capitolo 7

Processo di produzione del software

7.1 Cos' un modello di un processo di produzione del software?


7.2 Perch i modelli di processo di sviluppo del software sono importanti?
7.3 Attivit principali della produzione del software
7.3.1 Studio di fattibilit
7.3.2 Acquisizione, analisi e specifica dei requisiti
7.3.3 Definizione e progettazione dettagliata dell'architettura software
7.3.4 Produzione di codice e test dei moduli
7.3.5 Integrazione e test del sistema
7.3.6 Rilascio, installazione e manutenzione
7.4 Visione d'insieme dei modelli di processo del software
7.4.1 Modello a cascata
7.4.2 Modelli evolutivi
7.4.3 Modello trasformazionale
7.4.4 Modello a spirale
7.4.5 Studio dei modelli di processo
7.5 Gestione del software esistente
7.6 Casi di studio
7.6.1 Caso di studio: un sistema di commutazione telefonica
7.6.2 Caso di studio: un sistema per il controllo del budget

298
299
300
300
302
304
306
309
330
340
342
345
346
349
368
370
373
376
378
381
382
387
387
388
392
403
413

417
419
421
423
424
424
432
432
433
433
435
435
443
446
449
450
453
454
454
459

7.7

7.8
7.9
7.10

7.6.3 Caso di studio: il processo "synchronize and stabilize" di Microsoft


7.6.4 Caso di studio: l'approccio open-source 465
Organizzazione del processo
7.7.1 Structured Analysis/Structured Design
7.7.2 Metodologia di Jackson
7.7.3 Processo unificato di sviluppo del software (UP)
Gestione delle configurazioni
Standard per il software
Osservazioni conclusive
Note bibliografiche

Capitolo 8

Gestione dell'ingegneria del software

8.1 Funzioni del management


8.2 Pianificazione di progetto
8.2.1 Produttivit del software
8.2.2 Persone e produttivit
8.2.3 Stima dei costi
8.3 Controllo di progetto
8.3.1 Schemi di scomposizione delle attivit
8.3.2 Diagrammi di Gantt
8.3.3 Diagrammi PERT
8.3.4 Gestione delle deviazioni rispetto al piano
8.4 Organizzazione
8.4.1 Organizzazione centralizzata
8.4.2 Organizzazioni decentralizzate
8.4.3 Organizzazioni miste
8.4.4 Valutazione delle organizzazioni di gruppo
8.5 Gestione dei rischi
8.5.1 Tipici rischi di management nell'ingegneria del software
8.6 II modello CMM
8.7 Osservazioni conclusive
Note bibliografiche

Capitolo 9

Strumenti e ambienti dell'ingegneria


del software

9.1 Evoluzione storica degli strumenti e degli ambienti


9.2 Parametri di confronto degli strumenti
9.3 Strumenti rappresentativi
9.3.1 Editor
9.3.2 Linker
9.3.3 Interpreti
9.3.4 Generatori di codice
9.3.5 Debugger
9.3.6 Strumenti utilizzati nel test
9.3.7 Analizzatori statici
9.3.8 Strumenti per le interfacce grafiche
9.3.9 Strumenti di gestione delle configurazioni
9.3.10 Sistemi per il tracciamento

464
466
467
472
477
481
484
485
488

491
493
494
496
503
504
512
512
513
515
518
519
522
524
525
526
527
528
530
532
536

539
540
541
545
546
547
547
548
549
550
552
554
556
560

9.3.11 Strumenti di reverse engineering e reingegnerizzazione


9.3.12 Strumenti di supporto al processo
9.3.13 Strumenti di management
9.4 Integrazione di strumenti
9.5 Fattori di influenza dell'evoluzione degli strumenti
9.6 Osservazioni conclusive
Note bibliografiche

Capitolo 10 Epilog o
10.1
10.2
10.3
10.4

Appendice

II futuro
Responsabilit etiche e sociali
Codice etico e deontologia dell'ingegneria del software
Commenti conclusivi
Note bibliografiche

Casi di studio

Caso di studio A: automazione di un ufficio legale


A. 1 Pianificazione economica e finanziaria
A.2 Pianificazione tecnica e gestione
A.3 Monitoraggio del progetto
A.4 Rilascio iniziale
A. 5 Un parziale recupero
Caso di studio B: costruzione di una famiglia di compilatori
B.l
Pianificazione iniziale del prodotto
B.2 Pianificazione economica e finanziaria
B.3 Pianificazione e gestione tecnica
B.4 Prime fasi dello sviluppo
B.5 Monitoraggio del progetto
B.6 Riesame del progetto, ristrutturazione e precisazione
degli obiettivi
B.7 Assegnamento di responsabilit
B.8
Progresso continuo e rilascio del prodotto
Distribuzione del prodotto
B.9
B.10 Commenti
Caso di studio C: sviluppo incrementale
Caso di studio D: applicazione di metodi formali nell'industria
D.l
Formazione
D.2 Specifica dei requisiti
D.3 Convalida dei requisiti e pianificazione della verifica
D.4 Progettazione, implementazione e verifica
D.5 Valutazione complessiva
D.6 Impatto del progetto sulla strategia aziendale
Considerazioni finali
Note bibliografiche

561
562
563
564
565
566
568
571
571
574
575
576
576
579
579
581
581
583
583
584
584
584
585
586
586
587
587
589
590
590
590
593
594
596
596
598
599
600
602
603
604

Bibliografa

605

Indice analitico

637

PREFAZIONE

ALLA

SECONDA

EDIZIONE

La prima edizione di questo libro stata pubblicata nel 1991. Da quella data, numerosi
sono stati i progressi registrati nell'informatica e nel campo dell'ingegneria del software.
Certamente, la diffusione di Internet ha esercitato una profonda influenza sulla societ:
dalla formazione alla ricerca, dall'economia all'industria, al commercio. Abbiamo deciso
di scrivere la seconda edizione per aggiornare il libro rispetto ai progressi nell'ingegneria
del software nell'ultimo decennio.
In tutti questi anni abbiamo avuto conferma della validit dell'approccio su cui il libro basato, e cio la durabilit e l'importanza dei principi, che hanno retto il passaggio
del tempo: nonostante la tecnologia sia migliorata, i principi dell'ingegneria del software
sono rimasti invariati. Abbiamo, quindi, potuto aggiornare tutti i capitoli senza modificare la struttura originaria del libro, che rimane la seguente:

Introduzione: Capitoli 1-3;

Il prodotto: Capitoli 4-6;

Processo e gestione: Capitoli 7-8;

Strumenti e ambienti: Capitolo 9.

I Capitoli relativi al prodotto seguono questo ordine: progetto (Capitolo 4), specifica
(Capitolo 5) e verifica (Capitolo 6). Ci differisce dall'approccio tenuto da altri libri, che
trattano la specifica prima del progetto. La nostra scelta deriva invece dall'approccio basato sui principi seguiti dal libro. Le attivit di progetto, specifica e verifica permeano l'intero ciclo di vita del software. Ad esempio, l'attivit di progetto non riguarda solo la definizione dell'architettura del software, ma anche la produzione delle specifiche. L'approccio
basato sulla progettazione modulare ci aiuta a strutturare non solo il software, ma anche
i documenti di specifica. Altri libri trattano la specifica prima del progetto siccome, secondo i processi tradizionali di software, prima si specifica un software e poi lo si progetta. Al contrario, noi crediamo che apprendere prima gli approcci e l'attivit di progettazione crei la motivazione necessaria per lo studio della specifica e fornisca le tecniche e le
capacit per poter strutturare tali specifiche.
Rispetto alla pubblicazione della prima edizione di questo libro, tutte le aree del
software si sono comunque evolute, ma l'area che ha subito il cambiamento pi marcato

quella degli strumenti e degli ambienti di sviluppo. Il Capitolo 9, quindi, stato notevolmente rivisto, anche se il nostro approccio consiste nel presentare i principi pi che gli
specifici strumenti. Abbiamo visto durante gli anni che gli strumenti cambiamo di pari
passo con l'evoluzione tecnologica, e la scelta di quali particolari strumenti studiare dipende sia dal problema che dall'ambiente di lavoro. Ci concentreremo pertanto su un quadro di riferimento per lo studio e la valutazione degli strumenti software, senza trattarne
dettagliatamente nessuno in particolare.
Oltre ad aggiunte e cambiamenti minori, sono state apportate le seguenti modifiche.
Nel Capitolo 3 sono stati aggiunti due nuovi casi di studio, uno riguardante un semplice compilatore e l'altro riguardante il sistema di ascensori, che viene poi utilizzato ampiamente nel libro. I due casi di studio sono complementari, in quanto trattano di aree
applicative diverse e pongono problemi progettuali diversi. Vengono presentati in questo
capitolo in modo semplice e intuitivo per far riflettere lo studente riguardo ai problemi
dei sistemi complessi e per illustrare l'uso dei principi generali con esempi concreti.
Nel Capitolo 4 stata estesa la trattazione riguardante l'orientamento agli oggetti,
l'architettura software, i componenti e i sistemi distribuiti.
Nel Capitolo 5 stata aggiunta la trattazione riguardante Z e UML. Vi , inoltre,
un nuovo paragrafo che affronta in maniera pi sistematica l'ingegneria dei requisiti.
Nel Capitolo 6 sono stati aggiunti il model checking e GQM come tecniche di valutazione e verifica.
Nel Capitolo 7 sono state incluse trattazioni del processo software in connessione
con UML, del processo open-source e del processo synchronize-and-stabilize di Microsoft.
E stato inoltre aggiunto un nuovo caso di studio sull'ingegneria dei requisiti.
Nel Capitolo 8 sono stati aggiunti il capability maturty model e la descrizione delle
fabbriche del software di Nokia.
Nel Capitolo 9 stata aggiunta una trattazione di CVS.
Nel Capitolo 10 stata aggiunta una trattazione dei problemi di tipo etico nell'ambito dell'ingegneria del software.
Nell'Appendice, stato aggiunto un nuovo caso di studio riguardante l'uso dei metodi formali nell'industria.

Il ruolo dell'orientamento agli oggetti


Il libro tratta i principi del software orientato agli oggetti in modo equilibrato, piuttosto
che considerarlo l'unico metodo con cui produrre software. L'analisi, la progettazione e la
programmazione orientata agli oggetti si sono certamente evolute, diventando un approccio
dominante nell'ambito dell'ingegneria del software. Crediamo per che i principi di base
dell'ingegneria del software siano pi profondi. Ci che gli studenti dovrebbero imparare sono i principi e i metodi che possono essere usati in diversi approcci. Lo studente dovrebbe imparare come scegliere tra i vari approcci, ed essere in grado di ricorrere all'uso
di un approccio orientato agli oggetti quando questo si riveli la scelta migliore. Ad esempio, lo studente dovrebbe venire a conoscenza e imparare il principio di information hiding
prima di approfondire tecniche orientate agli oggetti quale l'ereditariet.

Lo scopo dei casi di studio


I casi di studio presentati nel libro e nell'appendice hanno due scopi: presentare gli argomenti trattati in un contesto pi ampio, per poter dare allo studente una visione pi ampia riguardo all'importanza dei principi e delle tecniche e fornire agli studenti che non
hanno mai visto progetti reali una "visione della realt pratica". I casi di studio, pur essendo semplificati per focalizzare l'attenzione sugli argomenti pi importanti, sono utili
soprattutto agli studenti meno esperti. Lo studio dell'ingegneria del software in un ambiente universitario pone dei problemi, in quanto lo studente medio non mai venuto in
contatto con i problemi che gli ingegneri del software trattano tutti i giorni; i casi di studio cercano pertanto di ovviare a questo problema.

Risorse per i docenti


E disponibile per i docenti un CD-ROM d'accompagnamento che comprende soluzioni agli
esercizi e un programma del corso d'esempio. anche disponibile, sia per gli studenti che
per i docenti, un sito Web complementare fornito dall'editore, attraverso il quale possibile scaricare le slide (in inglese) e contattare gli autori: www.prenhall.com/ghezzi. Commenti,
suggerimenti e feedback sono sempre ben accetti.

Carlo Ghezzi
Milano, Italia
Mehdi Jazayeri
Palo Alto, California
Dino Mandrioli
Lugano, Svizzera

Alle nostre famiglie,


per il loro sostegno e la pazienza.

PREFAZIONE

ALLA

PRIMA

EDIZIONE

Questo un libro di testo dedicato all'ingegneria del software. Il tema conduttore l'importanza del rigore nella pratica di questa disciplina. I libri di testo tradizionali sono basati sul modello di ciclo di vita dello sviluppo del software e su una illustrazione delle fasi che lo compongono: requisiti, specifica, progetto, codifica, manutenzione. Al contrario, la nostra presentazione si basa sui principi fondamentali che possono essere adottati
indipendentemente dal modello di ciclo di vita, e in tutte le sue fasi. Ci focalizzeremo,
pertanto, sull'identificazione e l'applicazione di principi fondamentali applicabili per tutto il ciclo di vita.
Vediamo ora le caratteristiche principali del libro.

Affronta i problemi dell'ingegneria del software, non quelli della programmazione. Ad


esempio, sono state omesse tutte le discussioni riguardanti i costrutti dei linguaggi
di programmazione, come goto, cicli, etc. Riteniamo che lo studente di ingegneria
del software debba gi avere familiarit con tali argomenti, che sono trattati nei libri di testo sui linguaggi di programmazione. Non viene neppure affrontato il problema di tradurre la descrizione di un progetto software in determinati linguaggi di
programmazione. II libro assume come prerequisito l'abilit di realizzare i singoli moduli che compongono un programma complesso e si concentra sui problemi di composizione di un progetto modulare.

Sottolinea i principi e le tecniche piuttosto che gli strumenti specifici. Oggi molte aziende stanno sviluppando strumenti e ambienti d'ingegneria del software, ed auspicabile che ne verranno inventati migliori e pi sofisticati al crescere delle conoscenze sulla disciplina. Una volta che lo studente abbia compreso i principi e le tecniche
sulle quali lo strumento si basa, gli sar facile padroneggiarlo. I principi e le tecniche sono applicabili a vari strumenti mentre l'acquisizione della padronanza nell'uso di un particolare strumento non prepara lo studente all'uso di altri strumenti.
Inoltre, rischioso usare strumenti senza comprenderne i principi sottostanti.

Presenta principi ingegneristici; non un manuale d'uso. I principi sono generali ed


molto probabile che rimangano applicabili per molti anni, mentre le tecniche cambieranno con la scoperta di nuove tecnologie e con l'acquisizione di nuove conoscenze.
Mentre un manuale d'uso pu essere consultato per capire come applicare una par-

ticolare tecnica seguendo un insieme di indicazioni pratiche, questo libro vuole metter in grado il lettore di comprendere perch debba, o non debba, essere usata una
particolare tecnica. Nonostante nella trattazione venga anche mostrato come certe
tecniche possono essere usate per implementare un principio specifico, lo scopo principale la comprensione del perch.
In questo senso, libro incarna la nostre convinzioni sull'uso dei principi fondamentali e
l'importanza delle teoria nella pratica dell'ingegneria, ed stato usato sia in corsi universitari che professionali sui diversi aspetti dell'ingegneria del software.

A chi rivolto
Il volume stato pensato per essere usato sia come libro di testo per studenti che frequentano
corsi di ingegneria del software che per autodidatti. Ingegneri, professionisti e manager
potranno trovare materiale che li convinca dell'utilit delle moderne pratiche dell'ingegneria del software e della necessit di adottarle. Pu essere usato da quanti siano disposti a una riflessione approfondita, mentre non del tutto appropriato per una rapida consultazione. In particolare, ove necessario, abbiamo sacrificato l'ampiezza dei temi trattati
per la profondit di analisi. Per i professionisti, le note sui possibili approfondimenti possono essere estremamente utili. E inoltre disponibile un manuale per docenti1, con idee
per l'organizzazione dei corsi e le soluzioni di alcuni esercizi.

Prerequisiti
Il libro pensato per studenti di informatica che conoscano la programmazione, gli algoritmi e le strutture dati e che siano esperti in almeno un linguaggio di programmazione. Il ragionamento analitico, anche se non strettamente necessario, agevoler notevolmente
la capacit del lettore di comprendere i concetti pi profondi del libro. Queste capacit
sono sviluppate tramite corsi di analisi matematica, matematica discreta e, ovviamente,
informatica teorica. Riteniamo infatti necessario che lo studente, indipendentemente dalla disciplina ingegneristica seguita, sia "matematicamente maturo".

Organizzazione e contenuti
L'ingegneria del software una disciplina vasta e multi-dimensionale. Organizzare quindi un libro di testo su questo argomento presenta numerose difficolt, in quanto dovrebbe presentare il materiale in maniera sequenziale, mentre i numerosi aspetti dell'ingegneria del software sono talmente correlati tra di loro da rendere impossibile una sequenza
ottimale di argomenti. Il libro stato organizzato in base a queste considerazioni:

Su C D - R O M , in lingua inglese (N.d.E.).

costruzione di un prodotto: il software;

utilizzo di un processo per costruire tale prodotto;

utilizzo degli strumenti per supportare tale processo.

Il libro suddiviso in tre blocchi, che trattano a turno il prodotto software (dal Capitolo
4 al 6), il processo di ingegneria del software e la sua gestione (Capitoli 7 e 8), gli strumenti (Capitolo 9). I Capitoli 1, 2 e 3 forniscono un'introduzione generale alla materia
e costituiscono un prologo per i capitoli successivi.
Nel Capitolo 2 vengono discussi i numerosi aspetti e le caratteristiche desiderabili del
software. Queste caratteristiche impongono dei limiti al costruttore di software e al processo da utilizzare. Nel Capitolo 3 sono presentati i principi per costruire software di alta qualit. Studiando i principi, invece degli strumenti specifici, lo studente acquisisce una conoscenza indipendente da una particolare tecnologia e dall'ambiente applicativo. Siccome la
tecnologia cambia e gli ambienti evolvono, lo studente dovrebbe cos disporre di fondamenti
e tecniche che possono essere utilizzate in diverse aree applicative. Dal Capitolo 4 al Capitolo
8 vengono affrontate le tecniche per applicare i principi del Capitolo 3 al progetto, alla specifica, alla verifica, al processo ingegneristico e alla sua gestione. Nel Capitolo 9, viene discusso l'uso dei computer come supporto alla creazione del software. Le discussioni sui vari strumenti specifici accennati nel libro sono rinviate a questo capitolo.
Mentre il materiale dei primi due blocchi dovrebbe in linea di massima reggere il
passaggio del tempo, molto probabile che il materiale della terza sezione diventi superato a causa dell'auspicabile sviluppo di nuovi e migliori strumenti. Siccome i linguaggi
di programmazione sono uno strumento fondamentale dell'ingegnere del software, il
Capitolo 9 serve da unione tra le problematiche della progettazione e il mondo dei linguaggi di programmazione.

Eserczi
Il libro contiene tre tipi di esercizi.

Esercizi brevi, con lo scopo di ampliare la conoscenza acquisita attraverso il libro o


di applicare tale conoscenza in modo pi specifico; questi esercizi sono distribuiti all'interno dei diversi capitoli.

Esercizi pi lunghi, alla fine del capitolo, che richiedono l'integrazione del materiale presente nel capitolo.

Progetti che richiedono lo sviluppo di software da parte di una piccola squadra.

Le soluzioni di alcuni esercizi sono fornite alla fine di ciascun capitolo. Altre soluzioni vengono invece fornite nel manuale per i docenti.

Casi di studio
Nel testo vengono utilizzati numerosi casi di studio per dimostrare l'integrazione di concetti differenti e per confrontare diversi approcci in situazioni realistiche. Inoltre, sono proposti e analizzati tre casi di studio di progetti reali. Questi casi di studio possono essere
letti ed analizzati in momenti diversi e con diversi scopi. Da questi casi di studio, lo studente con scarsa esperienza pu acquisire un'idea sintetica dei problemi che si incontrano nello sviluppo industriale. Mentre quello che disponga gi di competenze pratiche potr riconoscere certi aspetti e trarre arricchimento dall'altrui esperienza. Questi casi di studio possono anche essere analizzati durante la lettura del libro. Numerosi esercizi del libro si riferiscono a questi casi di studio.

Laboratorio
Molti corsi di ingegneria del software combinano lezioni tradizionali a progetti di laboratorio. E alquanto difficile svolgere tutte queste attivit in un singolo semestre. Il docente
si troverebbe a discutere problemi organizzativi mentre gli studenti sono concentrati sui
problemi giornalieri del debug. Crediamo che l'ingegneria del software debba essere insegnata, come tutte le altre discipline ingegneristiche, fornendo prima allo studente una solida base teorica. Solo dopo che questa stata raggiunta, l'attivit di laboratorio aumenter e potenzier le conoscenze dello studente. Ci implica che un progetto di laboratorio debba partire pi o meno a met del semestre, invece che all'inizio. Dal nostro punto
di vista, un approccio ancora migliore quello di dedicare un semestre alla teoria e un altro al laboratorio. II manuale per i docenti propone numerose idee per organizzare un corso di laboratorio basato su questo libro.

Percorsi di lettura
Il libro pu essere letto seguendo sequenze diverse e a vari livelli. I Capitoli dal 4 al 7 contengono materiale che pu essere omesso durante una prima lettura o nei corsi di studio
meno dettagliati. I Capitoli 1-3 sono necessari per un corretto inquadramento dei capitoli seguenti. Il grafo illustra la dipendenza tra i capitoli e i vari percorsi di lettura del libro. La notazione P indica la lettura parziale del Capitolo n; nC ne indica invece la lettura completa.
Il manuale per i docenti tratta diverse modalit di organizzazione di corsi basati sul
libro. Un convenzionale corso di ingegneria del software di un semestre potrebbe seguire
la sequenza: 1, 2, 3, 7P, 5P, 4P, 6P, 8, 9, 10. Noi preferiamo la sequenza 1, 2, 3, 4P, 5P,
6P, 7P, 8, 9, 10. In ogni caso, sarebbe preferibile che gli studenti iniziassero il progetto
dopo il 5P.

Ringraziamenti
Ringraziamo Reda A. Ammar, University of Connecticut; Larry C. Christensen, Brigham
University; William R Decker, University of Iowa; David A. Gustafson, Kansas State
University; Richard A. Kemmerer, University of California at Santa Barbara; John C.
Knight, University of Virginia; Seymour V. Pollack, Washington University e K. C. Tai,
North Carolina State University, per le revisioni delle bozze iniziali.
Ringraziamo anche le seguenti presone che hanno fornito importanti feedback su varie bozze del libro: Vincenzo Ambriola, Paola Bertaina, David Jacobson e Milon Mackey.
Gli Hewlett-Packard Laboratories, Alfredo Scarfone, HP Italia e il Politecnico di
Milano hanno reso possibile la nascita di questo libro supportando un corso tenuto da
Mehdi Jazayeri al Politecnico di Milano nella primavera del 1988. Vorremmo anche ringraziare il supporto degli Hewlett-Packard Laboratories, in particolare John Wilkes, Dick
Lampman, Bob Ritchie, Frank Carrubba a Palo Alto e Peter Porzer a Pisa. Ringraziamo
anche Bart Sears per il suo aiuto su vari sistemi e John Wilkes per l'utilizzo del suo database per la gestione dei riferimenti bibliografici. Ringraziamo infine per i contributi ricevuti il Consiglio Nazionale delle Ricerche.

Carlo Ghezzi
Milano, Italia
Mehdi Jazayeri
Palo Alto, California
Dino Mandrioli
Pisa, Italia

C A P I T O L O

Ingegneria del software: visione d'insieme

L'ingegneria del software il settore dell'informatica che si occupa della creazione di sistemi software talmente grandi o complessi da dover essere realizzati da una o pi squadre di
ingegneri. Di solito questi sistemi esistono in varie versioni e rimangono in servizio per parecchi anni. Durante la loro vita subiscono numerose modifiche: per eliminare difetti, per
potenziare caratteristiche gi esistenti, per implementarne nuove, per eliminare quelle obsolete, o per essere adattati a funzionare in un nuovo ambiente.
Possiamo definire l'ingegneria del software come "l'applicazione dell'ingegneria al
software". Pi precisamente, l'IEEE Standard 610.12-1990 Glossario standard della terminologia dell'ingegneria del software (ANSI) definisce l'ingegneria del software come l'applicazione di un approccio sistematico, disciplinato e quantificabile nello sviluppo, funzionamento
e manutenzione del software.
Parnas [1978] ha definito l'ingegneria del software come la "creazione di software multiversione da parte di pi operatori". Questa definizione mette in risalto l'essenza dell'ingegneria del software e sottolinea le differenze tra programmazione e ingegneria del software.
Un programmatore scrive un programma completo, mentre un ingegnere del software scrive un componente software che sar poi combinato con componenti scritti da altri ingegneri del software per creare un sistema. Il componente scritto da un ingegnere del software
pu essere modificato da altri ingegneri del software e potr essere usato da altri per creare
versioni diverse del sistema, anche se il suo creatore ha abbandonato il progetto da tempo.
La programmazione un'attivit individuale mentre l'ingegneria del software essenzialmente
un'attivit di gruppo.
Di fatto, la locuzione "ingegneria del software" fu coniata verso la fine degli anni Sessanta
quando ci si rese conto che tutto ci che si era appreso riguardo alle corrette tecniche di programmazione non aiutava a costruire sistemi software migliori. Mentre il campo della programmazione aveva fatto progressi straordinari - attraverso lo studio sistematico di algoritmi
e strutture dati e l'invenzione della "programmazione strutturata"- esistevano ancora notevoli
difficolt nella creazione di sistemi software complessi. Le tecniche utilizzate da un fisico per
scrivere un programma di calcolo per la soluzione di un'equazione differenziale, necessaria per
un esperimento, non erano adeguate per un programmatore che, all'interno di un gruppo, cercava di creare un sistema operativo o un sistema di gestione degli inventari. In questi casi complessi era necessario un classico approccio ingegneristico: definire chiaramente il problema e
quindi sviluppare e usare strumenti e tecniche per risolverlo.

Indubbiamente, l'ingegneria del software ha fatto progressi dagli anni Sessanta a oggi.
Sono state create tecniche standard e, pi che essere praticata come un qualcosa di artigianale, l'ingegneria del software ha acquisito una maggiore disciplina, cosa che tradizionalmente associata con l'ingegneria. Nonostante ci, le differenze con l'ingegneria tradizionale permangono. Nel disegnare un sistema elettrico, come ad esempio un amplificatore, l'ingegnere elettronico pu descrivere il sistema in modo preciso. Tutti i parametri e i livelli di
tolleranza sono stabiliti chiaramente e sono compresi facilmente e in modo chiaro sia dall'ingegnere sia dal cliente. Nei sistemi software tali parametri rimangono ancora sconosciuti. Non sappiamo ancora quali parametri specificare e come specificarli.
Nelle discipline ingegneristiche classiche, l'ingegnere ha strumenti e competenze matematiche per descrivere le caratteristiche del prodotto, separatamente da quelle del progetto. Per
esempio, un ingegnere elettronico pu fare affidamento sulle equazioni matematiche per verificare che un progetto non violer requisiti di alimentazione. Nell'ingegneria del software, tali strumenti matematici non sono ancora ben sviluppati e si sta tuttora discutendo sulla loro
reale applicabilit. L'ingegnere del software si appoggia pi sull'esperienza e sul suo giudizio
personale che su tecniche matematiche. Viceversa, mentre esperienza e giudizio sono necessari, anche strumenti di analisi formale sono essenziali nella pratica dell'ingegneria.
Questo libro presenta l'ingegneria del software come una disciplina ingegneristica, evidenziando determinati principi che riteniamo essere essenziali alla "creazione di software multiversione da parte di pi persone". Questi principi sono molto pi importanti di qualsiasi
particolare notazione o metodologia per costruire software e permettono all'ingegnere del
software di valutare differenti metodologie e usarle al momento opportuno. Il Capitolo 3
analizza i principi dell'ingegneria del software; quelli successivi mostrano invece la loro applicazione nei vari ambiti della disciplina.
In questo capitolo passiamo in rassegna l'evoluzione dell'ingegneria del software e le
sue relazioni con altre discipline. Scopo di questo capitolo offrire uno scorcio sul settore
dell'ingegneria del software.

1.1

II ruolo dell'ingegneria del software


nel progetto di un sistema

Un sistema sofware spesso un componente di un sistema pi vasto. L'ingegneria del software


quindi parte di un'attivit di progettazione di sistema molto pi vasta in cui i requisiti del
software interagiscono con i requisiti di altre parti del sistema durante la progettazione. Ad esempio, un sistema telefonico composto da computer, telefoni, linee telefoniche e cavi, altri componenti hardware come i satelliti e infine da software che controlla i vari componenti. E la
combinazione di tutti questi componenti che deve soddisfare i requisiti del sistema.
Requisiti come "il sistema non deve essere inattivo per pi di un secondo in venti anni" o "quando una cornetta telefonica viene alzata, il segnale di linea libera viene attivato
entro mezzo secondo" possono essere soddisfatti con una combinazione di hardware, software
e dispositivi speciali. La decisione su come meglio soddisfare i requisiti implica numerosi
compromessi. I sistemi delle centrali elettriche o di monitoraggio del traffico, sistemi bancari o di amministrazione ospedaliera sono altri esempi, che mostrano la necessit di vede-

re il software come un componente di un pi ampio sistema. Il software diviene sempre di


pi la parte interna intelligente di vari sistemi, dalle televisioni agli aeroplani. Si parla, in tal
caso, di software embedded. Avendo a che fare con tali sistemi, l'ingegnere del software deve
rivolgere uno sguardo pi ampio al problema pi generale dell'ingegneria di sistema. Ci richiede che l'ingegnere del software partecipi allo sviluppo dei requisiti di tutto il sistema e
che questi comprenda l'area applicativa prima di iniziare a pensare quali interfacce astratte
debbano essere soddisfatte dal software. Ad esempio, se il dispositivo hardware che svolge
funzione di interfaccia con l'utente ha capacit limitate per l'input di dati, nel sistema non
risulter necessario un word processor sofisticato.
Considerare l'ingegneria del software come parte dell'ingegneria dei sistemi ci fa riconoscere l'importanza del compromesso, che la caratteristica di ogni disciplina ingegneristica. Una classico compromesso concerne la scelta di ci che deve essere trasformato in
software e ci che deve essere trasformato in hardware. L'implementazione in software offre
maggiore flessibilit, mentre l'implementazione in hardware offre migliori prestazioni. Ad
esempio, nel Capitolo 2 vedremo un esempio di macchina operante a gettoni che pu essere costruita sia con varie fessure, ciascuna per un tipo diverso di gettone, sia con una sola
fessura, lasciando al software il compito di riconoscere i diversi gettoni. Un compromesso
ancora pi di base implica la decisione su cosa debba essere automatizzato e cosa debba essere eseguito manualmente.

1.2

Breve storia dell'ingegneria del software

La nascita e l'evoluzione dell'ingegneria del software come disciplina nel campo dell'informatica risale alla maturazione dell'attivit di programmazione. Agli albori dell'informatica,
il problema principale della programmazione era come mettere insieme una sequenza di istruzioni per fare in modo che il computer producesse qualcosa di utile. I problemi che venivano programmati erano ben compresi e conosciuti: ad esempio, come risolvere un'equazione
differenziale. Il programma era scritto, ad esempio, da un fisico per risolvere un'equazione
di proprio interesse e il problema era circoscritto tra il computer e l'utente-programmatore;
nessun'altra persona era coinvolta.
Con la diminuzione dei prezzi dei computer e la loro diffusione, un numero sempre
maggiore di persone iniziarono a utilizzarlo. I linguaggi di alto livello furono inventati nei
tardi anni Cinquanta per rendere pi facile comunicare con le macchine, ma nonostante ci,
l'attivit di far eseguire al computer qualcosa di utile rimaneva sempre essenzialmente il compito di una sola persona che doveva scrivere un opportuno programma per un ben determinato compito.
In questo periodo, "programmare" divenne una professione: una persona poteva chiedere al programmatore di scrivere un programma, invece di realizzarlo per conto proprio.
Questo accordo introdusse una separazione tra utente e computer: l'utente specificava cosa
voleva ottenere da una data applicazione utilizzando un linguaggio diverso dalla notazione
di programmazione. Il programmatore leggeva quindi tale specifica e la traduceva in un insieme ben preciso di istruzioni macchina. Questo, ovviamente, port a volte a un'interpretazione erronea delle intenzioni dell'utente da parte del programmatore, anche per problemi relativamente semplici.

Nei primi anni Sessanta i progetti di software complessi furono veramente pochi e quei
pochi furono intrapresi da pionieri del campo dell'ingegneria, che erano molto esperti. Ad
esempio, il sistema operativo CTSS sviluppato al MIT fu un progetto vasto, sviluppato da
individui estremamente motivati e competenti.
Nella seconda parte degli anni Sessanta, vi furono tentativi di creare grandi sistemi di
software commerciali. Di questi progetti, quello meglio documentato fu il sistema operativo OS 360 per la famiglia di computer IBM 360. Le persone che lavoravano in questi ambiti presto si resero conto che costruire vasti sistemi di software era decisamente diverso rispetto a quelli pi piccoli. Vi erano delle difficolt profonde nel cercare di adattare le tecniche di sviluppo di piccoli programmi allo sviluppo di grossi software. L'espressione "ingegneria del software" fu coniata in questo specifico periodo e vennero tenute conferenze per
discutere sui problemi che tali progetti incontravano nello sviluppare il prodotto promesso.
I progetti di grossi software, generalmente, sforavano il budget prefissato ed erano in ritardo rispetto al termine previsto. Un'altra locuzione coniata in quel periodo fu "crisi del
software".
Si comprese che i problemi riscontrati nella creazione di grandi sistemi software non
riguardavano la pura e semplice capacit di combinare tra loro le istruzioni del computer.
Piuttosto, i problemi che dovevano essere risolti non erano ben compresi, almeno non da
tutte le persone coinvolte nel progetto. Coloro che lavoravano sul progetto spendevano molto pi tempo per comunicare tra di loro che per scrivere codice. Addirittura a volte alcune
persone abbandonavano il progetto, influenzando cos non solo il lavoro che avevano fatto,
ma anche il lavoro di quanti facevano affidamento su di loro. Sostituire un individuo richiedeva
il trasferimento orale dei requisiti del progetto e del modello del sistema. Ci si rese conto
che ogni cambiamento dei requisiti del sistema originale influenzava numerose parti del progetto, ritardando inoltre la consegna del sistema. Questo tipo di problemi non esisteva ai
tempi della "programmazione" e richiedeva quindi un nuovo tipo di approccio.
Vennero proposte e provate numerose soluzioni. Alcuni suggerirono un miglioramento delle tecniche di organizzazione e gestione dei progetti; altri proposero una diversa organizzazione in squadre. Altri ancora sostennero che fossero necessari linguaggi di programmazione e strumenti migliori; molti auspicarono l'adozione di standardizzazioni, come ad
esempio convenzioni di codifica uniformi. Alcuni invocarono l'uso di un approccio formale e matematico. Di certo non mancavano le idee. Alla fine si raggiunse un accordo sul fatto che il problema della costruzione di un software dovesse essere affrontato nello stesso modo adottato dagli ingegneri per costruire sistemi grandi e complessi come ponti, raffinerie,
fabbriche, navi e aeroplani. Lo scopo era vedere il sistema software finale come un prodotto complesso e la sua creazione come un lavoro ingegneristico. L'approccio ingegneristico richiedeva management, organizzazione, strumenti, teorie, metodologie e tecniche. Nacque
cos l'ingegneria del software.
In un articolo ormai classico del 1987, Brooks, parafrasando Aristotele, sostenne che
vi erano solo due tipi di sfide nello sviluppo del software: l'essenziale e l'accidentale. Le difficolt accidentali sono quelle che riguardano gli strumenti e le tecnologie correnti: ad esempio, i problemi sintattici che sorgono dal linguaggio di programmazione utilizzato. Si possono superare tali difficolt con strumenti e tecnologie migliori. Le difficolt essenziali, invece, non sono generalmente superate con l'uso di nuovi strumenti. Problemi di progettazione complessa ad esempio, la creazione di un modello utile per le previsioni atmosferi-

che o per l'economia - richiedono sforzo intellettuale, creativit e tempo. Brooks sostenne
che non vi era alcuna soluzione magica, nessun "proiettile d'argento"1 per risolvere i problemi essenziali incontrati dagli ingegneri del software.
Il ragionamento di Brooks mostra le false supposizioni dietro alla locuzione "crisi del
software", che venne ideata in quanto i progetti di software erano continuamente in ritardo
e oltre il budget prestabilito. La conclusione fu che il problema era temporaneo e avrebbe
potuto essere risolto con strumenti e tecniche di management migliori. In realt i progetti
erano in ritardo perch l'applicazione era complessa e scarsamente compresa sia dai clienti
che dagli sviluppatori e nessuno aveva alcuna idea di come stimare la difficolt del lavoro e
di quanto tempo ci sarebbe voluto per risolverlo. Nonostante l'espressione "crisi del software"
sia talvolta ancora usata, opinione generale che le difficolt inerenti lo sviluppo di software
non siano di breve periodo. Applicazioni nuove e complesse sono ardue da affrontare e non
sono di rapida soluzione.
La storia mostra la crescita dell'ingegneria del software partendo dalla programmazione. Alcune evoluzioni tecnologiche hanno giocato un ruolo importante nello sviluppo della disciplina. L'influenza maggiore stata il cambio di equilibrio tra i costi dell'hardware e
del software. Mentre un tempo il costo di un sistema computerizzato era determinato soprattutto dal costo dell'hardware, e il software era un fattore ininfluente, ora la componente software di gran lunga quella dominante nel costo di un sistema. La diminuzione del
costo dell'hardware e la crescita di quello del software ha sfavorito quest'ultimo, accentuando l'importanza economica dell'ingegneria del software.
Un altro orientamento evolutivo si creato anche all'interno del campo stesso. Vi
stata una crescente enfatizzazione a vedere l'ingegneria del software come una disciplina che
va ben oltre la pura attivit di codifica. Al contrario, il software viene considerato un prodotto che ha un intero ciclo di vita, partendo della sua concezione, continuando attraverso
la progettazione, lo sviluppo, la messa in funzione, la manutenzione e l'evoluzione. Lo spostamento dell'enfasi dalla codifica all'intero ciclo di vita del software ha favorito lo sviluppo
di metodologie e di sofisticati strumenti a sostegno delle squadre coinvolte.
Per parecchie ragioni, possiamo quindi pensare che l'importanza dell'ingegneria del
software continuer ad aumentare. Una prima ragione di tipo economico: le spese mondiali per il software sono passate da 140 miliardi di dollari nel 1985 a 800 miliardi di dollari nel 2000. Solo questo basterebbe a dimostrare quanto l'ingegneria del software crescer
come disciplina. In secondo luogo, il software ormai permea la nostra societ: sempre di pi
viene usato software per controllare funzioni critiche di varie macchine, come aeroplani e
dispositivi medici, e per supportare funzioni di importanza critica per il mondo intero, come ad esempio il commercio elettronico. Questo fatto garantisce una crescente attenzione
della societ verso software affidabile, al punto da promulgare legislazioni su standard specifici, requisiti e procedure di certificazione. Senza dubbio, imparare a creare software migliore in modo migliore continuer a essere fondamentale.

Il proiettile d'argento quello che nelle leggende uccide il lupo mannaro (N.d.T.).

1.3

II ruolo dell'ingegnere del software

L'evoluzione dell'ingegneria del software ha portato a definire il ruolo professionale dell'ingegnere del software, l'esperienza e la formazione richiesta. L'ingegnere del software deve, ovviamente, essere un buon programmatore, molto versato in strutture dati e algoritmi ed esperto in uno o pi linguaggi di programmazione. Questi sono dei requisiti per la cosiddetta "programmazione in piccolo", definita approssimativamente come la creazione di programmi che
possono essere scritti interamente da un unico individuo. Ma un ingegnere del software coinvolto anche nella "programmazione in grande", che richiede molte pi abilit.
L'ingegnere del software deve aver familiarit con pi approcci di progetto, deve essere capace di tradurre richieste e desideri vaghi in precise specifiche, e anche di interagire con
l'utente di un sistema nei termini dell'applicazione pi che nel gergo tecnico degli informatici.
Queste capacit richiedono a loro volta flessibilit e apertura mentale per comprendere e familiarizzare con i fondamenti delle differenti aree applicative. L'ingegnere del software deve
essere capace di muoversi attraverso diversi livelli di astrazione nei diversi stadi del progetto, dalle procedure applicative e dai requisiti di una specifica applicazione, alle astrazioni per
il sistema software, a uno specifico progetto del sistema fino al livello dettagliato della codifica in un linguaggio di programmazione.
Come in molti altri campi dell'ingegneria, l'ingegnere del software deve sviluppare capacit che gli permettano di costruire una vasta variet di modelli e di ragionare su questi per
poter operare scelte riguardo ai vari compromessi (trade-off) che si incontrano nel processo di
sviluppo del software. I modelli usati nella fase di definizione dei requisiti sono diversi da quelli usati nella progettazione dell'architettura software, i quali a loro volta sono diversi da quelli
usati nella fase di implementazione. In alcuni momenti, il modello pu essere usato per rispondere a domande sia sul comportamento del sistema che sulle sue prestazioni.
L'ingegnere del software anche un membro di un gruppo di lavoro e necessita quindi di capacit comunicative e interpersonali. Deve inoltre essere in grado di coordinare il lavoro, sia il proprio sia quello di altri.
Come gi detto in precedenza, un ingegnere del software ha molteplici responsabilit.
Sovente, molte organizzazioni dividono le responsabilit tra vari specialisti con qualifiche differenti. Ad esempio, un analista di sistema ha la responsabilit di ricavare i requisiti, di interagire con il cliente e di comprendere l'area applicativa, mentre un analista di prestazioni
ha la responsabilit di analizzare le prestazioni del sistema. A volte lo stesso ingegnere svolge ruoli differenti in momenti differenti del progetto o in progetti differenti.

1.4

II ciclo di vita del software

Il software subisce uno sviluppo e un'evoluzione, dall'idea iniziale di un possibile prodotto


o sistema software fino a quando viene implementato e consegnato al cliente (e anche in seguito) . Si dice che il software ha un ciclo di vita composto da varie fasi. A ciascuna di queste associato lo sviluppo di una parte del sistema o di qualche elemento a questo legato come, ad esempio, un manuale per l'utente o un piano di test. Nel modello tradizionale del
ciclo di vita, chiamato "modello a cascata", ogni fase ha un inizio e una fine ben definiti,

dei risultati parziali ("artefatti") che vengono trasferiti a una ben identificata fase sueva. Raramente per nella realt le cose sono cos semplici. Un modello a cascata como dalle seguenti fasi.
Analisi e specifica dei requisiti. L'analisi dei requisiti di solito la prima fase di un
progetto per lo sviluppo di un software di grandi dimensioni. Viene intrapresa dopo
aver compiuto uno studio di fattibilit per definire in modo preciso i costi e i benefici di un sistema software con lo scopo di identificare e documentare i requisiti del sistema. Questo studio pu essere compiuto dal cliente, dallo sviluppatore, da specialisti nell'analisi di mercato o da una qualsiasi combinazione di questi. Nei casi in cui i
requisiti non siano chiari (per esempio, un nuovo sistema che non mai stato precedentemente realizzato), necessario che vi sia ampia interazione tra il cliente e lo sviluppatore. In questa fase i requisiti dovrebbero essere scritti impiegando una terminologia comprensibile dall'utente finale, ma molte volte non cosi. Numerose metodologie di ingegneria del software ritengono che questa fase debba anche portare alla creazione di manuali per gli utenti e alla progettazione dei test di sistema che saranno effettuati alla fine, prima della consegna del sistema.
Progettazione di sistema e sua specifica. Una volta che i requisiti del sistema sono
stati documentati, gli ingegneri del software progettano un sistema software che li soddisfi. Questa fase a volte divisa in due sottofasi: il progetto architetturale, o di alto livello, e il progetto dettagliato. Il progetto architetturale affronta l'organizzazione globale del sistema in termini di componenti di alto livello e delle loro interazioni. Man
mano che ci si addentra in livelli di progettazione sempre pi dettagliati, i vari componenti vengono scomposti in moduli di basso livello, con interfacce definite in modo accurato. Tutti i livelli di progettazione vengono indicati in un documento di specifica che fornisce informazioni sulle decisioni di progettazione prese.
La separazione della fase di analisi dei requisiti da quella di progettazione un perfetto esempio della fondamentale dicotomia tra "che cosa" e "come" che spesso incontriamo nell'informatica. Il principio generale consiste nel fare una chiara distinzione
tra che cosa il problema e come si risolve tale problema. In questo caso, la fase di analisi dei requisiti cerca di specificare qual esattamente il problema. E per questo che
diciamo che i requisiti dovrebbero essere specificati in base alle esigenze dell'utente finale. Di solito vi sono numerosi modi di soddisfare i requisiti, a volte ricorrendo anche a soluzioni manuali che non richiedono l'uso del computer. Lo scopo della fase di
progettazione quella di giungere alla specifica di un particolare sistema software che
soddisfi i requisiti dichiarati. Anche in questo caso, vi sono numerosi modi per creare
il sistema specificato. Nella fase di codifica, che segue quella di progettazione, un preciso sistema viene codificato per soddisfare le specifiche di sistema. Nel prosieguo del
libro vedremo altri esempi della dicotomia del che "cosa/come".
Codifica e test di modulo. In questa fase, l'ingegnere produce il codice che sar consegnato al cliente sotto forma di un sistema funzionante. Anche altre fasi del ciclo di
vita del software potranno portare allo sviluppo di codice, ad esempio per effettuare
test, per sviluppare prototipi e test driver, ma in tutti questi casi il codice sviluppato
sar impiegato solamente dagli sviluppatori. Bisogna notare che i singoli moduli creati nella fase di codifica vengono testati prima di passare a quella successiva.

Analisi e specifica
dei requisiti
Progettazione di sistema
e sua specifica

Codifica e test
di modulo
Integrazione e test
di sistema

Consegna e
manutenzione

Figura 1.1

Modello del ciclo di vita a cascata del software.

Integrazione e test di sistema. Tutti i moduli sviluppati e testati singolarmente nella fase precedente vengono in questa assemblati, integrati, e testati come un unico sistema.

Consegna e manutenzione. Una volta che il sistema supera tutti i test, viene consegnato al cliente ed entra nella fase di manutenzione in cui vengono incorporate tutte
le modifiche apportate al sistema dopo la prima consegna.

La Figura 1.1 offre una visione grafica del ciclo di vita dello sviluppo di un software e offre
un chiarimento grafico del termine "cascata". Ciascuna fase crea risultati che "fluiscono" in
quella seguente e idealmente il processo procede in modo ordinato e lineare.
Per come stato qui presentato, questo modello offre una visione parziale e semplificata del convenzionale ciclo di vita a cascata del software. Il processo pu essere scomposto
in un diverso insieme di fasi, con nomi diversi, diversi fini e una diversa granularit. Potrebbero
anche essere proposti schemi di cicli di vita totalmente differenti, non basati strettamente
su fasi sviluppate in cascata. Ad esempio, ovvio che se un test dovesse scoprire i difetti del
sistema, occorrerebbe tornare indietro almeno fino alla fase di codifica e forse anche a quella di progettazione per correggere gli errori. In generale, ogni fase potrebbe portare alla luce problemi nelle fasi antecedenti, e quando ci dovesse accadere sarebbe necessario tornare indietro e rifare parte del lavoro gi svolto. Ad esempio, se la fase di progettazione del sistema scopre inconsistenze o ambiguit nei requisiti del sistema, si dovr riesaminare la fase di analisi dei requisiti per determinare quali erano quelli che realmente si intendeva specificare e quali quelli errati.
Un'altra semplificazione fatta con la precedente rappresentazione del ciclo di vita a cascata riguarda il fatto che si suppone che ciascuna fase sia completata prima che la successiva abbia inizio. Nella pratica invece, spesso vantaggioso iniziare una fase prima che la precedente sia finita. Questo pu succedere, ad esempio, se dei dati necessari al completamento della fase di specifica dei requisiti non sono disponibili per un po' di tempo. Oppure pu

essere necessario perch le persone pronte a iniziare la fase successiva sono disponibili e non
hanno altri lavori da fare. Un'altra ragione per sovrapporre le varie fasi pu anche essere la
riduzione del tempo di lancio del prodotto. Il termine usato per definire tale moderno processo organizzativo, che cerca di abbreviare il tempo di consegna dei prodotti introducendo
del parallelismo nelle fasi di sviluppo di processi precedentemente sequenziali ingegneria
concorrente. Posporremo questa e altre questioni legate al ciclo di vita del software fino al
Capitolo 7.
Molti libri sull'ingegneria del software sono organizzati seguendo il modello tradizionale del ciclo di vita del software, con paragrafi o capitoli incentrati solamente su una fase.
Invece, questo libro stato organizzato facendo riferimento a una serie di principi. Una volta che tali principi vengono compresi e conosciuti a fondo, possono essere usati dall'ingegnere del software in tutte le fasi di sviluppo del software, anche in modelli di ciclo di vita
diversi da quelli basati su uno sviluppo in fasi, come si detto prima. Infatti l'esperienza
maturata e alcune ricerche hanno mostrato che vi un'ampia variet di modelli del ciclo di
vita del software e che nessuno di essi adatto a tutti i sistemi software. Nel Capitolo 7 esamineremo diversi modelli di ciclo di vita.

1.5

Rapporto tra l'ingegneria del software


e altri campi dell'informatica

L'ingegneria del software, che ormai diventata una disciplina a s stante, un settore importante dell'informatica. Occorre notare che esistono numerosi rapporti sinergici tra questa e altri numerosi ambiti dell'informatica che influenzano e sono a loro volta influenzati
dall'ingegneria del software. Affrontiamo ora alcuni casi.

1.5.1

Linguaggi di programmazione

L'influenza dell'ingegneria del software sui linguaggi di programmazione evidente. I linguaggi di programmazione sono tra gli strumenti principali impiegati nello sviluppo di
software e di conseguenza hanno un profondo impatto sul corretto raggiungimento dei traguardi dell'ingegneria del software.
Uno dei pi chiari esempi di questa influenza l'inclusione di caratteristiche modulari, come la compilazione separata e indipendente e la separazione tra specifiche e implementazione, in modo da favorire la suddivisione in squadre del lavoro di sviluppo di software
di grandi dimensioni. Linguaggi di programmazione come Ada 95 e Java, ad esempio, supportano lo sviluppo di "pacchetti" - che permettono la separazione della loro interfaccia dall'implementazione - e di librerie di pacchetti che possono essere utilizzati come componenti
nello sviluppo di sistemi software indipendenti. Questo offre la possibilit di creare software
scegliendo da un catalogo di componenti preesistenti gi disponibili e combinandoli tra di
loro, come si fa per l'hardware. Un altro esempio l'introduzione di costrutti che gestiscono eccezioni nei linguaggi di programmazione, per permettere di scoprire e rispondere ad
eventuali malfunzionamenti che possono verificarsi mentre il software in esecuzione. Tali
costrutti consentono all'ingegnere del software di costruire applicazioni pi affidabili.
Viceversa, anche i linguaggi di programmazione hanno influenzato l'ingegneria del software.

Un esempio che i requisiti e il progetto debbano essere descritti in modo preciso, usando
possibilmente un linguaggio rigoroso ed elaborabile da una macchina come un linguaggio
di programmazione. Un altro esempio il fatto di trattare l'input in un sistema software come un programma codificato in una sorta di linguaggio di programmazione. I comandi che
un utente pu immettere in un sistema non sono una collezione casuale di caratteri, bens
formano un linguaggio usato per comunicare con il sistema. Progettare un linguaggio di input appropriato fa parte della progettazione dell'interfaccia del sistema.
I vecchi sistemi operativi, come ad esempio OS 360, avevano un'interfaccia complicata e criptica - chiamata linguaggio di controllo di lavoro (JCL,job control language) - che
era usata per dare ordini al sistema operativo. I successivi sistemi operativi in particolare
UNIX - introdussero i linguaggi a linea di comando, specificatamente ideati per programmare il sistema operativo. L'approccio attraverso i linguaggi rese l'interfaccia molto pi facile da comprendere e utilizzare.
Una conseguenza del considerare l'interfaccia di sistemi software come un linguaggio
di programmazione che gli strumenti di sviluppo dei compilatori che sono molto avanzati - possono essere usati per lo sviluppo di software generico. Ad esempio, possiamo impiegare grammatiche per specificare la sintassi dell'interfaccia e generatori di analizzatori sintattici per individuare inconsistenze e ambiguit nell'interfaccia, e quindi generare automaticamente la "facciata" del sistema.
Le interfacce per utenti sono un caso interessante, perch in esse possiamo vedere un'influenza nel senso opposto: le problematiche di ingegneria del software riguardo alle interfacce utente grafiche hanno favorito lo studio di linguaggi di programmazione visuali. Questi
linguaggi cercano di riprodurre la semantica delle finestre e dei paradigmi di interazione offerti dai dispositivi di visualizzazione grafica. Un'altra influenza del settore dei linguaggi di
programmazione sull'ingegneria del software riguarda le tecniche implementative, sviluppate per anni per l'elaborazione dei linguaggi. Un approccio generativo all'ingegneria del software
si basa sulla lezione appresa con i linguaggi di programmazione, che la formalizzazione porta all'automazione: creare una grammatica formale per un linguaggio permette di produrre
automaticamente un analizzatore sintattico. Questa tecnica ampiamente sfruttata in molte aree dell'ingegneria del software per la creazione di specifiche formali e la generazione automatica di software.

1.5.2

Sistemi operativi

L'influenza dei sistemi operativi sull'ingegneria del software notevole, soprattutto perch
questi furono i primi sistemi di software di ragguardevoli dimensioni a essere realizzati, e
quindi costituirono il primo esempio di software che richiedeva un lavoro di ingegnerizzazione. Molte originarie idee di progettazione di software nacquero dai primi tentativi di realizzazione dei sistemi operativi.
Concetti quali le macchine virtuali, i livelli di astrazione e la separazione delle politiche dai meccanismi nascono nel campo dei sistemi operativi, ma possono essere applicati
a qualsiasi sistema software di grosse dimensioni. Ad esempio, l'idea di separare la politica
che un sistema operativo vuole imporre, come quella di assicurare che tutti i processi vengano attivati in modo da garantire l'avanzamento della loro esecuzione (task scheduling),
dal meccanismo usato per raggiungere tale parallelismo, ad esempio la divisione del tempo (time sharing), costituisce un modello di separazione del "che cosa" dal "come" o della

specifica dall'implementazione. L'idea dei livelli di astrazione un altro modo di modularizzare il progetto di un sistema.
Esempi dell'influenza delle tecniche di ingegneria del software sulla struttura dei sistemi operativi sono i sistemi operativi portabili e i sistemi operativi che sono strutturati in
modo da contenere un piccolo micro-kernel "protetto", che provvede a fornire solo una minima parte di funzionalit per interfacciarsi con l'hardware, mentre la maggior parte delle
funzionalit usualmente associate ai sistemi operativi vengono fornite da una parte "non protetta". Ad esempio, la parte non protetta potrebbe permettere all'utente di controllare lo schema di gestione delle pagine di memoria, che sempre stato tradizionalmente visto come parte integrante del sistema operativo.
Allo stesso modo, nei primi sistemi operativi, l'interprete di comandi era parte integrante di essi. Oggi, invece, considerato come qualsiasi altro programma {utility), il che
permette a ogni utente di avere una versione personalizzata dell'interprete. In molti sistemi
UNIX ci sono almeno tre di questi interpreti.

1.5.3

Basi di dati

Le basi di dati rappresentano un'altra classe di sistemi software di grandi dimensioni il cui
sviluppo ha influenzato l'ingegneria del software attraverso la scoperta di nuove tecniche di
progettazione. Probabilmente, l'influenza pi importante rappresentata dalla nozione di
"indipendenza dei dati", che ancora un ennesimo esempio di separazione della specifica
dall'implementazione. Una base di dati permette di scrivere applicazioni che usano tali dati senza preoccuparsi della loro rappresentazione sottostante. Questa indipendenza fa in modo che la base di dati possa essere in qualche modo modificata ad esempio, per aumentare le prestazioni del sistema - senza che sia necessario cambiare anche le applicazioni che interagiscono con essa. Questo un esempio dei benefici dell'astrazione e della separazione
degli interessi (separation of concerti), due principi fondamentali dell'ingegneria del software
come vedremo nel Capitolo 3.
Un altro interessante influsso della tecnologia delle basi di dati sull'ingegneria del software
la possibilit di usare sistemi di basi di dati come componenti di sistemi software di grosse dimensioni. Siccome le basi di dati hanno risolto molti dei problemi legati alla gestione
degli accessi concorrenti da parte di pi utenti a grandi quantit di informazioni, non c'
alcun bisogno di re-inventare quelle soluzioni quando stiamo creando un sistema software;
possiamo semplicemente usare un sistema esistente per la gestione di basi di dati come un
componente.
Un'interessante influenza dell'ingegneria del software sulla tecnologia delle basi di dati ha le sue origini nei primi tentativi di usare le basi di dati per supportare gli ambienti di
sviluppo del software. Questa esperienza mostr che la tecnologia tradizionale delle basi di
dati era incapace di trattare i problemi posti dai processi di ingegneria del software. Ad esempio, un database tradizionale non in grado di gestire in maniera ottimale le seguenti richieste: memorizzare grossi oggetti non strutturati come programmi sorgente, manuali per
utente o codice eseguibile; mantenere diverse versioni dello stesso oggetto; memorizzare oggetti, come un prodotto, con molti ampi campi, strutturati e non, come codice sorgente,
codice oggetto e un manuale utente.
Un'altra difficolt legata alla lunghezza delle transazioni. Le basi di dati tradizionali supportano transazioni corte, come un deposito o un prelievo su un conto bancario. Gli ingegneri

del software invece necessitano di transazioni molto lunghe: un ingegnere pu richiedere un


lavoro prolungato per ricostruire un sistema costituito da molti moduli o potrebbe prelevare
dal database (check out) un programma e lavorarci per settimane prima di riconsegnarlo (check
in). Il problema per la base di dati come trattare il blocco di codice durante quelle settimane. Se l'ingegnere del software volesse lavorare solo su una piccola parte del programma? Durante
tale periodo proibito l'accesso a tale programma a tutti i programmatori?
Queste richieste hanno stimolato progressi nell'area delle basi di dati che hanno portato
a nuovi modelli di database e di transazioni o all'adattamento dei modelli preesistenti.

1.5.4

Intelligenza artificiale

L'intelligenza artificiale un altro campo che ha esercitato un'influenza sull'ingegneria del


software. Molti sistemi software creati nella comunit di ricerca dell'intelligenza artificiale
sono sistemi complessi e di grosse dimensioni, ma sono sempre stati significativamente diversi dagli altri. Molti di essi furono costruiti avendo solo una vaga idea di come il sistema
avrebbe funzionato. Il termine "sviluppo esplorativo" stato impiegato per il processo utilizzato per costruire questi sistemi.
Lo sviluppo esplorativo l'opposto dell'ingegneria del software tradizionale, ove il progettista svolge determinati passi per cercare di produrre un progetto completo prima di procedere con la codifica. Con l'intelligenza artificiale sono nate nuove tecniche per gestire le
specifiche, la verifica e l'analisi quando vi un dato grado di incertezza. Altre tecniche portate dall'intelligenza artificiale includono l'uso della logica sia nella specifica di un software
che nei linguaggi di programmazione.
Tecniche di ingegneria del software sono state usate nei sistemi di intelligenza artificiale chiamati sistemi esperti. Questi sistemi sono modularizzati, con una chiara divisione
tra i "fatti" conosciuti dal sistema e le "regole" usate dal sistema per elaborare tali fatti, ad
esempio, una regola per decidere il corso di un'azione. Questa separazione ha permesso la
creazione e la commercializzazione di shell (letteralmente "gusci") di sistemi esperti che includono solo regole. Un utente pu applicare quindi la shell a un'applicazione di suo interesse fornendole fatti specifici dell'applicazione. L'idea che la conoscenza riguardo l'applicazione sia fornita dall'utente, mentre i principi generali su come applicare tale conoscenza
a un qualunque problema sia fornita dalla shell.
Alcuni ricercatori hanno provato a usare tecniche di intelligenza artificiale per perfezionare i compiti dell'ingegneria del software. Ad esempio, sono stati sviluppati "assistenti
di programmazione" che svolgano funzione di consulenti per il programmatore, controllando
frasi di programmazione idiomatiche o i requisiti del sistema.
Il problema di fornire interfacce agli utenti inesperti - ad esempio, attraverso l'uso del
linguaggio naturale - fu affrontato inizialmente dall'intelligenza artificiale. Per tracciare il
profilo dell'utente furono utilizzati modelli cognitivi. Questi studi hanno influenzato l'area
dell'ingegneria del software che si occupa dell'interfaccia utente.

1.5.5

Modelli teorici

L'informatica teorica ha sviluppato vari modelli che sono diventati poi strumenti utili nell'ingegneria del software. Ad esempio, le macchine a stati finiti sono servite sia come basi
per le tecniche di specifica del software sia come modelli per la progettazione e la struttura

del software. Protocolli di comunicazione e analizzatori di linguaggio utilizzano macchine a


stati finiti come modelli di processo.
Sono stati impiegati anche gli automi a pila (pushdoum automato), ad esempio, per specifiche operazionali di sistemi e per costruire processori per tali specifiche. E interessante rimarcare il fatto che gli automi a pila sono essi stessi scaturiti da tentativi pratici di creazione di analizzatori sintattici e compilatori per linguaggi di programmazione.
Le reti di Petri, descritte nel Capitolo 5, sono un altro contributo dell'informatica teorica all'ingegneria del software: vennero inizialmente usate per creare modelli di sistemi
hardware, ma furono sempre pi utilizzate anche per modellare il software.
Infine, un ulteriore importante esempio fornito dalla logica matematica, che stata
la base di molti linguaggi di specifica.
Allo stesso tempo, l'ingegneria del software ha influenzato l'informatica teorica.
Specifiche algebriche e tipi di dati astratti sono nati per motivi di ingegneria del software.
Inoltre nell'area delle specifiche, l'ingegneria del software ha focalizzato maggiore attenzione su teorie logiche non del primo ordine, come ad esempio la logica temporale. I logici matematici hanno sempre prestato molta pi attenzione alle teorie di primo ordine
che a quelle di ordini pi elevati, perch le due sono equivalenti in potenza, ma le teorie
di primo livello sono, da un punto di vista matematico, pi essenziali. Esse per non sono cos espressive come le teorie di ordine pi elevato. Un ingegnere del software, a dispetto di un informatico teorico, interessato sia alla potenza sia all'espressivit di una
teoria. Per esempio, la logica temporale assicura un stile pi naturale e compatto per la
specifica dei requisiti di un sistema concorrente rispetto alle teorie di primo ordine. Le
necessit dell'ingegneria del software hanno quindi ispirato nuovi studi dei teorici verso
tali teorie di ordine superiore.

1.6

Relazioni tra l'ingegneria del software


e altre discipline

Nelle sezioni precedenti abbiamo analizzato i rapporti tra ingegneria del software e gli altri
campi dell'informatica. In questo paragrafo vedremo come l'ingegneria del software ha rapporti con altri campi al di fuori dell'informatica.
L'ingegneria del software non pu essere praticata su una torre d'avorio. Vi sono molti problemi non specifici dell'ingegneria del software che sono stati risolti da altri campi. Tali
soluzioni possono essere adattate all'ingegneria del software. In questo modo non c' bisogno di reinventare ogni singola soluzione. Ad esempio, le scienze cognitive possono aiutarci a sviluppare interfacce utenti migliori e le teorie economiche ci supportano nella scelta
tra diversi modelli dei processi di sviluppo.

1.6.1

Scienze organizzative

Gran parte dell'ingegneria del software coinvolge ambiti organizzativi e gestionali. Come in
ogni grande progetto, cui partecipano molte persone, occorre effettuare stime, pianificare,
gestire le risorse umane, scomporre e assegnare compiti e monitorare il processo. Inoltre
necessario assumere persone, motivarle e assegnare a ciascuno il compito pi adatto.

Le scienze organizzative studiano esattamente questi problemi. Sono stati sviluppati molti modelli gestionali che possono essere applicati all'ingegneria del software e, facendo ricorso alle scienze organizzative, possibile sfruttare i risultati di decenni di studi ed esperienze.
Allo stesso tempo l'ingegneria del software ha fornito alle scienze organizzative un nuovo dominio nel quale testare teorie e modelli organizzativi e gestionali. I modelli tradizionali adatti alla produzione per linea non sono applicabili ad attivit incentrate sul lavoro umano quale l'ingegneria del software.

1.6.2

Ingegneria dei sistemi

L'ingegneria dei sistemi si occupa dello studio dei sistemi complessi. L'ipotesi sottostante che
certe leggi governino il comportamento di qualunque sistema complesso costituito da molti
componenti con complicate interrelazioni. L'ingegneria dei sistemi utile quando l'interesse
dello studio incentrato sul sistema piuttosto che sui suoi componenti. L'ingegneria dei sistemi tenta di scoprire approcci comuni che si applicano a sistemi diversi come impianti chimici, costruzioni di edifici e di ponti.
Il software spesso un componente di un sistema molto pi grande. Per esempio il software
di gestione di una fabbrica o il software di controllo del volo di un aereo sono componenti di
sistemi pi complessi. Le tecniche dell'ingegneria dei sistemi possono essere applicate allo studio di questi sistemi. Possiamo anche considerare un sistema software composto da migliaia di
moduli come un sistema candidato per essere studiato con le leggi dell'ingegneria dei sistemi.
Allo stesso tempo l'ingegneria dei sistemi si arricchita di un insieme crescente di modelli analitici, che tradizionalmente erano basati su metodi matematici classici, fino a includere
modelli discreti che sono di uso comune nell'ingegneria del software.

1.7

Osservazioni conclusive

L'ingegneria del software una disciplina ingegneristica in evoluzione che studia gli approcci sistematici per costruire software di grosse dimensioni attraverso il lavoro di gruppo. Abbiamo
illustrato la storia della sua evoluzione, presentato le relazioni con altre materie e abbiamo
elencato le qualifiche che un ingegnere del software deve possedere. In questo libro vedremo
i principi essenziali per la creazione di software mediante un approccio ingegneristico.

Note bibliografiche
La definizione di ingegneria del software citata all'inizio del capitolo tratta da Parnas [1978]. La distinzione tra "programmare in piccolo" e "programmare in grande" e la constatazione che l'ingegneria del software concerne la programmazione in grande di D e R e m e r e Kron [1976].
Il termine "ingegneria del software" fu usato per la prima volta in una conferenza della N A T O ,
tenutasi a Garmisch, Germania, nel 1968. Un rapporto su tale conferenza si trova in un libro p u b blicato da N a u r et al. [1976].
Per la terminologia standard del campo, il lettore pu far riferimento alla collezione degli standard
pubblicata dalla IEEE [1999].
Boehm [1976] port all'attenzione di tutti l'ingegneria del software e le sue sfide.
Le difficolt pratiche incontrate nello sviluppo di prodotti software industriali sono trattate nel
libro classico di Brooks, The MythicalMan-Month [1975, 1995]. Di Brooks [1987] anche l'ormai
classico articolo sul "proiettile d'argento". Boehm [1981, 1995] fornisce le f o n d a m e n t a per modellare e valutare i costi di un software.
Gli articoli di Parnas [1985] e Brooks [1987] contengono acute discussioni sulla natura del
software e le difficolt a esso inerenti. Un'opinione provocatoria contenuta nel dibattito riportato
in D e n n i n g [1989], che contiene un discorso tenuto da Dijkstra [1989] e confutazioni da parte di
molti dei pi importanti informatici.
Per un dibattito sulla relazione tra ingegneria del software e linguaggi di programmazione, si
consulti Ghezzi e Jazayeri [1998], che fornisce anche una visione globale dei linguaggi di programmazione, dei loro concetti e della loro evoluzione.
Molti lavori sui sistemi operativi h a n n o influenzato la progettazione del software; citiamo, in
particolare, i lavori iniziali di Dijkstra [1968a e b, e 1971], Hoare [1972, 1974, 1985] e Brinch Hansen
[1977]. L'interazione tra sistemi operativi e ingegneria del software discussa da Browne [1980],
Le basi di dati sono studiate da Ullman e W i d o m [1997] e Date [2000], Gli specifici requisiti
delle basi di dati richiesti dall'ingegneria del software sono analizzati da Dittirch et al. [2000].
I rapporti tra ingegneria del software e intelligenza artificiale sono analizzati in vari articoli, e
le opinioni sono spesso controverse. Ad esempio, Simon [1986] e T i c h y [1987] sostengono che l'ingegneria del software dovrebbe adottare i metodi e gli strumenti dell'intelligenza artificiale, mentre
Parnas [1988] sostiene il contrario e fornisce una visione critica dell'intelligenza artificiale. Alcuni approcci al software basato sulla conoscenza sono descritti da Kant e Barstow [1981], dall'edizione speciale dell'IEEE Transactions o n Software Engineering curato da Mostow (TSE [1985]), Goldberg
[1986], e Rich e Waters [1988],
Una discussione dei rapporti tra informatica teorica e sviluppo di software p u essere trovata
in Mandrioli e Ghezzi [1987]
Boehm [2000] sottolinea l'importanza dei rapporti tra ingegneria del software e ingegneria dei
sistemi. Spector e Gifford [1986] discutono delle relazioni tra ingegneria del software e un altro campo dell'ingegneria, il progetto di ponti.
N e u m a n n [1995] fornisce u n allarmante elenco di rischi per il pubblico, dovuti a software difettosi, e solleva il fondamentale problema della responsabilit sociale di u n ingegnere del software.
Bohem e Sullivan [2000] discute dell'importanza dell'economia nell'ingegneria del software.

CAPITOLO

Il software: natura e qualit

Obiettivo di ogni attivit ingegneristica costruire qualcosa, un artefatto' o un prodotto;


cos l'ingegnere civile costruisce un ponte, l'ingegnere aerospaziale un aereo, l'ingegnere elettronico un circuito. Il prodotto dell'ingegneria del software un sistema software: questo
non tangibile alla pari degli altri, ma comunque un manufatto in grado di rispondere a
una specifica funzione d'uso.
La caratteristica che forse distingue maggiormente il software dagli altri prodotti il
fatto che sia "duttile": ossia, possibile modificare il prodotto anzich modificare il progetto in maniera molto facile e questo lo rende sostanzialmente diverso dagli altri prodotti, come le automobili o i forni.
Questa peculiarit del software spesso male utilizzata. Quantunque sia possibile modificare un ponte o un aereo per soddisfare nuove necessit (per esempio, adeguare il ponte al crescente flusso di traffico o consentire a un aereo di trasportare pi merci) questa modifica non viene mai intrapresa a cuor leggero, e certamente non viene tentata senza prima
rielaborare il progetto e verificare l'impatto del cambiamento in maniera approfondita. Al
contrario, agli ingegneri del software viene spesso richiesto di effettuare modifiche sostanziali. La duttilit del software porta a pensare che apportare modifiche sia banale, ma in pratica non cos.
facile cambiare il codice attraverso un editore di testi, ma non facile fare in modo che il software soddisfi i fabbisogni per i quali era richiesto un cambiamento. bene
dunque che il software sia trattato alla pari degli altri prodotti ingegneristici: una modifica nel software deve essere vista come un cambiamento nel progetto piuttosto che nel
codice. Una propriet come la duttilit pu essere vantaggiosamente sfruttata, ma occorre farlo con disciplina.
Un'altra caratteristica del software che la sua creazione human intensive, ossia richiede un'elevata intensit di lavoro; richiede essenzialmente un'attivit di "ingegneria" piuttosto che di "fabbricazione". Nella maggior parte delle altre discipline, il processo di fab-

1
Come precisato in seguito (Paragrafo 2.1.2), utilizziamo il termine artefatto come traduzione dell'inglese artifact per indicare la specifica accezione di "elaborato intermedio", distinguendolo cos da "elaborato", considerato come prodotto finale, consegnato al committente (N.d.T.).

bricazione determina il costo finale del prodotto; inoltre il processo di produzione deve essere gestito in maniera accurata, in modo che non vengano introdotti difetti indesiderati nel
prodotto. Le stesse considerazioni si applicano ai prodotti hardware, mentre invece per il
software la fabbricazione si riduce a un banale processo di duplicazione. Il processo di produzione del software consiste essenzialmente nel progetto e nell'implementazione, e non nella fabbricazione. Questo processo deve soddisfare opportuni criteri che assicurino la produzione di software di elevata qualit. E auspicabile che un prodotto soddisfi determinate necessit e rispetti standard di accettazione che prescrivono le qualit che deve possedere. Ad
esempio, la funzionalit di un ponte quella di rendere facile il collegamento da un posto
a un altro; una delle qualit attese che questo non crolli in presenza di vento molto forte
o quando attraversato da una fila di camion.
Nelle discipline dell'ingegneria tradizionale, l'ingegnere dispone di strumenti per descrivere le qualit del prodotto in maniera distinta rispetto al progetto del prodotto stesso. Nell'ingegneria del software questa distinzione non cosi chiara: le qualit di un prodotto software
sono molte volte mescolate nelle specifiche, insieme alle qualit intrinseche del progetto.
In questo capitolo esamineremo le qualit rilevanti nei prodotti software e nei processi di produzione del software. Queste qualit diventeranno i nostri obiettivi da perseguire
nella pratica dell'ingegneria del software. Nel capitolo successivo presenteremo i principi dell'ingegneria del software applicabili per raggiungere questi obiettivi. E inoltre importante verificare e misurare la presenza di qualit: questi argomenti sono introdotti nel Paragrafo 2.4
e approfonditi nel Capitolo 6.

2.1

Classificazione delle qualit del software

Esistono molte qualit desiderabili per il software; alcune si applicano sia al prodotto che al
processo utilizzato per il suo sviluppo.
L'utente richiede che il prodotto software sia affidabile, efficiente e facile da usare. Il
produttore del software desidera che sia verificabile, manutenibile, portabile ed estendibile.
Il manager di un progetto software desidera che il processo di sviluppo sia produttivo, prevedibile e facile da controllare.
In questo paragrafo analizzeremo due diverse classificazioni delle qualit relative al
software: qualit interne e qualit esterne da un lato, e qualit di prodotto e qualit del processo dall'altro.

2.1.1

Qualit interne ed esterne

Possiamo dividere le qualit del software in due categorie: interne ed esterne. Le qualit esterne sono visibili agli utenti del sistema mentre le qualit interne riguardano gli sviluppatori.
In generale gli utenti del software hanno interesse soltanto nelle qualit esterne, ma sono le
qualit interne, che in larga misura hanno a che fare con la struttura del software, ad aiutare gli sviluppatori a raggiungere le qualit esterne. Per esempio la qualit interna di verificabilit necessaria per ottenere la qualit esterna di affidabilit. In molti casi tuttavia le qualit sono tra di loro strettamente correlate e la distinzione tra qualit interne ed esterne non
cos marcata.

2.1.2

Qualit del processo e qualit del prodotto

Per realizzare un prodotto software si utilizza un processo. E possibile attribuire alcune qualit a un processo, anche se le qualit del processo sono spesso correlate con quelle del prodotto. Per esempio, l'affidabilit di un prodotto aumenta se il processo relativo richiede
un'accurata pianificazione dei dati di test prima che sia effettuata un'attivit di progettazione e di sviluppo del sistema. Nell'affrontare le qualit del software bene comunque cercare di distinguere tra qualit del processo e qualit del prodotto.
II termine prodotto di solito si riferisce a quanto viene alla fine consegnato al committente. Quantunque questa sia una definizione accettabile dal punto di vista del committente, non corretta per lo sviluppatore, in quanto questi, nel corso del processo di sviluppo,
produce una serie di prodotti intermedi. Il prodotto effettivamente visibile al committente
consiste nel codice eseguibile e nei manuali utente, ma lo sviluppatore produce un numero
elevato di altri artifatti, quali i documenti di specifica dei requisiti e di progetto, i dati di
test e cos via. Noi useremo il termine artefatto per denotare questi prodotti intermedi e per
distinguerli dal prodotto finale consegnato al committente. Questi prodotti intermedi sono
spesso soggetti agli stessi requisiti di qualit del prodotto finale. Poich esistono molti prodotti intermedi, possibile che diversi sottoinsiemi di questi vengano poi resi disponibili a
diversi committenti.
Per esempio, un costruttore di computer potrebbe vendere a un'azienda produttrice
di sistemi di controllo di processo il codice oggetto che deve essere installato nell'hardware
specializzato di un'applicazione embedded; questa potrebbe poi distribuire il codice oggetto e i manuali utente ai rivenditori di software. Infine, potrebbe cedere il progetto e il
codice sorgente ai produttori di software, i quali potrebbero modificarli per costruire altri prodotti.
L'attivit di gestione delle configurazioni (configuration management) costituisce quella parte del processo di produzione del software che affronta il problema di mantenere e controllare le relazioni tra tutti i diversi prodotti intermedi delle varie versioni di un prodotto.
Gli strumenti di gestione delle configurazioni supportano la manutenzione di famiglie di
prodotti e dei loro componenti e aiutano a controllare e a gestire i cambiamenti ai prodotti intermedi. Discuteremo l'attivit di gestione della configurazione nel Capitolo 7.

2.2

Principali qualit del software

In questa sezione presentiamo le pi importanti qualit dei prodotti e dei processi software;
ove appropriato, analizziamo una qualit con riferimento alle classificazioni che abbiamo descritto nel paragrafo precedente.

2.2.1

Correttezza, affidabilit e robustezza

I termini correttezza, affidabilit e robustezza sono strettamente correlati e insieme caratterizzano una qualit del software secondo la quale l'applicazione realizza la sua funzionalit attesa. Vediamo ora in dettaglio questi tre termini, analizzandone le loro mutue relazioni.

2.2.1.1

Correttezza

Un programma deve soddisfare la specifica dei suoi requisiti funzionali (functional requirements specification)-, esistono tuttavia altri requisiti, quelli di prestazioni e di scalabilit, i quali non fanno riferimento alle funzionalit del sistema; questi sono chiamati requisiti non funzionali (nonfunctional requirements). Un programma funzionalmente corretto se si comporta secondo quanto stabilito dalle specifiche funzionali. Spesso si usa il termine "corretto" invece di "funzionalmente corretto", e analogamente, in questo contesto si usa il termine "specifiche" invece di "specifiche dei requisiti funzionali". Noi seguiremo questa convenzione quando il contesto chiaro.
La definizione di correttezza assume che le specifiche del sistema siano disponibili e
che sia possibile determinare in maniera non ambigua se un programma soddisfi le specifiche. Tali specifiche raramente sono disponibili per la maggior parte dei sistemi software esistenti. Se una specifica esiste, questa normalmente scritta in linguaggio non formale usando il linguaggio naturale; pertanto probabile che contenga ambiguit. Tuttavia la definizione di correttezza utile perch cattura un obiettivo desiderabile dei sistemi software.
La correttezza un propriet matematica che stabilisce l'equivalenza tra il software e la
sua specifica. Ovviamente, possiamo essere tanto pi sistematici e precisi nel valutare la correttezza quanto pi rigorosi siamo stati nello specificare i requisiti funzionali. Come vedremo nel Capitolo 6, si pu valutare la correttezza di un programma mediante vari metodi,
alcuni basati su un approccio sperimentale (ad esempio il testing), altri basati su un approccio analitico, come le ispezioni del codice o la verifica formale della sua correttezza. La correttezza pu essere migliorata usando strumenti adeguati quali linguaggi di alto livello, in
particolare quelli che supportano un'analisi statica approfondita dei programmi. Analogamente, la correttezza pu essere migliorata usando ben noti algoritmi standard o librerie
di moduli standard, piuttosto che inventarne ogni volta di nuovi. Infine, la correttezza pu
essere migliorata utilizzando metodologie e processi di provata efficacia.
2.2.1.2

Affidabilit

Informalmente, il software considerato affidabile nella misura in cui l'utente pu fare affidamento sulle sue funzionalit 2 . La letteratura specializzata definisce l'affidabilit in termini statistici, ovvero come la probabilit che un software operi come atteso in un intervallo
di tempo determinato. Approfondiremo questo approccio pi avanti, per ora ci accontenteremo invece di una definizione informale.
La correttezza una qualit assoluta: una qualunque deviazione rispetto a quanto stabilito rende un sistema non corretto, indipendentemente dalla seriet delle conseguenze di tale
deviazione. La nozione di affidabilit invece relativa. Se la conseguenza di un errore software
non grave, un software non corretto pu continuare a essere considerato affidabile.
comune attendersi che i prodotti dell'ingegneria siano affidabili: quelli non affidabili di solito scompaiono velocemente dal mercato. Sfortunatamente i prodotti software non
hanno raggiunto questo invidiabile stato, e vengono spesso rilasciati insieme a una lista di
malfunzionamenti noti; gli utenti del software accettano come ineluttabile il fatto che la ver-

II termine che viene spesso usato come sinonimo di affidabile (reliable) dependable.

Figura 2.1

Relazione tra correttezza e affidabilit nel caso ideale.

sione 1 di un prodotto contenga errori (un prodotto del genere viene definito "bacato", in
inglese buggy). Questo uno dei sintomi pi evidenti dell'immaturit dell'ingegneria del
software rispetto agli altri campi dell'ingegneria3.
Nelle discipline classiche, un prodotto non viene rilasciato se pu generare malfunzionamenti. Nessuno, infatti, accetterebbe di acquistare un automobile insieme a una lista
di inconvenienti noti, o attraverserebbe un ponte che presenta un cartello di avvertimento
sui pericoli del suo utilizzo. Gli errori di progettazione sono rari e qualora si manifestino
vengono pubblicati sui giornali come fatti di cronaca. Il crollo di un ponte comporta inevitabilmente la denuncia dei progettisti.
Al contrario, gli errori di progettazione del software vengono spesso trattati come inevitabili e, quando troviamo errori in un'applicazione, invece di essere sorpresi, in un certo
senso li accettiamo con una sorta di rassegnazione. Mentre i manufatti convenzionali sono
accompagnati da una garanzia che ne certifica l'assoluta affidabilit, di solito i prodotti
software sono accompagnati da un avvertimento, nel quale il produttore afferma che non si
ritiene responsabile degli errori presenti nel prodotto e dagli eventuali danni che possono
provocare. L'ingegneria del software potr davvero essere considerata una disciplina ingegneristica solo nel momento in cui si riuscir a raggiungere con il software un'affidabilit
paragonabile a quella raggiunta dagli altri prodotti dell'ingegneria.
Nella Figura 2.1 illustrata la relazione tra affidabilit e correttezza, nell'ipotesi che la
specifica dei requisiti funzionali colga esattamente tutte le propriet desiderabili di un'applicazione e non contenga invece, erroneamente, propriet indesiderabili. La figura dimostra che l'insieme di tutti i programmi affidabili include l'insieme dei programmi corretti,
ma non viceversa. Sfortunatamente, in pratica, la situazione diversa: infatti la specifica
un modello di ci che l'utente desidera, ma il modello non necessariamente una descrizione accurata dei requisiti effettivi dell'utente. Ci che il software pu fare soddisfare i
requisiti specificati dal modello, non pu invece assicurare l'accuratezza del modello.
La Figura 2.1 rappresenta una situazione idealizzata nella quale si assume che i requisiti siano corretti, e cio che siano una rappresentazione fedele di ci che l'implemen-

Dijkstra [1989] afferma che l'utilizzo del termine gergale bug (letteralmente "cimice"; in italiano, nel
contesto informatico, "baco"), spesso usato dagli ingegneri del software, sintomo di mancanza di professionalit.

razione deve assicurare al fine di soddisfare i fabbisogni degli utenti previsti. Come tratteremo in modo approfondito nel Capitolo 7, si pongono frequentemente ostacoli insormontabili al raggiungimento di questo obiettivo; la conseguenza che talvolta vi sono applicazioni corrette sviluppate sulla base di requisiti non corretti, sicch la correttezza del
software non sufficiente a garantire all'utente che il software si comporti come effettivamente desiderato.
2.2.1.3

Robustezza

Un programma robusto se si comporta in modo accettabile anche in circostanze non previste nella specifica dei requisiti, per esempio quando vengono inseriti dati di input non corretti o in presenza di malfunzionamenti hardware (come la rottura di un disco). Un programma non robusto se assume dati di ingresso perfetti e genera un errore non recuperabile durante l'esecuzione, qualora l'utente prema inavvertitamente un tasto sbagliato. Esso
tuttavia pu essere corretto, se la specifica dei requisiti non indica che cosa il programma
dovrebbe fare in corrispondenza di un input scorretto. Naturalmente la robustezza una qualit difficile da descrivere: se potessimo definire esattamente che cosa occorrerebbe fare per
rendere un'applicazione robusta, saremmo sempre capaci di specificare, in maniera completa, il comportamento che ci attendiamo da un programma, e pertanto la robustezza diventerebbe equivalente alla correttezza o all'affidabilit nel senso della Figura 2.1.
Ancora una volta, l'analogia con i ponti risulta istruttiva. Due ponti sullo stesso fiume
sono entrambi corretti se soddisfano i requisiti di partenza; se invece durante un improvviso e inatteso terremoto solo uno dei due crolla, possiamo definire quello rimasto intatto pi
robusto dell'altro. Si noti che la lezione che si pu trarre dal crollo di un ponte porter a requisiti pi completi per il futuro, stabilendo la resistenza ai terremoti come un requisito di
correttezza. In altre parole, non appena il fenomeno studiato diventa meglio conosciuto,
possibile approssimare meglio il caso ideale mostrato nella Figura 2.1, dove le specifiche colgono i requisiti attesi in maniera esatta.
La quantit di codice che affronta i requisiti di robustezza dipende dall'area applicativa; ad esempio, un sistema scritto per utenti inesperti di computer deve avere una maggiore predisposizione a reagire a input formattati in maniera scorretta, rispetto a un sistema
embedded, che riceve i dati da sensori. Questo naturalmente non significa che i sistemi
embedded non debbano essere robusti; al contrario, siccome spesso controllano dispositivi
critici, richiedono di conseguenza un'elevata robustezza.
In conclusione, possiamo dire che la robustezza e la correttezza sono caratteristiche strettamente correlate, senza che esista un linea divisoria netta tra le due. Se un requisito diventa parte della specifica, il suo soddisfacimento diventa un problema di correttezza; se invece lasciamo un requisito fuori dalla specifica, allora esso pu diventare un problema di robustezza. La linea di demarcazione tra le due qualit la specifica del sistema. Inoltre l'affidabilit ha un ruolo importante, in quanto non tutti i comportamenti scorretti implicano
malfunzionamenti di eguale severit; vale a dire, alcuni comportamenti scorretti possono in
pratica essere tollerati.
Possiamo inoltre usare i termini correttezza, robustezza e affidabilit anche in relazior : al processo di produzione del software. Un processo robusto, per esempio, se pu far
fronte a cambiamenti inattesi relativi all'ambiente, come il rilascio di un nuovo sistema operativo o l'improvviso trasferimento di met dei dipendenti dell'azienda a un'altra sede. Un

processo affidabile se porta comunque alla realizzazione di prodotti di elevata qualit. In


molte discipline ingegneristiche una considerevole attivit di ricerca stata sviluppata per
individuare processi affidabili.

2.2.2

Prestazioni

E comune attendersi che un prodotto dell'ingegneria offra buone prestazioni. Diversamente


da altre discipline, nell'ingegneria del software molte volte confondiamo le prestazioni con l'efficienza, malgrado questi termini non denotino esattamente le stesse caratteristiche. L'efficienza
una caratteristica interna che si riferisce al "peso" che un software ha sulle risorse del computer. La prestazione invece una qualit esterna basata sui requisiti dell'utente. Ad esempio,
potrebbe essere richiesto a un sistema telefonico di gestire 10.000 chiamate all'ora. L'efficienza
influenza e, frequentemente, determina le prestazioni di un sistema.
Le prestazioni sono importanti in quanto influiscono sull'utilizzabilit di un sistema;
infatti, se un sistema software troppo lento, pu ridurre la produttivit dell'utente, magari fino al punto da non soddisfare pi le sue esigenze. Se un sistema software utilizza troppo spazio su disco, potrebbe anche essere molto costoso; se usa troppa memoria potrebbe
influire sulle applicazioni che sono in esecuzione nello stesso sistema, oppure potrebbe rallentare la propria esecuzione mentre il sistema operativo cerca di bilanciare l'uso della memoria da parte delle varie applicazioni in esecuzione in quel momento. Proprio per questi
motivi diffcile definire esattamente il concetto di efficienza, perch questo si evolve e trasforma con l'evolversi della tecnologia.
Anche il concetto di "troppo costoso" in costante cambiamento a causa dei continui
progressi tecnologici che ridefiniscono continuamente i limiti di ci che la tecnologia in
grado di assicurare. I computer di oggi costano molto meno rispetto a quelli di qualche anno fa, e allo stesso tempo sono molto pi potenti.
Le prestazioni sono inoltre importanti in quanto influenzano la capacit di crescita
[scalability) di un sistema software. Un algoritmo di complessit quadratica potr funzionare per input di piccole dimensioni, ma di sicuro non funzioner per input di grandi dimensioni. Ad esempio, un compilatore che usa un algoritmo di allocazione di registri il cui
tempo di esecuzione dipende dal quadrato del numero di variabili del programma sar sempre pi lento man mano che aumenteranno le dimensioni del programma da compilare.
Ci sono vari modi per valutare le prestazioni di un sistema, ad esempio analizzando
la complessit degli algoritmi usati. Vi una teoria che classifica il funzionamento di un
algoritmo nella situazione peggiore (worst case) e in quella media (average), in termini di
tempo e spazio o, addirittura, in termini del numero di messaggi scambiati nei casi di sistemi distribuiti.
L'analisi di complessit degli algoritmi fornisce informazioni solo sui casi medio e peggiore, ma non fornisce alcuna informazione specifica su di una determinata implementazione.
Per avere informazioni pi specifiche e dettagliate, possiamo usare le tecniche di valutazione delle prestazioni. I tre metodi principali per valutare le prestazioni di un sistema sono:
misura (measurement), analisi (analysis) e simulazione {simulatiori). Possiamo misurare le prestazioni reali di un sistema grazie a sistemi hardware e software di monitoraggio, che collezionano dati mentre il sistema in esecuzione e quindi ci permettono di scoprire eventuali
colli di bottiglia nel sistema. In questo caso estremamente importante impostare dati di
input che portino a esecuzioni che siano rappresentative dell'uso tipico del sistema. Il se-

condo approccio si basa sulla costruzione di un modello del prodotto e al suo successivo studio analitico. Il terzo approccio invece si basa sulla costruzione di un modello che simula il
prodotto. I modelli analitici, usati nel secondo approccio, solitamente basati sulla teoria delle code, sono facili da costruire ma poco accurati, mentre i modelli di simulazione sono pi
costosi ma anche pi accurati. A volte queste due tecniche possono essere combinate tra loto: all'inizio di un progetto di grandi dimensioni, un modello analitico pu essere utile per
avere una visione generale delle aree critiche nelle prestazioni del sistema e quindi individuare le aree in cui sono necessari ulteriori studi; successivamente si pu passare alla costruzione dei modelli simulativi per tali aree.
In molti progetti di sviluppo di software, le prestazioni vengono prese in considerazione solo dopo l'implementazione della versione iniziale del prodotto. E molto difficile a volte proprio impossibile - arrivare a significativi miglioramenti nelle prestazioni senza
dover riprogettare il software. Anche un modello molto semplice, tuttavia, pu essere utile
per prevedere le prestazioni di un sistema e indirizzare le scelte di progettazione in modo da
minimizzare la riprogettazione.
In alcuni progetti complessi, per i quali potrebbe non essere ovvia la raggiungibilit di
determinati requisiti di prestazioni, ci si impegna a creare modelli per l'analisi delle prestazioni. Tali progetti iniziano con un modello, utilizzato inizialmente per rispondere a domande
e dubbi inerenti la fattibilit e successivamente per prendere decisioni. Questi modelli possono, ad esempio, aiutare a decidere se una determinata funzione dovr essere fornita dal
software o da un dispositivo hardware specialpurpose.
Le precedenti osservazioni sono utili a livello globale, quando cio viene concepita la
struttura generale del software. Spesso, invece, non si possono applicare "in piccolo", quando i singoli programmi prima vengono progettati prestando attenzione alla correttezza e poi
vengono modificati singolarmente per aumentarne l'efficienza. Ad esempio, i cicli interni
sono degli ovvi candidati a modifiche volte a migliorare l'efficienza. Occorre pertanto distinguere le diverse modalit per operare in the large e in the small.
Il concetto di prestazioni si pu anche applicare al processo di sviluppo; in tal caso prende il nome di produttivit, argomento che, per la sua importanza, sar trattato autonomamente nel Paragrafo 2.2.10.

2.2.3

Usabilit

Un software usabile (usable) o, pi comunemente, user friendly (letteralmente "amichevole con l'utente"), se i suoi utenti lo reputano facile da utilizzare. Questa definizione riflette
la natura soggettiva dell'usabilit. Le qualit che rendono un'applicazione user friendly ai principianti sono diverse da quelle desiderabili per gli utenti esperti. Ad esempio, un principiante
potrebbe gradire messaggi prolissi, mentre un utente esperto probabilmente li ignorerebbe.
Allo stesso modo, mentre un programmatore potrebbe trovarsi pi a suo agio con comandi
testuali, un utente meno smaliziato potrebbe preferire l'uso di menu.
L'interfaccia utente influisce molto sull'amichevolezza4 di un'applicazione. Un sistema

Termine con il quale indicheremo sinteticamente il concetto di intuitiva, immediata fruibilit - a prima vista - del software (look andfeel) (N.d.T.).

software che fornisce a un principiante un'interfaccia a finestre e l'impiego del mouse molto pi amichevole di un sistema che richiede di digitare una serie di comandi. Allo stesso
tempo, per, un utente esperto potrebbe preferire una scelta di comandi da tastiera, piuttosto che navigare attraverso un'interfaccia eccessivamente elaborata.
Comunque, esistono altri componenti, oltre all'interfaccia utente, che influiscono sul
grado di usabilit di un'applicazione. Ad esempio, un sistema software embedded non ha alcuna interfaccia utente ma interagisce con componenti hardware o con altri software. In questo caso l'usabilit si basa sul grado di facilit di configurazione del software e di adattamento
all'ambiente hardware circostante.
In generale, l'usabilit di un sistema dipende dalla coerenza e dalla prevedibilit della
sua interfaccia nei confronti dell'utente o dell'operatore. Chiaramente, per, altre qualit gi
precedentemente citate, come la correttezza e le prestazioni, influiscono sul livello di facilit
d'uso. Un sistema che elabora risposte sbagliate non amichevole, indipendentemente da
quanto sofisticata sia la sua interfaccia utente. Analogamente, un sistema software che elabora le risposte pi lentamente di quanto sia tollerabile non amichevole, anche se tali risposte sono mostrate con colori accattivanti.
L'usabilit studiata anche dalla disciplina scientifica che si occupa del "fattore umano" (human factor) e i risultati sono estremamente importanti in molti ambiti dell'ingegneria. Ad esempio, i progettisti di automobili dedicano molto tempo per definire il posizionamento ergonomico dei vari pulsanti di controllo sul cruscotto. Anche i costruttori di elettrodomestici, quali televisori e forni a microonde, cercano di realizzare prodotti di facile utilizzo. In questi campi dell'ingegneria classica, le decisioni riguardo l'interfaccia utente vengono prese dopo lunghi studi delle necessit e attitudini dell'utente, coinvolgendo specialisti di varie discipline, tra le quali disegno industriale e psicologia.
La facilit d'uso in molte discipline ingegneristiche viene raggiunta con la standardizzazione dell'interfaccia uomo-macchina. Questa consente all'utente, una volta che ha imparato a far funzionare il televisore di una certa marca, di poter facilmente utilizzare anche
quelli di altre case produttrici. Negli ultimi anni si pu notare una marcata tendenza nel
software ad adottare interfacce utente standard, come nel caso dei browser per il Web.
Esercizio
2.1

2.2.4

Discutete come l'interfaccia utente possa influire sull'affidabilit.

Verificabilit

E molto importante poter facilmente procedere alla verifica della correttezza e delle prestazioni di un sistema software: questa caratteristica detta, appunto, verificabilit. Come vedremo nel Capitolo 6, la verifica potr essere svolta attraverso metodi di analisi formali o
informali, oppure attraverso test. Una tecnica comune per aumentare la verificabilit si basa sull'uso di software monitor (codice inserito nel software per mantenere sotto controllo determinati aspetti della qualit, come le prestazioni o la correttezza). Al fine di migliorare, fin
dall'inizio, la verificabilit, si possono adottare metodi di progettazione modulare, norme sistematiche di codifica e l'uso di linguaggi di programmazione appropriati alla scrittura di
codice ben strutturato.

La verificabilit di solito una qualit interna, nonostante talvolta possa diventare anche esterna. Ad esempio, per molte applicazioni in cui la sicurezza svolge un ruolo critico,
l'utente richiede la verificabilit di determinate caratteristiche. Il livello pi alto negli standard di sicurezza per un computer arriva a richiedere la verificabilit del nucleo del sistema
operativo.

2.2.5

Manutenibilit

Il termine "manutenzione del software" viene comunemente usato per indicare le modifiche effettuate su un software dopo il suo rilascio iniziale. Inizialmente, si riteneva che la
manutenzione riguardasse esclusivamente l'eliminazione degli errori; tuttavia stato dimostrato che la maggior parte del tempo per la manutenzione viene di fatto impiegato
per migliorare il prodotto implementando caratteristiche che inizialmente non erano presenti nelle specifiche originali, o per correggere specifiche che erano state introdotte in maniera scorretta.
In effetti, il termine "manutenzione" viene impiegato in modo improprio. Innanzitutto,
per come viene oggi usato, denota un ampio spettro di attivit che hanno tutte a che fare
con la modifica di un frammento esistente di software al fine di migliorarlo. Il termine che
probabilmente meglio esprime l'essenza di questo concetto "evoluzione del software".
Inoltre, in altri prodotti ingegneristici quali l'hardware dei computer, le automobili o gli elettrodomestici, il termine manutenzione si riferisce alle azioni svolte al fine di continuare a
garantire il buon funzionamento del prodotto, malgrado il graduale deterioramento delle
parti dovuto all'uso continuativo. Ad esempio, i sistemi di trasmissione devono essere lubrificati e i filtri dell'aria vanno periodicamente sostituiti a causa delle polveri che vi si depositano. L'uso del termine manutenzione nel caso del software fornisce un'impressione sbagliata, dal momento che il software non si deteriora. Purtroppo, per, il termine usato in
maniera cos diffusa che ormai siamo obbligati a continuare a utilizzarlo.
I fatti dimostrano che i costi della manutenzione superano il 60 per cento dei costi totali del software. Per analizzare i fattori che influenzano tali costi comune classificare la
manutenzione del software in tre categorie: correttiva, adattativa e perfettiva.
La manutenzione correttiva riguarda la rimozione di errori residui presenti nel prodotto
al momento del rilascio, come pure l'eliminazione di errori introdotti nel software durante
l'attivit di manutenzione stessa. Alla manutenzione correttiva pu essere attribuito circa il
20 per cento dei costi totali di manutenzione.
Le attivit di manutenzione adattativa e perfettiva derivano dalle richieste di cambiamenti nel software e richiedono, da parte dell'applicazione, una capacit di evolvere come
qualit fondamentale. Richiedono anche la capacit di anticipare i cambiamenti (definita
nel Capitolo 3), quale principio generale che dovrebbe guidare l'ingegnere del software nello svolgimento delle attivit progettuali. Alla manutenzione adattativa pu essere attribuito
circa il 20 per cento dei costi di manutenzione, mentre la manutenzione perfettiva assorbe
dunque pi del 50 per cento dei costi.
La manutenzione adattativa riguarda le modifiche dell'applicazione in risposta a cambiamenti nell'ambiente come, per esempio, una nuova versione dell'hardware, del sistema
operativo o del DBMS con il quale il software interagisce. In altri termini, nella manutenzione adattativa la necessit dei cambiamenti del software non tanto da attribuire a problemi del software stesso, quali ad esempio errori residui o incapacit di fornire alcune fun-

zioni richieste dall'utente, quanto a cambiamenti che avvengono nell'ambiente nel quale il
software inserito.
Infine, la manutenzione perfettiva riguarda i cambiamenti nel software per migliorarne alcune qualit. In questo caso i cambiamenti sono dovuti alla necessit di modificare le
funzioni offerte dall'applicazione, aggiungerne di nuove, migliorare le prestazioni, renderne
pi facile l'utilizzo, etc. Le richieste di manutenzione perfettiva possono provenire direttamente dall'ingegnere del software, con l'obiettivo di migliorarne la presenza sul mercato, oppure dal committente, al fine di rispondere a nuovi requisiti.
Il termine legacy software (software ereditato) si riferisce al software che esiste in un'organizzazione da lungo tempo e che pertanto incorpora una parte notevole delle conoscenze dei
processi dell'organizzazione. Pertanto tale software possiede per l'organizzazione un valore strategico, rappresenta investimenti passati e non pu essere sostituito facilmente. D'altra parte, a
causa della sua et, in genere questo software scritto in linguaggi di programmazione ormai
desueti e utilizza tecnologia obsoleta. Il legacy software pertanto diffcile da modificare e mantenere. Il legacy software rappresenta una sfida all'evoluzione del software. Le tecniche e le tecnologie di reverse engingeering (letteralmente, "ingegneria inversa") e reenginereering ("re-ingegnerizzazione") hanno l'obiettivo di aiutare a scoprire la struttura del legacy software per ristrutturarlo o, almeno, aiutare a migliorarlo.
La manutenibilt pu essere vista come un insieme di due diverse qualit: la riparabilit e l'evolvibilt. Il software riparabile se consente facilmente di eliminare i difetti; evolvibile se facilita i cambiamenti che gli permettono di soddisfare nuovi requisiti.
La distinzione tra evolvibilit e riparabilit non sempre chiara; ad esempio, se le specifiche dei requisiti sono vaghe, pu non essere evidente se un'attivit di modifica sia volta
all'eliminazione di un difetto o al soddisfacimento di un nuovo requisito.
2.2.5.1

Riparabilit

Un sistema software riparabile se i suoi difetti possono essere corretti con una quantit ragionevole di lavoro. In molti prodotti ingegneristici la riparabilit un obiettivo progettuale fondamentale. Per esempio, un'automobile costruita in modo che le parti maggiormente
soggette a guasti o a usura siano agevolmente accessibili. Nell'ingegneria dell'hardware dei
computer esiste una particolare specializzazione chiamata RAS (dalle iniziali di repairability,
availability e serviceability) che affronta queste problematiche.
In altri campi dell'ingegneria, quando il costo di un prodotto diminuisce e il prodotto diventa di uso quotidiano, assumendo lo stato di commodity, la necessit di riparabilit
decresce: di fatto diventa pi economico sostituire l'intero prodotto o gran parte di esso piuttosto che ripararlo. Ad esempio, nei primi apparecchi televisivi capitava di dover sostituire
una singola valvola: oggi invece viene sostituita l'intera parte circuitale.
Una tecnica comune per ottenere la riparabilit dei prodotti consiste nell'utilizzo di
parti standard, facilmente sostituibili. A titolo di esempio, i personal computer inizialmente
erano costruiti con componenti ad hoc e mediante meccanismi di interconnessione di tipo proprietario. Oggi, invece, sono costruiti con componenti standard e con sistemi di
interconnessione {bus) standard. Questa standardizzazione ha portato a\ proliferare di produttori, ciascuno specializzato in singole parti. Attraverso la specializzazione, questi produttori hanno potuto dedicarsi al miglioramento dell'affidabilit di ciascuna parte, riducendone anche i costi. Di conseguenza, la produzione di un computer diventata pi ve-

loce e meno costosa, e un guasto pu essere riparato sostituendo la parte difettosa. Nel
caso del software, le parti che costituiscono un'applicazione non si deteriorano. Pertanto,
mentre l'uso di parti standard pu ridurre il tempo e il costo di produzione, il concetto
di parti riparabili non sembra applicarsi in maniera analoga al software. Il fatto di costruire
un software a partire da componenti riduce solo il tempo di progettazione, in quanto il
progettista si concentra sul combinare insieme componenti ben noti, piuttosto di dover
partire da zero nel progetto.
La riparabilit influenzata anche dal numero di parti in un prodotto. Per esempio,
pi difficile riparare un difetto nella carrozzeria di un'automobile costituita da un unico pezzo, di quanto non succeda per una carrozzeria costituita da numerosi componenti con forma regolare. In questo secondo caso possibile sostituire un singolo pezzo molto pi facilmente rispetto alla sostituzione dell'intera carrozzeria. Naturalmente, se la carrozzeria fosse
costituita da un numero eccessivo di parti, richiederebbe molte connessioni tra questi, il che
implicherebbe a sua volta una probabilit non trascurabile che le connessioni stesse debbano essere riparate.
Una situazione analoga si applica al software: un prodotto software che consiste di moduli ben progettati pi facile da analizzare e riparare di un sistema monolitico. Il semplice aumento del numero dei moduli per non rende di per s pi riparabile un prodotto. E
necessario scegliere la corretta struttura dei moduli, con le giuste interfacce che evitino l'insorgere di interconnessioni e interazioni troppo complesse tra i moduli. La giusta modularizzazione promuove la riparabilit, consentendo all'ingegnere di localizzare pi facilmente
gli errori. Nel Capitolo 4 esamineremo diverse tecniche di modularizzazione, tra le quali l'information hiding (incapsulamento di informazioni) e i tipi di dati astratti.
La riparabilit pu essere migliorata anche attraverso l'uso di strumenti adeguati; per
esempio, l'utilizzo di un linguaggio di alto livello piuttosto del linguaggio assembler conduce a una migliore riparabilit. Strumenti come i debugger possono aiutare a isolare e riparare gli errori.
Infine la riparabilit di un prodotto ne influenza l'affidabilit, e la necessit di riparabilit diminuisce con l'aumentare dell'affidabilit.
2.2.5.2

Evolvibili

Come altri prodotti dell'ingegneria, i prodotti software sono modificati in continuazione al


fine di fornire nuove funzioni o modificare quelle gi esistenti. Il fatto che il software sia intrinsecamente duttile rende la modifiche molto facili da effettuare direttamente sull'implementazione. Esiste tuttavia una sostanziale differenza tra le modifiche nel campo del software
e le modifiche negli altri prodotti ingegneristici. In quest'ultimo caso le modifiche iniziano
al livello del progetto e solo successivamente si procede verso modifiche all'implementazione del prodotto. Ad esempio, se si decide di aggiungere un piano a un edificio, innanzitutto occorre eseguire uno studio di fattibilit per valutare se questa aggiunta possa effettivamente essere apportata in maniera sicura. Successivamente il progetto deve essere approvato, dopo aver verificato che non violi alcuno dei vincoli esistenti. Solo a questo punto pu
essere firmato un contratto per la costruzione del nuovo piano.
Questo approccio sistematico di solito non applicabile per il software. Anche nel caso di cambiamenti radicali a un'applicazione, spesso la fasi di studio di fattibilit e di analisi del progetto vengono saltate e si procede immediatamente a effettuare le modifiche sul-

l'implementazione. Peggio ancora, una volta che i cambiamenti sono stati effettuati, questi
non vengono documentati neppure a posteriori; vale a dire, le specifiche non vengono aggiornate in modo da riflettere i cambiamenti effettuati, e questo rende eventuali futuri cambiamenti ancora pi difficili da apportare.
Allo stesso tempo, occorre osservare che prodotti software di successo hanno una lunga vita. Il loro rilascio iniziale il primo di una lunga serie di rilasci, ciascuno dei quali costituisce un passo nell'evoluzione del sistema. Se il software viene progettato avendo in mente la sua evoluzione e se ogni modifica viene progettata e poi messa in pratica attentamente,
allora il software pu effettivamente evolvere in maniera ordinata e controllata.
Con il crescere del costo della produzione del software e della complessit delle applicazioni, l'evolvibilit assume ancora maggiore importanza. Uno dei motivi la necessit di
far fruttare gli investimenti fatti nel software, a fronte dei continui cambiamenti della tecnologia hardware. Ancora oggi alcuni dei primi grandi sistemi sviluppati negli anni Sessanta
continuano a evolversi per sfruttare i vantaggi dei nuovi dispositivi hardware e delle tecnologie di rete. Per esempio, il sistema di prenotazione SABRE dell'American Airlines, sviluppato inizialmente negli anni Sessanta, andato evolvendosi per decenni al fine di includere
un insieme sempre pi ricco di funzionalit.
Nelle fasi iniziali, la maggior parte dei sistemi software in grado di evolvere con facilit, ma purtroppo, dopo continui cambiamenti, si raggiunge uno stadio in cui ogni ulteriore modifica rischia di introdurre malfunzionamenti nelle caratteristiche esistenti.
Inizialmente, l'evolvibilit viene raggiunta attraverso un'adeguata modularizzazione del sistema, ma i cambiamenti progressivi tendono a ridurre la modularit del sistema originario,
tanto pi quando le modifiche vengono effettuate senza un attento studio del progetto originario e senza che venga stilata una descrizione precisa dei cambiamenti, sia nei documenti di progetto che in quelli di specifica dei requisiti.
E stato dimostrato da studi empirici sull'evoluzione di grandi sistemi software che la
capacit di evolvere tende a diminuire con i rilasci successivi del prodotto. Ogni rilascio
complica la struttura del software e rende le modifiche future sempre pi difficili da effettuare. Al fine di superare questo problema occorre fare ogni sforzo perch sia il progetto iniziale che i successivi cambiamenti vengano effettuati prestando estrema attenzione al problema dell'evolvibilit. Questa un'importante qualit del software per il suo impatto economico; molti dei principi presentati nel prossimo capitolo aiutano a ottenere
una migliore evolvibilit. Nel Capitolo 4 illustreremo concetti quali le famiglie di programmi, le famiglie di prodotti e le architetture software, che hanno lo scopo di consentire una pi facile evoluzione dei prodotti software, L'approccio delle famiglie di prodotti, spesso chiamate "linee di prodotti" {produci line), ha proprio l'obiettivo di trovare un
approccio sistematico all'evolvibilit.

2.2.6

Riusabilit

La riusabilit per molti aspetti simile all'evolvibilit. Nell'evoluzione di un prodotto, questo viene modificato per dar vita a una nuova versione; nel caso del riuso, questo viene utilizzato - magari con un numero inferiore di modifiche - per dar vita a un prodotto diverso. La riusabilit pu essere applicata a vari livelli di granularit dall'intera applicazione a
specifiche routine - tuttavia il concetto sembra applicarsi meglio ai componenti software piuttosto che ai prodotti completi.

Un buon esempio di prodotto riusabile lo shell di UNIX, cio l'interprete del linguaggio di comandi, che accetta ed esegue i comandi dell'utente. Lo shell stato progettato per essere utilizzato sia in modo interattivo che in batch. La possibilit di far eseguire un
nuovo shell con un file che contiene una lista di comandi shell permette di scrivere programmi,
detti script, nel linguaggio di comandi dello shell. Possiamo dunque vedere il programma
come un nuovo prodotto che usa lo shell come componente. Per incoraggiare l'uso di interfacce standard, l'ambiente UNIX di fatto supporta il riuso di uno qualunque dei suoi comandi nel fornire funzionalit pi complesse.
Le librerie numeriche sono state i primi esempi di componenti riusabili. Parecchie
librerie software, inizialmente scritte in FORTRAN e ora riscritte in C, C++ e altri linguaggi, esistono ormai da molti anni. Gli utenti possono acquistare queste librerie e utilizzarle nello sviluppo dei loro prodotti senza dover reinventare o ricodifcare algoritmi
ben noti. Vi sono ormai vari produttori di software che si sono specializzati nella produzione di queste librerie e oggi esistono librerie riusabili per aree diverse, quali le interfacce grafiche, la simulazione, etc. Uno degli obiettivi dei ricercatori quello di aumentare
la granularit dei componenti che possono essere riusati. Una delle finalit della programmazione object-oriented esattamente quella di raggiungere sia una migliore riusabilit che una migliore evolvibilit.
Oltre ai componenti software, il concetto applicabile in maniera pi ampia e, a diversi livelli, pu riguardare sia il prodotto sia il processo. In generale, qualunque artefatto del processo software, quale ad esempio la specifica dei requisiti, pu essere reimpiegato. Pertanto, la
possibilit di riusare questi artefatti, per intero o solo in alcune parti, dipender da quanto sia
modulare il progetto. Per esempio, un documento di specifica dei requisiti riusabile consente
di riutilizzare in futuro parti del risultato di analisi e di comprensione del problema.
La riusabilit si applica anche al processo: le varie metodologie di sviluppo del software
possono essere infatti viste come tentativi di riutilizzare lo stesso processo per costruire prodotti diversi. Anche i modelli di ciclo di vita possono essere visti come tentativi di riuso, ad
alto livello, dei processi software. Discuteremo tutto ci nel Capitolo 7.
La riusabilit di parti standard caratterizza la maturit di un settore industriale. Si
pu infatti osservare un forte riutilizzo in aree mature come l'industria automobilistica o
l'elettronica di consumo. Per esempio, un'autovettura viene costruita assemblando molti
componenti fortemente standardizzati e che vengono riutilizzati per molti modelli dello
stesso produttore, cos come avviene per i progetti e per gli stessi processi di produzione.
Il livello di riuso in forte aumento anche nell'ambito dell'ingegneria del software, ma
siamo ancora ben lontani da quanto avviene nei settori pi tradizionali dell'ingegneria.
Esercizi
2.2

Discutete q u a n t o la riusabilit possa influenzare l'affidabilit dei prodotti.

2.3

II riuso di u n c o m p o n e n t e pu richiedere qualche attivit di adattamento. Discutete come l'ereditariet fornita da un linguaggio object-oriented, quale Java o C++, consenta l'adattabilit
di un c o m p o n e n t e .

2.2.7

Portabilit

Il software portabile se pu essere eseguito in ambienti diversi. Il termine ambiente pu


riferirsi alla piattaforma hardware o all'ambiente software, come il particolare sistema operativo nel quale l'applicazione viene eseguita. La portabilit economicamente importante,
in quanto consente di ammortizzare gli investimenti in un sistema software su diversi ambienti e diverse generazioni dello stesso ambiente. Al giorno d'oggi molte applicazioni sono
indipendenti dalla piattaforma hardware, in quanto il sistema operativo fornisce portabilit
tra le diverse piattaforme; sono invece spesso dipendenti dal sistema operativo e dagli altri
sistemi software di base, quali i DBMS o il sistema di supporto delle interfacce utente. La
portabilit pu essere ottenuta modularizzando il software, in modo tale che le dipendenze
dall'ambiente vengano isolate in pochi moduli, modificabili in caso di trasporto del software
in un ambiente diverso. A causa della proliferazione dei sistemi distribuiti in rete, la portabilit ha assunto ulteriore importanza, in quanto gli ambienti di esecuzione, che consistono
di diversi tipi di computer e di sistemi operativi, sono per loro natura eterogenei. Infine, in
molti casi, possono essere diversi i dispostivi sui quali il software pu essere eseguito. Ad esempio i browser devono poter essere eseguiti non solo su workstation (stazioni di lavoro) o personal computer ma anche su palmari o telefoni cellulari.
Taluni sistemi software sono specifici della macchina. Ad esempio un sistema operativo viene scritto per controllare un ben preciso computer e un compilatore produce codice
per un determinato hardware. Tuttavia anche in questi casi possibile raggiungere un certo
livello di portabilit. UNIX e la sua variante, Linux, sono esempi di sistemi operativi che
sono stati portati su molte e diverse piattaforme hardware. Naturalmente lo sforzo di trasporto pu richiedere mesi di lavoro; tuttavia possiamo definire il software come portabile
quando scriverlo da zero nel nuovo ambiente richiederebbe uno sforzo maggiore di quanto
non lo richieda l'adattamento.
Esercizi
2.4

Analizzate la portabilit quale caso speciale di riusabilit.

2.5

Possiamo applicare il concetto di portabilit alle pagine Web. Discutete che cosa significa dire che una pagina web portabile.

2.2.8

Comprensibilt

Alcuni sistemi software sono pi facili da capire di altri. La comprensibilit di un sistema


software dipende in parte da come il software progettato, ma in larga misura dipende dal
problema affrontato dal software: alcuni problemi sono per loro natura pi complessi di altri. Per esempio, un sistema che effettua le previsioni del tempo, anche se ben progettato e
implementato, sar pi difficile da capire rispetto a un software che si limita a stampare un
indirizzario. Dati due compiti di difficolt simile, possibile seguire alcune linee guida che
consentono di sviluppare progetti e scrivere programmi pi facilmente comprensibili. Per
esempio, i concetti di astrazione e modularit migliorano la comprensibilit di un sistema.
Durante l'attivit di manutenzione di un software fondamentale la comprensione dei
programmi. Gli ingegneri che effettuano manutenzione passano gran parte del loro tempo

a cercare di scoprire la logica dell'applicazione e solo una piccola porzione del loro tempo
viene impiegata per effettuare i cambiamenti necessari. La comprensibilit una qualit interna del prodotto che aiuta a raggiungere molte delle altre qualit, quali l'evolvibilit e la
verificabilit. Da un punto di vista esterno, l'utente considera un sistema comprensibile se
ha un comportamento prevedibile. La comprensibilit esterna un fattore importante che
contribuisce all'usabilit del prodotto.

2.2.9

Interoperabilit

L'interoperabilit si riferisce alla capacit di un sistema di coesistere e cooperare con altri sistemi come, ad esempio, la capacit di un sistema di elaborazione testi di incorporare un
diagramma prodotto da un pacchetto grafico e la capacit del pacchetto grafico di rappresentare dati prodotti da un foglio elettronico, oppure la capacit del foglio elettronico di elaborare un'immagine acquisita da uno scanner.
L'interoperabilit molto diffusa negli altri prodotti ingegneristici. Per esempio, un impianto audio pu essere costituito da elementi di diversi produttori ed essere connesso a una
televisione e a un videoregistratore; inoltre, sistemi audio datati (anche di decenni) possono
facilmente inserirsi in un impianto nel quale esistono lettori di compact disc. Al contrario,
i primi sistemi operativi dovettero essere modificati, talvolta in maniera molto significativa,
prima che potessero essere utilizzati con nuovi tipi di dispositivi. La generazione dei sistemi
operativi cosiddetti plug-and-play (letteralmente "collega e usa") tenta di risolvere questo problema individuando e interfacciandosi automaticamente con i nuovi dispositivi.
L'ambiente UNIX, con le sue interfacce standard, offre un primo esempio di interoperabilit all'interno dello stesso ambiente. UNIX incoraggia la progettazione di applicazioni con interfacce molto semplici e standard, che consentono all'output di un'applicazione di essere usato come input di un'altra. Purtroppo, per, l'interfaccia standard di
UNIX primitiva, basata sui caratteri, e risulta poco conveniente quando invece necessario usare dati strutturati, ad esempio un foglio elettronico o un'immagine, prodotti da
altre applicazioni.
Mediante l'interoperabilit, un produttore pu sviluppare diversi prodotti e far s che
l'utente, se necessario, possa combinarli liberamente, scegliendo, di volta in volta, quali funzioni acquistare. L'interoperabilit pu essere raggiunta attraverso la standardizzazione delle interfacce. Un esempio fornito dai web browser, che supportano diversi tipi di plug-in
consentendo cos, per esempio, di aggiungere un nuovo programma per la lettura di file audio fornito da un venditore al browser fornito da un altro.
Un concetto correlato con l'interoperabilit quello di sistema aperto, vale a dire una collezione estendibile di applicazioni scritte in modo indipendente tra di loro, ma che funzionano come un sistema intergrato. Un sistema aperto consente l'aggiunta di nuove funzionalit,
sviluppate da organizzazioni indipendenti, dopo che il sistema stato consegnato. Questo viene ottenuto, per esempio, rilasciando il sistema insieme alla specifica delle sue interfacce "aperte". Ogni sviluppatore di applicazioni pu avvantaggiarsi dalla conoscenza di queste interfacce, che possono essere usate per far comunicare tra di loro parti diverse. I sistemi aperti consentono di fare interoperare diverse applicazioni, scritte da diverse organizzazioni.
Un interessante requisito dei sistemi aperti che possono essere aggiunte nuove funzionalit senza dover necessariamente fermare l'esecuzione del sistema (propriet tipica del
plug-and-play). Un sistema aperto dunque analogo a un'organizzazione sociale che cresce

ed evolve nel tempo, adattandosi ai cambiamenti nell'ambiente. L'importanza dell'interoperabili ha dato luogo al crescente interesse verso i sistemi aperti, e ci a sua volta ha generato sforzi di standardizzazione, quali CORBA, che definisce le interfacce che supportano lo sviluppo di componenti da utilizzare in sistemi distribuiti aperti. CORBA verr trattato nel Capitolo 4.
Esercizio
2.6

Discutete le relazioni tra evolvibilt e sistemi aperti.

2.2.10

Produttivit

La produttivit una qualit del processo di produzione del software: ne indica l'efficienza
e le prestazioni. Un processo efficiente d luogo a una consegna pi rapida del prodotto
finale.
Ciascun ingegnere produce software alla propria velocit, anche se ci sono molte variazioni tra individui con diverse capacit di programmazione. Quando gli individui sono
parte di un gruppo, la produttivit del gruppo una funzione della produttivit degli individui. Spesso la produttivit combinata molto inferiore alla produttivit che risulterebbe
dalla somma delle parti, a causa del tempo necessario per la comunicazione e il coordinamento all'interno del gruppo. Il management del progetto cerca di organizzare i gruppi di
lavoro e di adottare processi di sviluppo in modo tale da sfruttare al massimo la produttivit individuale di ciascun membro.
La produttivit del personale tecnico influenza la scelta del processo, e viceversa. Per
esempio, un processo che richiede specializzazione dei membri del gruppo di lavoro pu
dar luogo a un'elevata produttivit nello sviluppo di un certo prodotto, ma non altrettanto per un'ampia gamma di prodotti. Il riuso del software una tecnica che migliora la
produttivit complessiva di un'organizzazione per un insieme di prodotti, ma il costo di
sviluppo dei moduli riutilizzabili pu essere ammortizzato solo attraverso lo sviluppo di
pi prodotti.
La produttivit del software una caratteristica difficile da misurare, anche se riveste un
grande interesse pratico, a causa dei costi crescenti dello sviluppo. Se vogliamo poter confrontare tra di loro diversi processi in funzione della loro produttivit, dobbiamo ovviamente possedere una metrica per misurarla. Le metriche individuate in un primo tempo, quali le linee
di codice prodotte, hanno numerosi inconvenienti. Nel Capitolo 8 tratteremo le metriche per
quantificare la produttivit e come organizzare gruppi di lavoro per ottimizzarla. Come per
ogni altra disciplina ingegneristica, constateremo che l'efficienza del processo fortemente influenzata dall'automazione. I moderni ambienti e strumenti dell'ingegneria del software sono
finalizzati al miglioramento della produttivit, e saranno trattati nel Capitolo 9.
Esercizio
2.7

Valutate criticamente il n u m e r o di linee di codice come misura della produttivit. (Questo


problema verr analizzato a p p r o f o n d i t a m e n t e nel Capitolo 8.)

2.2.11

Tempestivit

La tempestivit una qualit del processo che indica la capacit di rendere disponibile un
prodotto al momento giusto. Storicamente, questa qualit stata carente nei processi di sviluppo del software e ci ha condotto alla cosiddetta "crisi del software" che, a sua volta,
stata responsabile della nascita dell'ingegneria del software come disciplina nell'ambito dell'informatica. Al giorno d'oggi, soprattutto a causa dei mercati sempre pi competitivi, i
progetti software affrontano sfide ancora pi stringenti in termini di tempo di sviluppo dei
prodotti.
Il seguente esempio illustra come venivano gestite le difficolt di consegna verso la fine degli anni Ottanta. Una societ di software aveva promesso di rilasciare una prima versione del proprio compilatore per il linguaggio Ada entro una certa data. Alla scadenza prevista, i committenti che avevano effettuato l'ordine, anzich ricevere il prodotto, si videro
recapitare una lettera, nella quale si annunciava la decisione di ritardare la consegna in quanto il prodotto era instabile e conteneva ancora molti difetti; la consegna sarebbe stata prorogata di tre mesi. Dopo quattro mesi il prodotto fu consegnato insieme a una lettera, nella quale si dichiarava che molti difetti erano stati corretti, ma molti altri erano ancora presenti. Tuttavia, il produttore aveva deciso che era meglio che i clienti ricevessero il compilatore Ada, nonostante contenesse ancora numerosi gravi difetti, in modo tale che potessero
iniziare a sviluppare le applicazioni che utilizzavano il linguaggio Ada. Si era infatti valutato che i rischi connessi all'utilizzo di un prodotto prematuro fossero controbilanciati dal poter utilizzare il prodotto per lo sviluppo delle applicazioni nelle quali il committente era coinvolto. Il risultato fu comunque che il compilatore fu consegnato in ritardo e con difetti.
Di per s la tempestivit non sempre una qualit utile, anche se talvolta arrivare in
ritardo alla consegna di un prodotto pu precludere interessanti opportunit di mercato. Non
ha molto senso consegnare alla data prevista un prodotto privo di altre qualit, quali l'affidabilit e le prestazioni. Tuttavia alcuni sostengono che la consegna tempestiva di una versione preliminare di un prodotto, ancorch instabile, favorisca la successiva accettazione della versione finale. Internet ha facilitato questo approccio, in quanto i produttori di software
possono esporre versioni iniziali di prodotto attraverso i propri siti, e rilasciarne l'utilizzo agli
utenti interessati, i quali possono fornire utili suggerimenti e critiche.
La tempestivit richiede un'attenta pianificazione temporale del processo, un'accurata
stima delle attivit e una specifica chiara degli obiettivi intermedi (milestone). Tutte le discipline ingegneristiche usano tecniche di gestione del progetto che favoriscono la tempestivit. Esistono addirittura molti strumenti di gestione dei progetti, supportati dal computer,
che facilitano queste attivit.
Le tecniche standard di gestione dei progetti sono talvolta difficili da applicare nell'ambito dell'ingegneria del software, a causa delle difficolt intrinseche nella definizione dei
requisiti e a causa della natura astratta del bene da produrre. Queste difficolt, a loro volta,
danno luogo a problemi nel misurare la quantit di lavoro necessaria per produrre un componente software, a problemi nel quantificare la produttivit dei progettisti o addirittura
a individuare metriche affidabili per definire la produttivit e a problemi nella definizione di milestone precise e verificabili.
Un'altra causa di difficolt nel raggiungimento della tempestivit nel processo software
costituita dalla continua evoluzione dei requisiti dell'utente. La Figura 2.2 illustra graficamente l'andamento delle modifiche nei requisiti dell'utente rispetto a quanto effettivamen-

/
Capacit
attuali
del sistema

'o

h h

'4

Tempo

Figura 2.2

Mancanza di tempestivit del software. (Da Davis et al. 11998] 1998 IEEE, riprodotto
con il permesso dell'IEEE.)

te offerto dal sistema e indica come mai gran parte dei progetti di sviluppo del software falliscono. Il tempo t0 indica il momento in cui ci si rende conto della necessit di sviluppare
un sistema software. A questo istante lo sviluppo inizia con una conoscenza incompleta degli effettivi requisiti dell'utente e pertanto, il prodotto iniziale consegnato al tempo t\ non
solo non soddisfa i requisiti iniziali al tempo t0, ma neppure quelli che si sono andati evolvendo dall'istante t0all'istante t{. Tra gli istanti ,e ^sul prodotto viene eseguita della manutenzione, al fine di approssimare meglio le richieste dell'utente. All'istante t2 possiamo immaginare che termini lo sviluppo di una nuova versione del sistema, che soddisfa i requisiti che l'utente aveva inizialmente, all'istante tv Per la ragione che abbiamo visto nel Paragrafo
2.2.5, al tempo t} il costo di manutenzione diventato cos alto che lo sviluppatore del
software decide di effettuare una riprogettazione radicale del sistema. La nuova versione diventa pertanto disponibile al tempo t4, ma a questo punto la distanza del sistema rispetto
alle necessit dell'utente diventata ancora pi grande di quanto lo fosse precedentemente.
Al di fuori dell'ingegneria del software, un classico esempio delle difficolt che si incontrano nella comprensione di requisiti complessi viene offerta dai sistemi avanzati di difesa. In parecchi casi descritti in letteratura, i sistemi diventano obsoleti prima ancora di essere consegnati, oppure non soddisfano i requisiti iniziali. Ovviamente, dopo dieci anni di
sviluppo o pi, diffcile decidere che cosa fare di un prodotto che non soddisfa i requisiti
di dieci anni prima. Il problema chiave in questi casi la formulazione precisa dei requisiti, in quanto questi dovrebbero riuscire a prefigurare il sistema pi avanzato possibile, ovvero nel momento in cui sar consegnato e non nel momento in cui i requisiti sono formulati.
Una tecnica per ottenere la tempestivit attraverso la consegna incrementale del prodotto {incrementai delivery). Questa tecnica illustrata attraverso questo ulteriore esempio
di sviluppo di un compilatore Ada, eseguito da una societ di software diversa da quella citata precedentemente. Questa societ fu in grado di consegnare molto presto un compilatore che supportava un sottoinsieme estremamente ridotto del linguaggio Ada; sostanzial-

mente, si trattava di un sottoinsieme equivalente al Pascal, con in pi il costrutto package.


Il compilatore non supportava alcuna delle caratteristiche innovative del linguaggio, quali
la programmazione concorrente e la gestione delle eccezioni. Il risultato fu per la consegna
molto rapida di un prodotto affidabile. Gli utenti iniziarono a sperimentare con il nuovo
linguaggio e la societ che aveva il compito di sviluppare il compilatore riusc gradualmente a comprendere e imparare a trattare le sottigliezze e le difficolt delle nuove caratteristiche del linguaggio. Il compilatore per il linguaggio completo fu consegnato attraverso una
serie di rilasci intermedi, che avvennero in un periodo di circa due anni. I rilasci incrementali consentono al prodotto di essere reso disponibile in tempi brevi e l'uso del prodotto contribuisce a raffinare i requisiti del prodotto stesso, in modo incrementale.
Ovviamente, la possibilit di una consegna incrementale dipende dalla capacit di
spezzare l'insieme delle funzionalit richieste in sottoinsiemi che possono essere sviluppati successivamente. Se non possibile identificare questi sottoinsiemi, impensabile che
si possa adottare un processo che renda disponibile il prodotto in modo incrementale.
Tuttavia, un processo non incrementale impedisce la produzione di sottoinsiemi, anche
se questi sottoinsiemi sono identificabili. Pertanto, la combinazione di un prodotto che
pu essere spezzato in sottoinsiemi e di un processo incrementale che ci consente uno sviluppo tempestivo.
E ovvio che la consegna incrementale di sottoinsiemi di scarsa utilit, non ha alcun valore. La tempestivit deve essere combinata con le altre qualit del software. Nel Capitolo 4
discuteremo altre tecniche che consentono di sviluppare sottoinsiemi di un prodotto e nel
Capitolo 7 vedremo le tecniche per ottenere processi incrementali.

2.2.12

Visibilit

Un processo di sviluppo del software visibile se tutti i passi successivi (step) e lo stato attuale sono documentati in modo chiaro. Un altro termine utilizzato per caratterizzare questa propriet trasparenza. L'idea che i passi e lo stato del processo siano disponibili e facilmente accessibili per un esame esterno.
In molti progetti software, gli ingegneri e i manager non hanno una completa consapevolezza di quale sia, in ogni istante, lo stato del progetto. Alcuni ingegneri potrebbero essere nella fase di progetto, altri nella fase di codifica e altri ancora nella fase di testing. Ci
di per s non genera inconvenienti; tuttavia se un ingegnere inizia la riprogettazione di una
parte del codice immediatamente prima che venga da altri iniziato il test di integrazione, i
rischi che possano sorgere problemi seri e che ci possa causare forti ritardi sono ovviamente molto alti.
La visibilit consente agli ingegneri di soppesare l'impatto sul progetto complessivo delle proprie azioni e pertanto di guidarli nelle loro decisioni. Essa consente a tutti i membri di
un gruppo di lavorare nella stessa direzione, piuttosto che, come spesso accade, in direzioni
contrastanti. Un tipico esempio di questa situazione si ha nel caso in cui un gruppo, durante
l'integrazione, ha testato la versione del software assumendo che la versione successiva ne eliminasse i difetti, mentre invece il gruppo di ingegnerizzazione decide di effettuare una riprogettazione radicale, al fine di aggiungere funzionalit. Questa tensione, tra un gruppo che cerca di stabilizzare il prodotto e un altro che lo destabilizza, una situazione piuttosto comune.
Il processo di sviluppo adottato deve invece incoraggiare il fatto che esista una visione coerente dello stato del sistema e che vi siano obiettivi comuni a tutti i partecipanti.

La visibilit non solo una qualit interna, ma anche esterna. Nello sviluppo di un
progetto di lunga durata sorgono spesso richieste relative allo stato corrente del progetto.
Talvolta queste richieste esigono che venga stilata una presentazione formale di tale stato, in
altri casi invece si tratta di richieste del tutto informali. Talvolta le richieste giungono dal
management interno all'organizzazione, al fine di un'ulteriore pianificazione del progetto;
altre volte invece provengono dall'esterno, ad esempio dal committente. Se il processo di sviluppo software ha una bassa visibilit, questi rapporti sullo stato di avanzamento non possono essere accurati, oppure richiedono un notevole sforzo per la loro preparazione.
Una delle maggiori difficolt che si incontrano nella gestione di grossi progetti deriva
dal ricambio di personale (turnoverj. In molti progetti software alcune informazioni critiche, relative ai requisiti del software o alle attivit di progettazione, sono tramandate oralmente e sono note soltanto alle persone che hanno lavorato nel progetto fin dall'inizio, o
comunque per un tempo sufficientemente lungo. In queste situazioni, la perdita di un ingegnere che svolge una funzione chiave all'interno dell'organizzazione, o l'aggiunta di nuovi ingegneri al progetto, pu generare grosse difficolt. Infatti l'inserimento di nuove persone spesso riduce la produttivit dell'intero progetto, dal momento che l'informazione tramandata oralmente (intrinsecamente poco strutturata) viene trasferita lentamente alle nuove persone inserite nel gruppo di lavoro.
Quanto detto mostra come la visibilit del processo richieda non solo che i passi del
processo stesso siano ben documentati, ma anche che lo stato corrente dei prodotti intermedi (e cio il prodotto stesso) abbia un'elevata visibilit. Intuitivamente, un prodotto visibile se chiaramente strutturato come una collezione di componenti, con funzioni ben
comprensibili e con un'accurata documentazione disponibile.

2.3

Requisiti di qualit in diverse aree applicative

Le qualit descritte nei paragrafi precedenti sono generiche, nel senso che si applicano a un
qualunque sistema software. Il software viene per costruito al fine di automatizzare una specifica applicazione e quindi pu essere caratterizzato sulla base dei requisiti dell'area applicativa. In questo paragrafo identificheremo quattro principali aree applicative ed esamineremo i requisiti specifici che queste impongono ai sistemi software. Mostreremo anche come queste aree sollecitino prioritariamente alcune delle qualit generali di cui abbiamo discusso in precedenza.

2.3.1

Sistemi informativi

I sistemi informativi costituiscono una delle aree applicative pi importanti dell'informatica; sono cos chiamati in quanto il loro scopo primario quello di gestire informazioni. Esempi
di sistemi informativi sono i sistemi bancari, quelli bibliotecari, quelli per la gestione del personale. Il cuore di tali sistemi costituito da una base di dati, utilizzata da transazioni che
creano, ricercano, modificano o cancellano dati. Molti di questi sistemi forniscono un'interfaccia web per operare sulle informazioni memorizzate.
I sistemi informativi hanno acquisito una grande importanza, in quanto l'informazione diventata una risorsa sempre pi preziosa. I dati gestiti da questi sistemi costituiscono

le risorse pi significative di un'organizzazione. I dati riguardano sia i processi e le risorse interne all'impresa (impianti, beni materiali, personale, etc.) sia informazioni sul mondo esterno (concorrenti, fornitori, clienti, etc.)
I sistemi informativi sono applicazioni orientate alla gestione dei dati e possono essere caratterizzati in base al modo in cui i dati vengono elaborati. Alcune delle qualit che caratterizzano i sistemi informativi sono:
spesso

Integrit dei dati. In quali circostanze i dati possono essere corrotti a causa di malfunzionamenti del sistema?

Sicurezza. Quale livello di protezione fornisce il sistema per negare accessi non autorizzati ai dati?

Disponibilit dei dati. In quali condizioni i dati possono essere non disponibili? E
per quanto tempo?

Prestazioni delle transazioni. Qual il numero di transazioni eseguibili per unit di


tempo dal sistema?

Un'altra caratteristica importante dei sistemi informativi la necessit di supportare l'interazione con gli utenti finali, che hanno spesso scarsa predisposizione all'utilizzo di strumenti
tecnologici (ad esempio addetti alle vendite, personale amministrativo e manager). Pertanto,
i requisiti di interazione uomo-macchina, quale l'amichevolezza delle interfacce, hanno
un'importanza primaria. Queste interazioni dovrebbero essere mediate da interfacce grafiche basate su menu, che a loro volta dovrebbero essere progettati in maniera uniforme e con
una facile navigabilit attraverso le diverse funzionalit. Gli utenti non dovrebbero avere mai
la sensazione di sentirsi persi durante l'uso del sistema, e dovrebbero invece avere sempre il
controllo dell'interazione con l'applicazione; anche l'applicazione dovrebbe vigilare affinch
gli utenti non usino il sistema in maniera scorretta.
I moderni sistemi informativi vanno in questa direzione: non solo supportano un facile accesso da parte degli utenti, ma addirittura ne incoraggiano il coinvolgimento nella creazione di alcune funzioni applicative. Oltre a fornire un insieme fisso di funzionalit, molti
sistemi offrono semplici caratteristiche di personalizzazione. In questo modo gli utenti possono, ad esempio, definire nuovi tipi di rapporti che l'applicazione pu generare, o nuovi
formati per tali rapporti. Questa caratteristica viene spesso chiamata end-user computing.

2.3.2

Sistemi in tempo reale

I sistemi in tempo reale (real-time system) costituiscono un'altra importante categoria di


sistemi software. La loro caratteristica principale che debbono rispondere a determinati eventi entro un periodo di tempo predefinito e spesso molto limitato. Ad esempio, in
un sistema di monitoraggio di un impianto, il software deve rispondere a cambiamenti
improvvisi di temperatura, attivando certi dispositivi o inviando segnali di allarme.
Analogamente, il software di controllo del volo di un aereo deve monitorare le condizioni ambientali e la posizione corrente dell'aereo, e controllare la traiettoria di volo in funzione di queste.
I sistemi in tempo reale in genere si associano ai sistemi di automazione di fabbrica, ai
sistemi di sorveglianza, etc. Tuttavia, requisiti di risposta in tempo reale possono essere tro-

vati anche in molte altre situazioni tradizionali. Un interessante esempio dato dal software
di gestione del mouse che si trova in ogni computer, il quale deve rispondere ai segnali di
pressione del mouse (clic) entro un certo periodo di tempo. In molti sistemi un singolo clic
un comando per selezionare un oggetto (per esempio, un'icona che rappresenta un file),
mentre un doppio clic purch le due pressioni siano sufficientemente ravvicinate nel tempo - un comando che implica l'apertura dell'oggetto (per esempio, il file rappresentato
dall'icona). Questo tipo di interfaccia fa dunque nascere un requisito di tempo reale per il
software, che deve elaborare il primo clic con una velocit tale da far s che possa accettare
il secondo clic, determinando se l'utente abbia effettivamente inteso inviare un "doppio
clic", o invece due singoli clic, che hanno un significato diverso.
Spesso, erroneamente, si dice che un sistema in tempo reale un sistema che richiede
tempi di risposta veloci. Ci non n vero n sufficientemente preciso. La velocit infatti una propriet qualitativa di un'applicazione; ci che necessario affinch un sistema sia
definibile "in tempo reale" che esista una nozione di tempo di risposta specificabile e quantificabile in maniera precisa. In alcuni sistemi in tempo reale, una risposta che viene fornita troppo presto pu essere altrettanto scorretta quanto una risposta elaborata troppo tardi.
Nel precedente esempio, se il primo clic elaborato troppo velocemente, il "doppio clic"
potrebbe non essere identificato correttamente.
I sistemi in tempo reale sono stati studiati approfonditamente. Mentre possiamo definire "orientati ai dati" i sistemi informativi, i sistemi in tempo reale si possono definire
"orientati al controllo". La parte fondamentale di un sistema in tempo reale un pianificatore (scheduler) il quale ordina o "schedula" la azioni del sistema. Vi sono due tipi fondamentali di algoritmi di schedulazione: quelli basati su priorit e quelli basati su deadline. Nella
schedulazione basata su priorit, a ogni azione associata una priorit. In ogni istante viene eseguita l'azione che ha priorit pi elevata. Invece, in uno schedulatore basato su deadline, ogni azione ha un tempo associato entro il quale essa deve essere iniziata o completata. responsabilit dello schedulatore assicurare che le azioni siano ordinate in modo tale
da rispettare i requisiti specificati.
Un'altra classificazione dei sistemi in tempo reale quella che distingue tra i sistemi
basati su eventi e quelli basati sul tempo. Nei sistemi basati su eventi, gli eventi causano l'attivazione dei vari componenti, che vengono eseguiti in risposta a essi. In un sistema basato
sul tempo, i componenti eseguono le loro azioni a determinati istanti di tempo predeterminati; la sincronizzazione temporale assicura che tutti i componenti osservino Io stesso tempo (siano sincronizzati).
Oltre a soddisfare le generiche qualit del software, i sistemi in tempo reale devono soddisfare i requisiti in termini di tempi di risposta. Mentre in altri sistemi la risposta temporale spesso una pura questione di efficienza del sistema, in questi sistemi il tempo di risposta diventa un criterio di correttezza. Inoltre i sistemi in tempo reale sono spesso usati
per operazioni critiche (quali il monitoraggio di pazienti, i sistemi di difesa e i sistemi di
controllo di processo) che hanno requisiti molto stringenti in termini di affidabilit5.

Spesso questi sistemi vengono caratterizzati mediante il termine inglese mission criticai, per indicare la
criticit della missione loro affidata.

Nel caso di sistemi critici si utilizza spesso il termine safety (traducibile con "innocuit")
per denotare l'assenza di comportamenti indesiderabili che possono causare situazioni di pericolo. La safety richiede che il sistema venga eseguito senza generare rischi inaccettabili.
Diversamente dai requisiti funzionali, che descrivono il comportamento corretto di un sistema in termini delle relazioni ingresso-uscita, i requisiti di safety descrivono ci che non
dovrebbe mai capitare durante l'esecuzione del sistema. In un certo senso sono dei requisiti negativi: specificano lo stato in cui un sistema non dovrebbe mai entrare. Ad esempio, in
un sistema di raggi X per applicazioni medicali, la propriet di safety da rispettare impone
che la radiazione applicata sia sempre al di sotto di un certo limite.
Esistono infine altre qualit importanti nel caso dei sistemi in tempo reale. Come per
i sistemi informativi, gli aspetti di relazione uomo macchina sono molto importanti. Ad esempio, l'interfaccia esterna di un sistema di controllo di un impianto industriale critico deve
essere progettata in maniera tale che l'operatore comprenda perfettamente lo stato del sistema che sta controllando, per poter sempre operare sull'impianto senza generare inavvertitamente situazioni pericolose.

2.3.3

Sistemi distribuiti

I progressi nell'ambito dell'hardware e delle tecnologie di rete hanno reso possibile costruire sistemi distribuiti, che consistono di macchine indipendenti o semi-indipendenti collegate da una rete di telecomunicazioni. Le reti a banda larga rendono possibile lo sviluppo
di applicazioni distribuite in cui i diversi componenti sono eseguiti da computer diversi.
Oltre alle qualit generiche del software descritte in precedenza, il software distribuito ne possiede altre peculiari. Ad esempio, l'ambiente di sviluppo deve supportare lo sviluppo
dell'applicazione su una molteplicit di computer, sui quali gli utenti devono essere in grado di compilare, collegare e testare il codice.
La possibilit di caricare ed eseguire componenti su macchine diverse ha favorito lo sviluppo di nuovi linguaggi quali Java e C#. Per esempio, Java definisce un linguaggio intermedio (bytecode) che pu essere interpretato in maniera efficiente su ogni computer del sistema distribuito. Ci consente ai componenti di essere caricati dalla rete in maniera dinamica quando ci risulta necessario.
Tra le caratteristiche importanti di un sistema distribuito abbiamo:
1. il livello di distribuzione supportato (per esempio: sono distribuiti i dati, l'elaborazione o entrambi?),
2. la possibilit di tollerare il partizionamento (ad esempio, quando la mancanza di un collegamento di rete rende impossibile a due sottoinsiemi di computer di comunicare),
3. la possibilit di tollerare che uno o pi computer non siano funzionanti.
Uno degli aspetti interessanti dei sistemi distribuiti che offrono nuove opportunit per il
raggiungimento di alcune delle qualit analizzate in precedenza. Per esempio, possibile aumentare l'affidabilit di un sistema mediante la replicazione di dati su pi computer e migliorarne le prestazioni e l'affidabilit distribuendo i dati su pi computer. Naturalmente, la
replica o la distribuzione dei dati non cos semplice come si potrebbe pensare, e richiede
una significativa attivit di progettazione; esistono molte tecniche consolidate che permettono di affrontare questi problemi.

Un'altra possibilit interessante consiste nello sfruttamento della tecnologia di mobilit del codice, vale a dire la possibilit che il codice migri sulla rete durante l'esecuzione. Il
codice mobile ha il vantaggio, rispetto ai tradizionali sistemi client-server, che le connessioni di rete non sono necessarie in modo permanente per supportare le interazioni tra il client
e il server. Ci possono inoltre essere vantaggi dal punto di vista delle prestazioni, se si trasferisce il codice al nodo che memorizza i dati sui quali il codice deve operare. Le applet Java
sono un semplice esempio di codice mobile.

2.3.4

Sistemi embedded

I sistemi embedded sono sistemi nei quali il software solo uno dei componenti e spesso
non ha interfacce rivolte all'utente finale, ma verso altri componenti del sistema che esso
controlla. Il software embedded viene usato in aerei, robot, elettrodomestici, automobili, telefoni cellulari, etc.
Ci che distingue un sistema embedded rispetto agli altri tipi di software proprio la
mancanza di dispositivi di interfaccia rivolti a operatori umani, combinata con la presenza
di interfacce verso altri tipi di dispositivi. Per esempio, invece di mostrare i dati mediante
un grafico sullo schermo, il software invia dati di controllo ai motori di un robot. Questo
porta a una sensibile riduzione dei requisiti di interfaccia e permette quindi di raggiungere
vari compromessi nella decisione delle interfacce di cui dotare il sistema. Ad esempio, spesso possibile modificare l'interfaccia software verso un dispositivo, complicando il codice pur
di semplificare lo schema di un dispositivo hardware.
Si consideri, ad esempio, una macchina a gettoni distributrice di bibite che accetta monete di dimensioni diverse. possibile costruire un dispositivo hardware per determinare il
valore di ciascuna moneta, ad esempio attraverso differenti fessure per inserire i vari tipi di
monete, oppure fare in modo che l'hardware calcoli il peso e la dimensione di ciascuna moneta e fornisca questi valori al software. Nel secondo caso il software responsabile della determinazione di ciascuna moneta e pertanto della decisione se siano state inserite un numero sufficiente di monete per poter effettuare l'acquisto. Il fatto di assegnare al software l'attivit di decisione in merito alle monete, permette di sviluppare un sistema pi flessibile riguardo, ad esempio, alla possibilit che la macchina venga adattata a nuovi tipi di monete,
all'aumento dei prezzi degli articoli che contiene o all'uso della macchina stessa in un'altro
Paese, senza che sia necessaria l'aggiunta di ulteriori componenti hardware. Infatti, con
un'appropriata progettazione del software, questi cambiamenti richiedono soltanto la modifica di alcune tabelle o di alcuni dati interni.
Anche se la nostra precedente trattazione ha assunto che le quattro aree applicative siano distinte, in pratica, molti sistemi hanno caratteristiche comuni a diverse aree. facile,
infatti, immaginare un sistema informativo che abbia requisiti tipici di un sistema in tempo reale. Tale sistema pu anche essere distribuito o essere una parte embedded all'interno
di un complesso sistema di monitoraggio. Come ulteriore esempio, segnaliamo che i sistemi embedded, in genere, sono sistemi in tempo reale.
Un sistema di monitoraggio dei pazienti di un ospedale un buon esempio di sistema
che pu avere diverse caratteristiche: deve mantenere una base di dati delle anamnesi dei pazienti; pu essere distribuito al fine di supportare l'interazione con la stazioni di lavoro delle infermiere o diversi laboratori; pu avere caratteristiche in tempo reale, per esempio il monitoraggio di dispositivi nelle sale di rianimazione; infine vi potrebbero essere requisiti di si-

stemi embedded, derivanti dall'interazione con i dispositivi di laboratorio per aggiornare i


dati clinici dei pazienti in maniera automatica.

2.4

Misura della qualit

Una volta stabilite le qualit che si vogliono raggiungere come obiettivo dell'ingegneria del
software, necessario disporre di principi e di tecniche che consentano di raggiungerle. Occorre,
inoltre, saper misurare con precisione le singole qualit. Nelle organizzazioni che sviluppano
software, questa attivit viene chiamata assicurazione della qualit (quality assurance).
Se si ritiene che una qualit sia importante, fondamentale poter misurare quanto prossimi si sia al suo raggiungimento. Ci a sua volta richiede che ciascuna qualit sia definita
in maniera sufficientemente precisa in modo da poter essere misurata. Senza strumenti di
misurazione non abbiamo alcuna legittimazione a parlare di miglioramenti della qualit, e
senza definire in maniera precisa una qualit non c' speranza che questa possa essere quantitativamente misurata.
Le discipline ingegneristiche consolidate dispongono di tecniche standard per la misura della qualit. Ad esempio, l'affidabilit di un amplificatore pu essere misurata determinando l'intervallo di valori entro i quali opera correttamente. L'affidabilit di un ponte
pu essere misurata, tra le altre cose, in base al peso che pu sostenere. Questi livelli di tolleranza vengono spesso rilasciati insieme al prodotto e fanno parte della sua specifica.
Per quanto riguarda il software, alcune caratteristiche, quali le prestazioni, possono essere misurate in maniera precisa, mentre per gran parte delle altre qualit non esistono metriche
universalmente accettate. Ad esempio, il fatto che un sistema possa evolvere pi facilmente di
un altro viene di solito valutato in termini soggettivi. Ciononostante, le metriche sono necessarie e in effetti, ancora oggi, molti gruppi di ricerca cercano di affrontare il tema della definizione di metriche oggettive per il software. Di questo parleremo nel Capitolo 6.

2.5

Osservazioni conclusive

L'ingegneria del software riguarda l'applicazione dei principi ingegneristici alla costruzione dei
prodotti software. Questo testo si pone l'obiettivo di identificare un insieme di principi che si
applicano nello sviluppo di un'ampia casistica di prodotti software. Per ottenere questo, necessario identificare , come primo passo, l'insieme di qualit che caratterizzano i prodotti. Ci
quanto abbiamo fatto in questo capitolo. Nel seguito vedremo come progettare software in
modo da assicurare il raggiungimento dei livelli di qualit richiesti o desiderati.
Ulteriori esercizi
2.8

In questo capitolo a b b i a m o trattato le qualit del software che consideriamo pi importanti.


Altre qualit sono la testabilit, l'integrit, la facilit d'uso, la facilit di operazione, l'apprendibilit. D e f i n i t e ciascuna di queste ed, eventualmente, altre qualit; fornite alcuni
esempi e analizzate le relazioni tra queste qualit e quelle che sono state trattate in questo
capitolo.

2.9

Classificate ciascuna delle qualit discusse nel capitolo come interne, esterne, di prodotto, o
di processo f o r n e n d o n e esempi. Tali classi n o n sono m u t u a m e n t e esclusive.

2.10

Mostrate graficamente l'interdipendenza delle qualit discusse in questo capitolo. Disegnate


un grafo in cui ciascun n o d o rappresenti una qualit software e u n collegamento orientato dal
nodo A al n o d o B indichi che la qualit A contribuisce a raggiungere la qualit B. C h e cosa
ci mostra questo grafo, relativamente all'importanza relativa delle qualit software? Vi sono
cicli? C h e cosa implica u n ciclo?

2.11

Talvolta i manager riutilizzano molte delle tecniche gi utilizzate in un precedente progetto.


Usando questo come esempio, discutete il concetto di riusabilit applicato al processo software.

2.12

Se avete familiarit con i protocolli T C P / I P (per esempio ftp e telnet), analizzate il loro ruolo nell'interoperabilit.

2.13

Abbiamo discusso l'interoperabilit come una qualit del prodotto. Possiamo anche parlare di
interoperabilit del processo. Ad esempio, il processo adottato da un'organizzazione per l'assicurazione della qualit, deve essere compatibile con quello adottato dall'organizzazione che
effettua lo sviluppo all'interno della stessa compagnia. Un altro esempio viene offerto da una
compagnia che stipula contratti con un'organizzazione indipendente per produrre la documentazione di un prodotto. Utilizzate questo e altri esempi per analizzare l'interoperabilit quando viene applicata a un processo.

Suggerimenti e tracce di soluzioni


2.1

In alcuni casi, le decisioni relative all'interfaccia utente possono influenzare l'affidabilit di un


sistema. Per esempio, si dovrebbe assicurare che due pulsanti, che inviano comandi con effetti
opposti, n o n siano adiacenti, al fine di evitare errori nella selezione.

2.2

Poich i componenti sono sempre pi riusabili, essi diventeranno sempre pi affidabili, in quanto gli errori residui saranno progressivamente eliminati.

Note bibliografiche
Per una discussione sul software, la sua natura e la sue caratteristiche, si faccia riferimento a Boehm
[1976], Wegner [1984], Parnas [1985], Freeman [1987] e Brooks [1987]. Weinberg [1971] e W e i n b e r g
e Schulman [1974] affrontano il tema degli aspetti u m a n i nel processo di sviluppo.
Boehm [1981], Agresti [1986], Curtis et al. [1988], H u m p h r e y [1989] e Cugola e Ghezzi
[1999] forniscono diverse viste del processo di sviluppo del software.
Boehm [1976] presenta una classificazione delle qualit del software; Boehm et al. [1978] le
discute in dettaglio. Per ciascuna qualit esiste a sua volta una bibliografia specializzata. La conferenza internazionale sul reliable software dell'ACM [1975] ha avuto u n ruolo fondamentale nello stimolare ricerca in quest'area.
Musa et al. [1987] fornisce una vista approfondita dell'affidabilit del software, che deriva da
un approccio statistico. La correttezza dei programmi viene studiata da M a n n a [1974] e Mandrioli e
Ghezzi [1987]. Ritorneremo su questi temi nel Capitolo 6. Il concetto di safety stato studiato da
Leveson [1986], la security da McLean [1990] e i c o m p u t e r a elevata sicurezza {trusted computer?) da
Arnes et al. [1983].
Alcuni testi generali sulle prestazioni sono Ferrari [1978] e Smith [1989]; un testo classico sul-

la complessit computazionale A h o et al. [1974], Bentley [2000, 1988] illustra approcci per la scrittura di programmi efficienti.
L'amichevolezza e il tema dell'interazione uomo-macchina sono trattati da Rubinstein e Hersch
[1984], Schneiderman [1998] e N o r m a n e Draper [1986]. N o r m a n [1998] presenta un saggio divertente e facile da leggere su c o m e fare in m o d o che l'interfaccia di un c o m p u t e r sia cos naturale da
n o n essere notata.
La manutenzione e l'evoluzione del software sono studiate intensivamente da Lientz e Swanson
[1980], Belady e Lehman [1979] e Lehman e Belady [1985]. La distinzione tra manutenzione correttiva, adattativa e perfettiva dovuta a Lientz e Swanson, i quali h a n n o fornito anche alcuni dati
generali che abbiamo fornito in questo capitolo. C u s u m a n o e Yoffie [1999b] discutono l'approccio
di Netscape per la progettazione "cross-piattaforme" e la portabilit e le prime delusioni della societ
produttrice e dei problemi con Java.
Freeman [1987b] e Biggerstaff e Perlis [1989] discutono il tema della riusabilit.
Kernighan e Pike [1984] illustra q u a n t o l'ambiente U N I X influenzi l'interoperabilit.
La produttivit viene discussa a p p r o f o n d i t a m e n t e da Boehm [1981] e Capers Jones [1986].
C u s u m a n o e Yoffie [1999a] discute l'impatto dei requisiti di veloce commercializzazione {internet tim)
nello sviluppo del software.
W i r t h [1977] e Stankovic [1988] h a n n o offerto una caratterizzazione dei sistemi in tempo reale. Kopetz [1997] affronta la progettazione dei sistemi in t e m p o reale. Leveson [1995] discute i rischi relativi alla safety nel software e alcuni modi per evitarli.
Le prime rassegne sullo stato dell'arte riguardo alle metriche per le qualit del software sono
dovute a Basili [1980] e C o n t e et al. [1986]. Alcuni avanzamenti sono illustrati nell'edizione speciale di IEEE Software [1990b]. Fenton e Pleeger [1998] offre u n o studio completo delle metriche di
qualit del software.

C A P I T O L O

Principi dell'ingegneria del software

Questo capitolo illustra alcuni principi generali che sono fondamentali per lo sviluppo del
software: principi che si riferiscono sia al processo che al prodotto finale. Un processo adeguato aiuta a produrre un prodotto adeguato e a sua volta il prodotto finale influenza la scelta del processo da adottare. Prodotto e processo sono due aspetti fondamentali dell'ingegneria
del software, non ha senso enfatizzare uno a scapito dell'altro: entrambi sono importanti.
I principi che svilupperemo in questo capitolo sono sufficientemente generali da essere applicabili lungo l'intero processo di costruzione e gestione del software. I principi, tuttavia, non sono sufficienti a guidare lo sviluppo del software. Questi, infatti, descrivono propriet desiderabili del processo e dei prodotti in termini generali e astratti. Per poterli applicare, l'ingegnere del software deve disporre di metodi appropriati e di tecniche specifiche
che lo aiutino a incorporare le propriet desiderate nei processi e nei prodotti.
Si noti che abbiamo distinto tra i termini "metodi" e "tecniche". I metodi sono delle linee guida generali che governano l'esecuzione di un'attivit e definiscono un approccio rigoroso, sistematico e disciplinato. Le tecniche sono di carattere pi meccanico rispetto ai metodi e spesso hanno un'applicabilit pi limitata. Tuttavia, in generale, la differenza tra i due termini non decisamente delineata, e pertanto spesso li useremo in modo interscambiabile.
Talvolta, i metodi e le tecniche sono confezionati insieme a formare una metodologia. Scopo di una metodologia promuovere un certo approccio alla soluzione di un problema attraverso l'individuazione dei metodi e delle tecniche che devono essere usati. Inoltre
possono essere forniti strumenti per aiutare l'applicazione delle tecniche, dei metodi e delle metodologie.
La Figura 3.1 mostra la relazione logica tra principi, metodi, tecniche, metodologie e
strumenti. Ciascun livello della figura basato sul livello o sui livelli sottostanti ed maggiormente suscettibile di cambiamenti con il passare del tempo. La figura mostra chiaramente
che i principi sono alla base di metodi, tecniche, metodologie e strumenti e potrebbe anche
essere usata per sintetizzare la struttura di questo libro. In questo capitolo mostriamo i principi fondamentali dell'ingegneria del software. Nei Capitoli 4, 5 e 6 esamineremo i metodi
e le tecniche basati sui principi esposti in questo capitolo. Il Capitolo 7 presenta alcune metodologie e il Capitolo 9 illustra gli strumenti e gli ambienti di supporto.
Nella nostra trattazione dei principi, cercheremo di essere sufficientemente generali da
coprire qualunque tipo di applicazione. Questo vale anche per i metodi e le tecniche che
svilupperemo nei capitoli successivi. I principi, i metodi e le tecniche su cui concentreremo

Figura 3.1

Relazione tra principi, metodi e tecniche, metodologie e strumenti.

la nostra attenzione costituiscono invece una scelta ben precisa. Tra le qualit che abbiamo
discusso nel capitolo precedente, porremo particolare enfasi sull'affidabilit del software e
sulla sua capacit di evolvere. Questa scelta influenza, a sua volta, l'enfasi che porremo nella discussione dei principi, dei metodi e delle tecniche.
Come affermato nel Capitolo 1, considereremo il caso in cui il software che deve essere sviluppato non un'applicazione sperimentatale, ossia che verr utilizzata poche volte
dal suo sviluppatore, bens un'applicazione i cui potenziali utenti potrebbero avere scarse competenze nell'udlizzo del computer e del software. Un altro caso che considereremo quello
del software di supporto ad applicazioni critiche, nelle quali gli effetti degli errori possono
essere molto seri o addirittura disastrosi. Per queste e altre ragioni, l'applicazione deve essere estremamente affidabile.
Considereremo anche il caso di applicazioni cos complesse da richiedere di essere
scomposte in parti per poterne gestire lo sviluppo. Ci vale per progetti di gruppo, ma vale anche nel caso in cui lo sviluppo dell'applicazione sia in carico a un singolo progettista.
In entrambi i casi necessario un approccio allo sviluppo del software che consenta di dominarne la complessit.
In tutti questi casi, che rappresentano situazioni tipiche dello sviluppo del software,
l'affidabilit e la capacit di evolvere giocano un ruolo fondamentale. evidente che, se il
software non dovesse avere requisiti di affidabilit e capacit di evolvere, non sarebbe necessario adottare particolari principi e tecniche dell'ingegneria del software. In generale, possiamo infatti affermare che la scelta dei principi e delle tecniche deriva dagli obiettivi di qualit del software.
In questo capitolo discuteremo sette fondamentali principi applicabili lungo l'intero
processo di sviluppo del software: formalit e rigore, separazione degli interessi (separation
ofconcerns), modularit, astrazione, anticipazione del cambiamento, generalit e incrementalit. La lista, per sua natura, non pu essere esaustiva, ma copre tuttavia gli aspetti fondamentali dell'ingegneria del software. Questi principi sono tra di loro strettamente correlati,
ma per chiarezza verranno in seguito descritti separatamente. Rivisiteremo questi principi
alla fine del capitolo, attraverso due casi di studio riassuntivi. Questi verranno poi ripresi in
maniera pi concreta e dettagliata nei capitoli seguenti. In particolare, il principio di modularit verr presentato nel Capitolo 4, come colonna portante della progettazione del
software.

3.1

Rigore e formalit

Lo sviluppo del software un'attivit creativa. In tutte le attivit creative esiste una naturale tendenza ad essere poco precisi e accurati e a seguire, piuttosto, l'ispirazione del momento in una maniera scarsamente strutturata. Il rigore, d'altra parte, un necessario complemento alla creativit in ogni attivit ingegneristica. Soltanto attraverso un approccio rigoroso possiamo infatti realizzare prodotti affidabili, controllarne il costo e aumentare la nostra fiducia nel loro corretto funzionamento. Il rigore non limita necessariamente la creativit, anzi pu essere visto come uno strumento concettuale che la amplifica. L'ingegnere riesce ad avere maggiore fiducia nei propri processi creativi se in grado di effettuare una rigorosa verifica dei risultati.
Paradossalmente, il rigore un concetto intuitivo che non pu essere definito in maniera rigorosa. Inoltre, esistono diversi livelli di rigore. Il livello pi alto pu essere chiamato formalit. La formalit un requisito pi forte del rigore, in quanto richiede che il processo di sviluppo del software sia guidato e valutato mediante leggi matematiche. Ovviamente
la formalit implica il rigore, ma non viceversa: si pu infatti essere rigorosi e precisi anche
in una situazione di informalit.
In ogni campo dell'ingegneria, la progettazione procede come un sequenza di passi ben
definiti e specificati in maniera precisa. A ogni passo l'ingegnere segue alcuni metodi e applica tecniche specifiche, che possono essere basate su una combinazione di aspetti teorici
che derivano da qualche modello teorico della realt, adattamenti empirici che tengono conto di fenomeni che non sono trattati dal modello e regole pratiche che incorporano i risultati di esperienze passate. Il "mix" di questi fattori d luogo a un approccio rigoroso e sistematico, la metodologia, che pu essere applicata in maniera continua e sistematica.
Non sempre necessario essere formali nell'attivit di progettazione, ma l'ingegnere deve essere in grado di capire dove e quando opportuno esserlo. Ad esempio, pu limitarsi
al ricorso all'esperienza passata e a regole empiriche nel caso del progetto di un ponte di piccole dimensioni, che deve essere usato per collegare temporaneamente due sponde di un fiume. Ma se il ponte dovesse essere di grandi dimensioni e di utilizzo permanente, l'ingegnere ricorrerebbe a modelli matematici che consentano di verificare la sicurezza del progetto.
Qualora il ponte dovesse essere di lunghezza eccezionale o dovesse essere costruito in un'area soggetta a movimenti sismici, i modelli matematici utilizzati sarebbero ancora pi sofisticati e terrebbero conto di fattori che potrebbero essere ignorati nel caso precedente.
Anche nel caso della matematica, possiamo osservare il ruolo del rigore e della formalit. I libri di testo sull'analisi funzionale sono rigorosi ma raramente sono formali: le
dimostrazioni dei teoremi sono sviluppate in modo molto accurato, attraverso sequenze
di deduzioni intermedie, che conducono all'affermazione conclusiva. Ciascun passo deduttivo si basa su una giustificazione intuitiva, che ha l'obiettivo di convincere il lettore
della sua validit. Raramente, invece, la derivazione viene espressa mediante regole di derivazione formale attraverso la logica matematica. Ci significa che, normalmente, il matematico ritiene soddisfacente una descrizione rigorosa del processo di derivazione di una
dimostrazione e non necessita di una sua completa formalizzazione. In alcuni casi critici,
tuttavia, quando la convalida di alcune deduzioni intermedie non del tutto ovvia, il matematico cerca di formalizzare il ragionamento informale, con l'obiettivo di convalidarne
la sua validit.

Questi esempi mostrano che l'ingegnere (e il matematico) devono essere capaci di scegliere il livello di rigore e di formalit da raggiungere, in funzione della difficolt concettuale
e della criticit del compito che stanno affrontando. Questo livello pu differire a seconda
delle diverse parti di uno stesso sistema. Ad esempio, alcune parti critiche, come nel caso
dello schedulatore di processi del kernel di un sistema operativo real-time o del componente che controlla la sicurezza in un sistema di commercio elettronico, possono richiedere una
descrizione formale del funzionamento richiesto e una dimostrazione formale che questo funzionamento sia effettivamente ottenuto. Invece, parti pi standard e meglio comprese dell'applicazione non necessitano di approcci cos formali.
Questa situazione si applica in tutte le aree dell'ingegneria del software. Nel Capitolo 5
affronteremo il tema in dettaglio nel contesto delle specifiche del software mostrando, per esempio, che la descrizione di ci che un programma svolge pu essere fornita in maniera rigorosa, usando il linguaggio naturale, ma pu anche essere espressa in modo formale, attraverso un
linguaggio basato sulla logica. Il vantaggio della formalit rispetto al solo rigore informale
che la formalit pu essere alla base di processi automatizzati. Per esempio, possibile che la
descrizione formale di un programma possa essere utilizzata per generare automaticamente il
programma stesso, se questo non esiste, o per dimostrare che il programma corrisponde alla
sua descrizione formale, nel caso in cui il programma e la descrizione formale esistano.
La fase in cui tradizionalmente si utilizza un approccio formale la fase di programmazione, in quanto i programmi sono oggetti formali, scritti in un linguaggio la cui
sintassi e semantica sono perfettamente definite. I programmi sono descrizioni formali che
possono essere manipolate automaticamente dai compilatori: verificate nella loro correttezza formale, trasformate in una rappresentazione equivalente in un altro linguaggio (assembler o linguaggio macchina), stampate secondo regole di incolonnamento particolari
per migliorarne l'aspetto, etc. Queste operazioni meccaniche, rese possibili dall'uso della
formalit nella programmazione, aiutano a migliorare la verificabilit e l'affidabilit nei prodotti software.
Rigore e formalit non sono per limitati alla programmazione, ma possono essere applicati lungo l'intero processo di produzione del software. In seguito mostreremo come possano essere applicati nel caso della progettazione (Capitolo 4), nel caso della specifica del
software (Capitolo 5) e nel caso della sua verifica (Capitolo 6).
La nostra discussione ha finora incentrato la sua attenzione sulla relazione tra rigore e
formalit, da un lato, e affidabilit e verificabilit del software, dall'altro. Il rigore e la formalit hanno anche effetti benefci sulla manutenibilit, la riusabilit, la portabilit, la comprensibilit e l'interoperabilit. Ad esempio, una rigorosa documentazione del software, anche se non formale, pu avere effetti benefici su tutte queste qualit rispetto a una documentazione informale, sovente anche ambigua, inconsistente e incompleta.
Il rigore e la formalit si applicano anche al processo di sviluppo del software. Una documentazione rigorosa del processo aiuta a riusare tale processo in altri progetti. Sulla base
di tale documentazione i manager posso prevedere i passi attraverso i quali il nuovo progetto dovr evolvere, assegnare risorse appropriate al processo, etc. Analogamente, una documentazione rigorosa aiuta nel processo di manutenzione di un prodotto esistente; se i vari
passi, attraverso i quali il progetto andato evolvendo, sono ben documentati, possibile
modificare un prodotto esistente partendo da un livello intermedio della sua derivazione, e
non dal prodotto finale. Infine, se il processo software specificato in modo rigoroso, i ma-

nager possono monitorarlo in maniera accurata, valutando il rispetto dei tempi previsti e
migliorando la produttivit.

3.2

Separazione degli interessi

Il principio di separazione degli interessi ci consente di affrontare differenti aspetti del problema, concentrando la nostra attenzione su ciascuno di essi in maniera separata. Questo
principio una pratica di uso comune nella vita quotidiana, che ci consente di superare molte delle difficolt che incontriamo, e pu anche essere applicato nello sviluppo del software
per dominarne la complessit.
Numerose sono le decisioni che occorre prendere durante lo sviluppo di un prodotto
software. Alcune di queste riguardano le caratteristiche che il prodotto dovr avere: le funzionalit che dovr offrire, l'affidabilit richiesta, l'efficienza in termini di quantit di memoria occupata e tempo di esecuzione, le relazioni tra il prodotto e l'ambiente nel quale dovr essere utilizzato (ad esempio, l'hardware o le risorse software richieste), le interfacce utente e cos via. Altri aspetti riguardano invece il processo di sviluppo: quale debba essere l'ambiente di sviluppo, la struttura organizzativa del gruppo di lavoro, la tempistica di sviluppo,
le procedure di controllo, le strategie di progettazione e i meccanismi di gestione di eventuali malfunzionamenti. Altre ancora riguardano aspetti di tipo economico e finanziario.
Queste differenti decisioni possono essere scollegate le une dalle altre e in tal caso ovvio
che debbano essere trattate separatamente.
Spesso tuttavia, le decisioni sono correlate tra di loro. Per esempio, decisioni di progetto, quale il trasferimento di dati dalla memoria centrale al disco, possono dipendere dalla dimensione della memoria del computer utilizzato (e pertanto dal costo della macchina)
e, a loro volta, queste decisioni possono influenzare le scelte adottate per le politiche di gestione dei malfunzionamenti. Quando diverse decisioni sono strettamente correlate tra di
loro, sarebbe utile che lo stesso gruppo di progettisti potesse, allo stesso tempo, analizzare
tutti gli aspetti; cosa che, purtroppo, nella pratica non sempre pu avvenire.
Il solo modo per dominare la complessit di un progetto quello di concentrarsi separatamente sui diversi aspetti. Innanzitutto, isolando quelli scarsamente correlati tra loro
e, successivamente, considerando ciascun aspetto separatamente e approfondendo i dettagli
importanti per il suo trattamento.
Gli aspetti possono essere separati in differenti modi. Innanzitutto esiste una separazione in diversi periodi di tempo. Facendo riferimento a un esempio di vita quotidiana, un professore potrebbe applicare questo principio pianificando le attivit didattiche, quali le lezioni,
i seminari, le ore di ricevimento e gli incontri all'interno del dipartimento dalle 9 del mattino
alle 14 di ciascun giorno della settimana, da luned a gioved, riservando il venerd alle attivit
di consulenza e dedicando alla ricerca il resto del proprio tempo. Questa separazione delle diverse attivit in diversi periodi temporali consente una pianificazione precisa delle attivit ed
elimina gli inconvenienti che sorgono quando si deve passare continuamente da un'attivit all'altra. Come abbiamo visto nel Capitolo 1, e come vedremo in maggiore dettaglio nel Capitolo
7, questa separazione degli interessi in termini del tempo la motivazione principale che sta
alla base dei modelli di ciclo di vita del software, i quali definiscono le sequenze di attivit che
devono essere seguite nel processo di produzione del software.

Un altro tipo di separazione degli interessi riguarda le qualit che devono essere considerate separatamente. Ad esempio, nel caso del software, potremmo voler trattare separatamente l'efficienza e la correttezza di un programma. Si potrebbe inizialmente decidere di
progettare il software in modo che la sua correttezza sia assicurata, a priori, dall'adozione di
un approccio molto strutturato alla progettazione e, successivamente, ristrutturare in maniera parziale il programma iniziale, al fine di migliorarne l'efficienza. Similmente, durante
la fase di verifica si potrebbe, innanzitutto, verificare la correttezza funzionale del programma e, separatamente, la sua efficienza. Entrambe le attivit possono essere fatte in maniera
rigorosa, applicando procedure sistematiche o addirittura formali, ad esempio usando prove di correttezza formale e di analisi di complessit.
Un alto importante tipo di separazione degli interessi riguarda l'analisi di viste diverse
di un'applicazione software. Ad esempio, quando analizziamo i requisiti di un'applicazione,
pu essere utile concentrarsi separatamente sul flusso di dati da un'attivit all'altra all'interno del sistema e sul flusso di controllo che governa il modo con il quale le diverse attivit
sono sincronizzate. Entrambe le viste aiutano a capire il sistema, anche se nessuna di esse ne
fornisce una visione completa.
Ancora un altro tipo di separazione di interessi riguarda la capacit di affrontare le
diverse parti del sistema separatamente. In tal caso, la separazione delle parti riguarda la dimensione del sistema. Questo un concetto fondamentale che dobbiamo imparare a governare per controllare la complessit di produzione del software ed cos importante che
ne discuteremo specificamente nel paragrafo successivo, dedicato alla modularit.
Esiste, ovviamente, un inconveniente intrinseco alla separazione degli interessi: separando due o pi aspetti possibile perdere la possibilit di effettuare alcune ottimizzazioni
globali, che sarebbero possibili se gli aspetti venissero affrontati contemporaneamente. Anche
se ci vero in linea di principio, la nostra capacit di prendere decisioni ottimizzanti nel
caso di sistemi complessi in pratica molto limitata. Quando si affrontano troppi aspetti
contemporaneamente, molto probabile che si finisca con l'essere sopraffatti dalla quantit
di dettagli e dalla complessit che si deve affrontare. Una decisione importante, che i progettisti software devono essere in grado di prendere, riguarda proprio quali aspetti debbano
essere trattati separatamente e quali debbano essere necessariamente trattati insieme.
Nel caso in cui due aspetti di un problema siano difficilmente separabili, sovente possibile prendere alcune decisioni globali di progetto in una prima fase e quindi separare tra
di loro i diversi aspetti solo in una seconda fase. Ad esempio, si consideri un sistema in cui
transazioni on-line debbano accedere concorrentemente a una base di dati. In una prima
implementazione del sistema, si potrebbe introdurre un semplice schema di blocco (locking)
in modo che ciascuna transazione blocchi l'accesso all'intera base di dati all'inizio della transazione e lo sblocchi alla fine. Si supponga ora che una preliminare analisi di prestazioni evidenzi come una transazione ad esempio t i ; il cui scopo potrebbe essere quello di stampare un rapporto complesso estratto da molti dati della base di dati - impieghi un tempo talmente lungo da non consentire un accesso accettabile ad altre transazioni. In questo caso il
problema quello di rivedere l'implementazione, al fine di migliorare le prestazioni, mantenendo, tuttavia, la correttezza complessiva del sistema. E chiaro che i due aspetti - correttezza funzionale e prestazioni - sono strettamente interconnessi e pertanto la prima decisione deve riguardare entrambi: ti non viene pi implementata come una transazione atomica, ma viene spezzata in diverse transazioni elementari tiU ti2, ..., t i n , ciascuna delle qua-

li atomica. La nuova implementazione pu influenzare la correttezza complessiva del sistema, in quanto l'esecuzione di due sequenze di transazioni elementari pu alternarsi in maniera arbitraria. Si noti tuttavia che, avendo separato i due aspetti di verifica della correttezza funzionale del sistema e di analisi delle sue prestazioni, possiamo svolgere le due analisi indipendentemente e separatamente, eventualmente facendo anche ricorso a progettisti
diversi con differenti livelli di competenza specifica.
Una delle pi importanti applicazioni del principio di separazione degli interessi riguarda
la separazione tra aspetti relativi al dominio del problema da quelli relativi al dominio dell'implementazione. Le propriet specifiche del dominio del problema valgono indipendentemente dagli aspetti implementativi. Ad esempio, nella progettazione di un sistema di gestione del personale dobbiamo trattare separatamente i problemi che riguardano il personale in generale da quelli che sono conseguenza delle nostre scelte implementative, relative alla struttura dati che viene adottata per rappresentare i dipendenti. Nel dominio del problema noi parliamo di relazioni tra dipendenti quali: "l'impiegato A dipende gerarchicamente
dal dirigente B" mentre nel dominio dell'implementazione noi potremmo parlare di un oggetto che contiene un puntatore a un altro oggetto. Purtroppo, nella pratica si tende a mescolare tra di loro gli aspetti relativi all'implementazione con quelli relativi al problema.
Da ultimo, si noti che il principio di separazione degli interessi pu dar luogo a una
distinzione di responsabilit nel trattare i diversi interessi. Pertanto questo principio diventa la base per suddividere il lavoro necessario per un problema complesso in assegnamenti
specifici di lavoro a diverse persone, eventualmente con diverse capacit e responsabilit. Ad
esempio, possibile separare le responsabilit manageriali da quelle tecniche all'interno di
un processo di sviluppo del software; oppure, avendo separato gli aspetti di analisi e specifica dei requisiti dalle altre attivit all'interno di ciclo di vita del software, possibile assumere analisti specializzati con competenze nel dominio dell'applicazione, invece di fare affidamento su risorse interne. Gli analisti a loro volta possono concentrarsi separatamente sui
requisiti funzionali e non funzionali del sistema.
Esercizi
3.1

Mostrate, in un semplice p r o g r a m m a (o in un f r a m m e n t o di programma) a vostra scelta, come possibile concentrarsi separatamente sugli obiettivi di correttezza e di efficienza.

3.2

Leggete alcuni articoli relativi al tema dell'aspect-oriented p r o g r a m m i n g e valutateli in relazione al tema della separazione degli interessi. C o m e viene affrontata la separazione degli interessi dall'aspect-oriented programming?

3.3

Modularit

Un sistema complesso pu essere suddiviso in parti pi piccole chiamate moduli. Un sistema composto da moduli detto modulare. Il vantaggio principiale della modularit quello di consentire di applicare il principio di separazione degli interessi in due fasi. In una prima fase si possono trattare i dettagli di un singolo modulo separatamente, ignorando i dettagli degli altri e, in una seconda fase, si possono esaminare le caratteristiche complessive di

tutti i moduli e le loro relazioni, in modo da integrarli in un sistema coerente. Quando le


due fasi vengono eseguite concentrandosi inizialmente sui moduli e quindi sulla loro composizione, diciamo che il sistema viene progettato bottom-up. Al contrario, quando scomponiamo innanzitutto un problema in moduli e ci concentriamo successivamente sulla progettazione di ciascuno di questi, il processo viene chiamato progettazione top-down.
La modularit una propriet importante di quasi tutti i processi e prodotti ingegneristici. Ad esempio, nell'industria automobilistica, la costruzione delle auto procede assemblando blocchi costitutivi che sono progettati e costruiti separatamente. Inoltre, le parti sono spesso riusate da un modello all'altro, dopo aver apportato minimi cambiamenti. Anche i processi industriali sono modulari, costituiti da moduli di lavorazione che sono combinati in maniera semplice (in sequenza o in parallelo) al fine di raggiungere i risultati desiderati.
Esercizio
3.3

Descrivete i moduli lavorativi necessari per la costruzione di una casa e indicate come questi
possano essere organizzati in maniera sequenziale e parallela.

Il prossimo capitolo tratta la modularit nel contesto della progettazione del software. La
modularit comunque non solo un principio di progettazione, ma permea l'intero processo di sviluppo del software. In particolare, la modularit d luogo a quattro fondamentali tipi di benefici:

la capacit di scomporre un sistema complesso in parti pi semplici

la capacit di comporre un sistema complesso a partire dai moduli esistenti

la capacit di capire un sistema in funzione delle sue parti

la capacit di modificare un sistema modificando soltanto un piccolo insieme delle sue


parti.

La capacit di scomporre un problema in parti consente di suddividere il problema originario top-down in sottoproblemi e di applicare la scomposizione successivamente e ricorsivamente a ciascun sottoproblema. Questo modo di procedere riflette il motto latino divide
et impera, il quale ben descrive la filosofia degli antichi romani nel dominare le altre nazioni: dividere e isolare innanzitutto, e quindi conquistare singolarmente ciascun paese.
La capacit di comporre un sistema basata invece su un procedimento bottom-up, a
partire da componenti elementari combinati successivamente fino a raggiungere il sistema
completo. Ad esempio, un sistema di automazione di ufficio pu essere progettato aggregando componenti hardware esistenti, quali personal computer, una rete locale, periferiche
e componenti software quali il sistema operativo, strumenti di produttivit personale (software
per la gestione di documenti, di basi di dati, di fogli elettronici). L'automobile un altro ovvio esempio di sistema costruito a partire da componenti: la carrozzeria, il sistema di alimentazione elettrica, il sistema di trasmissione, il motore. Ciascuno di questi elementi, a sua
volta, viene costruito a partire da componenti standard. Ad esempio: batteria, fusibili, cavi,
etc., formano il sistema elettrico. Quando si genera un malfunzionamento, si sostituiscono
i componenti difettosi con quelli nuovi.

Idealmente, nel processo di produzione del software vorremmo poter essere in grado
di costruire nuove applicazioni attraverso l'interconnessione di componenti (moduli) prelevati da una libreria e combinati in maniera tale da fornire le funzionalit richieste. I moduli dovrebbero essere progettati con l'obiettivo di costruire componenti riutilizzabili. Attraverso
l'uso di componenti riutilizzabili possibile aumentare la velocit di costruzione del sistema e della sua messa a punto. Ad esempio, diventa possibile sostituire un componente con
un altro che fornisce la stessa funzionalit, ma che differisce in termini di utilizzo di risorse
computazionali.
La capacit di comprendere la struttura interna di un sistema e quella di modificarlo sono strettamente connesse tra di loro, in quanto la comprensione di un sistema spesso il primo passo nell'effettuare modifiche. Abbiamo enfatizzato la capacit di evolvere come obiettivo di qualit, in quanto gli ingegneri del software devono spesso ritornare su
un'applicazione precedentemente sviluppata per modificarla. In un sistema che pu essere compreso soltanto nella sua interezza, le modifiche sono diffcili da effettuare e il risultato di una modifica produce probabilmente un software inaffidabile. Quando necessario riparare un difetto o migliorare una funzionalit, la modularit aiuta a confinare
la ricerca di un difetto o del punto in cui intervenire per il miglioramento, limitandola a
un singolo componente. Pertanto la modularit diventa fondamentale ai fini della capacit del software di evolvere.
Per ottenere la capacit di comporre, scomporre, comprendere e modificare il software
in maniera modulare, l'ingegnere del software deve progettare i moduli in modo che abbiano alta coesione e basso accoppiamento.
Un modulo ha un'alta coesione se tutti i suoi elementi sono strettamente connessi. Gli
elementi di un modulo (vale a dire, le istruzioni, le procedure e le dichiarazioni) sono raggruppati per un motivo logico, non in maniera puramente casuale, e cooperano tra di loro
in modo tale da raggiungere un obiettivo comune: la realizzazione della funzione richiesta
per il modulo.
Mentre la coesione una propriet interna al modulo, l'accoppiamento caratterizza la
relazione del modulo con altri moduli. L'accoppiamento misura l'interdipendenza di due moduli; ad esempio, un modulo A chiama una funzione definita nel modulo B o accede a una
variabile dichiarata dal modulo B. Due moduli hanno un elevato accoppiamento se dipendono strettamente l'uno dall'altro. desiderabile avere moduli con un basso livello di accoppiamento, in quanto ci rende possibile analizzare, capire, modificare, testare o riusare
ciascun modulo separatamente. La Figura 3.2 alla pagina seguente fornisce una visione grafica della coesione e dell'accoppiamento.
Un buon esempio di sistema con elevata coesione e basso accoppiamento il sistema
elettrico all'interno di una casa. Il sistema ha un basso livello di accoppiamento in quanto
costituito da un insieme di dispositivi e apparecchiature che forniscono funzioni ben identificabili e che sono connessi attraverso semplici cavi elettrici di collegamento. Il sistema ha
un'elevata coesione in quanto ciascuna apparecchiatura e ciascun dispositivo costituito internamente da componenti elementari che si trovano all'interno del dispositivo o del componente, proprio per assicurare la funzione che esso deve fornire.
Strutture modulari con alta coesione e basso accoppiamento ci consentono di vedere i
moduli come delle black box (scatole nere) quando si vuole descrivere la struttura complessiva del sistema, e vedere invece ciascuno di essi nei suoi dettagli quando necessario de-

Figura 3.2

Visione grafica della coesione e dell'accoppiamento, (a) Una struttura fortemente


accoppiata, (b) Una struttura con alta coesione e basso accoppiamento.

scrivere e analizzare la struttura interna del modulo. In altre parole, la modularit supporta
l'applicazione del principio di separazione degli interessi.
Esercizi
. :

3.4

S u p p o n i a m o di voler modularizzare la descrizione di un'auto considerandola come composta


da parti di 5 cm di lato. Discutete questa modularizzazione in termini di coesione e di accoppiamento. Si proponga u n eventuale sistema migliore di modularizzare la descrizione e si
traggano alcune conclusioni generali circa il m o d o con il quale dovrebbe essere modularizzato un sistema complesso di questo tipo.

3.5

Fornite alcuni esempi di cause e di eventuali rimedi per la bassa coesione di un m o d u l o


software.

3.6

Esponete alcune delle cause e dei possibili rimedi per lo stretto accoppiamento tra due moduli software.

3.4

Astrazione

L'astrazione uno strumento fondamentale per capire e analizzare problemi complessi, poich ci consente di identificare gli aspetti fondamentali di un fenomeno e di ignorare i suoi
dettagli. Pertanto, l'astrazione un caso particolare della separazione degli interessi che ci
permette di separare gli aspetti importanti da quelli che contengono dettagli secondari.
Ci che consideriamo un dettaglio dal quale noi vogliamo astrarre, dipende dallo scopo dell'astrazione. Ad esempio, consideriamo un orologio digitale. Un'astrazione utile per il
suo possessore la descrizione degli effetti della pressione di vari pulsanti, che consentono
all'orologio di passare attraverso diversi modi di funzionamento e di reagire in maniera dif-

ferenziata alle sequenze di comandi. Un'astrazione utile per chi deve riparare l'orologio,
quella di una scatola che pu essere aperta per poter sostituire la batteria. Sono necessarie
ancora diverse astrazioni del dispositivo al fine di poter intervenire sull'orologio per ripararlo, o addirittura per riprogettarlo in parte. Possono quindi esistere molteplici astrazioni
della stessa realt, ciascuna delle quali fornisce un differente punto di vista della realt e serve a uno specifico scopo.
Esercizio
3.7

Diverse persone che interagiscono con un'applicazione software possono richiedere differenti
astrazioni. C o m m e n t a t e brevemente quali tipi di astrazioni siano utili per l'utente finale, il
progettista e il m a n u t e n t o r e dell'applicazione.

L'astrazione un tecnica di progettazione utilizzata dagli ingegneri in ogni settore per dominare la complessit. Ad esempio, la rappresentazione di un circuito elettrico in termini di resistenze, capacit, etc., ciascuna delle quali caratterizzata da un insieme di equazioni, definisce un'astrazione ideale del dispositivo. Le equazioni sono modelli semplificati che approssimano il comportamento dei componenti fisici. Molte volte, le equazioni
ignorano i dettagli, quali l'inesistenza di connessioni "pure" tra componenti, che richiederebbero di essere modellate in termini di resistenze, capacit, etc. Il progettista ignora
questi fatti in quanto gli effetti che essi inducono sono trascurabili al fine di descrivere i
risultati osservati.
Questo esempio illustra un'idea generale molto importante: i modelli che costruiamo
per i fenomeni, ad esempio le equazioni che descrivono i dispositivi, sono un'astrazione della realt che ignora alcuni aspetti e si concentra su altri ritenuti pi importanti. Lo stesso
vale per i modelli costruiti e analizzati dagli ingegneri del software. Ad esempio, quando si
analizzano e si specificano i requisiti di una nuova applicazione, gli ingegneri del software
costruiscono un modello della potenziale applicazione. Come vedremo nel Capitolo 5, questo modello pu essere espresso in vari modi, che dipendono dal livello desiderato di rigore
e formalit. Qualunque sia il linguaggio usato per esprimere i requisiti, sia esso il linguaggio naturale o un linguaggio formale di formule matematiche, ci che viene fornito un
modello che astrae da numerosi dettagli che i progettisti decidono di poter ignorare senza
conseguenze.
L'astrazione un concetto chiave della programmazione. I linguaggi di programmazione
che noi impieghiamo sono astrazioni costruite sull'hardware: essi ci forniscono utili ed efficaci costrutti che possiamo utilizzare per scrivere programmi, ignorando dettagli quali il numero di bit usati per rappresentare i numeri o i particolari meccanismi di indirizzamento adottati dal computer. Questo aiuta il progettista a concentrarsi sulla soluzione del problema che deve risolvere, piuttosto che sulla modalit di istruire la macchina sulla sua risoluzione. I programmi sono a loro volta astrazioni. Per esempio, una procedura di pagamento degli stipendi
un'astrazione della procedura manuale che essa sostituisce: realizza l'essenza del comportamento della procedura manuale, ma non ne riproduce esattamente i dettagli.
L'astrazione un principio importante che si applica sia ai prodotti software che ai processi. Ad esempio, i commenti che noi spesso usiamo nell'intestazione di una procedura so-

no un'astrazione che descrive l'effetto della procedura stessa. Quando si analizza la documentazione di un programma, questi commenti dovrebbero fornire tutte le informazioni necessarie a comprendere l'utilizzo della procedura da parte di altri componenti del programma.
Come esempio di utilizzo dell'astrazione nei processi software, si consideri il caso della stima dei costi per una nuova applicazione. Un modo possibile per effettuare una stima
consiste nell'identificare alcuni fattori chiave del nuovo sistema, ad esempio, il numero di
progettisti che dovranno essere coinvolti nel processo e la dimensione attesa del prodotto finale, ed estrapolare il risultato dal profilo di costi di sistemi precedentemente realizzati che
realizzano funzionalit analoghe. I fattori chiave usati per effettuare l'analisi sono astrazioni
del sistema che vengono fatte allo scopo di consentire la stima dei costi.
Esercizi
3.8

Le variabili fornite da un linguaggio di programmazione possono essere viste come astrazioni


delle celle di memoria. D a quali dettagli astrae il concetto di variabile di un linguaggio di programmazione? Quali sono i vantaggi di usare questa astrazione? Quali gli svantaggi?

3.9

Le variabili in un programma sono usate c o m e astrazioni di concetti che appaiono nel dominio del problema. Spiegate in che senso una variabile chiamata "dipendente" sia un'astrazione del concetto di dipendente, che appare nel d o m i n i o del problema.

3.10

Un modello di ciclo di vita del software, quale il modello a cascata tratteggiato nel Capitolo
1, un'astrazione di u n processo software. In che senso?

3.5 Anticipazione del cambiamento


Il software viene modificato in continuazione. Come abbiamo visto nel Capitolo 2, i cambiamenti sono dovuti sia alla capacit di riparare il software, eliminando gli errori che non
erano stati scoperti prima del rilascio dell'applicazione, sia alla necessit di supportare l'evoluzione dell'applicazione, a seguito dell'insorgere di nuovi requisiti o di cambiamenti in
quelli vecchi. Per questo motivo abbiamo identificato la manutenibilit come caratteristica
fondamentale di qualit del software.
La capacit del software di evolvere non nasce dal nulla, ma deve essere pianificata e
anticipata con estrema cura. I progettisti possono cercare di identificare quali siano i futuri
cambiamenti e possono intervenire con particolare cura nel progetto al fine di rendere agevole la loro applicazione. Vedremo in dettaglio questo importante aspetto nel Capitolo 4,
parlando di progettazione e mostrando come il software possa essere progettato in maniera
tale da poter facilmente incorporare sia i probabili cambiamenti, anticipati durante la fase
di analisi dei requisiti, sia i cambiamenti pianificati, come parte della strategia di progetto.
In sostanza, i probabili cambiamenti dovrebbero essere attribuibili a specifiche porzioni del
software e il loro effetto dovrebbe essere ristretto a tali porzioni. In altre parole, l'anticipazione del cambiamento dovrebbe essere alla base della strategia di modularizzazione.
L'anticipazione del cambiamento forse uno dei principi che maggiormente distingue
il software dagli altri tipi di prodotti industriali. In molti casi, un'applicazione software viene sviluppata quando i suoi requisiti non sono completamente noti. Successivamente, una

volta che viene rilasciata, sulla base delle reazioni degli utenti, l'applicazione pu evolvere in
funzione dei nuovi requisiti che vengono scoperti o dei vecchi requisiti che vengono parzialmente modificati. Per di pi, le applicazioni sono spesso immerse in un ambiente, quale ad esempio una struttura organizzativa. L'ambiente risulta influenzato dall'introduzione
dell'applicazione, e ci genera nuovi requisiti che non potevano essere evidenziati all'inizio.
Pertanto l'anticipazione del cambiamento un principio che pu essere utilizzato per ottenere la capacit di evolvere dell'applicazione.
La riusabilit un'altra caratteristica che risulta fortemente influenzata dall'anticipazione del cambiamento. Come abbiamo visto, un componente riusabile se pu essere direttamente riusato per produrre un nuovo prodotto o, pi realisticamente, se deve essere sottoposto solo a limitati cambiamenti per poter essere riusato. Pertanto la riusabilit pu essere vista come l'evolvibilit a livello dei singoli componenti. Se potessimo anticipare i cambiamenti del contesto nel quale un componente pu essere inserito, potremmo progettare
il componente in modo che questi cambiamenti possano essere ottenuti facilmente.
L'anticipazione dei cambiamenti richiede adeguati strumenti per la gestione delle diverse versioni e delle revisioni del software. Deve essere possibile immagazzinare e ritrovare
la documentazione, i moduli sorgente, i moduli oggetto, etc., su una base di dati che funga da deposito centralizzato dei componenti riusabili. L'accesso alla base di dati deve essere
controllato e deve sempre essere disponibile una visione consistente del sistema, anche a seguito dei cambiamenti apportati ai suoi componenti. Come abbiamo gi citato in precedenza
e vedremo in maggiore dettaglio nei Capitoli 7, 8 e 9, la disciplina che studia questa classe
di problemi chiamata gestione delle configurazioni (configuration management).
Nella trattazione del principio di anticipazione del cambiamento abbiamo finora focalizzato l'attenzione sui prodotti software invece che sui processi. Il principio per si applica anche alla gestione dei processi software. Per esempio, i manager dovrebbero prevedere gli effetti del ricambio di personale; inoltre quando viene progettato il ciclo di vita di
un'applicazione, necessario prevedere un'articolazione della fase di manutenzione. I manager dovrebbero essere in grado di stimare i costi e progettare la struttura organizzativa
che favorisce l'evoluzione del software, una volta che si deciso quali cambiamenti anticipare. Infine, i manager dovrebbero decidere se e quando importante investire tempo e
sforzo nella produzione di componenti riusabili, sia come sottoprodotto di uno specifico
progetto di sviluppo di software, sia come sforzo specifico investito per la produzione di
componenti riusabili.
Esercizio
3.11

Considerate un qualunque classico p r o g r a m m a di o r d i n a m e n t o e discutetelo dal p u n t o di vista della riusabilit. L'algoritmo scelto fa assunzioni sul tipo di elementi da ordinare? Sarebbe
possibile riusare l'algoritmo per tipi di elementi diversi? C h e cosa succede se la sequenza di
valori da ordinare lunga e pertanto deve essere immagazzinata su memoria di massa? C o m e
potrebbe essere modificato il p r o g r a m m a al fine di migliorarne la riusabilit in funzione di
queste caratteristiche? Producete una lista generale di suggerimenti che favoriscano i cambiamenti in un programma, sulla base di questa esperienza.

3.6

Generalit

Il principio di generalit pu essere cos descritto.


O g n i volta che si deve risolvere un problema, si cerca di scoprire qual il problema pi generale che si nasconde dietro lo specifico problema da risolvere. Talvolta, p u capitare che il problema generale sia pi complesso del problema originario, ma sovente accade, invece, che sia
pi semplice. Inoltre, probabile che la soluzione al problema generalizzato abbia un maggior
potenziale di riusabilit. Pu anche accadere che la soluzione sia gi fornita da un c o m p o n e n te commerciale. Infine, pu capitare che attraverso la generalizzazione del problema si giunga
a progettare u n c o m p o n e n t e che p u essere utilizzato in diversi punti dell'applicazione, invece
di dover utilizzare ogni volta una soluzione specifica.

Una soluzione generalizzata non ha solo vantaggi, ma pu essere pi costosa in termini di


velocit d'esecuzione, occupazione di memoria o tempo di sviluppo, rispetto a una soluzione specializzata. In generale pertanto necessario valutare i pr e i contro della generalit
esaminandone i costi e l'efficienza al fine di decidere se sia utile risolvere il problema generalizzato anzich concentrarsi sul problema specifico al quale si posti di fronte.
Ad esempio, si supponga di dover effettuare la fusione di due file ordinati in un singolo file, anch'esso ordinato. Sulla base dei requisiti, sappiamo che i due file originali non
contengono elementi duplicati, con lo stesso valore di chiave. Generalizzando la soluzione potremmo risolvere il problema per file che contengono elementi duplicati con lo stesso valore di chiave. In tal caso costruiremmo un programma che ha un grado di riusabilit pi elevato. Potrebbe anche succedere che, fatta la generalizzazione, si scopra l'esistenza
di un programma di libreria che risolve esattamente il problema generalizzato di fusione
dei file.
Come ulteriore esempio, si supponga di dover progettare un'applicazione che gestisce
una piccola libreria di ricette di cucina, e si supponga anche che le ricette abbiano un'intestazione, che contiene informazioni circa il nome, la lista degli ingredienti e le informazioni sulla modalit di cottura, e una parte testuale che descrive come eseguire la ricetta. Oltre
a immagazzinare le ricette nella libreria, deve anche essere possibile effettuare ricerche sofisticate in funzione degli ingredienti disponibili, della quantit massima di calorie, etc. Invece
di progettare un nuovo insieme di programmi, queste ricerche potrebbero essere viste come
un caso speciale di funzionalit di manipolazione di testi, quali le funzionalit fornite dal
programma AWK disponibile nei sistemi UNIX, o il linguaggio PERL. Prima di iniziare la
progettazione delle routine di ricerca specializzate, il progettista dovrebbe considerare se pu
essere utile l'adozione di uno strumento generalizzato di manipolazione di testi. Lo strumento
generalizzato sicuramente pi affidabile del programma specializzato che verrebbe progettato e consentirebbe di incorporare pi facilmente futuri cambiamenti nei requisiti o l'insorgere di nuovi requisiti. In termini negativi, invece, va considerato il costo dell'acquisizione e l'inefficienza indotta dall'uso dello strumento generalizzato.
La generalit un principio fondamentale che ci consente di sviluppare strumenti generali o componenti da rendere disponibili sul mercato. Il successo di strumenti quali i fogli elettronici, i sistemi di gestione di basi di dati, i word processor, deriva dal fatto che sono
sufficientemente generali da coprire gran parte delle richieste di utenti che desiderano gestire le proprie applicazioni personali attraverso un computer. Invece di produrre soluzioni

ad hoc per ciascun uso personale, sicuramente pi economico utilizzare prodotti gi disponibili sul mercato.
L'esistenza di prodotti di uso generale (ojf-the-shelf) rappresenta una linea di tendenza fondamentale nel software. Sono infatti sempre pi disponibili pacchetti generali e componenti che forniscono soluzioni standard a problemi di uso comune per gran parte delle
aree applicative. Tutte le volte che affrontiamo un problema che pu essere riformulato come un particolare caso di un problema risolto da un componente presente sul mercato, pu
risultare pi conveniente adottare questo componente invece di una soluzione specializzata.
Questa linea di tendenza simile a quanto accade in altri settori dell'industria. Ad esempio, agli albori dell'industria automobilistica era possibile realizzare automobili personalizzate, che rispondessero ai requisiti specifici dell'acquirente. Con la maturazione e l'industrializzazione del settore, gli acquirenti possono solo scegliere i modelli da un catalogo fornito dal costruttore, in cui ogni modello corrisponde a una soluzione preconfezionata. Oggi
praticamente impossibile acquistare un'automobile il cui progetto sia personalizzato sulle
esigenze dell'acquirente.
Il passo successivo di questa linea di sviluppo dell'industria del software costituito dai
server applicativi {application server) che forniscono le funzionalit generalizzate su macchine
{server) remote. In questo modo gli utenti non devono neppure installare l'applicazione sulle
loro macchine, ma possono invece utilizzarne le funzionalit, che sono rese disponibili in modo remoto. Ad esempio, sono gi fornite, mediante soluzioni di mercato, applicazioni che consentono di gestire la posta elettronica o le agende con gli impegni delle persone.

3.7

Incrementalit

Parliamo di incrementalit nel caso di un processo che procede attraverso passi, detti incrementi. In questa maniera cerchiamo di raggiungere l'obiettivo desiderato attraverso una serie di approssimazioni successive.
Ogni approssimazione un incremento rispetto alla precedente. L'incrementalit si applica a molte attivit ingegneristiche. Nel caso del software, intendiamo che l'applicazione
desiderata viene prodotta come il risultato di un processo evolutivo.
Un modo di applicare un principio di incrementalit consiste nell'identifcare da subito
dei sottoinsiemi di un'applicazione che possano essere sviluppati e consegnati ai committenti,
in modo da ottenere un immediato feedback. Ci consente all'applicazione di evolvere in modo controllato nei casi in cui i requisiti iniziali non sono stabili o pienamente compresi. La
motivazione che porta all'uso dell'incrementalit che in gran parte dei casi non c' modo di
ottenere tutti i requisiti in maniera completa ed esatta prima che l'applicazione venga sviluppata. Invece, i requisiti emergono contemporaneamente alla disponibilit dell'applicazione o
di sue parti. Di conseguenza, prima riusciamo a ricevere un feedback dai committenti riguardo l'utilizzo dell'applicazione, pi facile sar incorporare i cambiamenti desiderati all'interno
del prodotto. Pertanto l'incrementalit strettamente correlata con l'anticipazione dei cambiamenti ed uno dei principi su cui si fonda la capacit di evolvere di un sistema.
L'incrementalit si applica a gran parte delle qualit del software che abbiamo discusso
nel Capitolo 2. E possibile aggiungere progressivamente funzioni all'applicazione sviluppata, partendo da un insieme minimale iniziale che comunque in grado di rendere il si-

stema utilizzabile, anche se incompleto. Ad esempio, nei sistemi di automazione di attivit commerciali, alcune funzioni potrebbero essere temporaneamente svolte manualmente, mentre altre verrebbero svolte in maniera automatica dall'applicazione parzialmente sviluppata.
E possibile anche migliorare le prestazioni in maniera incrementale. Vale a dire: la versione iniziale dell'applicazione pu porre maggior attenzione all'interfaccia utente e all'affidabilit, piuttosto che alle prestazioni, mentre i rilasci successivi potrebbero migliorarne l'efficienza in spazio e tempo.
Tutte le volte che un'applicazione viene sviluppata in maniera incrementale, i passi intermedi costituiscono, in un certo senso, dei prototipi del prodotto finale; essi sono soltanto una sua approssimazione. L'idea della prototipazione rapida spesso proposta come modo per sviluppare in maniera progressiva un'applicazione, man mano che i requisiti vengono meglio compresi. Un ciclo di vita basato sulla prototipazione ovviamente piuttosto diverso dal modello a cascata che abbiamo descritto in precedenza, il quale prevede che venga svolta una completa e specifica analisi dei requisiti prima di intraprendere lo sviluppo dell'applicazione. Al contrario, la prototipazione basata su un modello di sviluppo pi flessibile e iterativo. Questa differenza modifica non solo gli aspetti tecnici di un processo, ma
anche quelli organizzativi e gestionali.
Come abbiamo gi visto nel caso dell'anticipazione del cambiamento, un processo di
tipo evolutivo richiede un'attenzione particolare nella gestione dei documenti, dei programmi, dei dati di test, etc., che vengono sviluppati per le varie versioni del software. Ogni
passo incrementale deve essere accuratamente catalogato e documentato; la documentazione deve essere facilmente rintracciabile e i cambiamenti devono essere effettuati in modo
controllato. Se non si opera con estrema cura, un processo evolutivo pu rapidamente trasformarsi in uno sviluppo di software indisciplinato e si perderebbero tutti i potenziali vantaggi dell'evolvibilit.
Esercizio
3.12

3.8

Discutete il concetto di p r o t o t i p o software qui illustrato, c o n f r o n t a n d o l o con il concetto di


p r o t o t i p o usato in altri settori dell'ingegneria (ad esempio, il p r o t o t i p o di un p o n t e o di
un'auto).

Illustrazione dei principi dell'ingegneria


del software attraverso due casi di studio

In questo paragrafo verranno presentati due casi di studio che ci aiuteranno a capire meglio
i principi che abbiamo illustrato nel capitolo. Il primo fa riferimento a un prodotto software
abbastanza tipico: un compilatore; il secondo esamina un sistema non puramente software:
un ascensore. Entrambi i casi mostrano come i principi illustrati nel capitolo siano principi
ingegneristici generali. Entrambi servono a illustrare meglio gli aspetti comuni e le differenze
tra l'ingegneria tradizionale e l'ingegneria del software. Il secondo caso di studio aiuta an-

che a capire che, in molte situazioni, il software solo un componente di un sistema complesso che integra componenti di diverso tipo.

3.8.1

Caso di studio nella costruzione di un compilatore

Esaminiamo come i principi illustrati in questo capitolo possano essere applicati nello sviluppo di un compilatore.
3.8.1.1

Rigore e formalit

Esistono molti motivi per cui i progettisti dei compilatori dovrebbero essere rigorosi e, se
possibile, formali. Innanzitutto, un compilatore un prodotto critico, in quanto la generazione di codice erroneo porterebbe il computer a eseguire istruzioni scorrette. Un compilatore scorretto genererebbe dunque applicazioni scorrette, indipendentemente da altre eventuali qualit possedute. Inoltre, poich un compilatore potrebbe venire usato per generare
codice per software del quale viene fatto un uso di massa, l'effetto di un errore nel compilatore si moltiplicherebbe su larga scala. Pertanto, in generale, importante che lo sviluppo
di un compilatore venga affrontato in modo rigoroso, per produrre uno strumento di alta
qualit.
La costruzione di compilatori uno dei campi dell'informatica nei quali la formalit
stata ben sfruttata da lungo tempo. Infatti, la teoria degli automi e dei linguaggi formali
stata motivata in larga misura dalla necessit di rendere la costruzione di compilatori sistematica e affidabile. Al giorno d'oggi la sintassi dei linguaggi di programmazione viene definita in modo formale, attraverso formalismi quali la forma di Backus-Naur (BNF). Non a
caso, molte volte i problemi associati con la correttezza dei compilatori derivano dagli aspetti semantici del linguaggio, definiti sovente in maniera informale, e non da quelli sintattici,
che sono ben definiti dalla BNF.
La formalit che possibile raggiungere attraverso la BNF e l'applicazione della teoria
degli automi forniscono inoltre benefici in termini di generalit, come vedremo nel Paragrafo
3.8.1.6.
3.8.1.2

Separazione degli interessi

La costruzione di compilatori riguarda numerosi aspetti. La correttezza l'obiettivo primario di chi sviluppa un compilatore: infatti necessario produrre codice oggetto che sia consistente con il codice sorgente e produrre appropriati messaggi d'errore nel caso di programma sorgente scorretto. Altri aspetti importanti sono l'efficienza e l'amichevolezza dell'interfaccia. L'efficienza pu riguardare sia gli aspetti relativi al tempo di compilazione, il
che significa velocit nell'analisi del codice sorgente e della traduzione e utilizzo limitato della memoria, ma riguarda anche il tempo di esecuzione del programma, nel qual caso ci significa produrre un codice oggetto che sia esso stesso efficiente. L'amichevolezza dell'interfaccia ha diversi aspetti, che vanno dalla precisione e completezza dei messaggi diagnostici,
alla facilit di interazione attraverso un'interfaccia utente, per esempio un sistema di finestre ben progettato e altri supporti di tipo grafico.
Questi aspetti del compilatore dovrebbero essere analizzati separatamente, per quanto
possibile. Ad esempio, non c' ragione di preoccuparsi per i messaggi diagnostici mentre si
sta progettando un algoritmo sofisticato per l'ottimizzazione dell'allocazione dei registri. Ci

non significa che i diversi aspetti non si influenzino l'un l'altro. Tipicamente, infatti, nel cercare di generare codice oggetto il pi efficiente possibile, potremmo incorrere nel sovraccarico d'uso di alcuni registri. Nel tentativo di produrre una buona diagnostica durante l'esecuzione del programma, ad esempio verificando che gli indici di un array restino nel proprio campo di variabilit, si potrebbero produrre delle inefficienze a run-time.
La diagnostica a run-time e l'efficienza sono tipici casi nei quali la separazione degli
interessi pu e deve essere applicata tenendo ben presente per che esistono mutue dipendenze tra i diversi aspetti. Nell'esempio specifico, i progettisti tengono di solito i due aspetti ben separati, tant' che spesso possibile che l'utente abiliti o disabiliti le verifiche a runtime. Durante le fasi di sviluppo e verifica di un programma, nelle quali la preoccupazione
maggiore di un progettista software quella di produrre programmi corretti, le verifiche a
run-time dovrebbero essere abilitate in modo tale da rendere possibile la diagnostica del programma. Una volta che il programma stato verificato a fondo, l'efficienza diventa invece
la preoccupazione maggiore e pertanto i progettisti potrebbero disabilitare la generazione delle verifiche a run-time effettuate del compilatore.
3.8.1.3

Modularit

Un compilatore pu essere modularizzato in vari modi. Qui di seguito proponiamo una modularizzazione piuttosto elementare e tradizionale, basata su diversi passi effettuati dal compilatore sul codice sorgente. Questa struttura modulare accettabile come soluzione iniziale. Nel Capitolo 4 vedremo alcune critiche a questo schema e mostreremo soluzioni alternative, che possono produrre risultati migliori dal punto di vista degli altri principi, quali la
generalit e la progettazione per il cambiamento.
La letteratura esistente sulla costruzione dei compilatori suggerisce che la compilazione proceda attraverso una serie di fasi o passi, ciascuno dei quali effettua una traduzione parziale da una rappresentazione intermedia a un'altra, in modo tale che l'ultimo passo trasformi
il suo input nel codice oggetto finale, pronto per essere eseguito.

Figura 3.3a

Struttura modulare di un compilatore. Le scatole rappresentano i moduli e le frecce


gli input e gli output.

Le fasi principali di un compilatore tradizionale sono:

Analisi lessicale, con la quale vengono analizzati gli identificatori che appaiono nel programma, sostituendoli con una rappresentazione interna e costruendo una tabella dei
simboli, che contiene una loro descrizione. Questa fase produce anche una prima serie di messaggi diagnostici se il codice sorgente contiene errori lessicali, ad esempio,
identificatori malformati.

Analisi sintattica o parsing, che prende il risultato prodotto dall'analisi lessicale e costruisce una struttura dati chiamata albero sintattico, la quale descrive la struttura sintattica del codice originario. Questa fase produce anche un secondo insieme di messaggi diagnostici riferiti alla struttura sintattica del programma, ad esempio la mancanza
di parentesi.

Generazione di codice, che produce il codice oggetto. Quest'ultima fase essa stessa svolta attraverso pi passi. Ad esempio, molte volte viene prima prodotto un codice intermedio indipendente dalla macchina e successivamente questo viene tradotto in codice oggetto specifico per la macchina. Ognuna di queste traduzioni parziali pu includere una fase di ottimizzazione che ristruttura il codice al fine di renderlo pi efficiente.

La descrizione che abbiamo appena dato suggerisce una descrizione modulare della struttura del compilatore, descritta graficamente nella Figura 3.3a.
Malgrado le semplificazioni presenti nella figura possiamo comunque derivare alcune
caratteristiche distintive di un progetto modulare:

I moduli di un sistema possono essere disegnati in maniera del tutto naturale come scatole di una qualche forma, nel nostro caso sono rettangolari.

Le interfacce dei moduli possono essere disegnate come delle linee orientate che collegano le scatole che rappresentano i moduli. Un'interfaccia un'informazione che collega, in qualche modo, diversi moduli e rappresenta qualunque cosa sia condivisa da

Figura 3.3b

Ulteriore modularizzazione del modulo di generazione del codice.

essi. Si noti che la metafora grafica suggerisce che tutto ci che si trova all'interno della scatola sia nascosto dall'esterno. Il resto del sistema pu comunicare con un modulo esclusivamente attraverso la sua interfaccia. Nella figura conveniente rappresentare le interfacce con delle frecce, per enfatizzare il fatto che l'informazione descritta
l'output di un modulo e l'input di un altro. Vedremo casi in cui la nozione di interfaccia pu essere resa pi simmetrica, ad esempio, una struttura dati condivisa. In questi casi pi conveniente rappresentare la relazione con una linea non orientata.
Si noti anche che le linee che rappresentano il codice sorgente, i messaggi diagnostici
e il codice oggetto sono input o output dell'intero "sistema"; pertanto sono disegnate
senza un collegamento specifico a una sorgente e a una destinazione.

La struttura modulare della Figura 3.3a favorisce essa stessa una naturale iterazione del
processo di scomposizione. Ad esempio, in accordo con quanto detto nella descrizione della fase di generazione di codice, la scatola che rappresenta questo passo pu essere raffinata come rappresentato nella Figura 3.3b.

I diagrammi che abbiamo qui utilizzato vengono chiamati informalmente "diagrammi a scatole e linee" (box-and-line diagram), e vengono usati comunemente per mostrare in modo
informale la struttura complessiva l'architettura - dei sistemi software. Esistono molte varianti dei diagrammi a scatole e linee che sono state proposte per rendere la notazione pi
formale. Vedremo esempi di tali notazioni nei Capitoli 4 e 5.
3.8.1.4

Astrazione

L'astrazione pu essere applicata in diverse direzioni nell'ambito del progetto del compilatore. Dal punto di vista sintattico tipico distinguere tra sintassi concreta e sintassi astratta. La sintassi astratta ha l'obiettivo di focalizzare l'attenzione sulle caratteristiche fondamentali
dei costrutti del linguaggio, ignorando dettagli che non hanno effetto sulla struttura del programma. Ad esempio, un'istruzione condizionale consiste di una condizione insieme a un'istruzione, da eseguirsi nel caso in cui la condizione sia vera, ed eventualmente un'istruzione da eseguirsi nel caso la condizione risulti falsa. Questa descrizione valida sia che si includa la parola chiave t h e n prima dell'istruzione da eseguire nel caso positivo, come nel caso del Pascal, sia nel caso in cui ci non avvenga, come capita in C. Analogamente, in C si
usano le coppie di parentesi graffe, mentre nei linguaggi Algol-like si usano le coppie di identificatori b e g i n - e n d .
Nel caso di generazione del codice si applica un'altra tipica astrazione. Come abbiamo
visto nella sezione precedente, la prima fase della generazione di codice produce un codice
intermedio, che pu essere visto come il codice di una macchina astratta, la seconda fase trasforma successivamente il codice di questa macchina astratta nel codice della macchina concreta finale. In questa maniera gran parte della costruzione del compilatore astrae dalle specificit del particolare processore che deve eseguire il codice oggetto. Il linguaggio Java definisce una macchina virtuale Java (JVM, Java Virtual Machine) il cui codice (Java bytecode)
pu essere eseguito interpretandolo su diverse macchine concrete.
In entrambi gli esempi di astrazione che abbiamo fornito in questo paragrafo, l'astrazione si combina in modo naturale con il principio di generalit. Ad esempio, la produzione di codice intermedio per una macchina astratta, invece della produzione del codice oggetto finale per una macchina concreta permette di costruire un compilatore generale che

pu essere adattato, con modifiche di minor complessit, alla produzione di codice per macchine diverse, migliorando anche la riusabilit.
3.8.1.5 Anticipazione del cambiamento
Durante la vita di un compilatore possono intervenire diversi motivi di cambiamento.

E possibile che avvengano nuovi rilasci del processore, con nuove e pi potenti istruzioni.

E possibile che siano introdotti nuovi dispositivi di input-output (I/O) che richiedono nuovi tipi di istruzioni.

I comitati di standardizzazione potrebbero definire cambiamenti ed estensioni al linguaggio sorgente.

Il progetto di un compilatore dovrebbe anticipare questi cambiamenti. Ad esempio, il linguaggio Pascal tent di "congelare" le istruzioni di I/O attraverso una definizione rigida. Questa
decisione entr in conflitto con dipendenze tipiche dalla macchina e il risultato fu la nascita di numerosi dialetti del linguaggio, i quali differivano essenzialmente per gli aspetti di I/O.
Successivamente fu riconosciuto che il tentativo di "congelare" il linguaggio per le istruzioni di I/O era risultato inefficace. Pertanto, linguaggi come C e Ada incapsularono le istruzioni di I/O all'interno di librerie standard, riducendo cos la quantit di lavoro necessario,
in corrispondenza di cambiamenti nell'I/O.
Per quanto riguarda, infine, la capacit del compilatore di adattarsi a diverse architetture hardware, si dimostrata particolarmente efficace la separazione della fase di generazione di codice nelle due sottofasi discusse in precedenza.
3.8.1.6

Generalit

Come l'astrazione, la generalit pu essere perseguita in vari modi durante la costruzione


del compilatore, in funzione degli obiettivi complessivi del progetto. Ad esempio, potrebbe
suggerire la realizzazione di un'ampia famiglia di prodotti rispetto alla produzione di un compilatore fortemente specializzato per una singola macchina.
Abbiamo discusso in precedenza la necessit di essere parametrici rispetto all'architettura finale. Il caso del bytecode di Java un esempio convincente dei benefici che si possono raggiungere con un progetto che ha come obiettivo la generalit. Infatti il bytecode indipendente dal linguaggio sorgente, e pertanto pu essere usato esso stesso come linguaggio
oggetto per altri linguaggi, oltre che Java.
Talvolta un compilatore pu essere parametrico anche rispetto al linguaggio sorgente.
Un esempio che radicalizza questo concetto di generalit viene fornito dai cosiddetti "compilatori di compilatori" (compiler compiler). Si tratta di software che prende in ingresso la
definizione del linguaggio sorgente ed eventualmente del linguaggio oggetto e automaticamente produce un compilatore che traduce il linguaggio sorgente nel linguaggio oggetto.
L'esempio di compilatore di compilatori pi noto e di maggior successo quello illustrato
dai programmi lex e yacc all'interno del sistema UNIX, che vengono utilizzati per ottenere in maniera automatica le componenti di analisi lessicale e sintattica di un compilatore.
Questa generalit pu essere ottenuta grazie alla formalizzazione della sintassi del linguaggio; pertanto il principio di generalit viene sfruttato congiuntamente con quello di for-

malit. Esiste un'altra relazione tra i principi di generalit e di anticipazione del cambiamento.
L'essere parametrici, generali, rispetto alle caratteristiche che pi probabilmente sono soggette a cambiamento favorisce la capacit del software di evolvere.
3.8.1.7

Incrementalit

Anche l'incrementalit pu essere ottenuta in modi diversi. Ad esempio, possiamo dapprima consegnare una versione parziale del compilatore, la quale riconosce soltanto un sottoinsieme del linguaggio sorgente e quindi, attraverso consegne successive, fornire sottoinsiemi sempre pi estesi del linguaggio. In alternativa, la versione iniziale rilasciata del sistema potrebbe offrire soltanto i componenti essenziali del compilatore, che consentono la traduzione nel linguaggio oggetto, con solo un minimo di capacit diagnostica. Successivamente
si possono aggiungere maggiori caratteristiche diagnostiche e operazioni di ottimizzazione
migliori. L'uso sistematico di librerie fornisce un altro modo naturale per sfruttare l'incrementalit. abbastanza comune che la versione rilasciata inizialmente includa solo librerie
di carattere minimale (ad esempio, librerie di I/O e per la gestione della memoria) e che vengano rilasciate solo in una fase successiva librerie pi complete e ricche, per esempio librerie grafiche e matematiche.

3.8.2

Caso di studio nell'ingegneria dei sistemi

Si supponga di voler progettare un sistema di ascensori che debba essere incluso come parte di uno o pi edifci. Si noti che stiamo parlando di progetto e non di un singolo esemplare fisico di ascensore.
La domanda preliminare che ci poniamo la seguente: che cosa ha a che fare il progetto di un sistema di ascensori con l'ingegneria del software? Questo un tipico esempio della
forte relazione che l'ingegneria del software ha con l'ingegneria di sistemi. Come abbiamo detto in precedenza nel Paragrafo 1.6.2, i prodotti software sono spesso parte di un sistema pi
complesso, quale un impianto manifatturiero, un edificio o un'automobile. Pertanto, l'ingegnere del software in alcune situazioni deve operare come un ingegnere dei sistemi. Gran parte dell'analisi e della progettazione iniziale di un sistema deve essere fatta a livello del sistema
e pertanto deve coinvolgere specialisti di diversi settori dell'ingegneria. Solo in fasi successive
i progettisti possono concentrarsi esclusivamente sugli aspetti software, come la codifica dei
programmi che controllano i dispositivi informatici, i quali a loro volta controllano l'intero sistema. Gran parte dei principi che abbiamo esaminato nei Capitoli 2 e 3 si applicano inoltre
a qualunque attivit ingegneristica e non solo alla costruzione di software.
Verifichiamo, dunque, come i principi esaminati in questo capitolo si possano applicare alla progettazione di un sistema di ascensori. Useremo questo stesso esempio in altre
parti del libro per illustrare altre tecniche.
3.8.2.1

Rigore e formalit

Esistono naturali motivazioni perch il progettista del nostro ipotetico sistema di ascensori debba operare in modo rigoroso, attraverso tutte le fasi del progetto. Innanzitutto, il sistema ha
caratteristiche di criticit (safety criticai), in quanto eventuali malfunzionamenti possono provocare danni gravi e persino perdite di vite umane. Pertanto si deve innanzitutto definire in
modo rigoroso quali siano i requisiti di safety per il nostro sistema. Per esempio:

un ascensore deve essere in grado di trasportare fino a 400 kg senza generare malfunzionamenti;

qualora i cavi di sostegno si stacchino, i freni di emergenza devono essere in grado di


fermare l'ascensore entro un metro oppure entro due secondi dal distacco, in qualunque circostanza ci avvenga;

deve essere generato un segnale sonoro nel caso in cui l'ascensore sia sovraccarico e, in
tal caso, deve risultare impossibile l'utilizzo dell'ascensore.

Una volta progettato l'ascensore, occorrer verificare che questi requisiti siano effettivamente
rispettati dal progetto.
Secondariamente, occorre essere rigorosi e precisi al fine di evitare dispute di natura
contrattuale. Ad esempio, se la specifica iniziale che viene usata come base del contratto tra
il fornitore del sistema e il committente non definisce vincoli sulla velocit dell'ascensore,
com' possibile poi lamentarsi per la lentezza dell'ascensore dopo la sua installazione?
In terzo luogo, si supponga che durante il test di sistema si verifichino il corretto funzionamento e la validit delle prestazioni attraverso vari comandi che comportano la pressione dei pulsanti interni ed esterni. Ad esempio, si verifica che la pressione del pulsante interno
con il numero 4 fa in modo che l'ascensore raggiunga il quarto piano entro il tempo limite
specificato. Successivamente, durante l'effettivo impiego dell'ascensore, pu capitare che una
combinazione strana di pressione di pulsanti, esterni e interni, provochi un sovraccarico della
memoria del microprocessore che controlla il sistema. A sua volta ci pu causare un comportamento indesiderato, quale il salto di un piano da parte dell'ascensore. Questo comportamento anomalo avrebbe dovuto essere escluso preventivamente attraverso un'analisi rigorosa
di tutti gli eventi possibili che possono capitare durante il funzionamento del sistema.
Infine si supponga che, sotto pressione dei committenti, venga firmato un contratto
che contiene i seguenti requisiti.

Date alcune condizioni probabilistiche circa la richieste degli utenti e la velocit dell'ascensore, il funzionamento deve minimizzare il tempo medio di attesa degli utenti.

Ogni richiesta deve essere, prima o poi, soddisfatta.

Pu succedere che, adottando una politica che ottimizza le prestazioni da un punto di vista
statistico, non si possa garantire l'obiettivo di fairness (imparzialit), cio che ogni richiesta
sia prima o poi soddisfatta. Pertanto, un'analisi rigorosa dei requisiti potrebbe scoprire questa propriet ed evitare di fornire specifiche tra di loro conflittuali.
Come abbiamo visto nel Paragrafo 3.1, l'applicazione di opportune tecniche formali
pu aiutare a essere rigorosi nella specifica e nella verifica di requisiti simili a quelli di cui
abbiamo appena discusso.
3.8.2.2

Separazione degli interessi

Un sistema di ascensori pone il progettista di fronte a diversi aspetti che sono abbastanza tipici di molti progetti ingegneristici quali:

sicurezza

prestazioni

usabilit (in termini di spazio, accessibilit, illuminazione dei pulsanti e cos via)

costo

Naturalmente, gran parte di questi aspetti sono in larga misura intercorrelati e, pertanto,
una decisione progettuale relativa a uno pu influenzarne un altro. Ad esempio, se noi riduciamo i costi utilizzando materiale economico, potremmo mettere in pericolo la sicurezza. Ciononostante, la separazione degli interessi resta un principio di progetto importante. Ad esempio, possiamo effettuare l'analisi dei costi e l'analisi della sicurezza separatamente, tenendo comunque presente che entrambe la propriet devono essere prima o
poi verificate. Analogamente possiamo concentrare l'attenzione sull'usabilit in un momento successivo, verificando che le scelte adottate non portino a superare i limiti di costo accettabili.
3.8.2.3

Modularit

Una sintetica e grossolana rappresentazione del sistema di ascensori mostrata nella Figura
3.4a. Anche qui possiamo commentare alcune caratteristiche distintive di un progetto modulare, a partire da questo diagramma.

Come nel caso della struttura modulare della Figura 3.3a, usiamo scatole per denotare moduli e linee per denotare interfacce. In questo caso, tuttavia, usiamo linee non
orientate, in quanto le informazioni di interfaccia, tipicamente segnali elettrici, fluiscono in entrambe le direzioni. A un livello di progetto successivo e pi dettagliato potremmo poi utilizzare collegamenti direzionali. Ad esempio, potremmo rappresentare
il comando fornito da un apparato di controllo al motore dell'ascensore mediante una
freccia orientata. Analogamente potremmo rappresentare l'informazione relativa alla po-

Figura 3.4a

Descrizione modulare di un semplice sistema di ascensori.

sizione corrente dell'ascensore mediante una freccia orientata dalla scatola dell'ascensore alla scatola che rappresenta il modulo di controllo.

L'esempio mostra anche che conviene modularizzare un sistema descrivendolo come


una collezione di oggetti. Ci accade spesso per molti sistemi, nei quali gli oggetti possono essere visti come le naturali unit di modularizzazione. La nozione di oggetto deve essere tuttavia vista in un'accezione pi generale di quella di oggetto fisico.
Tipicamente una porzione di software, quale una tabella di nomi o una coda di richieste
da soddisfare, pu essere vista come un oggetto. Si consideri la differenza tra l'ascensore, visto come insieme di oggetti che cooperano tra di loro, e il compilatore, visto
come una collezione di moduli associati a diversi funzioni o passi di compilazione. Nel
Capitolo 4 discuteremo i problemi della progettazione orientata agli oggetti rispetto a
una progettazione orientata alle funzioni.

Anche in questo caso, la struttura modulare della Figura 3.4a pu essere ulteriormente scomposta in modo del tutto naturale. Ad esempio, la scatola che rappresenta l'ascensore pu essere raffinata come suggerito nella Figura 3.4b.

L'apparato di controllo pu essere descritto come l'insieme di un microprocessore (la parte


hardware) e del software che implementa le politiche di controllo, ad esempio gestendo la
coda di richieste, inviando comandi al motore o ai freni e governando l'illuminazione dei
pulsanti dell'ascensore. Anche in questo caso si vede chiaramente che la nozione di oggetto
trascende chiaramente quella di oggetto fisico.
I pulsanti, a loro volta, sia quelli ai piani che quelli all'interno dell'ascensore, possono
essere definiti con maggior dettaglio, mostrando ciascun pulsante a ogni piano e ciascun pulsante all'interno dell'ascensore.

Freni

Figura 3.4b

Pulsanti
interni

Ulteriore scomposizione del sistema di ascensori.

3.8.2.4

Astrazione

Il principio di astrazione pu essere applicato al progetto del sistema di ascensori in molti


modi. Innanzitutto notando che le parti a e b della Figura 3.4 sono a loro volta astrazioni
dell'intero sistema, le quali si focalizzano sulla struttura modulare, trascurando altri aspetti
quali il comportamento meccanico ed elettrico dell'ascensore e del suo motore.
Una diversa vista astratta potrebbe invece concentrarsi su questi fattori, che sono trascurati nelle due figure, al fine di decidere quale sia la potenza di alimentazione per il motore e
per i freni. Ancora un'altra astrazione potrebbe riguardare l'astrazione dei pulsanti, usando una
variabile booleana per rappresentare il fatto che un pulsante sia illuminato e, pertanto, astraendo dalla potenza di alimentazione della lampadina che si trova nel pulsante fisico.
Un'astrazione ancora diversa potrebbe descrivere la disposizione esterna dei pulsanti rispetto all'aspetto di usabilit; occorrerebbe decidere la loro dimensione, la potenza della loro illuminazione, l'altezza a cui si devono trovare rispetto al fondo dell'ascensore, etc.
3.8.2.5

Anticipazione del cambiamento, generalit e incrementalit

I principi di anticipazione del cambiamento, generalit e incrementalit mettono in luce le


differenze fondamentali tra l'ingegneria del software e l'ingegneria dei sistemi pi tradizionale, differenza dovuta alla malleabilit del software. Ad esempio, mentre del tutto naturale costruire e consegnare un sottoinsieme del compilatore e successivamente aggiungere
funzionalit attraverso nuove librerie, del tutto improbabile che si possa consegnare un ascensore senza porte e consegnare le porte e altri accessori successivamente. Questi principi, tuttavia, hanno una loro rilevanza anche nell'ambito dell'ingegneria di sistemi, anche se la loro applicazione prevalentemente ristretta alla fase di progettazione, la quale a sua volta
ancor pi nettamente separata rispetto alle fasi di costruzione, installazione e manutenzione del prodotto.
A titolo di esempio, si consideri il progetto di un sistema di ascensori che possa applicarsi a diversi edifici simili, ma non identici. In questo caso possiamo decidere di rendere il
progetto parametrico rispetto ad alcune caratteristiche distintive, che cambiano da edificio
a edificio, ma il cui campo di variabilit pu essere formulato in maniera precisa a priori. E
possibile progettare un sistema adatto a grattacieli con un numero di piani da 30 a 80, con
un numero di ascensori da 4 a 10, con velocit e potenza variabili, etc. Successivamente,
quando necessario costruire un nuovo grattacielo, le cui caratteristiche rientrano all'interno di questi ambiti di variabilit, possiamo limitarci a specificare i parametri che abbiamo
lasciato liberi e con ci evitiamo di intraprendere il progetto da zero.
La stessa notazione di progetto pu essere adattata a enfatizzare questo tipo di parametricit del progetto. Ad esempio, la Figura 3.5 illustra il sistema della Figura 3.4a, ma modifica la scatola corrispondente all'ascensore e quella corrispondente ai pulsanti, in modo tale da denotare un numero parametrico di esemplari di oggetti dello stesso tipo. In questo
caso, la generalit pu andare oltre alla pura fase di progettazione. Infatti possiamo costruire diversi componenti, quali le cabine, i motori, gli apparati di controllo che sono utilizzabili in diversi edifci della stessa categoria. Successivamente possiamo costruire l'ascensore
che deve funzionare in un grattacielo specifico, semplicemente assemblando i componenti
in accordo con il progetto, che a sua volta consiste nel definire alcuni parametri.

Apparalo
di controllo

Ascensori

\
Pulsanti
di piano

Figura 3.5

3.9

Struttura parametrica di un sistema di ascensori

Osservazioni conclusive

In questo capitolo abbiamo discusso sette importanti principi dell'ingegneria del software,
che sono applicabili lungo tutto il processo di sviluppo e di evoluzione del software. Abbiamo
sottolineato innanzitutto che questi sono principi ingegneristici e che il fatto di analizzare
somiglianze e differenze nel modo in cui si applicano nei diversi campi dell'ingegneria pu
aiutare a comprenderli pi a fondo.
A causa della loro applicabilit generale, i principi sono stati presentati separatamente, come pietre miliari dell'ingegneria del software, piuttosto che nel contesto di fasi specifiche del ciclo di vita del software. Abbiamo anche deciso di presentare questi principi separatamente, al fine di offrire una terminologia uniforme, che verr utilizzata nel resto del
libro.
I principi dell'ingegneria del software, cos come sono stati qui descritti, potrebbero
sembrare troppo astratti. Noi li renderemo ben pi concreti con ulteriori dettagli nel resto
del libro, nel contesto delle attivit di progettazione, specifica, verifica e gestione del software.
Faremo ci sia in maniera esplicita, sottolineando dove i principi si applicano, sia in maniera implicita, lasciando al lettore lo stimolo di riconoscerli quando essi si manifestano.
Abbiamo enfatizzato il ruolo dei principi prima di presentare metodi, tecniche e strumenti specifici. La motivazione di ci che l'ingegneria del software, come ogni altro ambito dell'ingegneria, deve basarsi su un insieme di solidi principi. I principi, a loro volta, sono la base dei metodi, delle tecniche e degli strumenti che vengono usati nella vita di ogni
giorno.
Con l'evoluzione della tecnologia, anche l'ingegneria del software evolve. Con l'aumento
di conoscenze che si accumulano nell'ambito dell'ingegneria del software, anche i metodi e
le tecniche evolvono, anche se meno rapidamente degli strumenti. I principi, invece, rimangono stabili; essi costituiscono i fondamenti sui quali tutto il resto pu essere costruito, essi formano la base dei concetti che verranno discussi nel resto di questo libro.

Ulteriori esercizi
3.13

S u p p o n i a m o di voler scrivere u n p r o g r a m m a che manipola file, il quale offre (tra gli altri) comandi per ordinare i file in ordine ascendente e discendente. Alcuni dei file gestiti dal sistema sono memorizzati automaticamente in maniera ordinata, pertanto si potrebbe sfruttare il
fatto che, se u n file gi ordinato, n o n occorre effettuare alcuna azione mentre, qualora il file sia ordinato in ordine opposto, si possa semplicemente realizzare l'ordinamento invertendo
il contenuto dei file. Esponete pr e contro dell'impiego di queste soluzioni specializzate, invece di eseguire l'algoritmo di ordinamento in risposta al c o m a n d o di sort.

3.14

Indicate le relazioni tra principi di generalit e anticipazione del cambiamento.

3.15

Discutete brevemente le relazioni tra principi di generalit e astrazione.

3.16

Affrontate, in m o d o sintetico, le relazioni tra principi di incrementalit e tempestivit.

3.17

Definite le relazioni tra principi di formalit e anticipazione del cambiamento.

3.18

Completate il caso di studio del Paragrafo 3.8 lungo le seguenti direzioni: si tenga conto di
ulteriori requisiti; si vada pi a f o n d o nell'identificazione della struttura modulare del sistema; si studino meglio le interfacce del sistema con l'ambiente; si studino altri casi in cui si
possa applicare il principio di incrementalit.

3.19

Considerate un incrocio, in cui pi binari intersecano una strada. L'incrocio protetto da una
sbarra che deve automaticamente impedire alle automobili di attraversare q u a n d o sta passando u n treno.

3.20

Descrivete la struttura del sistema di controllo dell'incrocio, m o s t r a n d o i componenti e le relative interfacce.

3.21

Definite chiaramente e in maniera precisa i requisiti che devono essere soddisfatti dal sistema
al fine di operare in m o d o sicuro ma consentendo il passaggio dei veicoli. Ad esempio, la decisione di tenere sempre chiusa la sbarra avrebbe il vantaggio della sicurezza, ma sarebbe inutile, in q u a n t o nessuna macchina potrebbe mai attraversare l'incrocio.

3.22

Quali caratteristiche del sistema potrebbero cambiare in diversi contesti?

Suggerimenti e tracce di soluzione


3.5

La divisione di un p r o g r a m m a molto lungo in f r a m m e n t i contigui n o n d luogo normalmente a una buona struttura con alta coesione. I f r a m m e n t i di istruzioni che realizzano le stesse
f u n z i o n i da u n p u n t o di vista concettuale d a n n o invece luogo a una pi alta coesione.
Quest'approccio corrisponde alla tradizionale scomposizione di un programma in sottoprogrammi. E ancor meglio riuscire a raggruppare insieme i dati e i sottoprogrammi che acced o n o ai dati in quanto, cos facendo, si migliora la leggibilit e la modificabilit del sistema.

3.6

I moduli che n o n interagiscono tra di loro h a n n o il m i n i m o livello di accoppiamento, ma ci


significa anche che questi moduli n o n cooperano tra di loro in alcun m o d o . Il fatto che un
m o d u l o possa accedere alle variabili locali di un altro modulo, vale a dire che esso possa modificare lo stato dell'altro modulo, produce u n accoppiamento pi alto di q u a n t o n o n accada
nel caso di un m o d u l o che chiama un sottoprogramma definito in un altro.

3.7

L'utente finale interessato soltanto alla descrizione astratta di come poter operare con l'applicazione; i dettagli che riguardano il m o d o in cui l'applicazione stata implementata e progettata dovrebbero essere invece un dettaglio ignorato dall'astrazione. Il progettista dovrebbe

sapere quali sono i requisiti e, q u a n d o progetta una parte del sistema, dovrebbe poter disporre di una visione astratta del resto del sistema che gli consenta di avere un quadro chiaro di
come la parte che sta realizzando interagisce con il resto del sistema, senza dover prendere in
considerazione alcun dettaglio di esso. In fase di manutenzione di un sistema occorre poter
riferirsi a una visione astratta del razionale del progetto e cio delle motivazioni che h a n n o
portato a certe decisioni e del motivo per il quale altre sono state scartate. In questo m o d o il
sistema pi facilmente modificabile: le modifiche possono essere apportate senza danneggiare la struttura complessiva del sistema.
3.10

C o m e vedremo nel Capitolo 7, il ciclo di vita a cascata una visione ideale del processo di
sviluppo di software. Ad esempio, il modello della Figura 1.1 ignora il fatto che alcuni passi
debbano essere ripetuti q u a n d o una fase rivela inconsistenze o errori nella fase precedente.

3.12

II prototipo software qui illustrato un prototipo evolutivo, mentre in altri casi vediamo
esempi di prototipi "usa e getta". Questi termini verranno discussi nel Capitolo 7.

3.16

Consegnando un'applicazione in m o d o incrementale, possibile consegnare presto u n sottoinsieme utile dell'applicazione. C o n ci possibile presentare tempestivamente al mercato
un prodotto anche se incompleto.

Note bibliografiche
Numerosi libri presentano approcci formali alla programmazione; tra questi citiamo Alagic e Arbib
[1978], Gries [1981] e Mills et al. [1987a], Contributi fondamentali a questo settore sono forniti dai
libri di W i r t h [1971], Dahl et al. [1972] e Dijkstra [1976], Liskov e G u t t a g [1986] fornisce fondamenti rigorosi alla programmazione di sistemi di grosse dimensioni; l'approccio che essi propongono fondato sul concetto di astrazione dei dati. Liskov e G u t t a g [2001] applica questi principi a Java.
I lavori di Parnas sui metodi di progettazione e di specifica stanno alla base dei concetti di separazione degli interessi, astrazione, modularit e anticipazione del cambiamento. Tutti i lavori di Parnas
che vengono citati in bibliografa illuminano aspetti fondamentali di queste problematiche. In particolare, Parnas [1978] illustra i pi importanti principi dell'ingegneria del software. H o f f m a n e Weiss
[2001] h a n n o raccolto trenta tra i pi importanti articoli scritti da Parnas insieme ad alcuni aggiornamenti e a commenti critici.
La programmazione orientata agli aspetti {aspect-orientedprogramming)
u n tipo di programmazione che si pone l'obiettivo di supportare il principio di separazione degli interessi nella progettazione del software. Questa tecnica aiuta il programmatore, n o n solo a concentrarsi su u n aspetto
indipendentemente dall'altro, ma anche a esprimere aspetti che attraversano diversi insiemi di moduli. Ad esempio, il programmatore potrebbe desiderare di esprimere gli aspetti che riguardano la gestione della memoria, la sincronizzazione e la funzionalit di u n buffer di dimensioni limitate in diverse parti del p r o g r a m m a . Q u e s t a idea stata proposta inizialmente da Kiczales et al. [1997].
Numerosi articoli h a n n o in seguito approfondito questa idea e diversi sistemi prototipali l'hanno implementata nel contesto di Java e di altri linguaggi.
I concetti di coesione e accoppiamento sono trattati da Yourdon e Constantine [1979] e Myers
[1978], i quali cercano di fornire misure obiettive per valutare la bont di un progetto.
I principi di anticipazione del cambiamento, generalit e incrementalit sono a m p i a m e n t e giustificaci dai lavori di Belady e Lehman [1979], Lehman e Belady [1985] e Lientz e Swanson [1980],
i quali h a n n o approfondito il tema dell'evoluzione del software. Bennett e Rajlich [2000] afferma che
considerare la manutenzione, che ha dimostrato assorbire un'enorme porzione dei costi del software,

semplicemente come l'ultima fase del processo di sviluppo un errore. Gli autori propongono un ciclo di vita del software intrinsecamente basato sul concetto di evoluzione.
La gestione delle configurazioni viene discussa da Babich [1986], Tichy [1989], Estublier [2000]
e, successivamente in questo libro, nei Capitoli 7 e 9. Il linguaggio AWK stato presentato da A h o
et al. [1988],
Boehm et al. [1984] illustra il principio di prototipazione rapida; il n u m e r o speciale della rivista IEEE Computer curato da Tanik e Yeh ( C o m p u t e r [1989]) contiene svariati articoli su questo
argomento.
Sia il sistema di ascensori descritto nel Paragrafo 3.8 che il sistema di controllo dell'incrocio,
proposto nell'Esercizio 3.19, sono stati a m p i a m e n t e sviluppati in letteratura come casi di riferimento per dimostrare la validit di certe tecniche dell'ingegneria del software nella soluzione di problemi
realistici. Il sistema di ascensori stato inizialmente proposto in I W S S D [ 1 9 8 7 ] ; il sistema di controllo dell'incrocio viene presentato e studiato a f o n d o in Heitmeyer e Mandrioli [1996].

CAPITOLO

Progettazione e architetture software

La progettazione un'attivit umana fondamentale che, in senso generale, provvede a fornire una struttura a un qualsiasi artefatto complesso. Scompone un sistema in parti, assegna
responsabilit a ciascuna e assicura che queste, nel loro insieme, raggiungano gli obiettivi
globali. Ci risulta vero in ogni campo, non solo in quello del software. Cos, ad esempio,
gli architetti possono progettare centri commerciali (struttura degli immobili, distribuzione
degli spazi, parcheggi, impianti di condizionamento e di riscaldamento, rifornimento di
elettricit, etc.) e gli autori concepire romanzi (personaggi, trama nel suo complesso e sua
suddivisione in capitoli, etc.). Alcuni principi di progettazione come scomporre un sistema in parti, individuare quali propriet debbano avere, etc. - sono di natura generale; altri
sono specifici del dominio applicativo.
Nel caso del software, i concetti di progettazione sono applicabili in due contesti diversi ma strettamente correlati. Da una parte, la progettazione l'attivit che fa da ponte tra
i requisiti e l'implementazione del software. Una volta determinata la necessit di avere un
sistema software e decise le sue qualit desiderabili, inclusa l'interfaccia per l'interazione con
il mondo esterno, si deve procedere alla sua progettazione. Il primo risultato dell'attivit di
progettazione sar il progetto dell'architettura che mette in luce le parti principali del sistema e come queste si combinano e cooperano. L'architettura mostra a grandi linee la struttura del sistema. Dall'altra parte, ogni artefatto complesso va progettato. In questo contesto, la progettazione l'attivit che fornisce una struttura all'artefatto. Quindi, per esempio,
il documento di specifica dei requisiti deve essere "progettato"; ovvero, deve possedere una
struttura che lo renda facile da capire e da sviluppare ulteriormente. Questo capitolo affronter
il tema della progettazione sviluppando entrambe le accezioni, nei due contesti.
Esiste una mutua dipendenza tra questo capitolo e quello successivo, che parler di specifica. Da una parte, secondo il primo contesto di progettazione cui abbiamo accennato, all'interno di un tipico ciclo di vita del software, la specifica dei requisiti avviene prima della
progettazione dell'architettura. Ci spingerebbe ad affrontare la specifica prima della progettazione. Dall'altra parte, in base al secondo contesto di progettazione, i principi di strutturazione di artefatti complessi si possono applicare ugualmente bene alla strutturazione della specifica dei requisiti. Inoltre, il risultato della progettazione dell'architettura deve a sua volta essere specificato. Per questo, abbiamo deciso di affrontare la progettazione prima della specifica.
Questo capitolo inizia affrontando l'attivit di progettazione e i suoi obiettivi fondamentali. Successivamente, mostrer come possiamo ottenere le qualit illustrate nel Capito-

lo 2; in particolare, enfatizzer la necessit di metodi di progettazione in grado di produrre sistemi affidabili e facilmente modificabili. Il principio di rigore e formalit ci porter ad adottare notazioni appropriate per la descrizione dei progetti. Verranno applicati
i principi di separazione degli interessi, modularit e astrazione, per semplificare l'attivit
di progettazione, produrre progetti caratterizzati da un alto grado di comprensibilit e accrescere la nostra fiducia nella correttezza delle nostre soluzioni. Infine, l'anticipazione del
cambiamento e l'incrementalit ci permetteranno di progettare sistemi che siano in grado di evolvere facilmente in seguito al cambiamento dei requisiti, o che possono essere arricchiti progressivamente nelle loro funzioni, partendo da una versione iniziale, con funzionalit limitate. Progettare in vista del cambiamento il motto che adotteremo da Parnas
per sottolineare i principi di anticipazione del cambiamento e di incrementalit nel contesto della progettazione.
Affronteremo anche il problema della progettazione di famiglie di applicazioni. Molte
volte, le applicazioni non sono prodotti individuali, ma fanno parte di una famiglia di prodotti che possono differire tra loro per le funzionalit offerte, per le configurazioni hardware
su cui vengano eseguite, per l'insieme dei servizi predisposti, etc. Nonostante le differenze,
hanno per molti elementi in comune che possono essere analizzati e progettati in un'unica sede, per l'intera famiglia. I principi di generalit e anticipazione del cambiamento forniscono un supporto alla progettazione di famiglie di prodotti. Infatti, vari membri della famiglia possono essere progettati sulla base di una singola architettura. Un'architettura di famiglia attentamente progettata supporta lo sviluppo di progetti diversi per i singoli membri
della famiglia.
Per ottenere un alto livello qualitativo della progettazione, l'ingegnere informatico deve affrontare due questioni cruciali, strettamente correlate. Innanzitutto, deve fornire un'attenta definizione della struttura modulare del sistema, specificando i moduli e le relazioni
che intercorrono tra loro. Dovr, inoltre, scegliere un criterio appropriato per la scomposizione del sistema in moduli.
Il criterio principale che introdurremo nel Paragrafo 4.2.2 e che approfondiremo nel seguito quello dell'information hiding-. un modulo caratterizzato dalle informazioni che nasconde agli altri moduli, detti suoi client (clienti). Le informazioni nascoste rimangono un segreto per i moduli client. Nel paragrafo successivo, affronteremo quindi una notazione di progettazione (TDN/GDN) che pu essere utilizzata per documentare i risultati dell'attivit di
progettazione. Ne mostreremo una specializzazione, che si rende necessaria per poter trattare
moduli che incapsulano dati, e che portano al fondamentale concetto di tipo di dato astratto.
Introdurremo, infine, una tecnica chiamata stepwise refinement (progettazione per raffinamenti successivi). Questa tecnica produce progetti software con un approccio top-down mentre l'information hiding predilige principalmente un approccio di tipo bottom-up. Ci ci condurr a confrontare, da diversi punti di vista, queste due strategie di progettazione.
Per realizzare l'obiettivo di affidabilit, approfondiremo il problema della progettazione di software che sappia reagire a malfunzionamenti o a situazioni anomale comportandosi in una maniera ritenuta accettabile. Un'attenta attivit di progettazione deve occuparsi del
requisito di robustezza, estremamente importante nelle applicazioni dove la sicurezza rappresenta un elemento critico.
I principi di una buona progettazione non possono essere insegnati come un insieme
prefissato di regole da applicare secondo una ricetta rigida. Se vengono formulati in termi-

ni astratti, non forniscono ai progettisti discernimenti profondi o suggerimenti convincenti. La loro efficacia viene meglio illustrata mediante esempi. Sfortunatamente, per ragioni
di spazio, non possibile illustrare la progettazione completa di applicazioni reali in un libro di testo. Illustreremo, dunque, i vari concetti di progettazione prevalentemente attraverso piccoli esempi.
Nel Paragrafo 4.5 accenneremo a come gli aspetti di programmazione concorrente e distribuita e quelli di real-time possano influire sulla progettazione. Non approfondiremo l'argomento in questa fase, per diverse ragioni. Innanzitutto, si tratta di argomenti specializzati
che meritano un trattamento separato: tradizionalmente vengono affrontati all'interno di corsi e testi riguardanti i sistemi operativi, i sistemi distribuiti, il software real-time, etc. In secondo luogo, le problematiche di progettazione dipendono fortemente dalle funzionalit offerte dal sistema operativo o dal linguaggio di programmazione utilizzati per l'implementazione. Di conseguenza, nella nostra discussione, ci atterremo a concetti generali e faremo riferimento a schemi selezionati di concorrenza senza puntare alla completezza.
Nell'illustrare i principi di progettazione, faremo riferimento inizialmente ai sistemi tradizionali. Mostreremo poi come il concetto di information hiding e quello di tipi di dati
astratti abbiano trovato un'applicazione coerente nella progettazione orientata agli oggetti.
Discuteremo i concetti specifici introdotti da questa tecnica e mostreremo il modo in cui
supporta l'evoluzione e il riuso di software. Inoltre, introdurremo la notazione di progettazione standard UML (UnifiedModeling Language).
Nel caso di sviluppo di software, i progetti vengono prima o poi mappati sui programmi;
ovvero, le strutture e i componenti definiti durante l'attivit di progettazione sono rappresentati in termini di costrutti del linguaggio di programmazione che utilizziamo per l'implementazione. Questa mappatura dei progetti su programmi pu essere realizzata pi facilmente con alcuni linguaggi piuttosto che con altri; in particolare, esistono linguaggi per
i quali le tecniche di progettazione che illustreremo possono portare ai programmi in maniera quasi diretta. Per esempio, l'information hiding e le strutture di progettazione illustrate
in questo capitolo possono essere facilmente mappati sui linguaggi di programmazione modulari convenzionali come Ada. Linguaggi orientati agli oggetti, quali C++ o Java, sono i
naturali candidati per l'implementazione di progetti orientati agli oggetti.
Sempre di pi, il software non viene implementato partendo "da zero", ma piuttosto
integrando componenti che possono essere acquisiti sul mercato. L'obiettivo a lungo auspicato di riutilizzo attraverso la "componentizzazione", sta diventando realt, sia perch i nuovi linguaggi permettono la progettazione di componenti riutilizzabili, sia perch sta diventando disponibile un supporto generalizzato per lo sviluppo di differenti componenti integrabili in un'architettura coerente. Ne sono esempi la libreria STL di C++, Java e JavaBeans,
COM e CORBA. Inoltre, il problema della specifica di architetture software a un livello pi
alto rispetto a T D N / G D N o addirittura a UML sta assumendo sempre maggiore rilevanza,
e rappresenta un argomento di ricerca attiva. Altri problemi centrali sono l'identificazione
di pattern (modelli ricorrenti) di progettazione, che possono essere collezionati e classificati
per un uso successivo, e la definizione di architetture adattabili, che possano definire intelaiature generalizzate per applicativi. Questi argomenti, riguardanti lo sviluppo basato su componenti, verranno affrontati nel Paragrafo 4.7.
La progettazione un'attivit difficile e critica. E anche altamente creativa: in ogni
nuovo progetto, l'ingegnere inventa qualcosa. Numerose sono le decisioni e i compromes-

si che vanno affrontati strada facendo. Questo capitolo cerca di identificare i metodi che
possiamo utilizzare per superare queste difficolt e per guidare e disciplinare il processo creativo. I sistemi possono risultare molto complessi, i requisiti possono trovarsi in contraddizione e i metodi generali da applicare lontani dall'essere norme precise. Sfortunatamente
(o fortunatamente?) nella progettazione di software non esistono ricette generali, di facile
utilizzo, che possono essere adottate una volta per sempre e seguite fedelmente in ogni circostanza. Norme specifiche sono applicabili solo all'interno di domini ristretti. Il progettista deve per disporre di principi e metodi generali, che potranno essere applicati in maniera pratica a seconda del caso specifico, e di requisiti quali la qualit desiderata del prodotto, la composizione del team di sviluppo e i vincoli temporali. importante che il progettista applichi coerentemente i principi e i metodi che presenteremo, affinch diventino
una prassi abituale.
Per facilitare il loro impiego, a volte i metodi e i principi vengono "impacchetatti" in
modo da formare metodologie standardizzate. Esiste una forte richiesta di queste metodologie da parte dell'industria, in quanto tendono a standardizzare lo sviluppo di software, cos da uniformare l'applicazione dei metodi all'interno della stessa azienda. La standardizzazione, a sua volta, rende pi facile affrontare questioni manageriali quali il turnover del personale all'interno di gruppi di sviluppo di software. Nella pratica, alcune di queste metodologie sono state adottate in maniera estesa, anche se spesso non sono giustificate da principi dimostrati, generali e rigorosi. Nel Capitolo 7, che si occuper dell'organizzazione del ciclo di vita del software, forniremo un breve resoconto di alcune tra le pi importanti fra
queste metodologie. In questa parte, ci concentreremo invece sui principi generali di progettazione, indipendenti dall'applicazione che si vuole creare.

4.1

Attivit di progettazione del software


e suoi obiettivi

L'attivit di progettazione rappresenta una fase fondamentale del processo di sviluppo del
software, che trasforma progressivamente i requisiti di sistema, attraverso una serie di stadi
intermedi, in un prodotto finito. Il risultato dell'attivit di progettazione un progetto
software. Definiamo progetto software la scomposizione in moduli di un sistema e la descrizione di quali siano le funzioni di ciascun singolo modulo e delle relazioni che intercorrono tra loro. Spesso, prima del progetto software, viene prodotta un'architettura software
che guida lo sviluppo del progetto. L'architettura mette in luce, a grandi linee, la struttura
e l'organizzazione del sistema da definire. La descrizione di un'architettura software definisce i principali componenti di un sistema e come questi si relazionino gli uni con gli altri,
le effettive ragioni per cui si procede alla suddivisione in componenti e i vincoli che devono essere rispettati nella loro progettazione. L'obiettivo dell'attivit di progettazione architetturale definire l'architettura del sistema; l'obiettivo dell'attivit di progettazione del
software la definizione del progetto software in accordo con le linee guida stabilite nell'architettura del sistema. Dato che i principi utilizzati nello sviluppo dell'architettura e nello sviluppo del progetto sono simili, in questo capitolo faremo indistintamente riferimento
al progetto e all'architettura.

Possiamo intendere la progettazione come un processo nel quale le diverse viste del sistema sono descritte attraverso successivi passaggi, definiti con sempre maggiore dettaglio.
Partendo dai requisiti del sistema, viene sviluppata una prima architettura generale dell'applicazione, successivamente raffinata in un progetto ancora di alto livello, che viene quindi
dettagliato in un progetto di livello pi basso, e cos via. Ogni nuovo passaggio realizza i requisiti specificati in quello precedente. L'ultimo passaggio rappresenta l'implementazione,
che completa la trasformazione dell'architettura software in programmi.
Il principio di modularit di somma importanza nella progettazione del software, ed
il motivo per cui i componenti di un sistema, identificati durante l'attivit di progettazione, vengono chiamati, semplicemente, moduli. In letteratura, comunque, il concetto di modulo piuttosto ambiguo. A volte il termine viene utilizzato per indicare un elemento grafico, simile a una scatola, inteso a rappresentare un progetto. Altre volte, denota una parte
di un programma chiaramente identificata, come una collezione di routine. In altri casi ancora, specifica gli incarichi di lavoro individuali, all'interno di un sistema complesso.
Chiariremo la nostra idea di modulo pi avanti; per ora, ci affideremo alla nozione intuitiva
che racchiude tutte le possibilit sopra citate.
La scomposizione di un sistema in moduli pu essere realizzata in svariati modi e attraverso differenti passaggi. Per esempio, si potrebbe prima suddividere il sistema in componenti di alto livello, definendone le relazioni e specificandone i singoli comportamenti.
Ciascun componente verr quindi analizzato separatamente, iterando la procedura fino a raggiungere un livello di complessit sufficientemente ridotto da consentire a una sola persona
di poter agevolmente implementare il singolo componente.
Quando un modulo M viene scomposto in altri moduli, diremo che questi implementano M. In base a questo approccio, l'implementazione viene realizzata secondo la
scomposizione ricorsiva dei moduli in (sotto)moduli, fino a quando non si raggiunge il
punto in cui l'implementazione pu essere realizzata in maniera semplice, in un linguaggio di programmazione.
Oltre alla modularit, il lettore pu qui riconoscere diversi principi e concetti presentati nel Capitolo 3. Rigore e formalit sono utili nella descrizione delle architetture software:
pi precisa la descrizione, pi risulta agevole suddividere lo sviluppo del software in compiti separati che possano essere svolti in parallelo, con un basso rischio di inconsistenze. Inoltre,
la precisione semplificherebbe la comprensione del sistema, qualora dovesse sorgere la necessit di modificarlo. Infine, l'efficacia del processo di progettazione dipende da quanto le
tecniche di modularizzazione selezionate ci aiutano ad affrontare ogni modulo separatamente, in accordo con il principio di "separazione degli interessi". Usando due concetti gi
introdotti nel Capitolo 3, possiamo affermare che i moduli dovrebbero avere un alto grado
di coesione e un basso grado di accoppiamento.
In accordo con la definizione fornita nel Capitolo 3, il processo di scomposizione modulare appena descritto pu essere detto di tipo top-down. Ma anche possibile procedere
in una maniera bottom-up. Per esempio, un modulo pu essere progettato per fornire un
modo semplice e astratto per accedere a una periferica, nascondendo le primitive di basso
livello fornite dal dispositivo. Il modulo agisce come uno strato che applica un'astrazione al
dispositivo, facendolo apparire con un aspetto migliore e pi semplice da trattare. In questo caso, il processo intrinsecamente bottom-up: iniziamo da un oggetto esistente, intricato, e vi costruiamo attorno un'astrazione.

Secondo una strategia bottom-up, il processo di progettazione consiste nel definire moduli che possono essere combinati iterativamente fino a formare componenti di un livello pi
alto. E il tipico modo di procedere impiegato quando si riutilizzano i moduli presi da una libreria per costruire un nuovo sistema, invece di costruire il sistema da zero. L'intero sistema
viene costruito assemblando in modo iterativo componenti di livello pi basso.
L'argomento del confronto tra la progettazione bottom-up e quella top-down verr ripreso pi tardi, mostrando come sia possibile, e spesso conveniente, combinare i due approcci, per diverse parti del sistema o in differenti momenti dell'attivit di progettazione.
Prima di discutere uno dei criteri che possono essere seguiti per la modularizzazione
del sistema, esamineremo due importanti obiettivi che guidano la progettazione di un'architettura software: la progettazione in vista del cambiamento e le famiglie di prodotti. Il
primo concetto definisce un modo per progettare software facilmente modificabile, qualora dovessero sorgere cambiamenti nei requisiti. Analogamente, il secondo concetto consente di interpretare una serie di prodotti come membri di una famiglia, che condividono, in
diversi contesti, una singola architettura riutilizzata, specializzata e modificata per dare origine a differenti prodotti. Entrambi i concetti ricadono all'interno delle problematiche generali di riusabilit e supporto all'evoluzione del software.

4.1.1

Progettazione in vista del cambiamento

Nel Capitolo 3, abbiamo presentato l'anticipazione del cambiamento come un principio generale per supportare la natura evolutiva del software. Applicare questo principio nel contesto dell'attivit di progettazione significa anticipare i cambiamenti cui potr essere sottoposto durante il suo ciclo di vita e, di conseguenza, produrre un progetto software facilmente
adattabile a quei cambiamenti. Seguendo Parnas, ci riferiremo alle tecniche utilizzate per raggiungere questo obiettivo come progettazione in vista del cambiamento (design for chang).
La progettazione in vista del cambiamento promuove una progettazione sufficientemente
flessibile da facilitare i cambiamenti. Tale obiettivo, per, non pu essere raggiunto, in generale, per ogni tipo di cambiamento. indispensabile una cura speciale nelle fasi iniziali,
quando vengono forniti i requisiti software, per anticipare i probabili cambiamenti. In questa fase iniziale, non dovremmo concentrarci esclusivamente su cosa necessario fornire, in
quel dato momento, in termini di funzionalit o, pi in generale, di qualit da raggiungere.
Dovremmo anche considerare l'evoluzione prevista o possibile del sistema. Molte volte infatti, l'applicazione che stiamo progettando il primo di una sequenza conosciuta e pianificata di passi che porteranno al prodotto finale. In questi casi, dobbiamo assicurarci che il
progetto iniziale possa facilmente adattarsi all'anticipata evoluzione del prodotto.
Pi spesso, comunque, i cambiamenti richiesti non sono conosciuti a priori, ma appaiono, quasi inevitabilmente, a posteriori. In questi casi, le esperienze precedenti dell'ingegnere
del software e la profonda conoscenza del dominio del problema da parte dell'utente finale possono giocare un ruolo fondamentale nell'identificare potenziali aree di cambiamento e la futura evoluzione del sistema. Dopo che sono stati individuati i requisiti per il cambiamento, bisognerebbe assicurarsi che il progetto possa, in futuro, facilitare quei cambiamenti.
I progettisti del software devono capire l'importanza della progettazione in vista del cambiamento. Un errore comune consiste nel progettare i sistemi per soddisfare i requisiti di oggi, prestando poca (o nessuna) attenzione ai probabili cambiamenti. La conseguenza di un
tale approccio che anche un progetto meraviglioso pu risultare estremamente diffcile e

costoso da adattare, e dovr essere quasi interamente rifatto in modo da incorporare cambiamenti anche apparentemente "minori". Un'altra conseguenza negativa che il progettista, nel tentativo di facilitare cambiamenti, potrebbe dover compromettere progressivamente la struttura logica iniziale, rendendo l'applicazione sempre pi difficile da mantenere e
peggiorandone l'affidabilit.
4.1.1.1

Quali cambiamenti? La natura dell'evolvibilit

Quali tipologie di cambiamento deve tentare di anticipare un progetto? Per capire a fondo
la questione dobbiamo riprendere i problemi discussi nel Capitolo 2 e ricordare che secondo i dati che appaiono in letteratura, il 60 per cento dei costi del software deve essere attribuito ai costi di manutenzione. Uno dei motivi per cui i costi sono cos elevati che gli ingegneri del software tendono a sorvolare sulla questione della manutenibilit, o addirittura
a non considerarla, durante lo sviluppo del software.
Si ricordi, dal Capitolo 2, che la manutenibilit pu essere classificata in tre categorie:
perfettiva, adattiva e correttiva. La manutenzione adattiva e perfettiva sono le reali cause di
cambiamento nel software; hanno motivato l'introduzione dell'evolvibilit come una qualit fondamentale del software e l'anticipazione del cambiamento come un principio generale che dovrebbe sempre guidare l'ingegnere del software.
In questo paragrafo, affronteremo i cambiamenti che potrebbero ricadere nelle categorie di manutenzione perfettiva o adattiva. Questi cambiamenti non esauriscono la casistica, ma sono tra i pi comuni. Altri casi di cambiamenti importanti, che sono maggiormente sotto il controllo dell'ingegnere del software, capitano nel caso di una strategia di sviluppo basata sulla prototipazione iterativa. In una strategia di questo tipo, a un certo punto, alcune parti vengono progettate e implementate in una forma preliminare, per poi essere trasformate, a uno stadio successivo, in una versione definitiva.
Cambiamenti riguardanti gli algoritmi. Questo tipo di cambiamento , tra quelli che possiamo applicare al software, forse quello dalla motivazione pi chiara: pu consentire di migliorare l'efficienza di una sua parte, di poter affrontare casistiche pi generali, etc.
Consideriamo, ad esempio, gli algoritmi di ordinamento. Per scegliere tra i molteplici
algoritmi esistenti, dovremmo conoscere la dimensione della lista da ordinare, la probabile
distribuzione dei dati all'interno della lista, e cos via. Di conseguenza, la scelta dell'algoritmo che meglio si adatta all'applicazione potrebbe dipendere dai dati sperimentali disponibili dopo che il sistema diventato operativo. Potremmo cominciare con un algoritmo semplice come scelta iniziale e poi sostituirlo con una soluzione migliore, una volta disponibili
pi dati sperimentali. Se l'algoritmo confinato in un modulo ben identificato (ad esempio, una routine del linguaggio di programmazione), la soluzione potr essere agevolmente
applicata in quanto la porzione di programma che richiede il cambiamento facilmente identificabile, poich delimitata dai suoi punti univoci di ingresso e di uscita.
Esercizio
4.1

Fornite un esempio di due algoritmi di o r d i n a m e n t o i cui profili di esecuzione dipendano fortemente dalla distribuzione dei dati all'interno dell'array da ordinare. Esponete come la distribuzione dei dati condizioni i profili di esecuzione.

Cambiamenti riguardanti la rappresentazione dei dati. L'efficienza di un programma pu


cambiare fortemente se si cambia la struttura utilizzata per la rappresentazione dei suoi dati. Ad esempio, cambiare la struttura utilizzata per rappresentare una tabella da un array sequenziale a una lista concatenata o a una hash table pu cambiare l'efficienza delle operazioni che accedono a quella tabella. Tipicamente, inserire un elemento in un array risulta
costoso se gli elementi sono da tenere ordinati, ad esempio, per valori crescenti. Infatti, l'inserimento di un elemento nella posizione i deve essere preceduto da un'operazione che sposti tutti gli elementi nelle posizioni comprese tra la i-esima e la n-esima, essendo n il numero degli elementi in quel momento contenuti nell'array, in modo da creare spazio al nuovo elemento. L'operazione, detta di traslazione, il cui tempo medio di esecuzione risulta proporzionale a n, non sarebbe stata necessaria nel caso in cui avessimo scelto di utilizzare una
lista concatenata come implementazione della tabella.
Un altro esempio potrebbe essere quello di una struttura dati ad albero, implementata mediante puntatori, in cui ciascun nodo dell'albero possiede un puntatore al nodo fratello di destra e un puntatore al suo diretto discendente, qualora esistano, come illustrato
nella Figura 4.1 (a). Si potrebbe successivamente pensare di aggiungere un ulteriore puntatore al fine di rendere pi efficiente un'operazione che deve percorrere i nodi dell'albero, risalendo lungo la struttura dati da un nodo foglia verso la radice dell'albero. Il puntatore che
si andrebbe ad aggiungere - vedi Figura 4.1 (b) - connetterebbe un nodo qualsiasi al suo nodo padre.
Come ulteriore esempio di cambiamento nella rappresentazione dei dati, non dettato da
motivazioni riguardanti l'efficienza, si potrebbe voler aggiungere (o togliere) campi da un record man mano che diventi necessario salvare pi (o meno) informazioni all'interno di un file.
stato scoperto che i cambiamenti nelle strutture dati hanno una profonda influenza
sui costi di manutenzione: ad essi attribuibile circa il 17 per cento del totale dei costi di
manutenzione 1 .
Il cambiamento degli algoritmi e il cambiamento delle strutture dati, che abbiamo trattato separatamente, spesso sono strettamente correlati. Per esempio, potremmo applicare un
cambiamento in una struttura dati in modo da fornire un algoritmo migliore o viceversa, un
cambiamento in un algoritmo potrebbe richiedere un cambiamento nella struttura dati.
Esercizio
4.2

Discutete alcune possibili motivazioni per il cambiamento della struttura dati ad albero presentata nella Figura 4.1. Verificate se, e perch, il c a m b i a m e n t o della struttura dati potrebbe
richiedere un c a m b i a m e n t o di algoritmi.

Cambiamenti riguardanti la macchina astratta sottostante. I programmi che scriviamo


vengono eseguiti da una qualche macchina astratta (o virtuale). La macchina coincide con
l'hardware nel caso (fortunatamente improbabile) in cui non esista alcun linguaggio di pi
alto livello disponibile per la programmazione. Pi frequentemente, la macchina astratta che
utilizziamo corrisponde al linguaggio di alto livello in cui scriviamo i programmi, il sistema

Vedi Lientz e Swanson [1980].

(a)

(b)

Figura 4.1

Due esempi di strutture dati rappresentanti un albero.

operativo cui inoltriamo chiamate di sistema, il DBMS (database management system) che
usiamo per immagazzinare e estrarre dati persistenti, etc. Una tale macchina astratta cela i
dettagli della macchina fisica sottostante.
Molte volte, per, dobbiamo modificare le applicazioni in modo da poterle eseguire
sotto una nuova versione del sistema operativo, per sfruttare pienamente i nuovi servizi offerti. In maniera simile, potrebbe essere disponibile la nuova versione del compilatore che
stiamo utilizzando, grazie alla quale potrebbero essere svolte ulteriori ottimizzazioni in modo da fornire codice oggetto pi veloce e snello. O potrebbe essere disponibile la nuova versione del DBMS, che risparmia spazio su disco e offre funzioni migliorate in termini di protezione da accessi indesiderati e di recupero da guasti. O ancora, potrebbe diventare disponibile una versione pi nuova, efficiente e affidabile di una determinata libreria usata dal-

l'applicazione. Ci significa che la macchina astratta sottostante cambia, e i cambiamenti


potrebbero avere delle conseguenze sul nostro applicativo. Purtroppo, i benefci richiedono
una contropartita. Per esempio, se il nuovo DBMS in grado di immagazzinare i dati in
met dello spazio originale, dovremmo riformattare i nostri database. Potremmo dover cambiare anche i nostri programmi di accesso ai dati per sfruttare il risparmio di spazio su disco. Anche se le funzionalit offerte dal nostro software rimangono completamente inalterate, il cambiamento nella macchina astratta sottostante si riflette comunque nel software.
Esercizio
4.3

Avete avuto personali esperienze riguardanti modifiche apportate al software in seguito a cambiamenti avvenuti nella macchina astratta sottostante? Presentate sinteticamente la vostra esperienza ed esponete le ragioni per cui avete incontrato difficolt ad aggiornare il programma.

Cambiamenti dovuti a periferiche. Il cambiamento di una periferica ha strettamente a che


fare con un cambiamento nella macchina astratta sottostante. Possiamo vederlo come una
specializzazione di quel tipo di cambiamento nei casi di applicativi per sistemi embedded,
sistemi avionici, sistemi di controllo del processo o in tutti i casi in cui un software di controllo deve interagire con molte periferiche diverse tra loro e specializzate. Tali dispositivi
potrebbero essere soggetti a cambiamento; in particolare, stanno diventando sempre pi
"intelligenti", ovvero le funzioni per l'elaborazione dei dati vengono sempre pi decentralizzate per essere eseguite localmente, senza disturbare l'applicazione principale in esecuzione sulla macchina principale. Idealmente, vorremmo poter affrontare tali cambiamenti senza cambiare o riprogettare l'intero programma.
Cambiamenti dovuti all'ambiente sociale. Un cambiamento nell'ambiente sociale simile
ai due tipi di cambiamento precedenti. Non motivato da un bisogno di modifica che nasce nel software stesso, ma piuttosto l'ambiente in cui l'applicativo inserito che richiede
al software di cambiare.
Per esempio, in un'applicazione per calcolare le tasse, possiamo ipotizzare che un cambio nella legislazione richieda una lieve modifica nelle regole di detrazione delle spese sostenute. Il concetto di un onere deducibile rimane, ma l'elenco dei possibili oggetti cambia. Il
software deve cambiare di conseguenza, in modo da rendere l'applicazione valida anche per
le nuove leggi riguardanti le tasse.
Come ulteriore esempio, possiamo fare riferimento all'introduzione dell'euro. Questo
cambiamento si ripercosso sul software esistente. Pensiamo a una qualsiasi applicazione bancaria o a un qualsiasi sistema finanziario che debba gestire euro piuttosto che lire italiane o
scellini austriaci. Tutto il software che si occupava di transazioni monetarie nelle vecchie unit
di valuta ha dovuto essere modificato a causa di un cambiamento normativo.
Esercizio
4.4

Fornite un esempio, per un'applicazione esistente o ipotetica, di un cambiamento software che


potrebbe risultare necessario alla luce di u n m u t a m e n t o nell'ambiente sociale.

Cambiamenti dovuti al processo di sviluppo. Seguendo la motivazione discussa nel


Capitolo 3, il software viene spesso sviluppato in modo incrementale. L'incrementalit una
sorgente di cambiamento che richiede particolare attenzione. Per esempio, si potrebbe cercare di isolare porzioni dell'applicazione, rilasciarle incrementalmente in modo che il cliente possa cominciare presto a utilizzare il sistema e fornire feed-back basato sulle sue esperienze. Pi tardi, quando nuove parti vengono aggiunte al sistema, diventa importante concentrarsi sui nuovi sviluppi e lasciare inalterati i precedenti sottoinsiemi. Per rendere l'approccio fattibile, man mano che vengono rilasciate, le parti nuove devono potersi facilmente integrare con quelle vecchie e con il sistema funzionante ma ancora incompleto, senza richiedere complessi cambiamenti al software.

4.1.2

Famiglie di prodotti

In molte situazioni pratiche, i cambiamenti consistono nella costruzione di nuove versioni


dello stesso software; ogni versione costituisce un prodotto individuale, ma l'insieme delle
versioni costituisce una famiglia. In genere, si suppone che la nuova versione dovrebbe sostituire quella precedente, in quanto ne elimina gli errori e presenta caratteristiche migliorate. In altri casi, una nuova versione semplicemente un nuovo prodotto che coesiste con
quello precedente; potrebbe funzionare su un hardware differente, avere requisiti speciali di
occupazione di memoria, o fornire funzionalit diverse per alcune parti del sistema. Il motivo per cui consideriamo le diverse versioni di un software una famiglia, piuttosto che un
insieme di prodotti diversi, che i membri di una famiglia hanno molto in comune e sono
solo parzialmente diversi. Molte volte,condividono la stessa architettura. Progettando un'architettura comune per tutti i membri della famiglia, invece di sviluppare progetti separati
per ciascun membro, risparmieremo sui costi che insorgerebbero nel progettare separatamente
le parti comuni.
Tipico esempio di una famiglia di prodotti rappresentato dai telefoni cellulari le cui
funzionalit base rimangono invariate indipendentemente dal Paese in cui sono commercializzati (effettuare e ricevere chiamate, mantenere una lista di numeri di telefono, etc.), mentre variano gli standard per la rete, le lingue per l'interazione con l'utente, i requisiti di sicurezza, e cos via. Il software di base che controlla il telefono sempre lo stesso, cambia
l'interfaccia con l'ambiente in relazione alla localit geografica.
Un altro esempio potrebbe essere quello di un DBMS che debba operare su macchine
differenti, possibilmente su diversi sistemi operativi e con varie configurazioni.
In entrambi i casi, dovremmo innanzitutto identificare le parti comuni alle diverse versioni del software, e posticipare il pi possibile il punto in cui incominciano a differenziarsi. Pi poniamo l'accento sulle parti comuni, meno lavoro viene realizzato per ogni nuova
versione. Ci riduce le possibilit di inconsistenze e lo sforzo di manutenzione speso su tutti i prodotti.
I primi approcci allo sviluppo del software non prestavano particolare attenzione alla
progettazione di famiglie di prodotti, piuttosto procedevano da una versione all'altra in maniera sequenziale. Un errore comune illustrato nei diagrammi informali, ma intuitivi, mostrati nella Figura 4.2. Iniziando dai requisiti, viene sviluppata la Versione 1 dell'applicazione - corrispondente al nodo 3 nella Figura 4.2(a) - attraverso una sequenza di passi di progettazione (rappresentati dalle frecce). I nodi rappresentati da cerchi rappresentano descrizioni intermedie di progetto; i nodi rappresentati da quadrati rappresentano una versione

completa ed eseguibile del software. La Figura 4.2(a) illustra il caso ipotetico in cui i requisiti vengono dapprima trasformati nello stadio intermedio di progettazione 1, poi nello stadio 2 e, infine, nella Versione 1 del prodotto.
A questo punto, se nasce il bisogno di una seconda versione, si pu cominciare a modificare la Versione 1. Inizialmente, l'applicazione verrebbe messa nello stadio intermedio di
progettazione rappresentato dal nodo 4, Figura 4.2(b), cancellando alcune parti del codice
della Versione 1 ; poi verrebbe trasformata in una versione completa e funzionante, rappresentata dal nodo 5, che a sua volta potrebbe diventare il punto di inizio per la derivazione
di ulteriori versioni non illustrate nella figura. Un ramo rappresentante una versione diversa potrebbe anche iniziare dal nodo 3, come illustrato nella Figura 4.2(c).
Questo tipo di approccio per la derivazione di membri di una famiglia di prodotti non
soddisfacente. Infatti, la famiglia illustrata nella Figura 4.2 risulta condizionata dalle scelte progettuali fatte durante lo sviluppo della Versione 1, visto che le versioni 2 e 3 sono modifiche della Versione 1. Non stato fatto alcuno sforzo per isolare le parti comuni a tutte
le versioni e, iterativamente, a sotto-insiemi sempre pi piccoli della famiglia. Di conseguenza,
la derivazione di un nuovo membro della famiglia diventa particolarmente difficile se il nuovo membro differisce in maniera sostanziale dai membri precedenti.
Le nuove versioni del software vengono derivate modificando il codice delle versioni
precedenti in quanto, spesso, i passi di progettazioni intermedie (rappresentati nella figura

Requisiti

Requisiti

(b)
Figura 4.2

Progettazione sequenziale di una famiglia di prodotti.

Requisiti

(c)

con i cerchi) non sono documentati. I programmi stessi sono le uniche descrizioni disponibili, di cui ci si pu fidare, che possono essere utilizzate come punti di inizio per le modifiche. I programmi (anche quelli ben scritti e ben documentati), per, possono risultare difficili da capire in termini sufficientemente precisi da consentire di effettuare modifiche che
risultino affidabili. Non possiamo mai essere certi che una modifica a una parte del sistema
non vada a interferire in maniera imprevista con altre parti. Inoltre, potremmo inavvertitamente ripetere scelte progettuali gi scartate in precedenza ma mai documentate.
Un approccio sistematico alla progettazione di famiglie di prodotti che risolve questi
problemi verr presentato in seguito. Questo approccio basato sul principio generale di
progettazione in vista del cambiamento, dove i cambiamenti sono quelli che caratterizzano
i diversi membri della famiglia. Alla fine degli anni Novanta, furono sviluppate diverse tecniche per affrontare lo sviluppo sistematico di famiglie di prodotti. Queste tecniche sfruttano al meglio le tecniche di analisi, le architetture software e la modularizzazione.

4.2

Tecniche di modularizzazione

In questo paragrafo discuteremo le tecniche che possono essere utilizzate durante la progettazione, per raggiungere gli obiettivi identificati in precedenza. In particolare, distingueremo due aspetti complementari della progettazione: la definizione della struttura generale dell'architettura in termini di relazioni tra i moduli e la progettazione di ogni singolo modulo,
per la quale applichiamo il principio dell'information hiding.
Questi due aspetti vengono spesso chiamati progettazione architetturale (o di alto livello) e progettazione dettagliata. Anche se diverse metodologie di progettazione suggeriscono
che vengano eseguiti come due passi sequenziali, noi non li percepiamo come momenti distinti, dove il secondo segue necessariamente il primo. Piuttosto, percepiamo la progettazione come un processo continuo in cui l'interazione di queste due attivit avviene in maniera flessibile.
Per poter documentare e analizzare i nostri progetti, abbiamo bisogno di una notazione.
Introdurremo una notazione di progetto molto semplice nel Paragrafo 4.2.3. Lo scopo di questa notazione, sia testuale che grafica, di tipo puramente pedagogico. La notazione non intesa per l'uso nello sviluppo di software industriale. Piuttosto, la notazione servir a illustrare
quali siano le caratteristiche necessarie di una notazione. Pi avanti, quando discuteremo la
progettazione orientata agli oggetti, faremo riferimento a una notazione standard (UML).

4.2.1

La struttura modulare e la sua rappresentazione

Un modulo un componente ben definito di un sistema software. Talvolta si tende a sovrapporre i concetti di modulo e routine, ma un modulo un frammento software che corrisponde a qualcosa di pi di una semplice routine. Potrebbe essere una collezione di routine, una
collezione di dati, una collezione di definizioni di tipi, o un insieme di tutti questi. In generale, potremmo vedere un modulo come un fornitore di risorse computazionali o di servizi.
Quando scomponiamo un sistema in moduli, dobbiamo essere in grado di descrivere, in modo preciso, la struttura modulare generale e le relazioni che esistono tra i singoli
moduli.

possibile definire molte relazioni tra moduli. Per esempio, possiamo definire una relazione che indica che un modulo deve essere implementato prima di un altro o che pi
importante di un altro. La prima relazione potrebbe essere utilizzata da un manager per monitorare lo sviluppo del sistema; la seconda come linea guida per assegnare il lavoro ai programmatori in base alle loro capacit ed esperienze.
In questo caso, siamo interessati alle relazioni tra moduli che sappiano definire un'architettura software e che ci aiutino a capire com' organizzato un sistema complesso. Come
vedremo tra breve, abbiamo due relazioni tra moduli, molto utili, che possono essere impiegate per definire la nostra architettura di sistema.
Affronteremo tre problematiche.

Qual la struttura del software in termini dei moduli che la costituiscono?

Come possiamo definire questa struttura in maniera precisa?

Quali sono le propriet desiderabili per tale struttura?

Innanzitutto, da un punto di vista astratto, la struttura modulare di un sistema pu essere


descritta in termini di relazioni matematiche. Sia S un sistema software composto dai moduli M1; M2, . . ., Mn; cio,
S

{M J ,

MJ(

. . . ,

M}

Una relazione r su S un sottoinsieme di S x S. Se due moduli Mi e Mj sono in S, rappresentiamo il fatto che la coppia <M i( Mj> in r usando la notazione infissa Mjr Mj.
Siccome siamo interessati alla descrizione delle mutue relazioni tra i diversi moduli, assumeremo sempre, implicitamente, che le relazioni di interesse in questo testo non siano riflessive. Ci significa che Mi r Mi non pu sussistere per alcun modulo Mi in S.
La chiusura transitiva della relazione r su S nuovamente una relazione su S, e viene
indicata con r + . Presi due elementi qualsiasi di S, Mi e Mj, r + pu essere definito ricorsivamente: Mi r* Mj se e solo se Mi r Mj o se esiste un elemento Mk in S tale che Mi r Mk e
Mk r + Mj. Una relazione gerarchica se e solo se non esistono due elementi Mi, Mj tale che
Mi r + Mj e Mj r + Mi.
La chiusura transitiva di una relazione cattura la nozione intuitiva di relazioni dirette e
indirette. Per esempio, presi due moduli A e B, A CHIAMA* B implica che o A CHIAMA B
direttamente oppure che A chiama B indirettamente attraverso una catena di relazioni
CHIAMA.
Solitamente, i concetti matematici possono essere intesi intuitivamente con maggiore
efficacia se ne riusciamo a dare una rappresentazione grafica. Le relazioni sono buoni esempi di questo principio generale. Una relazione pu essere rappresentata in forma grafica mediante l'uso di grafi orientati, i cui nodi sono etichettati come elementi di S, ed esiste un arco orientato tra il nodo etichettato Mi e il nodo etichettato Mj se e solo se Mi r M,.
Una relazione gerarchica se e solo se non esistono cicli nel grafo della relazione;
questo tipo di grafo detto DAG {Directed Acyclic Graph, grafo orientato aciclico). Un
grafo generico illustrato nella Figura 4.3(a); la Figura 4.3(b) rappresenta una gerarchia
(un DAG).
Nel seguito discuteremo due tipi di relazioni tra moduli che sono molto utili per strutturare progetti software: USES e IS COMPONENT OF.

Figura 4.3

4.2.1.1

Rappresentazione mediante grafi delle relazioni tra moduli, (a) Grafo generico,
(b) Grafo orientato aciclico (DAG).

La relazione USES

Una relazione utile per la descrizione della struttura modulare di un sistema software la
cosiddetta relazione USES. Per due moduli distinti qualsiasi Mi e Mj; diciamo che Mi USES
Mj se Mi richiede la presenza di M,, in quanto Mj possiede una risorsa di cui ha bisogno Mi
per portare a termine il suo compito. Se M USES M3, diremo anche che Mi un client di
Mj, visto che Mi richiede un servizio fornito da Mj. Al contrario, Mj verr detto server. Pi
concretamente, una relazione USES viene stabilita se il modulo M accede a una risorsa fornita dal modulo Mj. Per esempio, Mi USES Mj se M contiene una chiamata a una procedura contenuta nel modulo Mj o se Mi fa uso di un tipo definito in Mj.
Un vincolo che pu convenire imporre alla relazione USES che sia una gerarchia. I sistemi gerarchici sono pi facili da capire rispetto a quelli non gerarchici: una volta che sono
chiare le astrazioni fornite dai moduli server utilizzati, i moduli client possono essere capiti
senza dover guardare alla realizzazione dei server. In altre parole, la separazione degli interessi pu essere applicata semplicemente attraversando la struttura USES, incominciando dai nodi del DAG che non fanno uso di altri nodi, fino ai nodi che non sono utilizzati da alcun altro nodo. Ogni volta che incontriamo un nodo, esso pu essere capito in funzione delle astrazioni fornite dai moduli usati, precedentemente incontrati e capiti.
Citando Parnas, si pu osservare che se la struttura non gerarchica, si incorre facilmente in un sistema "in cui niente funziona fino a quando tutto funziona" 2 . Infatti, la presenza di un ciclo nella relazione USES implica un forte legame tra tutti i moduli del ciclo:
significa che nessun sotto-insieme di moduli nel ciclo pu essere utilizzato o testato isolatamente. Ad esempio, se A USES B e B USES A devo necessariamente disporre sia di A che
di B per poter eseguire A o B.

Parnas [1979].

La restrizione a una gerarchia ha anche un'implicazione metodologica: la struttura risultante definisce il sistema attraverso livelli di astrazione. Questo termine, spesso usato (e
abusato), pu essere illustrato facendo riferimento alla Figura 4.3(b). Al livello pi astratto,
l'intero sistema pu essere visto come una serie di servizi definiti e forniti dal modulo Mj.
Per implementare tale servizi, il modulo Mj usa M i ^ M ^ j e M l i 3 . A sua volta, i servizi astratti forniti, ad esempio, da MI _ 2 sono implementati usando i moduli di livello pi basso M 1 | 2 1
e M1I2,2.

Definiamo il livello di un modulo all'interno di una gerarchia r nel seguente modo.


1. Il livello di un modulo Mi 0 se non esiste alcun modulo

tale che Mi r

2. Per ogni modulo Mi, se k il livello massimo di tutti i moduli Mj tali che Mi r Mj,
allora il livello di Mi k + l .
Un sistema descritto da una relazione U S E S gerarchica pu essere inteso in termini di successivi livelli di astrazione.Un modulo al livello i U S E S solo moduli di livello j tale che
i> j . Per esempio, nella Figura 4.3(b), i livelli di M l i 3 , M2 e M4 sono rispettivamente 3 , 1 e
0; M 1 3

USES

M2 e M 1 3

USES

M4.

Dati due moduli qualsiasi Mi eMj, i cui livelli siano rispettivamente i e j , diciamo che
Mi di un livello pi alto rispetto a Mj se e solo se i> j .
Un altro termine comunemente utilizzato in connessione alla relazione gerarchica
U S E S , specialmente nel caso della struttura di sistemi operativi, macchina astratta (virtuale). Il suo significato che l'insieme dei servizi forniti dai moduli di un dato livello corrisponde a una macchina astratta (o virtuale). Tali servizi sono utilizzati da un insieme di moduli di livello pi alto per implementare i servizi che sono tenuti a fornire. Nella loro implementazione, i servizi forniti al livello i vengono utilizzati come se fossero forniti da una
macchina virtuale. Le macchine virtuali vengono progressivamente dettagliate facendo affidamento su altre macchine virtuali, fino a quando non rimangono altri livelli.
Ad esempio, si consideri il caso di un modulo M che fornisce input/output di record
(struci). Immaginiamo che M faccia uso di un altro modulo M b che fornisce input/output
di un singolo byte alla volta. Quando M r viene utilizzato per realizzare l'output di record, il
suo compito consiste nel trasformare il record in una sequenza di byte e isolare un byte alla volta perch ne venga fatto l'output mediante l'operazione di output fornita da MB. I client
percepiranno M r come una macchina virtuale che implementa operazioni di input/output
sui record. Ma per poter portare a termine il servizio pubblicato, M utilizza un modulo di
livello pi basso Mb che corrisponde a una macchina virtuale pi semplice.
La relazione U S E S definita in maniera statica tra i moduli; cio, l'identificazione di
tutte le coppie < M i f M 3 > appartenenti alla relazione U S E S indipendente dall'esecuzione
del software. Infatti, lo scopo della progettazione proprio quello di definire la relazione
una volta per tutte. Per chiarire l'argomento, si consideri un modulo M che fa uso dei moduli Mx e M2 chiamando una delle loro procedure. Se il modulo client M contiene la struttura di codice
if cond

tben

proci

else

proc2

dove p r o c i una procedura del modulo MI e p r o c 2 una procedura del modulo M2, allora M U S E S MJ e M U S E S M 2 , anche se durante un'esecuzione particolare potrebbe accadere che venga invocato o MI o M2, ma non entrambi.

Come ulteriore esempio, si consideri la riconfigurazione dinamica di un sistema distribuito: durante l'esecuzione, M^ potrebbe utilizzare il modulo Mj fino a quando la riconfigurazione dinamica non faccia s che Mi usi il modulo Mk. Mj e Mk forniscono esattamente
le stesse funzionalit, ma risiedono su diversi nodi del sistema distribuito. Un guasto del nodo dove risiede Mj, fa s che le richieste di funzionalit che erano offerte da Mj siano ridirette verso Mk. Dunque, in termini della relazione USES, abbiamo sia Mi USES M-jsiaMi USES
Mk, anche se nei casi normali viene utilizzato solo M-,.
La rappresentazione grafica della relazione USES fornisce una descrizione intuitiva, anche se parziale delle associazioni tra moduli. Se ogni nodo del grafo connesso a ogni altro
nodo del grafo (cio il grafo completo: esiste una coppia <Mi, M.j> all'interno della relazione USES per ogni Mi, M-j in s), allora la struttura modulare risulta estremamente intricata e non fornisce un partizionamento gestibile dell'intero sistema.
In verit, questi commenti risultano veri per la maggior parte delle altre relazioni tra
moduli. Se il grafo di una relazione r tale che ogni modulo associato a ogni altro modulo allora nessuna parte risulta essere indipendente. In un caso simile, la cardinalit di r
risulta essere n ( n - 1 ), dove n la cardinalit di S. Dall'altra parte, se r vuoto allora la relazione descrive una struttura modulare in cui non esistono due moduli associati. Dunque,
il sistema pu essere diviso in parti assolutamente incorrelate, che sono progettabili e comprensibili isolatamente. Una relazione r vuota non significativa nella pratica, ma ci mostra che dovremmo cercare di ottenere strutture modulari dove la cardinalit di r molto
pi piccola di n 2 .
La relazione USES ci fornisce la possibilit di ragionare su un modo ben preciso di associare moduli. Facendo riferimento al grafo della relazione USES, possiamo distinguere tra
il numero di archi uscenti da un modulo (detto il fan-out del modulo) e il numero di archi
entranti in un modulo (detto il fan-in del modulo). E stato suggerito che una buona struttura progettuale dovrebbe tenere il fan-out basso e il fan-in alto. Un fan-in alto un'indicazione di una buona progettazione in quanto un modulo con un alto fan-in rappresenta
un'astrazione generale, molto utilizzata dagli altri moduli.
Per poter valutare la qualit di un progetto, comunque, non sufficiente la valutazione della struttura della relazione USES. importante anche la natura dell'interazione tra i
moduli. Ecco alcuni esempi di come i moduli possono utilizzarsi a vicenda.
1. Un'interazione poco desiderabile si ha quando un modulo modifica i dati - o addirittura le istruzioni - locali a un altro modulo. Questo pu succedere nel caso di linguaggi
di tipo assembler.
2. Un modulo potrebbe utilizzare un altro modulo comunicando attraverso un'area dati
comune, come una variabile statica in C o un blocco COMMON in FORTRAN.
3. I dati scambiati tra i due moduli potrebbero essere dati "puri" o informazioni di controllo (cosiddetti flag). Lo scambio di informazioni di controllo spesso risulta in un tipo di interazione che impoverisce la leggibilit dei programmi.
4. Un sottoprogramma potrebbe comunicare con un altro invocandolo e trasferendo parametri. Questo un modo tradizionale e disciplinato di interazione tra due moduli.
5. In un ambiente concorrente, un modulo client potrebbe comunicare con un server attraverso una chiamata a procedura remota (o invocazione di metodo remoto in Java).

Oppure, nel linguaggio di programmazione Ada, un modulo M che racchiude un compito CM potrebbe utilizzare un modulo M' che racchiude un compito CM. facendo una
chiamata all'ingresso del compito C M .. Questi sono metodi disciplinati per la comunicazione tra due moduli concorrenti.
Esercizi
4.5

Considerate il caso in cui la relazione U S E S sia definita da un albero. Cosa comporta che la
struttura sia un albero e non un DAG? In generale, preferibile un progetto in cui la relazione USES un albero o un progetto in cui un DAG?

4.6

Si supponga che la relazione USES corrisponda alle chiamate a procedure. Moduli mutuamente ricorsivi non formano una gerarchia. La ricorsione diretta all'interno di un modulo,
per, permessa in una gerarchia. Sono corrette queste affermazioni? Se lo sono, fornite una
giustificazione.

4.7

Si pu definire il concetto di livello per un grafo generico invece di un DAG? Perch? Perch
no? Che cosa implica per la struttura di progetto?

4.8

Supponiamo di impiegare un linguaggio che supporti l'uso di procedure come parametri. Per
esempio, il modulo MI pu chiamare una procedura P del modulo MJ, passando come parametro la procedura Q del modulo MK. C o m e potreste definire la relazione USES per MJ; considerando i moduli di cui fa uso chiamando la sua procedura come parametro formale?

4.2.1.2

Relazione IS_COMPONENT_OF

I S_COMPONENT_OF un'altra relazione tra moduli che risulta utile per la descrizione di
progetti. Questa relazione permette ai progettisti di descrivere un'architettura in termini di
un modulo che composto da altri moduli, che a loro volta possono essere composti da altri moduli, e cosi via.
Consideriamo un insieme di moduli S. Per qualsiasi Mi e M j ; in S, Mi IS_C0MP0NENT_0F
Mj significa che M, realizzato aggregando diversi moduli, uno dei quali Mi. E anche possibile definire COMPRISES come la relazione inversa di IS_C0MP0NENT_0F; ovvero per due
elementi Mi e Mj in s , diciamo che Mi c o m p r i s e s Mj se e solo seMj is_CC>mponent_of Mi.
ms _ i un sotto-insieme di s cos definito:
MSii

{M k M k

is

in

S and

Mk

I S_COMPONENT_OF

Mi)

Possiamo dire che M I S C O M P O S E D O F M S i i e, viceversa, cheM E i i IMPLEMENTS Mi.


Se un modulo Mi composto da un insieme di moduli M S i i , allora i moduli dell'insieme M S ; i forniscono, a tutti gli effetti, tutti i servizi che dovrebbero essere forniti da Mi:
sono il risultato della scomposizione in componenti di Mi, e dunque implementano Mi.
Nella progettazione, una volta che Mi viene scomposto nell'insieme dei costituenti MSi
viene sostituito da loro; ovvero, Mi diventa un'astrazione implementata in termini di astrazioni pi semplici. L'unico motivo per mantenere Mi nella descrizione modulare di un sistema quello di potervi fare riferimento, rendendo il progetto pi chiaro e comprensibile.
Alla fine del processo di scomposizione, comunque, solo i moduli non composti da altri moduli possono essere visti come componenti reali del sistema. Gli altri vengono mantenuti solo per motivi descrittivi.

Anche la relazione LS_COMPONENT_OF pu essere descritta da un grafo orientato, come illustrato nella Figura 4.4(a). La relazione non riflessiva e costituisce una gerarchia.
Dunque, anche in questa relazione, possiamo definire un modulo che sta a un livello pi alto di un altro modulo, come abbiamo fatto con la relazione USES. In pratica, pi utile introdurre il concetto di livello in riferimento nella relazione COMPRISES. La Figura 4.4(b)
descrive il sistema di Figura 4.4(a) in termini di questa relazione.
Il concetto di livello definito da IS_COMPOSED_OF tale che se MI IS_COMPOSED_OF
{ M i , w m ,2 / Mi, > allora Mi risulta essere di livello pi alto di qualsiasi dei moduli MI;1, MI,2 / / MI
Notiamo che il concetto di livello di astrazione pu risultare
ambiguo quando utilizzato nelle descrizioni di progetto, a meno che non venga specificato
esplicitamente se inteso come il livello rispetto alla relazione USES o alla relazione COMPRISES. Nel caso di USES, tutti i moduli M ^ , M1I2, .. ., MIIN usati da un determinato
modulo Mi risultano essere di un livello pi basso rispetto a M; dunque, Mi fornisce i servizi che esporta ai propri client usando i servizi forniti dai moduli di livello pi basso M^,,
M^ 2, . . ., M I N . Nel caso di COMPRI SES, tutti i moduli che implementano un dato modulo Mi sono di livello pi basso rispetto a M^ prendono il suo posto (cio, Mi raffinato mediante la sostituzione con MI;1, MI 2, . . ., MI N.
La rappresentazione grafica di IS_COMPONENT_OF descrive anche IS_C0MP0SED_0F,
IMPLEMENTS e COMPRISES. Per esempio, nella Figura 4.4, M2, M3 e M4 sono componenti
di M; M! I S _ C O M P O S E D _ O F {M 2 , M 3 , M 4 } ; { M 2 , M 3 J

M4} IMPLEMENTS MI;EM1 COM-

PRISES MI, per 2 s i < 4. L'intero sistema software in definitiva composto dai moduli M4,M5,M6,M7,ME e M,. Gli altri moduli che appaiono nel grafo non esistono fisicamente;
il loro unico scopo quello di aiutare a descrivere la struttura in una maniera gerarchica.
Per esempio, supponiamo che la Figura 4.4 descriva la struttura modulare di un'applicazione in cui M2 il modulo che fornisce servizi di input, M3 il cuore del sistema ed esegue tutte le istruzioni e M4 il modulo che fornisce servizi di output. A sua volta M2 composto da vari moduli (M7, ME e M9), i quali forniscono determinati servizi di input: ad esempio, input attraverso il riempimento dei campi di una tabella, attraverso una sequenza di comandi interattivi, etc. Il modulo M3 scomposto in M5 e M6. Il sistema finale contiene solo
moduli fisici che corrispondono agli elementi della relazione LS_COMPOSED_OF che non
sono scomposti in ulteriori moduli, per esempio, M4, M5, M6, M7, ME e M9.

(a)
Figura 4.4

(b)

Esempio della relazione I S_COMPONENT_OF (a) e la corrispondente relazione


COMPRISES

(b).

Finora, durante la discussione riguardante IS_C0MP0NENT_0F, abbiamo assunto che


un modulo possa essere componente al massimo di un modulo. Anche se ci rappresenta il
caso pi tipico, non imponiamo questa restrizione nella definizione della relazione I S _ C 0 M POSED_OF. Quindi, per esempio, possibile completare il grafo della Figura 4.4 con un arco orientato dal nodo M6 al nodo M per indicare che m6 componente sia di M3 sia di M.
Quando un modulo Mi componente di entrambi i moduli Mj e Mk, si pu dare una descrizione alternativa, che risulta ovvia, secondo la quale Mi componente solo di M3 e una
copia di Mi componente di Mk. Un'altra soluzione adottata da alcuni linguaggi consiste nella definizione di una macro o di un modulo generico (template) per la generazione successiva di istanze da utilizzare nei diversi contesti. Approfondiremo l'argomento pi avanti.
Le due relazioni USES e i s _ c o m p o n e n t _ o f possono essere, e spesso lo sono, utilizzate insieme. Per esempio, potremmo cominciare una descrizione di pi alto livello di un'architettura di sistema dicendo che s i s t e m a IS_C0MP0SED_0F i moduli M l5 M2 e m3, dove Mj USES sia M2 che M3. Pi tardi, potremmo specificare che Mi IS_COMPOSED_OF M, e
m5, e cos via.
Anche se abbiamo discusso le relazioni U S E S e l S _ C O M P O N E N T _ O F nel contesto della progettazione di architetture software, i concetti che queste relazioni esprimono possono essere ugualmente applicati a qualsiasi altro tipo di progetto. Nel contesto della specifica dei requisiti, ad esempio, dovremmo creare moduli di specifica e relazioni che descrivono le loro dipendenze. Un modulo di specifica potrebbe usare un altro modulo se fa riferimento a un concetto espresso da quest'ultimo. Un modulo di specifica potrebbe anche
essere componente di un altro modulo qualora specificasse una parte del sistema illustrata
da quest'ultimo.
Esercizi
4.9
4.10

IS

COMPOSED OF e IMPLEMENTS s o n o relazioni su S?

S u p p o n i a m o di avere deciso di seguire la seguente politica: u n m o d u l o Mt p u essere implem e n t a t o p r i m a di M 2 se M, n o n h a c o m p o n e n t i e n o n usa M 2 o qualsiasi altro m o d u l o che c o m p r e n d e M2. Descrivete questa politica in m o d o formale, c o m e u n a relazione tra m o d u l i .

4.2.1.3

Rivisitazione delle famiglie di prodotti

Possiamo utilizzare le relazioni U S E S e l S _ C O M P O N E N T _ O F per riformulare alcuni punti


riguardanti le famiglie di prodotti.
Supponiamo di progettare un sistema S e di comporlo in un insieme di moduli M,,M 2 ,
. . ., Mi, mediante la relazione USES. Supponiamo poi di affrontare la progettazione di uno
qualsiasi di questi moduli, ad esempio Mk, 1 s k s i . A questo punto, ci accorgeremmo
che qualsiasi decisione di progetto che possiamo prendere separer un sottoinsieme di membri della famiglia dagli altri; per esempio, Mk un modulo di output e la sua progettazione
potrebbe avere bisogno di discriminare tra output testuale e output grafico, da affrontare
mediante due membri differenti della famiglia. Supponiamo, inoltre, di prendere la decisione di seguire una delle opzioni di progettazione (nell'esempio, l'output grafico), che ci
porta a scomporre Mk in M k > 1 , Mk 2 , . . . , M k > i k , con la definizione di alcune relazioni USES
all'interno di questo sottoinsieme.

Bisognerebbe sempre annotare queste decisioni di progettazione in maniera accurata, in modo da poter effettuare cambiamenti futuri in maniera affidabile. Supponiamo di
dover progettare un diverso membro della famiglia in un momento successivo (per esempio, il sistema che fornisce il supporto per l'output testuale). Non si dovrebbe mai modificare l'implementazione finale, cambiando il codice, in modo da soddisfare i nuovi requisiti. Piuttosto, la documentazione della struttura dei moduli dovrebbe costringerci a
riprendere la progettazione dalla scomposizione del modulo Mk, in modo da fornire una
diversa implementazione in termini di moduli di pi basso livello. Notiamo, comunque,
che il resto del sistema non risulterebbe alterato; ovvero i moduli M1; . . ., Mk_i e Mk+1,
. . . , Mi non subirebbero ripercussioni per via della progettazione del nuovo membro della famiglia.

4.2.2

Interfaccia, implementazione e information hiding

Le relazioni U S E S e l S _ C O M P O N E N T _ O F forniscono solo una descrizione parziale di un'architettura software. Per esempio, rimangono ancora molte cose da dire riguardo all'interazione tra due moduli che partecipano alla relazione U S E S e riguardo ai dettagli di
l S _ C O M P O N E N T _ O F . Ovvero, quando un modulo Mi che U S E S un modulo Mj viene raffinato nei suoi componenti M i r l , M i i 2 , . . . , M i > n , diventa necessario dichiarare esattamente i significati della relazione U S E S tra i moduli del sotto-insieme {M^i, M i i 2 ,
M i i n } e M-,.

Intuitivamente, ci piacerebbe dividere il software in componenti tali che ognuno possa essere progettato indipendentemente dagli altri. Se ogni componente diventa un compito di un programmatore differente di una squadra, allora ciascun programmatore dovrebbe
poter lavorare sul proprio componente con meno conoscenza possibile riguardo a come gli
altri membri della squadra stanno implementando i propri. Ancora una volta, questa l'essenza dei principi di separazione degli interessi e di modularit, discussi in termini generali
nel Capitolo 3.
Per essere pi precisi, occorre definire come l'interazione tra moduli avviene veramente, ovvero, quale sia la natura esatta della relazione USES tra due moduli qualsiasi. L'insieme
dei servizi offerti da ogni modulo ai suoi client viene detto interfaccia. Si dice che i servizi
corrispondenti sono esportati dal modulo e importati dai client. Il modo in cui questi servizi vengono portati a termine dal modulo un segreto del modulo ed insito nella sua implementazione. La distinzione chiara tra l'interfaccia di un modulo e la sua implementazione un aspetto chiave in una buona progettazione, in quanto supporta il principio della separazione degli interessi.
L'interfaccia di un modulo M descrive esattamente ci che i moduli client devono conoscere per usufruire dei servizi offerti da M. L'interfaccia un'astrazione del modulo, che
descrive come questo viene visto dai suoi client. Il progettista a capo di M, al momento della sua progettazione, deve conoscere solo le interfacce degli altri moduli usati da M, e pu
ignorare la loro implementazione. L'interfaccia di M viene vista dal progettista come la descrizione del suo compito: il suo obiettivo quello di fornire quei servizi attraverso un'implementazione adeguata. L'implementazione di un modulo la sua scomposizione in termini di componenti, descritti dalla relazione l S _ C O M P O N E N T _ O F ; oppure, se il modulo
sufficientemente piccolo, la sua codifica in un determinato linguaggio di programmazione,
che potrebbe anche utilizzare servizi forniti da altri moduli di livello pi basso.

L'interfaccia di un modulo M pu essere vista come un contratto tra M e i suoi client;


ovvero, l'interfaccia registra solo le operazioni che il progettista a capo di M concorda di fornire agli altri progettisti. I client possono fare affidamento solo su quanto specificato nell'interfaccia. Di conseguenza, fino a quando l'interfaccia rimane la stessa, M pu cambiare
senza causare ripercussioni sui propri client.
Nella maggior parte dei casi pratici, le interfacce descrivono le risorse computazionali, come ad esempio una variabile che appartiene a un modulo e viene resa disponibile ad
altri moduli per fornire una forma di interazione o funzioni che devono essere chiamate per
l'esecuzione di una certa operazione. Le interfacce, comunque, non sono limitate a questi
tipi di risorse. Per esempio, le informazioni riguardanti il tempo di risposta di una routine
esportata potrebbero essere parte di una descrizione non funzionale dell'interfaccia nel caso
di un'applicazione real-time. E un tipo di informazione che il client deve conoscere per poter decidere se e come utilizzare il modulo.
Possiamo approfondire la distinzione tra interfaccia e implementazione introducendo
il concetto di information hiding. I client di un modulo conoscono i suoi servizi solamente
attraverso l'interfaccia; l'implementazione risulta essere cos nascosta. Ci significa che l'implementazione potrebbe cambiare senza conseguenze per i client del modulo, a patto che
l'interfaccia rimanga immutata. Dunque, un aspetto critico della progettazione consiste nella scelta precisa di che cosa presentare nell'interfaccia e dunque rendere visibile ai potenziali client - e di che cosa celare nell'implementazione, e dunque rendere modificabile in
qualsiasi momento senza conseguenze per i client. La disponibilit di costrutti linguistici per
la definizione di interfacce negli attuali linguaggi di programmazione fornisce un ottimo supporto all'attivit di progettazione del software.
Esercizi
4.11

Per il programmatore Ada: considerate la parte di specifica dei package Ada come la descrizione dell'interfaccia di un modulo. Q u a l la differenza tra esportare u n tipo ed esportare un
tipo privato? Descrivete la differenza in termini di funzionalit esportate.

4.12

Per il programmatore Java: il costrutto di interfaccia in Java permette a u n o sviluppatore di


specificare l'interfaccia di un m o d u l o i n d i p e n d e n t e m e n t e dalla sua implementazione. Quali
entit sono esportabili in Java? C o m e fa u n programmatore a fornire un'implementazione per
un'interfaccia? possibile avere due implementazioni della stessa interfaccia?

4.13

C o n f r o n t a t e il supporto alla definizione di interfacce presente in diversi linguaggi, quali Eiffel,


Ada 95, C++ e Java.

4.2.2.1

Come progettare interfacce per i moduli

Un'analogia spesso utilizzata per descrivere i concetti di interfaccia, implementazione e information hiding illustrata nella Figura 4.5. Un modulo come un iceberg: l'interfaccia la
sua punta, la parte visibile, e l'implementazione ci che rimane celato sotto la superfcie
dell'acqua. La punta solo una piccola parte dell'intero elemento.
In quest'analogia, comunque, esistono alcuni limiti. Ad esempio, la punta dell'iceberg
non fornisce un'astrazione soddisfacente dal punto di vista di una nave. Fare affidamento

Figura 4.5

L'interfaccia come punta dell'iceberg.

sulla forma della parte emersa non infatti sufficiente per evitare di cozzare con quella sommersa. Contrariamente alla punta dell'iceberg, l'interfaccia di un modulo descrive tutto ci
che si deve conoscere per poter utilizzare il modulo in maniera corretta.
Nonostante ci, l'analogia dell'iceberg ci pu illuminare su un punto molto importante:
cosa dovrebbe essere presentato nella descrizione dell'interfaccia e cosa invece celato all'interno dell'implementazione? Chiaramente, l'interfaccia di un modulo dovrebbe rivelare meno informazioni possibili, ma sufficienti affinch gli altri moduli possano utilizzare i servizi forniti. Rivelare informazioni non necessarie rende l'interfaccia inutilmente complessa e
riduce la comprensibilit del progetto del sistema. Inoltre, rivelando informazioni riguardanti
dettagli interni, non necessari, diventa pi probabile che un cambiamento al modulo produca conseguenze non solo sulla sua implementazione ma anche sulla sua interfaccia. Peggio
ancora, altri moduli potrebbero approfittare dell'informazione resa pubblica, operandovi in
maniera indesiderata. Dall'altra parte, non esportare i servizi che devono essere importati
dai client diminuirebbe l'usabilit del modulo.
Esercizio
4.14

Esponete che cosa rappresenta un telecomando, in termini di interfaccia, per u n utente che
vuole guardare la TV. L'interfaccia risulta adeguata se l'utente vuole connettere l'apparecchio
ad altre periferiche (per esempio, lo stereo, il videoregistratore o la videocamera) attraverso i
suoi canali di input e output?

L'Esercizio 4.14 affronta un concetto importante: l'interfaccia che progettiamo dipende


dai servizi che vogliamo offrire ai clienti e, per contro, da ci che decidiamo di nascondere all'interno del modulo. Possiamo celare determinate funzioni se riteniamo che gli utenti non ne facciano uso. L'arte del progettare interfacce per moduli consiste nel bilanciare
attentamente cosa vogliamo nascondere e cosa dobbiamo fornire. Se viene celato tutto, i
moduli non comunicheranno e non coopereranno: saranno sottosistemi autonomi. Se tutto viene reso visibile, la struttura del sistema risulter intricata e caratterizzata da eccessive interazioni.

ESEMPIO 4 . 1

Supponiamo di progettare un interprete per un linguaggio di programmazione molto semplice, M I N I , che opera su interi e array di interi. Forniamo un modulo che rappresenti la
classica tabella dei simboli di un interprete o compilatore, che registra informazioni riguardanti le variabili di un programma. Supponiamo che la tabella dei simboli esporti una
procedura GET che accetta in ingresso il nome simbolico di una variabile ed eventualmente
il valore di un indice (nel caso di un array) e restituisce il valore della variabile. In maniera del tutto simile, supponiamo che una procedura PUT renda possibile l'inserimento
di un nuovo valore per una data variabile. Infine, supponiamo che, quando viene incontrata la definizione di una nuova variabile, venga creata una nuova voce nella tabella dei
simboli mediante la chiamata alla procedura CREATE, passando come parametro di ingresso il nome della variabile e la sua dimensione (il numero delle voci di tipo intero che
rappresenta).
Lo scopo dell'interfaccia che stiamo progettando quello di nascondere ai clienti del
modulo la struttura fisica della tabella. Per avvertire i clienti quando viene tentata la lettura
o scrittura del valore di una variabile che non esiste, o viene tentato l'accesso a un array con
un valore di indice non valido, le procedure GET e PUT restituiscono un parametro aggiuntivo, POS. Il valore restituito di POS un puntatore alla variabile registrata nella tabella, se
esiste, oppure un puntatore n u l i se la variabile non esiste.
Questo progetto pu essere criticato per la ridondanza dell'interfaccia. Se il nostro scopo solo quello di fornire operazioni per la registrazione e il recupero di dati (e segnalare il
caso in cui l'accesso ai dati risulta non corretto), allora stiamo fornendo informazioni in eccesso (e cio, la posizione all'interno della tabella dove sono immagazzinati i dati). Tale ridondanza pu avere effetti negativi sulla facilit di effettuare cambiamenti al progetto, come vedremo tra breve. Inoltre viola il principio dell'information hiding.

Come conviene procedere nella progettazione dei moduli, con information hiding, in modo da migliorare la coesione e ridurre le interazioni, sia in termini di numero di interconnessioni presenti nel grafo della relazione USES sia in termini del tipo e della quantit di
informazioni esportate dall'interfaccia?
Per rispondere a questa domanda, dovremmo prima definire quale sia realmente il
principale obiettivo del nostro progetto. Come in precedenza, assumiamo che la semplicit nell'effettuare cambiamenti sia un obiettivo primario: vogliamo che il nostro progetto sia in grado di evolvere facilmente e in modo affidabile, in accordo con i cambiamenti previsti e con gli eventuali altri cambiamenti che si potrebbero prospettare. Il prossimo
paragrafo fornir alcune linee guida su come progettare moduli che sappiano assimilare i
cambiamenti futuri.
Esercizio
4.15

C o n s i d e r a t e u n c a m b i a m e n t o all'interfaccia del m o d u l o dell'Esempio 4.1 che preveda l'esistenza di un'altra p r o c e d u r a per p o t e r chiedere se u n a variabile esiste (e q u i n d i se p u essere
letta o scritta in m a n i e r a sicura). D i s c u t e t e il c a m b i a m e n t o in termini di qualit della struttura m o d u l a r e , efficienza del sistema, etc.

4.2.2.2

Segreti dei moduli e progettazione per il cambiamento

Per massimizzare la facilit di modifica dell'implementazione di un modulo, le sue interfacce


dovrebbero esporre meno dettagli possibili. Un altro obiettivo quello di nascondere i dettagli di basso livello e fornire un'interfaccia astratta in modo da rendere il progetto pi comprensibile. Una volta che i cambiamenti per agevolare ci sono stati identificati, si deve
strutturare il sistema in modo tale da nascondere nella parte implementativa le decisioni modificabili, cos che le interfacce rappresentino informazioni stabili (e cio, le informazioni
non coinvolte nei cambiamenti).
Diremo che le informazioni modificabili e nascoste diventano il segreto del modulo; inoltre, in accordo con un gergo largamente accettato, diremo che tali informazioni sono incapsulate all'interno dell'implementazione del modulo.
ESEMPIO 4 . 2

Il segreto del modulo della tabella dei simboli dell'Esempio 4.1 risiede nella struttura dati
scelta per la rappresentazione interna. Potremmo scegliere di usare un'array lineare, una hash
table, una lista concatenata, un albero binario, o addirittura strutture pi sofisticate.
L'importante obiettivo che vogliamo raggiungere risiede nella possibilit di modificare la struttura dati senza provocare conseguenze ai moduli client. Inoltre, intendiamo progettare e implementare il sistema velocemente, concentrandoci prima sulla struttura complessiva, senza
impiegare troppo tempo progettando e raffinando le strutture dati interne al modulo.
Desideriamo posticipare la decisione riguardante la natura delle parti interne di ciascun modulo, come ad esempio il "miglior" tipo di struttura dati, a un secondo momento, in cui
avremo terminato l'intera progettazione dell'interprete e possibilmente collezionato alcuni
profili di esecuzione.
Se esaminiamo l'interfaccia attentamente, per, possiamo osservare come questa riveli pi del necessario; in particolare, espone una parte del suo (supposto) segreto. Conoscendo
l'indirizzo di una variabile (rivelato da GET o PUT), consentito accedervi direttamente, senza necessariamente passare dalle procedure definite nell'interfaccia. Per esempio, se un client
accede ripetutamente alla stessa variabile semplice - diciamo X - potrebbe essere tentato di
ottenere il valore di POS e poi usare direttamente il puntatore per accedere alla variabile.
Questo metodo funzionerebbe correttamente solo se la posizione in cui viene registrato il
valore non cambiasse nel tempo, come nel caso di un'implementazione semplice della tabella dei simboli in cui per ogni nuova dichiarazione viene semplicemente allocato un elemento in coda a una struttura dati sequenziale.
Qualora, in una versione successiva della tabella dei simboli, la struttura dati sequenziale venisse sostituita da una struttura che tenesse ordinate le sue voci, l'applicativo diventerebbe evidentemente scorretto. Infatti, ogni volta che si incontrano nuove dichiarazioni,
queste verrebbero registrate nelle posizioni appropriate della struttura dati, implicando lo
spostamento di alcune voci gi inserite, e rendendo cos non validi i valori precedentemente restituiti da POS.

Come anticipato nel Paragrafo 4.1.1, i dettagli riguardanti la macchina astratta sottostante il
software sono esempi di informazioni che dovrebbero rimanere nascoste. Questi includono alcuni dettagli riguardanti le chiamate al sistema operativo o le complessit delle interazioni ri-

chieste con dispositivi periferici speciali. La principale ragione per cui occorre nascondere questi dettagli proteggere l'applicazione dai cambiamenti nella macchina astratta sottostante. Tali
cambiamenti potrebbero derivare dalla prevista evoluzione dell'hardware o dalla volont di rendere portabile l'applicazione. Un'altra importante motivazione per incapsulare in moduli ad
hoc gli aspetti dipendenti dalla macchina astratta data dal principio di separazione degli interessi: mescolare dettagli di basso livello dipendenti dalla macchina astratta con caratteristiche di alto livello dipendenti dall'applicazione ostacolerebbe la comprensibilit del software.
ESEMPIO 4 . 3

Supponiamo che venga utilizzato un computer per il controllo remoto di un impianto. Il


computer deve acquisire input per raccogliere misurazioni da alcuni dispositivi fisici dislocati in diversi punti dell'impianto. Ad esempio, il computer riceve i valori di temperatura
nei punti P^ P2 e P5, i valori di pressione nei punti Pj, P3 e P4, etc. In funzione dei dati di
input, devono essere rimandati segnali di controllo all'impianto, e si devono memorizzare
dati storici (history log) in modo da facilitare la manutenzione dell'impianto. In una prima
versione, i valori di input vengono ricevuti come sequenze di byte che devono essere decodificate dall'applicazione di controllo. Si prevede, per, che il sistema evolver in una nuova configurazione distribuita, in cui gli input fisici verranno processati remotamente da dispositivi specializzati e spediti al computer di controllo sotto forma di dati strutturati.
Un buon progetto dovrebbe definire un modulo per l'acquisizione dell'input, il cui segreto sarebbe il modo fisico in cui i dati di input vengono acquisiti. Tale modulo fornirebbe ai moduli client un'operazione di query da invocare per ottenere il successivo dato di input
(che tipo di misurazione stata eseguita, dove stata misurata, etc.) e il valore della misurazione (un intero o il valore reale, a seconda del tipo di misurazione).

In conclusione, lo scopo dell'information hiding quello di progettare moduli che proteggano le decisioni modificabili rendendole segrete e fornire un'astrazione significativa attraverso interfacce stabili. L'identificazione di probabili cambiamenti cruciale per questo approccio. Un insieme di probabili cambiamenti dovrebbe, infatti, trovarsi all'interno del documento dei requisiti che stabilisce gli obiettivi per l'applicazione. Come gi accennato,
quando si determinano i requisiti di un nuovo sistema, si dovrebbe porre particolare attenzione non solo a ci che necessario in quel momento ma anche a cosa sar, con buona probabilit, necessario in futuro. L'Esempio 4.3 illustra questo concetto. Altri concetti saranno
illustrati nell'esempio successivo.
ESEMPIO 4 . 4

Supponiamo che i requisiti per l'applicazione di controllo dell'Esempio 4.3 contengano una
descrizione di come debbano essere processati i dati storici. Supponiamo che descrivano anche un insieme predefinito di query che possono essere utilizzate per estrarre dati durante
la fase di manutenzione del software. Sono previste migliorie future per il sistema che consentiranno query mediante l'uso di linguaggio naturale.
Una buona modularizzazione, in questo caso, incapsuler in un modulo la struttura fsica dei file usati per immagazzinare i dati storici; ovvero, il modulo fornir procedure per

l'accesso alle varie voci registrate nella struttura dati. Un altro modulo fornir le query astratte; ovvero, incapsuler come le query verranno effettivamente formulate dall'utente mediante l'uso di linguaggio naturale o query dal formato prestabilito - e come dovranno essere analizzate in modo da estrarre il significato esatto della richiesta dell'utente.

Come abbiamo visto nel Capitolo 3, una classe importante di probabili cambiamenti ha a
che fare con la strategia seguita per produrre l'applicazione. La strategia dello sviluppo incrementale cerca di identificare sotto-insiemi utili dell'applicazione che potrebbero essere sviluppati e consegnati prima di altri. Anche se alcune parti del sistema non vengono affrontate subito ma rimandate, necessaria una cura attenta nello stadio di progettazione, per definire con precisione le interfacce relative alle parti che vengono destinate allo sviluppo successivo. Ci consentir di integrare successivamente nel sistema i moduli mancanti senza che
questi disturbino le funzionalit gi rilasciate. In altri casi, nello stadio iniziale, alcune parti del sistema possono essere deliberatamente sviluppate in modo semplificato per poi essere riprogettate e reimplementate in uno stadio successivo. La tabella dei simboli dell'Esempio
4.1 ne un esempio, come quello qui di seguito.
ESEMPIO 4 . 5

Supponiamo di dover sviluppare un nuovo tipo di DBMS che speriamo diventi un prodotto rivoluzionario all'interno del mercato. L'aspetto innovativo del sistema risiederebbe nel
linguaggio utilizzato per le query, che consentirebbe un sofisticato impiego di interazioni basate sia sul linguaggio naturale che su un linguaggio visuale.
Prima di cominciare lo sviluppo del nuovo sistema, vorremmo essere in grado di valutare la validit dell'approccio per quanto riguarda gli aspetti innovativi dell'interazione uomo-macchina. Decidiamo, dunque, di implementare l'interfaccia utente, tralasciando per il
momento l'implementazione del vero e proprio DBMS (e cio, la definizione delle strutture dei file fisici, dei vari algoritmi di memorizzazione e ricerca, delle procedure di recupero,
del controllo della concorrenza, etc.).
Quello che decidiamo di implementare un prototipo dell'applicazione, in grado di gestire solo una quantit limitata di informazioni, visto che tutti i dati verrebbero conservati
nella memoria centrale usando array. Verrebbe, poi, chiesto ai potenziali utenti di "giocare"
con il sistema, per fornire feed-back ai progettisti riguardo alla sua usabilit. Ovviamente gli
utenti verrebbero avvertiti che le prestazioni, la robustezza, l'affidabilit, etc., del prototipo
non hanno niente a che vedere con quelle del futuro sistema: dovranno quindi porre attenzione al modo in cui le query vengono sottoposte mediante l'uso congiunto di linguaggio
naturale e immagini.
Se l'interfaccia tra il modulo che fornisce le funzionalit interattive e il modulo che fornisce l'accesso al database stata progettata con attenzione, allora i due moduli potranno
evolvere indipendentemente. Per esempio, potremmo prima concentrarci sull'implementazione di una versione robusta del modulo di interazione e poi trasformare il prototipo del
modulo dei DBMS in una versione realistica, senza influire sulla correttezza complessiva del
sistema. In altre parole, se progettiamo interfacce stabili tra i vari moduli, questi potranno
evolvere in maniera indipendente, dalla versione prototipale a quella
finale.

4.2.2.3

Ancora sui probabili cambiamenti

Gli esempi forniti nel paragrafo precedente rappresentano solo una piccola parte delle richieste di cambiamento di software che si possono incontrare nella pratica. I cambiamenti prevedibili possono essere suddivisi in poche classi. I moduli che implementano l'information hiding dovrebbero essere progettati per consentire queste classi di cambiamenti in maniera affidabile ed efficiente. Nel Paragrafo 4.1.1, abbiamo discusso un elenco di probabili cambiamenti:
negli algoritmi, nella rappresentazione dei dati, nella macchina astratta sottostante, nell'ambiente sociale. L'incapsulamento all'interno di moduli attraverso XInformation hiding supporta questi cambiamenti. Per esempio, se dovessimo usare una procedura per incapsulare un algoritmo, modificare o addirittura sostituire l'algoritmo richiederebbe un cambiamento al corpo della procedura, e ci pu essere fatto senza intaccare i client della procedura. Analogamente,
celando una struttura dati e fornendo un interfaccia astratta per accedervi e modificarla, possiamo proteggere gli utenti della struttura dati da cambiamenti nella sua rappresentazione interna.
Le politiche sono un altro tipo di decisione di progetto che bene incapsulare all'interno di moduli che implementano l'information hiding. Molte volte, hanno a che vedere
con l'ordine in cui vengono eseguite certe operazioni. Ad esempio, supponiamo di progettare un modulo che fornisca ai clienti una lista ordinata di voci. Assumiamo anche le operazioni INSERT, per inserire una voce all'interno della lista, DELETE, per eliminare una voce della lista, PRINT, per stampare la lista dei nomi delle voci in ordine alfabetico. INSERT,
DELETE e PRINT costituiranno l'interfaccia del modulo.
Un tale modulo pu nascondere diversi tipi di politiche; per esempio, in una certa politica la lista potrebbe essere mantenuta ordinata man mano che viene inserita o cancellata una
voce; oppure in una politica incrementale, la lista potrebbe essere ordinata appena prima di essere stampata; oppure ancora, la lista non verrebbe mai mantenuta ordinata e sarebbe l'operazione di PRINT a stampare le voci nell'ordine corretto. Si pu notare come un cambiamento
di politica lasci indifferenti i clienti, visto che la politica un segreto del modulo. La politica
scelta avrebbe, comunque, ripercussioni sui tempi di esecuzione di ogni operazione. Ovvero,
la prima politica renderebbe possibile supportare un'operazione di PRINT rapida, a costo di
operazioni INSERT e DELETE pi lente, visto che devono tenere la lista ordinata.
Come ulteriore esempio di questo concetto, consideriamo il caso di un'applicazione
concorrente, dove indispensabile distinguere tra meccanismi e politiche. In questo tipo di
applicazione, abbiamo bisogno di meccanismi che sospendano i processi nel caso debbano
accedere a una risorsa condivisa (per esempio, una stampante o un buffer). Lo schedulatore
sottostante dovrebbe poi utilizzare una determinata politica per risvegliare un processo sospeso; ad esempio, potrebbe risvegliare i processi sulla base di una politica puramente firstin, first-out, o potrebbe usare politiche pi complesse basate, ad esempio, su concetti di priorit o tempi di esecuzione. Pu essere implementata una qualsiasi di queste politiche, a patto di fornire un modulo che esporti meccanismi di sospensione e risveglio dei processi:

suspend

(P) verrebbe invocata per sospendere il processo P

r s u m (P) verrebbe invocata per risvegliare il processo successivo; fornirebbe anche l'identificatore del processo risvegliato nel parametro di uscita.

Il modulo nasconderebbe la politica utilizzata per selezionare il successivo processo da risvegliare. Cambiamenti futuri nelle politiche si integreranno senza problemi con il resto del
sistema: verrebbero alterate solo le prestazioni, non la correttezza.

Ci che pu essere celato dipende anche dal tipo di applicazione. Ad esempio, in molte applicazioni real-time, le politiche di schedulazione non possono essere nascoste ai moduli client, ma devono essere parte dell'interfaccia. Non sono nascoste nell'implementazione perch non possono essere cambiate indipendentemente dal volere dei clienti. Il fatto che
certi eventi vengano trattati secondo una politica piuttosto che un'altra (per esempio FIFO
piuttosto che eventi con priorit) potrebbe avere conseguenze negative sulla capacit di un
modulo di reagire a uno stimolo entro determinati limiti di tempo, e ci potrebbe causare
effetti seri, pericolosi o addirittura catastrofici all'interno di un sistema real-time3.
Esercizio
4.16

Discutete l'esempio della lista ordinata di voci nel caso di un'applicazione real-time. Un cambiamento nella politica p u avere effetti per i clienti? Perch? Perch no?

4.2.2.4

Prime conclusioni

Qualsiasi sia il metodo che scegliamo di seguire per la modularizzazione di un'applicazione,


le interfacce dei moduli dovrebbero rappresentare solo le informazioni che i moduli client
devono conoscere per poter utilizzare i servizi offerti. I progettisti degli altri moduli dovranno
essere in grado di decidere se trarranno benefici dall'uso di quel modulo semplicemente esaminando l'interfaccia. Ovviamente, diviene necessario avere una notazione per descrivere le
interfacce dei moduli in maniera precisa, in modo che nessuna ambiguit possa sorgere nell'interpretazione dei servizi esportati. Esamineremo la questione nel prossimo paragrafo (e
parzialmente nel Capitolo 5). Prima di affrontare questo problema, comunque, si rendono
necessari due commenti riassuntivi.
Innanzitutto, una distinzione chiara tra l'interfaccia e l'implementazione e una definizione precisa di interfaccia sono necessarie per la (ri)usabilit di un modulo. Un modulo pu
essere (ri)usato in qualsiasi contesto, a patto che i servizi elencati nella sua interfaccia soddisfino le aspettative dei clienti, indipendentemente dall'implementazione.
In secondo luogo, l'interfaccia deve contenere tutte le informazioni necessarie a caratterizzare il comportamento del modulo, come esso viene osservato dai clienti. Come abbiamo indicato, nella maggior parte dei casi, le interfacce forniscono descrizioni di funzioni che
devono essere invocate dai moduli client. Possono anche fornire una descrizione di dati condivisi. Inoltre, nelle applicazioni real-time, il tempo di risposta di un'operazione esportata
parte dell'interfaccia.

4.2.3

Notazioni per la progettazione

Abbiamo finora discusso le problematiche di progettazione del software in modo informale: le architetture sono state descritte usando uno stile colloquiale. Ma la prosa italiana, o qualsiasi altra forma di descrizione basata sul linguaggio naturale, non un mezzo

In realt, spesso non necessario - o utile - rendere esplicita la politica nell'interfaccia. Ad esempio, si
potrebbe fornire una vista pi astratta indicando limiti per i tempi di risposta di determinate operazioni.

adeguato per descrivere artefatti come i progetti software. Per una descrizione non ambigua sono necessari maggiore precisione, rigore e formalit. Gli ingegneri del software,
dunque, hanno bisogno di notazioni speciali per la specifica dei loro progetti. In realt
ci risulta vero per ogni campo dell'ingegneria. Per esempio, gli ingegneri elettrici producono progetti in cui apparecchiature complesse vengono descritte in termini di simboli grafici interconnessi che rappresentano dispositivi elementari come le resistenze, le capacit e i transistor. Questi dispositivi elementari possono essere visti come componenti
standard assemblabili per produrre un nuovo sistema. Notazioni appropriate descrivono
i tipi di dispositivi standard da assemblare, ad esempio, il voltaggio da fornire tra due punti dati o il valore, in ohm, delle resistenze. L'impostazione di tali progetti standardizzata, e non sorgono ambiguit quando vengono interpretate le descrizioni durante la fase di
costruzione del circuito. Il progetto pu essere analizzato per scoprire inconsistenze o errori prima che cominci la fase di implementazione. Considerazioni simili possono essere
fatte per l'ingegneria civile o meccanica: in tutti questi casi, i progetti vengono espressi
mediante l'uso di una notazione grafica standard.
Non ancora emersa alcuna notazione standard per esprimere i progetti software anche se diverse proposte sono state prese in considerazione e alcune anche adottate nella pratica. UML ( Unified Modeling Language, linguaggio unificato di modellazione) una combinazione di diverse notazioni primitive ed j; stato promosso a standard universale per la progettazione orientata agli oggetti. Nel seguito illustreremo due notazioni, una basata su una
sintassi testuale simile a un linguaggio di programmazione (chiamata TDN) e l'altra basata
su una sintassi grafica (chiamata GDN). Queste notazioni hanno molti aspetti in comune
con le notazioni usate nella pratica. La ragione per cui abbiamo scelto di usare una nostra
notazione che non vogliamo essere distratti da dettagli di sintassi che non aggiungono molto all'espressivit della notazione. Pi tardi, quando affronteremo la progettazione orientata agli oggetti, faremo invece riferimento alla notazione standard UML.
Le notazioni che introdurremo descrivono l'architettura software mediante la specifica di moduli e delle relazioni che intercorrono tra questi. La notazione formale per quanto riguarda la sintassi delle interfacce. Ad esempio, dice, in una forma sintatticamente corretta, come formulare una richiesta per un servizio esportato da un modulo. Non specifica
formalmente la semantica dei servizi esportati (e cio, cosa realizza effettivamente un servizio, insieme a possibili limitazioni o propriet che i clienti devono conoscere). La semantica descritta solo informalmente, mediante l'uso di commenti. La specifica formale della
semantica dei moduli verr esaminata nel Capitolo 5.
4.2.3.1

TDN: notazione testuale per la progettazione

In questo paragrafo, illustreremo T D N , la nostra notazione testuale per la progettazione.


Essa si ispira, in parte, alla sintassi dei linguaggi di programmazione tradizionali quali Ada
o Modula-2, ma il suo obiettivo quello di focalizzarsi su problematiche di modularizzazione. Di conseguenza, sono state aggiunte delle caratteristiche, e molti dettagli tipici
dei linguaggi di programmazione sono deliberatamente ignorati. Inoltre, alcuni aspetti del
linguaggio sono stati volutamente lasciati informali e possono essere completati dal progettista, in base alle preferenze, in accordo con il tipo di applicazione che si sta progettando, con il linguaggio di programmazione che verr utilizzato per l'implementazione,
etc. Soprattutto a patto che il lettore conosca un linguaggio di programmazione modula-

re quale C++, Modula-2, Ada o Java - , la notazione non dovrebbe aver bisogno di molte
spiegazioni.
Assumiamo che un modulo possa esportare qualsiasi tipo di risorsa: una variabile, un
tipo, una procedura, una funzione o qualsiasi altra entit definita dal linguaggio. Come abbiamo accennato, vengono utilizzati commenti per fornire informazioni di natura semantica circa i servizi esportati. In particolare, i commenti sono utilizzati per specificare il protocollo che il cliente deve seguire affinch i servizi esportati vengano forniti in maniera corretta. Ad esempio, il protocollo potrebbe richiedere che una data operazione, che esegue l'i
nizializzazione del modulo, sia chiamata prima di qualsiasi altra operazione. Oppure, il protocollo potrebbe richiedere che i clienti di un modulo per la gestione di una tabella non inseriscano voci se questa risulta essere gi piena.
In generale, se un modulo necessita che venga seguito un protocollo speciale per poter richiedere i servizi esportati, allora questo requisito dovrebbe essere specificato sotto forma di un commento associato alla descrizione sintattica del servizio esportato nell'interfaccia del modulo. Anche se riportato informalmente, il protocollo una parte essenziale del
contratto tra i clienti del modulo e l'implementatore del modulo, e dovrebbe essere concordato
tra i progettisti e gli utenti di tali moduli.
I commenti sono utilizzati anche per descrivere la natura esatta della risorsa esportata,
una volta che il protocollo richiesto stato soddisfatto dai clienti. Infine, i commenti vengono utilizzati per specificare aspetti dell'interfaccia che non corrispondono a risorse computazionali, quali le funzioni e le variabili, ma a tempi di risposta o altri aspetti. Le limitazioni di tempo e qualsiasi altro tipo di limitazioni aggiuntive o di propriet delle entit
esportate possono essere specificate mediante commenti in linguaggio naturale.
Le parti della descrizione di un modulo fin qui discusse definiscono l'interfaccia, ovvero ci che visibile ai moduli client. T D N , in aggiunta, supporta la descrizione di altri
aspetti dell'architettura che possono risultare necessari per la sua documentazione. In particolare, una parte u s e s specifica i nomi dei moduli utilizzati (se esistono), e una parte
i m p l e m e n t a t i o n fornisce una descrizione di alto livello dell'implementazione, il che
potrebbe risultare utile per capire il fondamento logico del modulo. Tipicamente, la parte
i m p l e m e n t a t i o n fornisce un elenco dei componenti interni, in accordo a I S C O M P O S E D _ O F . Usando commenti informali possiamo descrivere anche quali segreti sono incapsulati all'interno del modulo e perch. Questa parte potrebbe rappresentare una linea guida per l'implementazione, oppure potrebbe documentare, dopo lo sviluppo, le importanti
scelte implementative operate. In ogni caso, non riguarda i clienti.
La Figura 4.6 fornisce un esempio di descrizione di un modulo mediante l'uso di TDN.
Il lettore invitato a leggerlo attentamente prima di procedere, osservando che la descrizione T D N non specifica un modulo preso singolarmente, ma piuttosto un modulo che parte di un'architettura.
Ci che caratterizza un modulo dal punto di vista dei clienti (e cio la sua interfaccia)
esattamente ci che appare nella sezione e x p o r t s . Il resto della descrizione non ha a che
vedere con l'interfaccia, ma serve a documentare l'architettura in modo preciso. Dunque,
un cambiamento nella clausola e x p o r t s avr delle conseguenze sulla correttezza funzionale dei clienti, mentre eventuali cambiamenti nelle altre sezioni non ne avranno.
I benefici dell'utilizzare una notazione di progettazione come TDN, invece di una descrizione non strutturata e colloquiale, non risiedono solo nel rigore e nella precisione di ta-

module
uses

Y, Z

exports
type

var

A:

integer;

B: a r r a y

procedure

( 1 . . 1 0 ) of

(D:

in out

real ;

B; E:

in

integer;

F:

in

real);

Esempio
di descrizione
facoltativa,
in linguaggio
naturale,
di cosa sono realmente
A, B e C, insieme
a possibili
limitazioni
o propriet
che i clienti
devono
conoscere;
ad esempio,
potremmo
specificare
che gli oggetti
di tipo B spediti
alla procedura
C
debbano
essere
inizializzati
dal cliente
e non contenere
mai
solo
zero.
implementation
Se servono,
seguono
commenti
generali
della modu1arizzazione,
suggerimenti
is c o m p o s e d

of

R,

circa
il fondamento
sull'implementazione,

logico
etc.

end X

Figura 4.6

Esempio di d e s c r i z i o n e di un m o d u l o mediante T D N .

le notazione, ma anche nel fatto che la descrizione del progetto diventa controllabile in
termini di correttezza e completezza. Il controllo pu essere svolto manualmente, esaminando
attentamente la descrizione testuale, o meccanicamente nel caso venisse fornito un tool specifico in grado di eseguirlo.
Nell'esempio della Figura 4.6, i moduli R e T dovranno prima o poi essere definiti; qualora ci non avvenisse avremmo un caso di manifesta incompletezza. Dato che R e T sostituiscono a tutti gli effetti X, uno dei due o entrambi devono utilizzare Y o Z o entrambi, altrimenti la clausola u s e s sarebbe errata. Oltre a importare da Y o Z, R e T possono importare l'uno dall'altro. Inoltre, ci che X esporta dovrebbe essere un sottoinsieme dell'unione
degli insiemi di risorse esportate da R e T4. Tutti questi obblighi dovrebbero essere controllati per accertare la coerenza e la completezza della descrizione. Una descrizione corretta dei
moduli R e T fornita nella Figura 4.7.
La clausola u s e s descrive, all'interno della specifica di un modulo, esattamente la relazione U S E S introdotta nel Paragrafo 4 . 2 . 1 . 1 . La clausola indica semplicemente che un modulo pu accedere a qualsiasi risorsa esportata da un altro modulo. Potrebbe essere utile raffinare la clausola u s e s indicando esattamente quali risorse vengono importate dal modulo.
Se ci dovesse essere richiesto, useremmo la notazione
uses

<nome_del_modulo>

imports

(<elenco_dei_nomi_de1le_risorse> ) ;

Se nessuna clausola i m p o r t s viene specificata, tutte le risorse esportate possono essere importate dal modulo. Un esempio di un modulo W che usa i moduli X e XX fornito nella
Figura 4.8. L'esempio dimostra come W importi risorse specifiche da X (selettive import,
import selettivo), e tutte le risorse esportate da XX.

Per semplificare a s s u m i a m o c h e gli insiemi esportati da R e T siano disgiunti.

module
uses

exports

var
type

K: r e c o r d
B: a r r a y

procedure

...

end;

( 1 . . 1 0 ) of

(D:

in out

real;

B; E:

in

integer;

F:

in

real);

i m p l e m e n t a t ion

end R

module
uses

Y,

Z, R

exports

var A:

integer;

implementation

end

Figura 4 . 7

Esempio di c o m p o n e n t i del m o d u l o x nella Figura 4 . 6 .

Quando facciamo riferimento a un'entit E esportata da un modulo M, possiamo


usare la cosiddetta dot notation M. E o, se non sorge alcuna ambiguit, semplicemente E.
Potremmo continuare ad aggiungere nuove caratteristiche a T D N e a definire tutti i dettagli sintattici e semantici. Ad esempio, se un modulo facesse uso di diversi altri moduli e da loro importasse risorse aventi lo stesso nome (all'interno dei corrispettivi moduli
esportatori), il linguaggio potrebbe fornire un modo per risolvere i conflitti di nome rinominando le risorse importate. Non seguiremo questa direzione, in modo da mantenere la notazione T D N il pi possibile concisa e generale. Aggiungendo caratteristiche

module
UB6S

X imports

(B,

C),

XX
exports

...

implementation

end W

Figura 4.8

Esempio di un m o d u l o c o n import selettivo.

nuove a T D N , la renderemmo pi simile ai linguaggi di programmazione, e questo limiterebbe il suo carattere generale. Lasciamo la possibilit di aggiungere nuove caratteristiche al
progettista, qualora dovesse risultare utile.
Se T D N dovesse essere utilizzata solo con un determinato linguaggio di programmazione, sarebbe possibile estenderlo con alcune caratteristiche specifiche a questo
linguaggio. Ma ci deve essere fatto con attenzione, visto che una notazione di progettazione utile dovrebbe rimanere lontana dai dettagli di basso livello di un linguaggio di
programmazione.
ESEMPIO 4 . 6

Gli Esempi 4.1 e 4.2 hanno introdotto il problema di scrivere un interprete per il linguaggio di programmazione MINI. Ora affronteremo il problema della definizione di un compilatore per MINI. Una possibile architettura potrebbe essere la seguente:
module

COMPILER

exports

procedure

MINI

(PR0G:

in

file

of

char;

C O D E : out file of c h a r ) ;
MINI chiamato
a compilare
il programma
contenuto
produrre
il codice
oggetto
nel file
COD
implementation

in PROG

e a

Un'implementazione
convenzionale
di compilatore.
ANALYZER
esegue
un'analisi
lessicale
e sintattica
e produce
un albero
astratto
e una serie
di voci nella
tabella
dei simboli;
CODE_GENERATOR
genera
codice
a partire
dall'albero
astratto
e dalle
informazioni
immagazzinate
nella
tabella
dei simboli.
Il modulo
MAIN
svolge
il compito
di supervisione
(job
coordinator).
is c o m p o s e d of A N A L Y Z E R , S Y M B O L T A B L E ,
ABSTRACT_TREE_HANDLER,
end

CODE_GENERATOR,

MAIN

COMPILER

I moduli MAIN, ANALYZER e CODE GENERATOR sono specificati nel seguente modo:
module

MAIN

uses ANALYZER,
exports

C0DE_GENERAT0R

procedure

MINI

(PROG:
CODE:

end

file

of

char;

file

of

char);

MAIN

module
uses

in
out

ANALYZER

SYMBOL_TABLE,

exports

procedure

ABSTRACTTREEHANDLER
ANALYZE

(SOURCE:

in

file

of

Viene analizzato
il codice
sorgente
(SOURCE).
albero
astratto
usando
i servizi
forniti
dal
e vengono
riconosciute
le entit,
con i loro
immagazzinati
nella
tabella
dei
simboli.

char);
Viene prodotto
un
gestore
dell'albero
attributi

end

ANALYZER

module

CODE_GENERATOR

uses SYMBOL_TABLE,
exports procedure

ABSTRACT_TREE_HANDLER
CODE

(OBJECT: out

file of

char);

L'albero
astratto
viene attraversato
usando le
operazioni
esportate
dal modulo ABSTRACT_TREE_HANDLER
e accedendo
alle
informazioni
contenute
nella tabella dei simboli in modo da
generare
il codice nel file di
output.

end

C0DE_GENERAT0R

Il lettore invitato a completare la descrizione dei moduli rimanenti come esercizio. In


particolare, per il modulo della tabella dei simboli, suggeriamo un ripasso degli Esempi
4.1 e 4.2.

Esercizio
4.17

La s t r u t t u r a del m o d u l o descritto nella Figura 4 . 6 rappresenta u n a gerarchia? Se n o n la rappresenta, c o m e si p o t r e b b e convertirla in u n a gerarchia? Se invece la rappresenta, c o m e si p o trebbe convertirla in u n a s t r u t t u r a n o n gerarchica?

4.2.3.2

GDN: notazione grafica per la progettazione

La ragione per cui gli ingegneri adottano frequentemente notazioni grafiche per i loro progetti che sono pi intuitive e pi facili da capire rispetto alle descrizioni testuali, secondo
il detto che un'immagine vale mille parole. In questo paragrafo, forniremo una notazione
grafica per la progettazione (GDN) che rifletta la descrizione testuale (TDN) definita nel
Paragrafo 4.2.3 e che assomigli alle notazioni grafiche cui abbiamo fatto riferimento in precedenza per descrivere le relazioni tra moduli.
Un modulo rappresentato da una scatola, le cui frecce in ingresso rappresentano la
sua interfaccia (ad esempio, le risorse esportate). La ragione per cui le risorse esportate vengono indicate con frecce entranti che le risorse esportate sono accessibili da fuori; ovvero,
rappresentano un percorso di accesso verso l'interno del modulo.
La Figura 4.9 fornisce una descrizione grafica del modulo x descritto testualmente nella Figura 4.6. Il fatto che X usi i moduli Y e Z viene indicato con frecce in neretto che connettono X con Y e Z. I dettagli delle risorse esportate come il numero di parametri per le
procedure, i loro tipi e i tipi delle variabili - vengono omessi per comodit, ma possono essere aggiunti come notazioni sulle frecce entranti.
Una scatola vuota se il modulo elementare, ovvero se non composto da sottocomponenti. Questo non il caso del modulo X, che composto da R e T. Siccome i mo-

Figura 4.9

Rappresentazione grafica del modulo x della Figura 4.6.

duli R e T sono componenti di x, possiamo espandere la loro definizione dentro x, secondo la Figura 4.7; il risultato la descrizione rappresentata nella Figura 4.10.
La Figura 4.10 mostra esplicitamente quali moduli interni diano origine effettivamente
alle risorse esportate dal modulo X: B e C sono forniti da R, e A fornito da T. Questa rela-

Figura 4.10

II modulo X composto dai moduli R e i .

Modulo L

Modulo N

Modulo M

Modulo R

Modulo S

Modulo M
Modulo H Modulo G

Figura 4.11

II modulo M membro sia di L che di N.

zione illustrata graficamente usando linee tratteggiate per unire le frecce di esportazione
del modulo X con le corrispondenti frecce di esportazione dei moduli R e T. In maniera simile, linee tratteggiate in neretto vengono impiegate per specificare chi, tra R e T, faccia effettivamente uso dei moduli usati da X.
Se un modulo M componente di entrambi i moduli L e N allora disegniamo una scatola intitolata M all'interno sia di L che di N. Se M dovesse essere composto da altri moduli,
la struttura I S _ C O M P O S E D _ O F verrebbe descritta separatamente. Ci illustrato nella Figura
4.11 per il caso in cui M sia composto da G e H.
Esercizi
4.18

Fornite una descrizione T D N del m o d u l o T nella Figura 4.10.

4.19

Descrivete la struttura del m o d u l o nella Figura 4.6, utilizzando G D N .

4.20

Usando T D N , descrivete la struttura del m o d u l o nella Figura 4.6.

4.2.4

Categorie di moduli

I moduli possono essere progettati per esportare qualsiasi combinazione di risorse (variabili, tipi, procedure, eventi, eccezioni, etc.). Ovviamente la natura delle risorse esportate dipende anche da cosa effettivamente supportato dal linguaggio di programmazione utilizzato per implementare il modulo. In generale, i moduli possono essere classificati in categorie standard.
Una tale categorizzazione utile in quanto fornisce uno schema di classificazione per la documentazione ed, eventualmente, per il ritrovamento all'interno di una libreria di moduli. Inoltre,
usare un insieme limitato di categorie di moduli rende un progetto pi uniforme e standard.
Come abbiamo visto nel Capitolo 2, parti standard sono l'indicazione di una disciplina inge-

gneristica matura, e quindi la categorizzazione dei moduli rappresenta un passo verso lo sviluppo di componenti standard per l'ingegneria del software.
In questo paragrafo illustreremo tre categorie standard: le astrazioni procedurali, le librerie e i pool di dati comuni. Due categorie pi generali e di pi alto livello saranno illustrate nei successivi paragrafi: oggetti astratti e tipi di dati astratti.
Un tipo di modulo che viene comunemente usato quello che fornisce semplicemente una procedura o una funzione che implementa alcune operazioni astratte. In altre parole, tali moduli forniscono uri astrazione procedurale che viene utilizzata per incapsulare un algoritmo. Tipici esempi sono i moduli di ordinamento, i moduli per la trasformata di Fourier
e i moduli che effettuano la traduzione da un linguaggio a un altro. L'utilit delle astrazioni procedurali stata riconosciuta fin dalle origini dell'informatica e i linguaggi di programmazione hanno fornito un supporto speciale attraverso l'uso delle funzioni.
Un modulo pu anche contenere un gruppo di astrazioni procedurali correlate tra loro. Un esempio tipico rappresentato dalle librerie di funzioni matematiche, che forniscono soluzioni ai problemi matematici pi comuni, come quelli riguardanti integrali e derivate. Un altro esempio pu essere quello di una libreria che fornisca funzioni per le operazioni algebriche sulle matrici. Ancora un altro esempio potrebbe essere quello di una libreria per la manipolazione grafica di oggetti. Moduli di questi tipi vengono usati per riunire
in un unico pacchetto un insieme di funzioni. Usiamo il termine libreria per indicare questa classe di moduli.
Un altro tipo comune di modulo fornisce un pool comune di dati. Una volta riconosciuta la necessit di condividere dati tra diversi moduli, possiamo raggruppare questi dati
in un pool comune che viene importato da tutti i moduli client, i quali possono dunque
manipolare i dati direttamente, in accordo con la struttura utilizzata per rappresentarli, che
diventa a loro visibile.
Un uso interessante di un modulo di pool comune di dati quello per cui si raggruppano in un unico luogo le costanti di configurazione di un sistema. Ad esempio, supponiamo che il supervisore di un sistema di controllo sia parametrizzato rispetto al numero di linee e alla lunghezza del buffer in cui vengono immagazzinati temporaneamente gli input.
Ogni installazione del sistema di controllo richiede che vengano assegnati valori costanti a
questi parametri, cui accedono i diversi moduli che compongono il sistema di controllo. Una
soluzione tipica potrebbe consistere nel raggruppare tutte le costanti di configurazione in un
pool comune di dati cui accedere con facilit durante la fase di configurazione.
In generale, comunque, un pool comune di dati rappresenta un modulo di livello piuttosto basso. Tale modulo non fornisce alcuna forma di astrazione, tutti i dettagli dei dati sono
visibili e manipolabili dai clienti. La possibilit di raggruppare dati condivisi in un blocco comune fornisce semplicemente un aiuto limitato in termini di leggibilit e modificabilit.
Stabilire pool comuni di dati viene implementato facilmente nei linguaggi di programmazione convenzionali. Per esempio, pu essere realizzato in FORTRAN mediante il
costrutto C O M M O N , o in C e Java con l'uso delle variabili statiche.
La maggior parte degli esempi forniti nei Paragrafi 4.2.2 e 4.2.3, tuttavia, necessitano di
moduli pi astratti che possano nascondere strutture dati particolari, rendendole segreti dei moduli. Ad esempio, il modulo della tabella dei simboli usato nell'interprete (Esempio 4.1 ed
Esempio 4.2) e nel compilatore (Esempio 4.6) del linguaggio MINI nasconde la struttura dati specifica utilizzata per rappresentare la tabella ed esporta le operazioni necessarie per acce-

dervi. Questo modulo un esempio di un importante tipo di moduli, che uniscono in un solo pacchetto sia dati sia funzioni (che verr affrontato in seguito).
4.2.4.1

Oggetti astratti

Abbiamo gi accennato al fatto che il 17 per cento dei costi della manutenzione del software
derivi dai cambiamenti nelle rappresentazioni dei dati. Dunque, un tipo di incapsulamento molto importante quello che nasconde i dettagli delle rappresentazioni dei dati e protegge i clienti dai cambiamenti che potrebbero essere effettuati nei confronti di queste
rappresentazioni.
Una tabella dei simboli un tipico modulo che nasconde la struttura dati (come fosse un segreto) ed esporta funzioni che possono essere utilizzate come operazioni per accedere ai dati nascosti e modificare i valori immagazzinati in essi. Dovesse cambiare la struttura dati, tutto ci che dovremmo cambiare sarebbero gli algoritmi che implementano le
funzioni di accesso, mentre i moduli client non dovrebbero essere cambiati, visto che continueranno a usare le stesse chiamate per eseguire gli accessi richiesti.
Dal punto di vista delle interfacce, questi tipi di moduli assomigliano a librerie.
Possiedono, per, una propriet speciale che le librerie matematiche non hanno: dispongono di una struttura dati permanente, nascosta e incapsulata nella loro parte implementativa, visibile alle funzioni interne al modulo ma nascosta dai moduli client. Nell'esempio della tabella simbolica, la struttura dati viene utilizzata per immagazzinare le voci, man mano
che sono inserite nella tabella.
La struttura dati nascosta fornisce a questi moduli uno stato. Infatti, come conseguenza delle chiamate alle funzioni esportate dal modulo, i valori immagazzinati nella struttura
potrebbero cambiare da una chiamata all'altra; dunque, i risultati ottenuti da due chiamate
con gli stessi parametri possono differire. Questo comportamento diverso da quello di un
pool di funzioni che costituiscono una libreria, in quanto la libreria non possiede uno stato: due chiamate successive alla stessa funzione con gli stessi parametri restituiranno sempre
lo stesso risultato.
La differenza tra un modulo con stato e moduli librerie non visibile attraverso la sintassi dell'interfaccia. In entrambi i casi il modulo esporta semplicemente un insieme di funzioni. Distingueremo, comunque, tra queste due tipologie di moduli nel nostro schema di
classificazione. Moduli che esibiscono uno stato verranno chiamati oggetti astratti. Useremo
un commento per indicare che un modulo un oggetto astratto.
ESEMPIO 4 . 7

Le espressioni aritmetiche possono essere scritte senza l'uso di parentesi impiegando la cosiddetta notazione postfissa polacca, dove gli operatori seguono i loro operandi. Un esempio di espressione scritta in forma postfissa polacca
a b c + * ,

che corrisponde all'espressione infissa


a *

(b+c).

Restringiamo la nostra attenzione alle espressioni aritmetiche con sole operazioni binarie e
operandi interi. Inoltre, assumiamo che la stringa di input sia un'espressione postfissa dalla
sintassi corretta.

Un modo per valutare le espressioni postfisse quello di utilizzare una struttura last-in,
first-out, ovvero una pila (stack). L'espressione viene letta da sinistra verso destra e i valori degli operandi sono depositati sulla pila man mano che vengono incontrati. Quando il simbolo letto un operatore, i due valori in cima alla pila sono prelevati, l'operatore applicato ai
due operandi e il risultato inserito in cima alla pila. Come esempio, il lettore invitato a simulare manualmente la valutazione dell'espressione a b c + *,dove a=2 , b=3 e c = 5 .
Le pile possono essere implementate in diversi modi, come pu illustrare un qualsiasi
libro di testo sulle strutture di dati. Se la loro dimensione limitata possiamo utilizzare un
array, altrimenti potremmo utilizzare una lista concatenata.
Se volessimo incapsulare una pila all'interno di un modulo potremmo definire la seguente interfaccia:
exports
procedure

PUSH

procedure

P0P_2

(VAL:

in

(VALI,

integer);
V A L 2 : out

integer);

La procedura PUSH verrebbe utilizzata per inserire un nuovo operando in cima alla pila; la
procedura POP_2 verrebbe utilizzata per estrarre la coppia di operandi che si trova in cima
alla pila.
La parte nascosta del modulo potrebbe allora scegliere una struttura dati qualsiasi per
rappresentare la pila; la struttura dati risulterebbe un segreto del modulo.

La progettazione dell'oggetto astratto descritto nell'Esempio 4.7 potrebbe essere criticata in


quanto fornisce un'operazione specializzata per rimuovere contemporaneamente due elementi dalla pila. Questo approccio utile quando abbiamo solo operatori binari, ma fallisce nel momento in cui estendiamo il valutatore di espressioni a casi pi generali dove possiamo avere anche operatori unari. Per poter trattare sia operatori binari che unari, si potrebbe fornire un'operazione di POP che estrae un elemento alla volta per poi lasciare che il
modulo client esegua l'operazione due volte quando necessario. Un altro difetto dell'Esempio
4.7 che il progetto si basa sull'assunzione che l'espressione da valutare sia corretta. Se ci
non dovesse essere vero, verrebbe generato un errore a run-time qualora tentassimo, ad esempio, di estrarre un elemento da una pila vuota.
Un progetto pi affidabile definirebbe un'altra funzione da esportare nell'interfaccia,
chiamata per esempio EMPTY, la quale restituirebbe un risultato booleano per indicare se la
pila fosse vuota. Ovviamente, questo progetto non previene l'errore a run-time, ma fornisce al cliente un modo per evitarlo. Notate come una soluzione del genere richieda pi lavoro da parte del cliente, ma il prezzo da pagare per rendere il nostro progetto maggiormente riutilizzabile e affidabile.
Esercizi
4.21

Riprogettate l'interfaccia del m o d u l o pila affinch prenda in considerazione i commenti


appena fatti. Discutete, inoltre, l'uso di una struttura dati di dimensioni fisse per l'implementazione della pila. In base a quali premesse risulterebbe corretta l'interfaccia? Il modulo risulterebbe veramente riusabile? Se cos non fosse, come p o t r e m m o migliorare la sua
riusabilit?

4.22

U n m o d u l o di o u t p u t utilizzato per la s t a m p a di caratteri singoli. Il cliente vede che l'outp u t avviene un carattere alla volta. Il m o d u l o di o u t p u t , per, n a s c o n d e il m o d o esatto in cui
viene eseguito l ' o u t p u t . C i p e r m e t t e la progettazione di u n a famiglia di p r o d o t t i dove i m e m bri differiscono per il tipo di periferica cui rivolgono il loro o u t p u t . Alcune periferiche eseg u i r a n n o l ' o u t p u t carattere per carattere, m e n t r e altre r i u n i r a n n o p i caratteri in sequenze pi
l u n g h e e a g g i u n g e r a n n o caratteri di controllo. Classifichereste questo m o d u l o di o u t p u t com e un'astrazione procedurale o u n o g g e t t o astratto? I m p o s t a t e le descrizioni T D N e G D N
del m o d u l o di o u t p u t nel caso in cui l ' o u t p u t fisico sia eseguito da u n m o d u l o (hardware) c o n
un b u f f e r lungo 16 caratteri.

4.2.4.2 Tipi di dati astratti


In questo paragrafo introdurremo i moduli per la descrizione di tipi di dati astratti, un'ulteriore categoria di moduli che ci possono aiutare nella strutturazione uniforme e standard
dei nostri progetti. Useremo l'Esempio 4.7 per motivare l'introduzione di questa nuova categoria. L'esempio faceva uso di un oggetto astratto rappresentante una pila. Ma cosa succederebbe nel caso in cui un'applicazione dovesse richiedere pi di una pila? In questa situazione avremmo bisogno di definire un tipo per poi generarne diversi esemplari. Avremmo
bisogno anche di un sistema (a) per associare un insieme di funzioni con un tipo, in modo
da poter manipolare gli esemplari di quel tipo, e (b) per incapsulare i dettagli del tipo all'interno del modulo, in modo che possa essere modificato senza conseguenze per l'interfaccia. La Figura 4.12 illustra questo tipo di modulo mediante l'uso della nostra notazione
di progettazione testuale.
Un nuovo espediente notazionale introdotto nella figura: il simbolo "?", che viene
utilizzato per esportare la definizione di un tipo, celando i dettagli della struttura dati corrispondente, posta nella parte implementativa del modulo. Il fatto che venga esportato un
tipo permette ai moduli client di dichiarare variabili di quel tipo; il fatto che rimanga nascosta la definizione del tipo implica, per, che le variabili di quel tipo possono essere manipolate solamente da procedure o da funzioni esportate dal modulo, visto che sono le sole

module

STACKHANDLER

exports
type STACK = ? ;
Questo un modulo di tipo di dato
un segreto nascosto
nella parte
procedure PUSH
procedure POP
function

EMPTY

(S: in out

STACK

(S: in out STACK


(S: in STACK)

astratto ; la struttura
implementativa.

; VAL: in

integer);

; VAL: out

integer);

: BOOLEAN;

end STACK

HANDLER

Figura 4.12

Modulo di tipo di dato astratto in T D N .

dati

a "conoscerne" i segreti. I moduli client dovranno passare le variabili del tipo in questione
alle funzioni esportate perch possano essere manipolate.
Un modulo tipo di dati astratto un modulo che esporta un tipo, insieme alle operazioni necessarie ad accedere e manipolare oggetti di quel tipo; inoltre, cela la rappresentazione del tipo e gli algoritmi utilizzati all'interno delle operazioni. Tale modulo pu essere
realizzato direttamente in Ada esportando un tipo privato (limitato), in Modula-2 esportando un tipo opaco, e in Java e C++ mediante l'uso di classi.
Gli esemplari di un tipo di dato astratto sono oggetti astratti che si comportano esattamente come quelli precedentemente discussi. In particolare, possono essere manipolati solo dalle funzioni implementate ed esportate dal modulo tipo di dato astratto5. Tali funzioni possono includere quelle necessarie per assegnare un oggetto astratto a una variabile e quelle necessarie per confrontare due oggetti astratti e verificare se sono uguali. Per semplificare
la notazione, invece di elencare questi operatori insieme alle funzioni esportate, useremo per
loro gli operatori convenzionali ": = " e "=" ed elencheremo gli operatori dopo il "?" nella
clausola di tipo. Dunque, scrivere
type

A_TYPE:

(:=,

=);

all'interno dell'interfaccia di un modulo significa che i clienti possono assegnare un oggetto del tipo A T Y P E a una variabile del medesimo tipo e confrontare due oggetti del tipo
A T Y P E per vedere se sono uguali. Se il simbolo ": = " o "=" manca nella dichiarazione del
tipo, la corrispondente operazione non consentita ai client.
ESEMPIO 4 . 8

Supponiamo di voler progettare un sistema per la simulazione di un'area di servizio. L'obiettivo


del sistema sar quello di scoprire la "dimensione ottimale" (in termini di numero di linee
di servizio, lunghezza delle linee, etc.) dell'area di servizio, conoscendo la frequenza di arrivo delle auto e l'entit delle richieste di servizio. Ogni richiesta di servizio sar caratterizzata da una certa durata.
Rappresentiamo ogni linea di servizio (benzina, auto-lavaggio, etc.) con un oggetto astratto che rappresenti le auto in attesa di essere servite. Ci sar un'operazione per aggiungere
un'auto alla linea di servizio, una per estrarre un'auto dalla linea, una per vedere se la linea
vuota e una per unire due linee associate allo stesso tipo di risorsa, qualora dovesse esaurirsi la risorsa fornita da una di esse. La politica sar rigorosamente first-in, first-out (FIFO)
per tutte le linee di servizio.
Introduciamo un modulo tipo di dato astratto chiamato F I F O C A R S che descrive
la coda FIFO delle auto. Assumiamo che le auto siano descritte da un altro modulo di tipo di dato astratto C A R S , il quale esporti il tipo CAR, utilizzato da F I F O C A R S per eseguire le operazioni sulle auto estratte dalle code. Ci che segue una bozza del modulo
FIFO

CARS:

5
L'unica differenza sintattica nel caso di un esemplare di un tipo di dato astratto che l'oggetto a cui
deve essere applicata un'operazione un parametro dell'operazione.

nodale
uses

FIFOCARS

CARS

exports
type

QUEUE

procedure

: ?;
ENQUEUE

procedure DEQUEUE
function IS_EMPTY
function LENGTH
procedure MERGE
Q u e s t o un modulo

(Q:

in out

QUEUE;

C:

in

(Q: in out Q U E U E ; C: out


(Q: in Q U E U E ) : B O O L E A N ;

CARS);
CARS);

(Q: in Q U E U E ) : N A T U R A L ;
(Ql, Q 2 : in Q U E U E ; Q: out Q U E U E ) ;
tipo di dato astratto
che rappresenta

una

coda

di automobili,
gestita
rigorosamente
secondo
una politica
FIFO;
le code non p o s s o n o essere
assegnate
o confrontate
per vedere
se
s o n o uguali,
visto che
e "=" non vengono
esportati.

end

FIFOCARS

Questo modulo permette ad altri moduli di dichiarare esemplari del tipo


esempio
gasolinel,
carwash:

gasoline_2,

gasoline_3:

QUEUE,

come ad

QUEUE;

QUEUE;

e operare su di loro usando le operazioni esportate. Per esempio, potremmo scrivere


ENQUEUE
MERGE

(car_wash,

(gasolinel,

thatcar);
gasoline_2,

gasoline_3);

Esiste un motivo importante per distinguere tra moduli oggetti astratti e moduli tipi di dati astratti, anche se un oggetto astratto pu indubbiamente essere ottenuto generando un
esemplare del tipo di dato astratto incapsulato nel modulo. La ragione che, intrinsecamente,
un modulo tipo di dato astratto pu generare un qualsiasi numero di istanze, mentre sappiamo a priori che oggetti astratti esistono solo in un singolo esemplare. Inoltre, un modulo oggetto astratto possiede uno stato, mentre un modulo tipo di dato astratto non lo possiede. Nei linguaggi orientati agli oggetti, i due concetti sono congiunti, in quanto i tipi di
dati astratti vengono implementati da classi e gli oggetti astratti esistono solo al momento
dell'esecuzione, come esemplari di un tipo di dato astratto.

Esercizio
4.23

U n m o d u l o p e r la g e s t i o n e d i c h i a v i f o r n i s c e u n a c h i a v e o g n i v o l t a c h e n e v i e n e r i c h i e s t a u n a
d a u n c l i e n t e . P u i n o l t r e c o n f r o n t a r e d u e c h i a v i p e r v e d e r e se s o n o u g u a l i e d e t e r m i n a r e q u a le delle d u e sia p i p i c c o l a . P r o g e t t a t e il m o d u l o d i g e s t i o n e d e l l e c h i a v i u s a n d o T D N .

4.2.4.3

Moduli generici

In questo paragrafo introdurremo un'estensione di T D N in grado di aiutare nella produzione di componenti riusabili. L'estensione, chiamata moduli generici, pu essere motivata
tornando all'Esempio 4.7. In quell'esempio, le scelte di come immagazzinare i valori e di

come gestire la struttura LIFO venivano nascoste ai clienti mediante un'interfaccia che elencava le operazioni appropriate da invocare. Se volessimo valutare espressioni di altri tipi, ad
esempio con valori reali o booleani, dovremmo fornire moduli nuovi, specializzati. Tutti questi moduli si comporterebbero, comunque, in maniera simile, differenziandosi solo per i tipi
di valori immagazzinati nei loro stack.
Sarebbe utile poter fornire una singola descrizione (astratta) per tutti i moduli che implementano un oggetto astratto, fattorizzando tutte le variazioni in un singolo modulo, invece di duplicarle in una serie di moduli quasi identici. Fornendo una sola descrizione per
tutti i moduli, eliminiamo la possibilit (e il rischio) di avere inconsistenze tra i diversi moduli; inoltre, confiniamo le possibili modifiche a una sola unit. Otteniamo, cos, un singolo componente pi facilmente riusabile.
Una soluzione a questo problema viene fornita estendendo T D N in modo che possa
supportare moduli generici. Un modulo generico un modulo che viene parametrizzato rispetto a un tipo. Nel nostro caso scriveremmo
generic
uses

module

GENERIC_STACK_2

(T)

. . .

exports

end

procedure

PUSH

procedure

P0P_2

(VAL

: in

(VALI,

T);

VAL 2

: out

T);

GENERIC_STACK_2

In questo esempio, il modulo G E N E R I C _ S T A C K _ 2 risulta essere generico rispetto al tipo T: le


funzioni PUSH e P 0 P _ 2 , inoltre, richiedono parametri di quel tipo. Un modulo generico non
direttamente utilizzabile dai clienti. Infatti, in senso stretto, non neppure un modulo, ma
piuttosto un template di modulo. Per poterlo utilizzare, deve prima essere stanziato fornendo
parametri reali. Ad esempio, per stanziare un modulo di stack per interi, scriveremmo
module

INTEGER_STACK_2

is

GENERIC_STACK_2

(INTEGER)

Se dovessero esistere limitazioni sui possibili tipi utilizzabili come parametri al momento di
istanziazione del modulo, queste dovrebbero essere specificate nell'interfaccia del modulo generico mediante l'uso di commenti.
Se il modulo generico dovesse richiedere il supporto a una data operazione da parte del
tipo di parametro, ci dovrebbe essere specificato nell'intestazione del modulo. Ad esempio,
generic
uses

end

module

M(T)

with

OP(T)

. . .

indicherebbe che l'operazione OP deve essere supportata da un qualsiasi parametro passato


al modulo M al momento dell'istanziazione. Sempre al momento dell'istanziazione, deve essere passata una procedura come parametro insieme al tipo, come nella dichiarazione:

modulo

M_A_TYPE

is M ( A _ T Y P E )

PROC

(M A

TYPE)

Come hanno dimostrato i precedenti esempi, i moduli generici consentono ai progettisti


software di fattorizzare diversi algoritmi all'interno di una singola rappresentazione, astratta e generica, che deve essere stanziata prima dell'uso. Un esempio tipico sarebbe un modulo generico di ordinamento parametrico rispetto al tipo di elementi da ordinare. Quindi
un modulo generico , intrinsecamente, un componente riusabile in quanto fattorizza diversi moduli in un'unica astrazione algoritmica, facile da riutilizzare in contesti diversi.
Situazioni simili possono nascere nel caso di tipi di dati astratti, i quali spesso possono
essere scritti come moduli generici, e poi stanziati in vari moduli specializzati. Cos, nell'Esempio
4.8, abbiamo introdotto un modulo per la rappresentazione di code FIFO di automobili.
Supponiamo ora di voler modellizzare le code in banca, dove i clienti si mettono in fila, in attesa di essere serviti. In entrambi i casi dobbiamo descrivere che cosa sia una coda; l'unica differenza risiede nel tipo di oggetti che inseriamo in coda. Possiamo dunque, ancora una volta,
risolvere il problema definendo un modulo tipo di dato astratto generico (chiamandolo GENERIC FIFO QUEUE) per poi generare le istanze di modulo necessarie.

L'uso di moduli generici pu essere visto come un'applicazione del principio di generalit. Ad esempio, invece di risolvere un problema specifico per gli interi, risolviamo un problema pi generico per una classe di tipi. Soluzioni specializzate possono essere successivamente derivate dalla soluzione generale. Visti cos, i moduli generici possono risultare utili
per lo sviluppo di famiglie di programmi.
Esercizi
4.24

Definite precisamente il m o d u l o G E N E R i c _ F i F O _ Q U E U E e stanziate un m o d u l o che rappresenti il tipo di dato astratto "coda di valori interi". Dimostrate, quindi, come si possa successivamente generare un esemplare di oggetto astratto.

4.25

Abbiamo descritto un m o d u l o generico come parametrizzato da tipi. Indicate altre possibilit


per la parametrizzazione di moduli.

4.26

Fornite un esempio di m o d u l o che consenta l'ordinamento di un array di elementi di tipo


qualsiasi. necessario che sia possibile confrontare gli elementi del tipo scelto per vedere quale sia il maggiore.

4.2.5

Tecniche specifiche per la progettazione


in vista del cambiamento

In questo capitolo abbiamo finora presentato un insieme di metodi generali che possono essere utilizzati per la progettazione di software ben strutturato, che possa essere facilmente
compreso e modificato. Questi metodi sono anche utili per il raggiungimento di due obiettivi significativi: la produzione di famiglie di programmi e la generazione di componenti riusabili. La modularizzazione mediante information hiding pu essere utilizzata per incapsulare le differenze esistenti tra i diversi membri di una famiglia, in modo che queste differenze siano invisibili al di fuori del modulo generico. In modo del tutto simile, la definizione di interfacce semplici, non ridondanti e chiare, pu favorire il riutilizzo di moduli: per
capire se un modulo riusabile, occorre fare riferimento alla sua interfaccia. Come abbiamo accennato prima, la riusabilit viene ulteriormente aumentata dalla generalit.

Come complemento al principio generale dell'information hiding e ai metodi fin qui


discussi, i paragrafi successivi illustreranno alcune tecniche specifiche di implementazione
di moduli che facilitano il cambiamento.
4.2.5.1

Costanti di configurazione

La modifica di software risulta diffcile quando informazioni specifiche, che devono essere cambiate, sono inserite e sparpagliate nel codice del programma. Come semplice esempio, consideriamo la dimensione di una tabella di interi che viene inizialmente impostata a 10, ma che si richiede diventi 50. Il sistema iniziale potrebbe contenere dichiarazioni come
a: array

(1..10) of

integer;

nel caso volessimo realizzare una copia in locale della tabella. Per controllare che un intero
k, utilizzato come indice nella tabella, non ecceda i suoi confini, il programma potrebbe contenere un'affermazione del genere
if k > 1 and k s
esegui

10

then

indicizzazione

else
esegui

altre

operazioni

end 1 f ;
Chiaramente, modificare il limite superiore dell'array a 50 richiederebbe un cambiamento
sia nelle dichiarazioni che nelle righe di codice come quelle appena mostrate. Tuttavia, nei
casi in cui i cambiamenti richiesti possono essere raggruppati in una serie di costanti (chiamate costanti di configurazione), il problema pu essere risolto cambiando i valori di quelle
costanti e ricompilando il programma.
Molti linguaggi, come C, Ada, Java e C++, forniscono le costanti simboliche come soluzione al problema di rendere i programmi facilmente adattabili al cambiamento delle
costanti di configurazione. Essendo costanti, i dati di configurazione non possono essere alterati inavvertitamente dal programma; essendo simboliche, possono possedere nomi in grado di suggerire il loro significato, migliorando cos la leggibilit e la modificabilit.
Come abbiamo accennato in precedenza, le costanti di configurazione possono essere
raggruppate in un unico modulo per fornire un pool comune di dati. Questo modulo, poi,
verrebbe utilizzato da tutti i clienti che devono accedere ai dati di configurazione.
Un altro esempio dell'uso di costanti simboliche di configurazione potrebbe essere il
caso di un gestore di un dispositivo in cui le lunghezze dei buffer possono cambiare di configurazione in configurazione. Ogni configurazione potrebbe essere vista come un diverso
membro della stessa famiglia, e i diversi membri di questa famiglia potrebbero essere generati ricompilando l'applicazione con valori diversi delle costanti di configurazione.
Esercizio
4.27

C a m b i a r e il valore di u n a costante di c o n f i g u r a z i o n e richiede la ricompilazione. E sempre necessario eseguire u n a ricompilazione completa (ad esempio, u n a compilazione di tutti i m o duli)? Discutete il p r o b l e m a f o r n e n d o esempi in C , Pascal, M o d u l a - 2 , Java, C++ o Ada.

4.2.5.2

Compilazione condizionale

Le costanti di configurazione supportano un modo molto semplice per rappresentare il


software multi-versione. Possono essere forniti schemi pi generali e flessibili con la compilazione condizionale.
Con questo approccio, tutte le versioni di una famiglia sono rappresentate da una singola copia di programma sorgente, mentre le differenze tra le varie versioni vengono prese
in considerazione dalla compilazione condizionale. Il codice sorgente rilevante solo per alcune versioni viene racchiuso da macro comandi riconosciuti dal compilatore. Quando viene invocato il compilatore, necessario che siano stati specificati alcuni parametri per la descrizione di quale versione del codice oggetto debba essere prodotta; il compilatore ignorer
automaticamente le righe di codice che non siano parte della versione corretta.
ESEMPIO 4 . 9

Supponiamo ci venga richiesto di scrivere un programma in cui alcune parti (ad esempio, i
driver per alcune periferiche) debbano essere adattate a un certo hardware. Durante la progettazione, cercheremo di tener separate queste parti da quelle che non dipendono dallo specifico hardware. Nel caso in cui il programma finale debba essere scritto nel linguaggio di
programmazione C, potremo poi utilizzare il preprocessore C per specificare quali parti debbano essere utilizzate per l'architettura hardware scelta. Ecco uno schema di come potrebbe
essere il programma C:
...frammento
#

ifdef

endif

ifdef

endif

di

codice

sorgente

comune

a tutte

le

versioni...

hardware-1
...frammento

per

hardware

1...

per

hardware

2...

hardware-2
...frammento

Se dovessimo specificare, al momento della compilazione, lo switch D =


solo il codice associato a h a r d w a r e - 1 verrebbe compilato.

hardware-1,

Esercizi
4.28

Discutete l'efficacia della compilazione condizionale nel caso in cui le differenze tra le varie
versioni dovessero diventare via via sempre pi complesse.

4.29

C o m e si p o t r e b b e r o utilizzare i costrutti generici di A d a per raggiungere gli stessi risultati


dell'Esempio 4.9, senza affidarsi alla compilazione condizionale?

4.2.5.3

La generazione del software

Le costanti simboliche e la compilazione condizionale consentono a un software di evolvere e di essere, allo stesso tempo, sufficientemente generalizzato da poter coprire alcuni cam-

biamenti previsti e in grado di essere specializzato al momento della compilazione. Un'ulteriore


strategia interessante quella di generare automaticamente una nuova soluzione per ogni
cambiamento richiesto.
I generatori sono stati utilizzati con successo in alcuni domini applicativi ristretti. Un
tipico esempio un generatore di compilatori, come yacc nell'ambiente UNIX, il quale in
grado di generare (parte di) un compilatore, data la definizione formale del linguaggio che
deve essere tradotto. Se decidessimo di cambiare il linguaggio sorgente per il quale abbiamo
sviluppato un compilatore, non dovremmo modificare direttamente il compilatore; piuttosto, rieseguiremmo yacc applicandolo alla definizione del nuovo linguaggio. Questo approccio risulta particolarmente utile quando il linguaggio sorgente non ancora "congelato", ma pu essere soggetto a modifiche.
Un ulteriore esempio un sistema per la generazione di interfacce utenti; tale sistema
pu essere facilmente trovato all'interno della maggior parte dei DBMS su personal computer. In questi sistemi, la disposizione dei pannelli utilizzati per interagire con l'utente viene "disegnata" sul monitor del computer. La descrizione dichiarativa poi automaticamente trasformata in azioni run-time che supportano l'interazione tra l'utente e l'applicazione.
Si pu quindi modificare la disposizione del monitor secondo le preferenze dell'utente, senza aggiunta di codice, mediante la semplice rigenerazione.
Verranno forniti ulteriori esempi di generatori di software nel Capitolo 5, dove illustreremo come alcuni linguaggi di specifica possano risultare eseguibili. In alcuni casi possibile tradurre la specifica in un'implementazione, generando, dunque, l'applicazione direttamente dalla descrizione astratta. Anche se, attualmente, questo approccio non rappresenta una pratica comune, viene utilizzato in domini ristretti all'interno di molti ambienti di
produzione software. Affronteremo nuovamente l'argomento nel Capitolo 9.

4.2.6

Raffinamento per passi successivi

I corsi introduttivi di programmazione focalizzano, molte volte, l'attenzione degli studenti


su approcci sistematici alla progettazione e convalida dei programmi. L'approccio pi popolare che viene seguito detto progettazione per raffinamenti o per passi successivi. Si tratta
di una strategia di progettazione facile da descrivere e da capire.
Come indica chiaramente il suo nome, il raffinamento per passi successivi un processo iterativo. A ogni passo, il problema da risolvere viene scomposto in sotto-problemi da
risolvere separatamente. Le sotto-soluzioni che costituiscono la soluzione del problema originale vengono poi riunite mediante l'uso di strutture di controllo semplici. Possono essere
eseguite in sequenza, selezionate alternativamente o iterate ciclicamente. Dunque, dato P,
dichiarazione originale del problema, P^ P2, . . ., P, dichiarazioni dei sotto-problemi, e C
un'espressione booleana che rappresenti una condizione, P potrebbe essere scomposto e risolto seguendo uno dei seguenti modelli:
(1)

P,;

(2)

if

P2;
C

then

end
(3)

i;

whi1e

loop
P,;

end

loop;

Molte volte, abbiamo bisogno di esprimere una selezione multi-ramifcata. Invece di utilizzare dichiarazioni i f profondamente annidate, che potrebbero incidere sulla leggibilit di
un programma, possiamo impiegare una dichiarazione c a s e generica:
(2')

case
C,: P,;
C 2 : P2;

C: P;
otherwise
end

P0;

case;

richiesto che tutte le C, ciascuna delle quali rappresenta un'espressione booleana, siano
mutuamente disgiunte.
La dichiarazione del problema, a ogni passo della scomposizione, viene solitamente fornita mediante descrizioni in linguaggi simili al linguaggio naturale. Ogni passo di raffinamento rappresentato riscrivendo la descrizione in termini di dichiarazioni di sotto-problemi,
collegate tra loro per mezzo delle strutture di controllo precedentemente illustrate. Le dichiarazioni dei sotto-problemi, a loro volta, vengono rese pi dettagliate al successivo passo
del raffinamento.
Il processo di progettazione incomincia, dunque, con una descrizione globale del problema da risolvere (la funzione "originale"), applica ricorsivamente una scomposizione funzionale e termina una volta che abbiamo raggiunto il punto in cui ogni sotto-problema risulta facile da esprimere in termini di poche righe di codice nel linguaggio di programmazione prescelto.
ESEMPIO 4 . 1 0

Descriviamo la derivazione di un algoritmo di ordinamento (selettive sort) mediante il raffinamento per passi successivi. Si tratta di un piccolo esempio di programmazione, non di un
esercizio di progettazione. L'esempio, per, illustra chiaramente come funzioni il raffinamento
per passi successivi. Successivamente, vedremo anche un esempio in cui il raffinamento per
passi successivi verr utilizzato a livello di progettazione. Ecco l'algoritmo di ordinamento.
Passo 1
sia
i

n la

:=

lunghezza

while

i < n

a da

ordinare;

loop

trova il minimo
pos izione
i;
i
end

dell'array

1 ;
tra

a ...

a e scambialo

con

1'elemento

in

:= i + 1 ;

loop;

(segue a p. suce.)

(segue da p. prec.)
Passo 2
sia
i

n la

:=

lunghezza

wbile

i < n
j

a da

ordinare;

loop

== n;

while

j > i loop
if

a(i) > a(j)

scambia
end
j
end
i
end

dell'array

1 ;

tra

then

loro

gli

elementi

in posizioni

j e i;

if ;

:=

j -

1;

loop;

:= i + 1 ;

loop;

Passo 3
sia n la l u n g h e z z a
i := 1 ;
while

end

i < n

dell'array

a da

ordinare;

loop

j := n;
w b i l e j > i loop
i f a(i) > a(j)then
x :=a(i); a(i)
end i f;
j := j - 1;
end l o o p ;
i := i + 1 ;
loop;

:= a ( j ) ;

a(j)

:=

x;

La progettazione per raffinamento per passi successivi pu essere rappresentata graficamente


mediante l'uso di un D T (decomposition tree, albero di scomposizione), un albero in cui
la radice viene etichettata con il nome del problema iniziale, in cui ogni altro nodo viene
etichettato con il nome di un sotto-problema e i nodi figli di un qualsiasi nodo, con i nomi dei sotto-problemi che lo dettagliano in un passo di raffinamento. L'ordine da sinistra
a destra dei nodi figlio di un dato nodo rappresenta l'ordine in cui i sotto-problemi dovranno essere risolti durante l'esecuzione del programma. Nodi rappresentanti sotto-problemi alternativi vengono identificati da una linea tratteggiata che raggruppa gli archi che
connettono i nodi al loro nodo padre; gli archi vengono etichettati anche con la condizione sotto la quale devono essere scelti i sotto-problemi. L'iterazione viene rappresentata da una linea solida, alla quale viene aggiunta, come etichetta, la condizione che governa la struttura w h i l e .
Per esempio, la Figura 4.13 rappresenta il D T che corrisponde al seguente raffinamento per passi:
Passo 1
P;

P e il problema

Pi; P2; P3;

da

risolvere.

Passo 2
viene

scomposto

nella

sequenza

Figura 4.13

Rappresentazione grafica del raffinamento per passi successivi. (Legenda: gli archi con
linea continua rappresentano iterazioni; quelli tratteggiati rappresentano selezioni).

Passo 3
Putritile C loop

Pj.I!
end
P3;

P2 viene

scomposto

in

un'iterazione.

loop;

Passo 4
Pw
while C

loop
P2, viene

if C, then

scomposto

in una

selezione.

Pi,.,.;
else

P2.1.2;
end
P,;

end i ;
loop;

Potremmo porci alcune domande in merito alle relazioni che intercorrono tra un D T e il
grafo della relazione IS C O M P O S E D OF o, equivalentemente, tra un progetto top-down ottenuto mediante la scomposizione di un modulo in componenti e il raffinamento per passi successivi. In effetti sono concetti simili che presentano, per, anche alcune differenze.
Ad esempio, supponiamo di voler descrivere il raffinamento per passi successivi illustrato nella Figura 4.13 in termini di relazioni I S _ C 0 M P 0 N E N T _ 0 F o, pi convenientemente,
in termini di L S _ C O M P O S E D _ O F . Siano M, M 1 ; M 2 e M 3 i moduli che rappresentano rispettivamente P, Pi, P2 e P3. Notiamo che non possibile formulare la relazione
M IS_COMPOSED_OF

{Mlr

M2,

M3)

in quanto non c' alcun componente del sistema responsabile per l'organizzazione del flusso sequenziale da
a M2 e infine a M3, implicito nella Figura 4.13. Dobbiamo, dunque,
introdurre un modulo aggiuntivo di controllo M4 che faccia da collante e imponga il flus-

so sequenziale da H, a H2 e infine a M3 Ci ci permetterebbe di affermare la seguente


relazione:
M ISCOMPOSEDOF

{M,,

M2,

MJ,

M,>

A sua volta M2 verrebbe scomposto in M2 _ ! (associato a P2, J e M2 _ 2 , il quale farebbe da modulo di controllo utilizzato per imporre l'uso iterativo di M 2 1 :
M2

ISCOMPOSEDOF

{M21,

M2,2}

Infine M 2 ; t verrebbe scomposto in M 2 I 1 I 1 , M ! i 1 i 2 (associati rispettivamente a P 2 i l i l e P 2 , i , 2 )


e M2 _ ! _ 3 il quale farebbe da modulo di controllo per la selezione tra M2 , I, I e M2 _ 2 _ 2 secondo
il valore di C ! :
M2,I

IS

COMPOSED

OF

{M2/11,

2 1 2

M2,13>

Questo esempio ci dimostra che un progetto prodotto dal raffinamento per passi successivi
pu essere descritto anche in modo top-down con l'uso della relazione I S _ C 0 M P 0 S E D _ 0 F .
Infatti, il metodo che abbiamo usato pu essere applicato, in generale, per effettuare una
trasformazione da una descrizione all'altra. La descrizione in termini di I S _ C O M P O S E D _ O F
che ne risulta non corrisponde, per, a una modularizzazione significativa. A tutti gli effetti, il raffinamento per passi successivi dovrebbe essere considerato un metodo per la descrizione della struttura logica di un dato algoritmo, implementato da un singolo metodo, piuttosto che un metodo per la descrizione della scomposizione in moduli di un sistema. La descrizione fornita per il programma di ordinamento illustra le virt di questo metodo quando viene applicato nel piccolo. Un sistema grande e complesso non pu essere progettato e
descritto mediante il raffinamento per passi successivi; piuttosto, la sua progettazione necessita della scomposizione in moduli, dello sviluppo separato di ogni modulo e dell'applicazione dell'information hiding.
Eserczi
4.30

Descrivete l'uso della relazione USES tra i m o d u l i che a b b i a m o i n t r o d o t t o per rappresentare


il r a f f i n a m e n t o per passi successivi illustrato nella Figura 4 . 1 3 , e m o s t r a t e n e la s t r u t t u r a m o dulare, u s a n d o G D N .

4.31

Descrivete il r a f f i n a m e n t o per passi successivi dell'esempio di o r d i n a m e n t o di u n a selezione


discusso nell'Esempio 4 . 1 0 in t e r m i n i del c o r r i s p o n d e n t e albero di scomposizione.

4.2.6.1

Una valutazione del raffinamento per passi successivi

Una convinzione errata a riguardo del raffinamento per passi successivi che possa fornire una strategia per trovare la soluzione a un problema suggerendo una scomposizione
quasi meccanica del problema in sotto-problemi. Questa convinzione nasce dagli esempi
di derivazione che possono essere trovati in alcuni libri di testo introduttivi, dove sembra
che il programma nasca in maniera naturale dal raffinamento per passi successivi. Il processo di derivazione di un programma, contrariamente alla apparenze, spesso richiede
creativit e potrebbe anche richiedere che vengano esplorate diverse alternative prima di
trovare la soluzione appropriata. Prendiamo un problema ben conosciuto come l'ordinamento; non certamente seguendo pedissequamente il raffinamento per passi successivi
che possiamo scoprire una buona soluzione come, ad esempio, il quicksort, rispetto a so-

luzioni pi banali come il bubblesort o il selection sort! Il raffinamento per passi successivi indubbiamente un modo efficace di descrivere una soluzione dopo che stata trovata. E un modo per descrivere a posteriori le motivazioni che stanno dietro a un algoritmo, postulando un processo ideale e razionale da cui deriva l'algoritmo. Di conseguenza il raffinamento per passi successivi pu essere una buona tecnica per la documentazione di programmi. Inoltre, se un codice viene scritto seguendo la tecnica del raffinamento per passi successivi il risultato sar facile da leggere e capire.
Il raffinamento una tecnica efficace per la descrizione di programmi piccoli. Fallisce,
per, nello scalare verso sistemi dalla complessit anche moderata. Il raffinamento per passi successivi dunque un metodo che funziona nel piccolo, ma fallisce nel grande. In particolare, non permette di ottenere gli obiettivi che l'information hiding tenta di raggiungere e non aiuta i progettisti nel riuso di componenti provenienti da applicazioni precedenti
0 nella progettazione di componenti riusabili per programmi pi grandi. Ecco alcuni motivi, responsabili di questi difetti.
1 sotto-problemi tendono ad essere analizzati isolatamente. Il raffinamento per passi successivi non pone alcuna enfasi sul tentativo di generalizzare i sottoproblemi in modo da renderli riusabili in diversi momenti della derivazione del sistema, anche in diversi progetti.
Quando un problema deve essere maggiormente dettagliato, viene studiato nel contesto dell'albero di scomposizione in cui appare. Invece, quando si sta scomponendo un problema in sotto-problemi potrebbe risultare utile verificare se un'adeguata generalizzazione
possa assimilarlo a un problema che si sta risolvendo da un'altra parte, cos da unificarli e
poter progettare un singolo modulo per entrambi.
Non viene prestata alcuna attenzione all'information hiding. Il raffinamento per passi
successivi non porta l'attenzione del progettista sulla necessit di incapsulare le informazioni modificabili all'interno di moduli. Infatti, i moduli che derivano dall'applicazione del raffinamento per passi successivi sono pure astrazioni procedurali. Un problema rappresentato da una funzione astratta viene scomposto ricorsivamente in sotto-problemi, i quali sono
tutti rappresentati da funzioni astratte.
Nel raffinamento per passi successivi la strategia non enfatizza mai il bisogno di raggruppare insieme funzioni per definire un oggetto astratto o un tipo di dato; non insiste neppure sulla derivazione di moduli che forniscano esportazioni selettive di collezioni di risorse. L'unico principio che guida la scomposizione funzionale nel raffinamento per passi successivi la volont di avere una soluzione finale leggibile.
Non viene prestata alcuna attenzione ai dati. Questo un corollario del punto precedente.
Il raffinamento per passi successivi non stressa l'uso dell'information hiding, e cio non focalizza l'attenzione del progettista sulla derivazione di moduli che nascondono una struttura dati ed esportano le operazioni astratte per accedervi.
La funzione originale potrebbe non esistere. Il metodo inizia esprimendo il problema originale che viene ricorsivamente reso pi dettagliato in termini di sotto-problemi. Una questione minore, ma fastidiosa, che il problema originale potrebbe risultare "innaturale" da
esprimere. Si ricordi che il problema originale dovrebbe descrivere il problema come una
funzione di alto livello che trasforma i dati in ingresso nei risultati attesi. Una tale funzione, per, non sempre esiste.

Ad esempio, quale potrebbe essere la funzione svolta da un elaboratore di testi?


Chiaramente, un elaboratore di testi un sistema che reagisce a comandi in ingresso forniti dagli utenti: comandi che creano testo, inseriscono nuovi caratteri in un file e realizzano
manipolazioni complesse sul testo. Ovviamente, si potrebbe sempre descrivere il problema
originale come "risponde a tutti i comandi degli utenti". Ma ci risulterebbe di scarso aiuto nei successivi passi di scomposizione.
Viene fatta una scelta prematura del flusso di controllo tra moduli. Lo studio del caso di
traduzione del raffinamento per passi successivi della Figura 4.13 in una gerarchia I S _ C 0 M P O S E D _ O F illustra chiaramente questo punto. Quando P 2 viene scomposto nell'iterazione di
P2;1, vengono introdotti concettualmente due moduli: M2;1 e M2 2. M2,i corrisponde a P2,i;
M2 _ 2 semplicemente un modulo di controllo che contiene la seguente istruzione:
while

C loop

P 2jl end

loop;

per costringere l'esecuzione ripetuta di M2;1. Una situazione simile nasce anche nella scomposizione di M2, IESEMPIO 4 . 1 1

Supponiamo di progettare un applicativo per il controllo della sintassi di un programma scritto in un dato linguaggio di programmazione. In accordo con il raffinamento per passi successivi potremmo scrivere:
Passo 1
Riconosci

un p r o g r a m m a

immagazzinato

in un

dato

file

f;

Passo 2
correct:=
analizza

true;
f secondo

if c o r r e c t

la d e f i n i z i o n e

del

linguaggio;

then

stampa

messaggio " p r o g r a m m a

corretto";

stampa

messaggio " p r o g r a m m a

non

else
end

corretto";

i f;

Passo 3
correct:=
esegui

if

true;

l'analisi

lessicale:

immagazzina
il programma
in una sequenza
di elementi
lessicali
(token)
nel file ft e la tabella
dei simboli
nel file fs e
imposta
la variabile
booleana
error_in_lexical_phase
a
seconda
del risultato
dell'analisi
del
lessico;
e r r o r _ i n _ l e x i c a l _ p h a s e then
correct:= false;

else
esegui
l'analisi
sintattica
sul
booleana
error_in_syntactic_phase
dell 'analisi ;
if e r r o r _ i n _ s y n t a c t i c _ p h a s e
correct:=

false;

then

file fe e imposta
a seconda
del

la
variabile
risultato

end i f ;
end i f;
if correct

then

stampa

messaggio

"programma

corretto";

stampa

messaggio

"programma

non

else
corretto";

end i f;

Senza procedere ulteriormente con l'esempio, possiamo vedere che sono stati presi impegni
pesanti per quanto riguarda il controllo del flusso fin dai primissimi stadi dello sviluppo. Ad
esempio, abbiamo deciso che l'analisi lessicale deve essere eseguita prima, che dovrebbe operare sull'intero programma in ingresso e che dovrebbe produrre la corrispondente sequenza
di elementi lessicali in un file intermedio, perch venga utilizzata nella fase successiva di analisi sintattica.
Supponiamo ora di decidere di voler cambiare strategia; per esempio, decidiamo di non
voler pi eseguire l'analisi in due fasi ma di voler lasciare che sia l'analizzatore sintattico a
guidare il processo. In questo caso, l'analizzatore sintattico attiver ripetutamente l'analizzatore lessicale durante il processo, chiedendo il successivo elemento lessicale. Questo cambiamento avrebbe un profondo impatto sulla struttura del programma, in base alla descrizione fornita dal raffinamento per passi successivi: occorre quindi procedere a un sostanziale rifacimento, a partire dal Passo 3.
L'impatto di questo cambiamento non sarebbe risultato cos drammatico nel caso in
cui avessimo seguito un approccio basato sull'information hiding, definendo i seguenti moduli di esempio:

nasconde la rappresentazione fisica del file in ingresso ed esporta operazioni per accedere al file sorgente un carattere alla volta;

CHAR_HOLDER:

SCANNER: n a s c o n d e i dettagli della struttura lessicale del l i n g u a g g i o dal resto del sis t e m a e d e s p o r t a u n ' o p e r a z i o n e per fornire il s u c c e s s i v o e l e m e n t o lessicale della sequenza;

P A R S E R : nasconde la struttura dati utilizzata per eseguire l'analisi sintattica (parse tree,
albero sintattico), la quale potrebbe essere incapsulata in un modulo di oggetto astratto
(PARSER).

Esercizio
4.32

4.2.7

C o m p l e t a t e la progettazione del riconoscitore di linguaggi. Utilizzate s i a T D N che G D N per


illustrare il progetto. Descrivete i c a m b i a m e n t i necessari per trasformare la soluzione in d u e
passi in u n a soluzione a u n singolo passo.

Progettazione top-down e bottom-up

Quale strategia dovremmo seguire quando progettiamo un sistema? Dovremmo procedere


in maniera top-down, applicando la scomposizione ricorsivamente secondo la relazione
I S_COMPOSED_OF, fino a quando il sistema stato scomposto in componenti gestibili? O

dovremmo procedere in maniera bottom-up, cominciando da ci che vogliamo incapsulare


in un modulo, definendo ricorsivamente interfacce astratte per poi raggruppare diversi moduli in un nuovo modulo di livello pi alto che li comprende?
Il raffinamento per passi successivi un metodo intrinsecamente top-down. Alcune
delle critiche che abbiamo sollevato a riguardo del metodo hanno a che vedere con le sue
caratteristiche specifiche. In particolare, l'impegno prematuro verso le strutture di controllo e il suo orientamento verso la progettazione in piccolo sono dovuti allo stile, basato sui
linguaggi di programmazione, utilizzato per descrivere i raffinamenti. Altre critiche importanti possono essere rivolte, in generale, alle strategie top-down. Tra queste, il fatto che i sotto-problemi tendono ad essere affrontati in isolamento, che nessuna enfasi viene posta sull'identificazione degli elementi in comune con altri o sulla riusabilit dei componenti, e che
poca attenzione viene prestata ai dati e, pi in generale, all'information hiding.
L'information hiding fondamentalmente una conseguenza dalle strategie bottom-up.
Suggerisce che dovremmo prima riconoscere che cosa vogliamo incapsulare all'interno di un
modulo per poi fornire un'interfaccia per la definizione dei confini del modulo. Notiamo,
comunque, che la decisione di che cosa celare all'interno del modulo (per esempio, certe politiche) potrebbe dipendere dal risultato di una qualche attivit di natura top-down. Dato
che l'information hiding risulta essere altamente efficace nel supporto alla progettazione in
vista del cambiamento, alle famiglie di prodotti e ai componenti riusabili, la sua filosofia
bottom-up dovrebbe essere seguita in modo sistematico.
Essendo, comunque, la progettazione un'attivit umana altamente critica e creativa, i
bravi progettisti non si limitano esclusivamente a seguire o la procedura top-down o quella
bottom-up. Cos, ad esempio, anche se dovessero decidere di procedere seguendo una logica
prevalentemente top-down, presterebbero ugualmente attenzione all'identificazione di parti
comuni e di possibili componenti riusabili, sostanzialmente combinando la strategia top-down
con un approccio bottom-up.
Una tipica strategia di progettazione potrebbe procedere parzialmente in modalit topdown e in parte in bottom-up, a seconda della fase del progetto e della natura dell'applicazione, in un modo che potrebbe essere denominato progettazione a yo-yo. Come esempio,
potremmo cominciare a scomporre un sistema, in maniera top-down, in sotto-sistemi e, in
un secondo momento, sintetizzare i sotto-sistemi in una gerarchia di moduli che implementino
l'information hiding.
L'approccio top-down, comunque, molte volte utile per documentare un progetto.
Anche se l'attivit di progettazione non dovrebbe essere costretta a seguire un modello prestabilito e rigido, ma essere una fusione di passi top-down e bottom-up, consigliabile che la
descrizione del progetto risultante sia fornita in una maniera top-down. Tali descrizioni rendono la comprensione del sistema pi semplice in quanto forniscono un quadro generale
prima di mostrare i dettagli.

4.3

Gestione delle anomalie

Un approccio sistematico alla progettazione seguito da un'implementazione rigorosa e disciplinata il modo migliore per dominare la complessit dello sviluppo di software e realizzare prodotti affidabili. Sfortunatamente, i prodotti software possono essere molto complessi, e soggetti all'errore umano. Anche quando lo sviluppo attuato con notevole atten-

zione non possibile fidarsi incondizionatamente del prodotto sviluppato. Questa mancanza
di fiducia pu essere frustrante per il programmatore coscienzioso, ma deriva dalla constatazione del carattere critico di molte applicazioni, per le quali un malfunzionamento potrebbe
portare a conseguenze disastrose.
Qualsiasi prodotto ingegneristico, dai ponti agli aerei al software, pu fallire. Il progettista deve anticipare i rischi di fallimento e decidere se evitarli o tollerarli. Ovvero, deve
ricorrere a una progettazione difensiva. Per questo cercher di proteggere l'applicazione da malfunzionamenti che potrebbero insorgere durante lo sviluppo o per via di circostanze avverse al momento dell'esecuzione. Costruir, inoltre, sistemi robusti, in grado di comportarsi
in modo ragionevole anche in circostanze non previste.
Diciamo che un modulo anomalo se fallisce nel fornire un servizio secondo le modalit previste e come specificato dalla sua interfaccia. Fino a questo punto, le nostre descrizioni
di progetto, testuali e grafiche, sono state principalmente di natura sintattica e non hanno supportato una descrizione precisa della semantica dei servizi esportati dal modulo. Un arricchimento semantico della notazione pu essere dato in modo formale, come vedremo nel Capitolo 5. Per semplicit, assumeremo qui che esso sia espresso nell'interfaccia, per mezzo di commenti. Estenderemo, inoltre, la nostra notazione di progettazione affinch associ un insieme
di eccezioni a ogni servizio esportato dal modulo. Le eccezioni associate a un servizio descrivono le anomalie che possono presentarsi durante l'esecuzione del servizio. Per semplicit, assumeremo che il servizio esportato dal modulo corrisponda a una funzione da invocare.
Un modulo o esegue correttamente il proprio compito, nel qual caso effettua il servizio richiesto e ritorna al cliente in maniera normale, oppure entra in uno stato anomalo. La
progettazione difensiva richiede che, nel secondo caso, il modulo segnali l'anomalia al cliente sollevando un'eccezione. In altre parole, distinguiamo tra il comportamento corretto e il
comportamento anomalo del modulo. Se qualcosa va storto e il modulo non riesce a completare correttamente il servizio richiesto, dovrebbe restituire un'indicazione della situazione anomala sollevando un'eccezione, che pu essere vista come un evento che viene segnalato al cliente. Il modulo server termina l'esecuzione e il client, informato del fatto che stata sollevata un'eccezione, risponde gestendola in maniera appropriata.
Perch un modulo M dovrebbe fallire nel fornire il suo servizio secondo le specifiche
date? Ci potrebbe accadere perch il client di M non soddisfa il protocollo richiesto per l'invocazione di uno dei servizi di M. Ad esempio, l'operazione op esportata da M potrebbe richiedere un parametro positivo, mentre il client potrebbe invocare op con un valore negativo del parametro. Un fallimento potrebbe anche presentarsi nel caso in cui M non soddisfi il protocollo richiesto per l'utilizzo di un servizio esportato da un altro modulo, diciamo
N. In questo caso, il fallimento di N viene segnalato a M, e il gestore delle eccezioni di M viene
attivato di conseguenza. Il gestore potrebbe tentare di recuperare dall'anomalia oppure potrebbe semplicemente fare un po' di pulizia dello stato del modulo e lasciare che la funzione fallisca, segnalando un'eccezione al modulo chiamante.
Se il recupero dovesse avere successo, M non fallirebbe; altrimenti, un po' di pulizia dello stato potrebbe essere necessaria per assicurare che l'uso di M, da parte di clienti successivi, non trovi il modulo in uno stato incoerente. Notiamo, comunque, che i gestori d'anomalie sono celati nel corpo del modulo; ovvero, come un modulo gestisce un'eccezione fa
parte dei segreti del modulo. Per questo motivo non approfondiremo la questione ora e non
esamineremo come vengono legate le eccezioni segnalate ai rispettivi gestori o cosa succede

module

exports
procedure

raises

(X:

INTEGGR;

...)

X_NON_NEGATIVE_EXPECTED,

INTEGER_OVERFLOW;
X deve essere
positivo ; se non lo viene
sollevata
l'eccezione
X_NON_NEGATIVE_EXPECTED;
1'eccezione
INTEGER_OVERFLOW
viene
sollevata
se la computaz ione interna
di P genera
un over flow

end

Figura 4.14

Interfaccia p a r z i a l e per un m o d u l o c h e i n c l u d e le e c c e z i o n i .

se un modulo client non possiede un gestore per l'eccezione che gli viene segnalata. Queste
questioni dipendono fortemente dal linguaggio di programmazione che potremmo scegliere per l'implementazione. Da un punto di vista della progettazione il punto importante
che i client possano determinare dalla sua interfaccia quali eccezioni possono essere sollevate da un modulo. In un sistema robusto i client anticiperanno e gestiranno tutte le eccezioni che possono essere sollevate dai moduli server di cui fanno uso.
A parte questi tipi di fallimenti, un modulo potrebbe fallire nel fornire il suo servizio per
via di una condizione non prevista, come ad esempio un overflow o un indice che esce dai limiti di un array durante l'esecuzione. In questo caso assumiamo che la macchina astratta sottostante sia in grado di catturare la condizione anomala e di passarla al software per una gestione adeguata. Molti linguaggi di programmazione sono anche in grado di scoprire e segnalare una violazione delle asserzioni di correttezza logica durante l'esecuzione. Queste violazioni possono essere trattate come i tipi di eccezioni precedenti. Inoltre, possibile specificare che
certe condizioni dovrebbero essere considerate come eccezioni che necessitano di un trattamento
particolare da parte del client dopo che sono state scoperte del modulo server.
Nella discussione che segue estenderemo le descrizioni di interfacce con T D N in modo che ai servizi esportati possa essere associato un elenco di nomi di eccezioni. Questi saranno i nomi delle eccezioni che potranno essere sollevate dal servizio per segnalare il proprio completamento anomalo.
Supponiamo che aJ momento di definire le interfacce i progettisti concordino certe restrizioni che si applicano ai parametri di una procedura P racchiusa nel modulo M. Ad esempio, potrebbero concordare che P debba ricevere un valore non negativo per il parametro x.
Questa decisione verrebbe registrata nell'interfaccia di M in qualit di commento (Figura 4.14).
Ovviamente, in un mondo perfetto, non ci sarebbe ragione di sospettare che i moduli client
possano non rispettare questi requisiti. La progettazione difensiva, invece, richiede che non
si confidi che i client si comportino in maniera appropriata e che si protegga M rimandando un'eccezione nel caso in cui P fosse chiamata con un valore negativo di X.
Come ulteriore esempio, consideriamo la Figura 4.15, in cui il modulo L usa il mo-

module L
UB6S

M Imports P (X:

exports

INTEGER;..)

... ;
procedure

raises

(...)
INTEGER_OVERFLOW;

implementation
Se viene sollevata
1'eccezione
INTEGE
OVERFLOW
quando viene invocata
P, l'eccezione
viene
propagata

end L
Figura 4.15

dulo

Frammento di progettazione con eccezione che viene propagata.

della Figura 4.14. Nel caso in cui dovesse essere sollevata l'eccezione I N T E G E R _
quando la procedura R di L chiama la procedura P, potremmo decidere che il
gestore di R faccia un po' di pulizia, per poi sollevare un'eccezione appropriata (per esempio, nuovamente I N T E G E R _ 0 V E R F L 0 W ) da far gestire al cliente di M. La stessa politica potrebbe essere seguita anche dal cliente, e cos via. In effetti, questa potrebbe essere una
maniera per eseguire una terminazione graduale del sistema come conseguenza di un errore
irrecuperabile.
Dal frammento della Figura 4.15 osserviamo che L non solleva un'eccezione corrispondente alla condizione X_NON_NEGATIVE_EXPECTED, la quale potrebbe essere sollevata da P. Questo significa che L garantisce che l'eccezione non verr mai sollevata oppure
che L sapr recuperare nel caso dovesse essere sollevata.
M

OVERFLOW

Esercizi
4.33

D e f i n i t e l'interfaccia di u n m o d u l o che i m p l e m e n t a il tipo di d a t o astratto S T A C K , dove l'operazione p o p solleva un'eccezione se c h i a m a t a a operare su u n o stack v u o t o .

4.34

S u p p o n i a m o che ci venga chiesto di costruire u n a tabella di riferimenti per le variabili che app a i o n o in u n p r o g r a m m a . U n a tabella di riferimenti u n a i u t o per ricostruire d o c u m e n t a zione p a r t e n d o d a altri p r o g r a m m i che si a s s u m o n o corretti. In accordo c o n le specifiche, n o n
dovrebbe mai accadere che u n a variabile sia utilizzata senza, o p r i m a di, essere dichiarata. Per
semplicit, a s s u m i a m o che il linguaggio n o n fornisca regole per la specifica dei limiti e n t r o i
quali u n a variabile sia visibile: tutti i n o m i delle variabili h a n n o visibilit globale.
P r o g e t t i a m o u n a tabella di r i f e r i m e n t o CRT, u n oggetto astratto, che esporta d u e operazioni:
(1) la p r o c e d u r a NOTIFY viene c h i a m a t a per inserire il n o m e di u n a variabile nella tabella,
insieme al n u m e r o della riga in cui stata dichiarata la variabile. (2) la p r o c e d u r a OCCUR viene c h i a m a t a per registrare la creazione di u n a variabile in u n a riga di codice, specificando il
n o m e della variabile e il n u m e r o della riga di codice in questione.

C o m e parte del c o n t r a t t o c o n gli altri m o d u l i client, specificheremo nell'interfaccia che la proc e d u r a NOTIFY n o n p u essere c h i a m a t a se u n a variabile con lo stesso n o m e risulta gi inserita nella tabella. La p r o c e d u r a OCCUR, invece, p o t r essere c h i a m a t a solo se la variabile che
stiamo t r a s m e t t e n d o c o m e p a r a m e t r o gi stata dichiarata (e q u i n d i gi presente nella tabella). Q u e s t i protocolli s o n o coerenti c o n l'assunzione che il p r o g r a m m a sorgente sia corretto.
Progettate u n m o d u l o CRT r o b u s t o e fornite la sua descrizione T D N . I m p l e m e n t a t e il progetto in u n linguaggio di p r o g r a m m a z i o n e di vostra scelta, a s s u m e n d o che ci siano m o d u l i
adeguati per l'uso di CRT. E s p o n e t e vantaggi e svantaggi del linguaggio per q u a n t o riguarda
la gestione delle eccezioni.
4.35

C o n f r o n t a t e le capacit di gestione delle eccezioni di C++ e Java. possibile, in u n o di questi linguaggi, che u n m o d u l o client n o n abbia u n gestore di un'eccezione che p o t r e b b e incontrare? Q u a l e di questi d u e linguaggi rafforza la progettazione difensiva?

4.4

Un esempio di progettazione

In questo paragrafo illustreremo i concetti precedentemente presentati nel contesto di un


esempio di progettazione. Il nostro obiettivo non quello di fornire una ricetta generale su
"cosa rende un progetto un buon progetto". La progettazione un'attivit creativa che non
pu essere svolta in maniera meccanica; richiede intuito ed esperienza. Di conseguenza, esamineremo un processo di progettazione ipotetico, mostrando alcuni tra i problemi che possono sorgere nella pratica e discutendo esempi di cosa rende un progetto un buon progetto.
Consideriamo un piccolo gruppo di progettisti che sviluppa un compilatore di un ulteriore linguaggio di programmazione: MIDI considerevolmente pi complesso del linguaggio MINI presentato negli Esempi 4.1 e 4.6 ed un linguaggio strutturato a blocchi
(ALGOL-like). La progettazione complessiva dell'Esempio 4.6 risulta essere valida anche qui
e, dunque, non verr pi discussa. Concentreremo la nostra attenzione sulla progettazione

module

SYMB0L_TABLE

Supporta
uses

. . imports

exports

fino

a MAX_DEPTH

(IDENTIFIER,

procedure

procedure

INSERT

annidati

(ID:

in

IDENTIFIER;

DESCR:

in

DESCRIPTOR);

RETRIEVE

(ID:

DESCR:

end

blocchi

DESCRIPTOR)

(ID:

in

out

procedure

LEVEL

procedure

ENTER_SCOPE;

in

procedure

EX I T _ S C O P E ;

procedure

INIT

IDENTIFIER;
DESCRIPTOR);

IDENTIFIER;

( M A X _ D E P T H:

in

L: out

INTEGER);

INTEGER);

SYMBOL_TABLE

Figura 4.16

Frammento di T D N che rappresenta la versione iniziale dell'interfaccia


della tabella dei simboli.

del modulo SYMBOL_TABLE. I progettisti, innanzi tutto, si accordano sulle direttive di progettazione, che avranno un impatto sulle interfacce dei moduli: in primo luogo, le operazioni di S Y M B O L _ T A B L E possono essere invocate solo se il programma sintatticamente e
semanticamente corretto. In particolare, possono essere invocate solo nel caso in cui i blocchi siano correttamente delimitati da coppie di b e g i n ed e n d , in cui non esistano due identificatori con lo stesso nome all'interno dello stesso blocco e in cui ogni variabile sia dichiarata prima di essere utilizzata. In secondo luogo, per ragioni di stile, predefinita la profondit massima di blocchi annidati, per cui programmi con un livello di annidamento pi elevato del valore predefinito saranno considerati erronei.
S Y M B O L _ T A B L E un oggetto astratto che nasconde la struttura dati fsica usata per
rappresentare la tabella. La sua interfaccia rappresentata dal frammento di T D N della
Figura 4.16, secondo la quale i moduli client possono inserire un identificatore, con i propri attributi, all'interno della tabella mediante la procedura INSERT. I clienti possono anche estrarre gli attributi degli identificatori precedentemente inseriti mediante la procedura
R E T R I E V E . Gli attributi dovrebbero essere immagazzinati all'interno di descrittori. Vengono fornite operazioni in grado di segnalare quando si entra in un nuovo blocco (mediante
la procedura E N T E R S C O P E ) e quando si esce da un blocco (mediante la procedura
EXIT_SCOPE). Un'operazione (LEVEL) viene resa disponibile per il calcolo del livello di annidamento lessicale di un identificatore. Il livello zero se l'identificatore dichiarato localmente, ovvero nel blocco in cui si attualmente entrati e dal quale non si ancora usciti; vale uno se l'identificatore non locale ma dichiarato nel blocco in cui si era entrati immediatamente prima; e cos via.
I progettisti del compilatore MIDI si rendono quindi conto che l'attuale versione dell'interfaccia di SYMBOL TABLE non soddisfacente. Le assunzioni che il programma sia sintatticamente e semanticamente corretto e che la profondit massima di blocchi annidati non
debba essere superata potrebbero essere violate da un comportamento scorretto da parte dei
moduli client. Conseguentemente, per migliorare la robustezza del compilatore, viene deciso che le invocazioni illegali sollevino eccezioni. Questo miglioramento nella progettazione
di S Y M B O L _ T A B L E presenta un'ulteriore vantaggio: rende il modulo riusabile in altri contesti, in cui le assunzioni di correttezza sintattica e semantica non risultano vere. In conclusione, vengono adottate le seguenti decisioni di progettazione.
1. L'operazione INSERT solleva un'eccezione se l'inserimento non pu essere portato a
termine in quanto esiste gi nel blocco corrente un identificatore con lo stesso nome;
2. Le operazioni R E T R I E V E e L E V E L sollevano un'eccezione se non in quel momento
visibile un identificatore con il nome specificato;
3. L'operazione E N T E R _ S C O P E solleva un'eccezione se viene superato il livello massimo
di profondit di blocchi annidati;
4. L'operazione E X I T _ S C 0 P E solleva un'eccezione se non esiste un blocco corrispondente
da cui uscire.
Basandosi su questi punti, i progettisti producono una revisione dell'interfaccia di
SYMBOL_TABLE (Figura 4.17).
Studiamo il lavoro del progettista di SYMBOL_TABLE. La struttura a blocchi del programma richiede che le informazioni riguardanti i blocchi vengano allocate e deallocate se-

module
uses

SYMBOLTABLE

...imports

(IDENTIFIER,

DESCRIPTOR)

exports
Supporta
fino a MAX_DEPTH
INIT
deve essere
chiamata
altra
operazione
procedure

INSERT

(ID:

in
in

raises

MULTIPLE_DEF,

(ID:

in

IDENTIFIER;

out

DESCRIPTOR)

raises

NOTVISIBLE;

(ID:

in

raises
procedure ENTER_SCOPE
procedure E X I T S C O P E
procedure

INIT

DESCRIPTOR)

DESCR:

L: out

end

procedura
annidati; la
invocare qualsiasi

IDENTIFIER;

DESCR:
procedure RETRIEVE

procedure LEVEL

blocchi
prima
di

IDENTIFIER;
INTEGER)
N0T_VISIBLE;

raises
raises

(MAX_DEPTH:

EXTRA_LEVELS;
EXTRA_END;

in

INTEGER);

SYMBOLTABLE

Figura 4.17

Frammento di T D N c h e rappresenta una versione revisionata dell'interfaccia


della tabella dei s i m b o l i .

condo una politica LIFO. Di conseguenza, quando si entra in un nuovo blocco, viene allocato un nuovo insieme di descrittori, mentre questi vengono deallocati nel momento in cui
si esce da un blocco. Possiamo, dunque, utilizzare uno stack per immagazzinare i descrittori. Grazie alle informazioni riguardanti la profondit massima consentita di blocchi annidati, il progettista decide di implementare lo stack come un array (di dimensioni M A X _ D E P T H )
di liste, ognuna delle quali rappresenta le dichiarazioni che avvengono in un blocco in termini di coppie c i d e n t i f i c a t o r e , d e s c r i t t o r e .
Definire una lista non un problema nuovo per il nostro progettista. Ha affrontato lo
stesso problema molte volte, ridefinendo una lista ex novo ogni volta che ne aveva bisogno,
il che risulta piuttosto frustrante! Il progettista decide, dunque, di definire un modulo generico per la gestione delle liste che possa essere riusabile in futuro.
L I S T viene progettata come un tipo di dato astratto. Essendo generico, pu essere stanziato in un modulo che gestisca una lista di elementi di un qualsiasi tipo specificato. Essendo
un tipo di dato astratto, permette l'istanziazione di diversi oggetti lista. Un'ipotetica versione dell'interfaccia del modulo viene mostrata nel frammento T D N della Figura 4.18.
LIST esporta una procedura SEARCH che cerca all'interno della lista un elemento che
"soddisfi" un determinato parametro di ricerca. Nell'esempio S Y M B O L _ T A B L E , dato che T
una coppia c i d e n t i f i e r , d e s c r i p t o r > , due elementi di tipo T si equivalgono se i loro
identificatori sono gli stessi. In generale, cosa significhi "equivalere" dovrebbe essere specificato da una procedura associata al parametro formale T e spedito come parametro al momento
dell'istanziazione del modulo. Inoltre, il modulo fornisce un procedura INSERT per im-

generic module LIST(T) with MATCH

(EL_1, EL_2: in T)

exports
type LINKED_LIST: ? ;
procedure
Dice

IS EMPTY

se la lista

procedure
Svuota

una

SETEMPTY

(L: in LINKED_LI ST) : BOOLEAN;


vuota.
(L: in out LINKED_LI ST) ;

lista.

procedure

INSERT

Inserisce

l'elemento

(L: in out LINKED_LIST;

procedure

SEARCH

nella

EL: in T) ;

lista

(L: in LINKED_L1ST;

EL_1: in T;

EL_2: out T; FOUND: out

boolean);

Cerca in L un elemento
EL 2 uguale a EL 1
e restituisce
il risultato
in FOUND.
end

LIST(T)

Figura 4.18

Frammento di T D N che rappresenta la versione iniziale dell'interfaccia di un modulo


per la gestione di una lista.

magazzinare un elemento di tipo T. L'interfaccia non specifica dove verr effettivamente immagazzinato l'elemento: potrebbe essere all'inizio di una lista, alla fine o in qualsiasi punto intermedio (magari secondo un dato ordinamento): la decisione viene lasciata all'implementazione.
Esercizi
4.36

C o n s i d e r a t e la progettazione del m o d u l o SYMBOL TABLE m o s t r a t a nella Figura 4 . 1 6 e c o n siderate u n p r o g r a m m a MIDI in cui il n u m e r o di simboli b e g i n sia maggiore del n u m e r o
di simboli e n d . O v v i a m e n t e , il p r o g r a m m a n o n sintatticamente corretto. C o m e si comporter
il m o d u l o nella Figura 4 . 1 6 in questo caso? C o m e si p u migliorare il progetto, in m o d o che
il m o d u l o sia in grado di a f f r o n t a r e u n a tale situazione?

4.37

II m o d u l o SYMBOL TABLE, m o s t r a t o nella Figura 4 . 1 6 , richiede che i m o d u l i client seguan o u n preciso protocollo nell'invocazione dei servizi esportati (INIT deve essere c h i a m a t a prim a di qualsiasi altra operazione). C o m e si p o t r e b b e i m p o r r e questa politica nell'interfaccia di
SYMBOL TABLE m e d i a n t e l'uso di eccezioni?

4.5

Software concorrente

Fino a questo punto, abbiamo implicitamente assunto che l'applicazione da progettare avesse un solo flusso di esecuzione (detto anche control thread) ovvero che fosse un sistema puramente sequenziale. Con la proliferazione delle reti di computer e dei sistemi distribuiti,
molte applicazioni devono poter gestire molteplici flussi di esecuzione, con conseguente, maggiore complessit sia del progetto che dell'analisi. Tali classi di applicazioni sono sempre pi

importanti e meritano un trattamento speciale. Solitamente, vengono studiati come argomento separato nell'ambito di corsi e di testi sui sistemi operativi, sui sistemi distribuiti o
sui sistemi real-time. Esamineremo qui le caratteristiche fondamentali di queste applicazioni in relazione agli altri tipi di software e mostreremo come le tecniche di progettazione, precedentemente illustrate, ne risultino influenzate.
Uno dei problemi principali nella progettazione di software concorrente quello di assicurare la coerenza dei dati condivisi tra i moduli eseguiti in concorrenza. Discuteremo il
problema e alcune soluzioni nel Paragrafo 4.5.1 e prenderemo poi in considerazione due classi particolari di software concorrente: il software real-time (Paragrafo 4.5.2.), e il software
distribuito (Paragrafo 4.5.3).

4.5.1

I dati condivisi

Possiamo generalizzare i concetti di modularit che abbiamo finora studiato per avere un
oggetto astratto cui pu accedere pi di un'attivit sequenziale (o processo)6 alla volta. Ad esempio, supponiamo di avere un oggetto astratto BUFFER di tipo QUEUE di caratteri. Questo
oggetto potrebbe essere un'istanza del tipo generico della Figura 4.19, ovvero
module

QUEUE_OF_CHAR

is G E N E R I C _ F I F O _ Q U E U E

(CHAR)

e ci che segue l'istanziazione di una variabile


BUFFER:

QUEUE_OF_CHAR.QUEUE;

assumendo che Q U E U E sia il nome del tipo esportato da G E N E R I C Q U E U E e utilizzando la


dot notation per specificare la selezione di una risorsa esportata da un'istanza specifica.
Assumiamo che siano disponibili le seguenti operazioni sugli oggetti di tipo Q U E U E di
caratteri:

PUT: inserisce un carattere in QUEUE

GET: estrae u n carattere da QUEUE

NOT FULL

NOT_EMPTY : restituisce t r u e se il parametro QUEUE n o n v u o t o .

: restituisce

true

se il parametro

QUEUE

non

pieno7

I processi client ( P R O D U C E R _ 1 , P R O D U C E R _ 2 , etc.) accedono all'oggetto B U F F E R


concorrentemente, producendo caratteri e chiamando l'operazione PUT per inserire un nuovo carattere nel buffer. Anche i processi client (C0NSUMER_1, C O N S U M E R _ 2 , etc.) accedono concorrentemente all'oggetto BUFFER, per rimuovere caratteri chiamando la procedura
GET, la quale estrae un carattere alla volta. Assumiamo che l'operazione PUT possa essere
chiamata solo se il buffer non pieno e che l'operazione GET possa essere chiamata solo se
il buffer non vuoto.

In generale si distingue tra thread e processi (attivit sequenziali all'interno di un singolo spazio di nomi o in diversi spazi di nomi). Per i nostri scopi, possiamo ignorare questa distinzione.
7
Assumiamo che le code abbiano una capacit finita. Le code non limitate sono simili (e pi semplici
da gestire), e non richiedono questa operazione.

Per usare il modulo BUFFER in maniera corretta potremmo tentare di inserire le chiamate GET e PUT eseguite dai client nelle seguenti strutture:
(i)

if

QUEUE_OF_CHAR.NOT_FULL

(BUFFER)

QUEUE_OF_CHAR.PUT
end

i f;

(ii)

if

QUEUE_OF_CHAR.NOTEMPTY

(BUFFER)

QUEUE_OF_CHAR.GET
end

then

(X,BUFFER);

then

(X,BUFFER);

i f;

Sfortunatamente, questo approccio non sufficiente per assicurare un accesso corretto al buffer
in quanto potrebbe accadere che C O N S U M E R _ 1 controlli il buffer e non lo trovi vuoto.
Sceglierebbe, di conseguenza, di entrare nel ramo t h e n e si preparerebbe a eseguire una GET.
Prima di eseguire effettivamente la GET, C 0 N S U M E R _ 2 potrebbe controllare il buffer e trovarlo non vuoto; anch'esso dunque, entrerebbe nel ramo t h e n e si preparerebbe a eseguire
una GET. Se B U F F E R , all'inizio, conteneva solo un carattere, raggiungiamo uno stato non
valido in cui sono state date due autorizzazioni a eseguire GET. Ci porter certamente a un
errore durante l'esecuzione.
Esercizio
4.38

S u p p o n i a m o che il codice che implementa l'operazione PUT contenga l'istruzione TOT : =


TOT + 1, con TOT uguale al numero totale di caratteri nel buffer, e che l'operazione GET
contenga l'istruzione TOT : = TOT 1. S u p p o n i a m o , inoltre, che P R O D U C E R I e
C0NSUMER 2 stiano eseguendo concorrentemente un'operazione di PUT e una di GET sul
buffer. Mostrate c o m e sia possibile che il sistema entri in u n o stato invalido.

L'esempio BUFFER illustra il bisogno di sincronizzare le attivit concorrenti. Due attivit concorrenti procedono in parallelo fino a quando le loro azioni non interferiscono le une con
le altre. Ma se devono cooperare o competere per l'accesso a una risorsa condivisa, come il
BUFFER dell'esempio, non possono procedere indipendentemente ma devono sincronizzare le loro azioni.
Ci sono diversi modi per sincronizzare i processi. Uno quello di assicurarsi che qualsiasi risorsa condivisa, cui i processi devono accedere, venga utilizzata in mutua esclusione.
Questo significa che, quando un processo sta effettuando una PUT (o una GET), nessun altro processo deve poter accedere a B U F F E R ; altrimenti potrebbe insorgere un errore, come
nell'Esercizio 4.38. Inoltre, quando un consumatore esegue l'operazione (ii) dovrebbe accedere all'oggetto in mutua esclusione; ovvero, nessun altro processo dovrebbe poter eseguire
alcun'altra operazione sul buffer condiviso. Lo stesso vale per il caso in cui un produttore
esegue l'operazione (i).
Pi generalmente, le operazioni che hanno conseguenze sullo stato interno di un oggetto condiviso dovrebbero sempre essere eseguite in mutua esclusione, in modo da lasciare l'oggetto in uno stato coerente. Lo stesso vale per le sequenze di operazioni che testano il
valore di un oggetto e, a seconda del risultato del test, ne modificano il valore.
Il problema dell'accesso a dati condivisi in un ambiente concorrente in realt una ge-

neralizzazione dello stesso problema in un ambiente sequenziale. Anche le variabili condivise da diversi moduli in un ambiente sequenziale devono essere sottoposte a cure particolari, in quanto due chiamate successive al modulo M potrebbero osservare diversi valori di
una variabile per via di una chiamata a M da parte di un altro modulo. Questa situazione
potrebbe essere intenzionale (nel caso di un oggetto astratto) oppure potrebbe essere un errore. In un ambiente sequenziale, tali interazioni tra moduli sono esplicite nella progettazione dell'applicazione.
In un ambiente concorrente, invece, le interazioni dipendono non solo dal progetto
dell'applicazione, ma anche dalla particolare implementazione della concorrenza nel sistema di esecuzione. Questa difficolt aggiuntiva dovuta al fatto che l'ordine di esecuzione
delle operazioni (per esempio, l'accesso ai dati condivisi) non pu in generale essere determinato nel momento in cui viene scritto il programma, ma dipende dalla velocit di esecuzione dei diversi flussi concorrenti. Infatti, questi potrebbero essere eseguiti su processori diversi, ed esibire differenti velocit durante varie esecuzioni dell'applicazione.
I potenziali problemi che abbiamo osservato nel caso di produttori e consumatori che
accedono allo stesso buffer in maniera concorrente sono dovuti a sequenze di azioni particolarmente sfortunate. Potrebbe accadere che il sistema funzioni correttamente nella maggior parte delle esecuzioni, ma fallisca quando le azioni accadono in una determinata sequenza.
Diverse sequenze di azioni di accesso al buffer potrebbero corrispondere a differenti velocit
di esecuzione dei processi.
Esistono svariati modi per influire sulla velocit di esecuzione dei processi. Innanzitutto,
i processi potrebbero condividere lo stesso processore e lo schedulatore potrebbe assegnare
una porzione prestabilita di tempo di processo a intervalli ciclici. O, in alternativa, alcuni
processi potrebbero avere una priorit pi alta rispetto ad altri. Oppure, ancora, ogni processo potrebbe eseguire su un processore fisico separato e dedicato. In questi tre casi la velocit di esecuzione dei processi soggetta a variazioni.
Vorremmo progettare il nostro software in modo da assicurare un comportamento corretto, indipendentemente dalla velocit di esecuzione dei processi. Il sistema dovrebbe avere lo stesso comportamento, sia che venga eseguito su una macchina monoprocessore sia che
venga eseguito da una macchina multiprocessore, sia che il processore condiviso, nel caso
della macchina monoprocessore, usi porzioni prestabilite di tempo di processo o priorit, e
cos via. Questo renderebbe la nostra soluzione pi generale, permettendone il funzionamento
su una famiglia di implementazioni della macchina astratta sottostante. Cambiare la macchina astratta sottostante, dunque, influirebbe solo sulle prestazioni del software e non sulla sua correttezza. Inoltre, ragionare sulla correttezza del progetto sarebbe pi semplice, visto che il progetto potrebbe essere valutato senza prendere in considerazione la velocit dei
processi.
Per fare questo, estenderemo i concetti e la notazione degli oggetti astratti e dei tipi di
dati astratti al caso del software concorrente. In particolare, seguiremo due paradigmi comuni della progettazione di software concorrente. Questi paradigmi, a loro volta, saranno
riflessi nei costrutti forniti da alcuni linguaggi di programmazione esistenti.
II primo approccio, ispirato dal linguaggio di programmazione Concurrent Pascal e ora
reso popolare da Java, porta alla nozione di monitor, che rappresenta oggetti cui si vuole accedere concorrentemente come entit passive protette. Questo approccio detto basato sui
monitor. Il secondo approccio, ispirato dal linguaggio di programmazione Ada, porta al con-

certo di guardiano di risorsa, usato per rappresentare un oggetto concorrente attivo. Questo
meccanismo per la sincronizzazione viene chiamato rendezvous\ per questo motivo l'approccio verr detto basato su rendezvous.
Anche se l'approccio scelto per descrivere il progetto software dovrebbe essere indipendente dal linguaggio di implementazione, la traduzione di un progetto su un programma risulta maggiormente diretta se entrambi sono basati sulla stessa filosofia. Certe strutture di progettazione (ad esempio, un progetto basato su rendezvous) sono pi semplici da
mappare su determinati linguaggi (per esempio, Ada). Inoltre, sar necessario uno sforzo sostanzialmente maggiore nel caso in cui il linguaggio scelto sia sequenziale e dunque la concorrenza sia da ottenere tramite chiamate al sistema operativo sottostante.
4.5.1.1

Monitor

Un monitor un oggetto astratto cui si pu accedere in un ambiente concorrente. Il monitor


garantisce ai propri clienti che le operazioni che esporta vengano eseguite in mutua esclusione. Se un processo P richiede l'esecuzione di un'operazione in un monitor gi impegnato da un altro processo, il monitor sospende l'esecuzione di P. L'esecuzione verr ripresa solo nel momento in cui P potr accedere in modo esclusivo alle operazioni del monitor.
Dal punto di vista del cliente, la mutua esclusione viene garantita dal monitor mediante la sua interfaccia; in realt, il modo in cui viene fornita dipender dall'implementazione del monitor. Se dovessimo implementare il nostro sistema in un linguaggio come
Concurrent Pascal o Java, la mutua esclusione potrebbe essere garantita direttamente dal
linguaggio. Se il linguaggio non dovesse fornire alcun sistema automatico per assicurare la
mutua esclusione, allora dovremmo garantirlo noi all'interno dell'implementazione della
nostra applicazione.
Ovviamente, la mutua esclusione, nell'esecuzione di operazioni individuali, non
sufficiente per garantire la correttezza nell'accesso agli oggetti condivisi. Come abbiamo
visto prima, due consumatori potrebbero invocare l'operazione NOT_EMPTY per controllare che il buffer non sia vuoto, ed entrambi potrebbero ottenere l'autorizzazione ad eseguire la rimozione di un carattere. Nel caso in cui il buffer, originariamente, contenesse solo un carattere, il secondo tentativo di rimuovere un carattere genererebbe uno stato
erroneo.
Per risolvere problemi di questo tipo, estenderemo la nostra notazione di progettazione testuale, facendo s che le operazioni esportate vengano associate a clausole opzionali
r e q u i r e s . Dal punto di vista dei clienti questa clausola verr controllata automaticamente all'atto di chiamare l'operazione. Se il risultato dovesse essere t r u e , allora l'operazione
verrebbe eseguita normalmente, ma in mutua esclusione. Se il risultato dovesse essere f a l s e ,
allora il processo chiamante verrebbe sospeso in attesa che la condizione diventi t r u e . La
sospensione dei processi rilascia la mutua esclusione precedentemente acquisita, in modo che
altri processi possano ottenere il permesso di entrare nel monitor. A un certo punto, un processo che esegue un'operazione del monitor potrebbe far s che la condizione su cui altri processi sono in attesa diventi t r u e . Tali processi potrebbero, allora, essere presi in considerazione per una ripresa dell'esecuzione. Una volta ripreso, il processo eseguirebbe l'operazione in mutua esclusione, come se avesse richiesto l'operazione solo in quel momento. In questo modo, il test sulla clausola r e q u i r e s e l'esecuzione dell'operazione associata risulterebbero
in un'azione atomica.

concurrent
Questo

module

CHARBUFFER

un monitor

in u n ambiente
u s e s ...
exports

(ad

un

esempio,

modulo

di

oggetto

astratto

concorrente).

p r o c e d u r e P U T (C: in C H A R ) r e q u i r e s N O T F U L L ;
p r o c e d u r e G E T (C: out C H A R ) r e q u i r e s N O T _ E M P T Y ;
NOT_EMPTY
e NOT_FULL
sono funzioni
booleane
nascoste
che,
rispettivamente,
restituiscono
TRUE se il buffer
non vuoto
o se non pieno.
Non vengono
esportate
come operazioni
perch
il loro scopo solo quello
di ritardare
le chiamate
a PUT e GET
se queste
vengono
effettuate
quando
il buffer
in uno stato per
non le pu
accettare.

end

CHAR

cui

BUFFER

Figura 4.19

Esempio di monitor in T D N .

Se, ad esempio, dovessimo scegliere Java come linguaggio di programmazione, tutte le


sospensioni e le riattivazioni necessarie per l'adeguata gestione della clausola r e q u i r e s sarebbero fornite automaticamente dall'implementazione del monitor. Se dovessimo impiegare un linguaggio di programmazione sequenziale, la mutua esclusione e la clausola
r e q u i r e s potrebbero essere implementate da appropriate chiamate al sistema operativo.
La Figura 4.19 un esempio di monitor che rappresenta un buffer di caratteri.

generic

concurrent

module

GENERIC_FIFO_QUEUE

Questo
un tipo di monitor
generico
astratto
cui si accede
in u n ambiente
uses . . .
exports
type QUEUE:
procedure

GET

requires

end

GENERIC

Figura 4.20

(Ql:in

out

NOT_FULL
(Q2:in

QUEUE;

(Ql:

out

FIFO_QUEUE

in

EL)

QUEUE);

QUEUE;

NOT_EMPTY(Q2:

El:
E2:

QUEUE);

(EL)

Esempio di tipo di monitor in T D N .

out

(EL)

esempio,
un
concorrente).

?;

PUT

requires
procedure

(ad

EL)

tipo

di

dato

Aggiungiamo semplicemente la parola chiave c o n c u r r e n t per specificare la semantica del


monitor per il modulo.
anche possibile definire monitor generici. Un esempio di monitor generico, che rappresenti code FIFO di componenti di qualsiasi tipo, illustrato nella Figura 4.20.
Le operazioni esportate da un monitor possono sollevare eccezioni, la cui sintassi rimane invariata. Ad esempio, nel caso del monitor C H A R B U F F E R , supponiamo che l'interfaccia specifichi che il carattere, parametro in ingresso di PUT debba soddisfare alcuni requisiti. La specifica di PUT verrebbe modificata e diventerebbe
procedure
raises

PUT

(C: in CHAR) requires

NOT_FULL

PAR_ERROR;

dove PAR ERROR u n ' e c c e z i o n e sollevata d a PUT q u a n d o il p a r a m e t r o n o n s o d d i s f a i requisiti specificati nell'interfaccia.

Concludiamo a questo punto la nostra breve discussione sui monitor e i tipi di monitor, senza aggiungere ulteriori dettagli alla nostra notazione per la progettazione. Entrare nei
dettagli solleverebbe diversi aspetti critici che renderebbero la nostra notazione pi complessa
ed eccessivamente orientata ai linguaggi di programmazione.
Esercizio
4.39

Estendete G D N , f o r n e n d o u n a n o t a z i o n e grafica per i m o n i t o r e i tipi di m o n i t o r .

4.5.1.2

Guardiani e rendezvous

L'approccio alla progettazione di software concorrente basato sui monitor vede il sistema
software composto da due tipi di entit: entit attive (ad esempio, processi) che hanno flussi di controllo indipendenti, e oggetti passivi. Gli oggetti passivi possono essere istanze di tipi astratti o di oggetti astratti. Gli oggetti passivi possono essere condivisi tra processi, o possono essere usati da un processo come risorsa privata. Un oggetto condiviso deve essere o un
monitor o un'istanza di un tipo di monitor; altrimenti non avremmo la garanzia che gli accessi all'oggetto conservino uno stato consistente.
Come abbiamo anticipato, esistono altri paradigmi per la progettazione di sistemi concorrenti. Un esempio di questi paradigmi costituito dall'approccio scelto dal linguaggio di
programmazione Ada. Secondo questo approccio gli oggetti privati sono le uniche entit passive di un sistema. Gli oggetti attivi (chiamati task in Ada) sono invece di due tipi: processi veri e propri e guardiani di risorse condivise.
I guardiani sono loro stessi task, il cui unico scopo quello di garantire un accesso ordinato al loro "segreto nascosto", che rappresenta una risorsa incapsulata, spesso una struttura dati. I guardiani sono task che non terminano, che ciclicamente aspettano richieste di
esecuzione di una determinata operazione. Un guardiano potrebbe accettare o rifiutare una
richiesta, secondo una determinata condizione basata sullo stato interno della risorsa controllata, accettandone, comunque, sempre una sola alla volta.
Un task che inoltra una richiesta al guardiano viene sospeso fino a quando il guardiano non accetta la richiesta e completa l'esecuzione dell'azione associata; secondo la terminologia di Ada questo tipo di interazione tra task e guardiano viene detto rendezvous.

La stessa notazione sintattica che abbiamo fornito nel caso dell'approccio basato su
monitor pu essere utilizzata per descrivere un approccio alla progettazione basato su rendezvous. Ci che cambia ovviamente la semantica. Come esempio, si prenda il modulo
concorrente della Figura 4.19. Se dovessimo interpretare la notazione di progettazione nel
contesto dell'approccio basato su rendezvous, C H A R B U F F E R sarebbe un task che accetta
richieste di operazioni sul proprio stato protetto mediante l'esecuzione o di GET O di PUT.
Una richiesta di GET viene accettata solo se il buffer non vuoto; una richiesta di PUT solo se il buffer non pieno. Un task che fa di queste richieste (mediante chiamate appropriate) sospeso fino a quando la richiesta non viene eseguita dal guardiano, ovvero fino
a quando il guardiano non vede che la clausola w h e n diventa true e decide di rispondere
alla richiesta ed eseguirne il contenuto. Il guardiano accetta richieste valide ciclicamente,
senza mai fermarsi.
Per chiarire queste questioni, si potrebbe assumere che, in un linguaggio basato su
rendezvous, le parti interne del modulo assomiglino alla bozza di programma della Figura
4.21. Il programma, scritto in un linguaggio autoesplicativo simile ad Ada, descrive la struttura di un guardiano che implementa il modulo concorrente della Figura 4.20. L'esempio
mostra come il guardiano controlli ripetutamente che non ci siano richieste da parte dei
clienti.
Sia l'approccio basato su monitor sia quello basato su rendezvous forniscono soluzioni non-deterministiche ai problemi di concorrenza. Il guardiano di CHAR BUFFER viene specificato come un server che accetta richieste di accesso al buffer, per aggiungervi un carattere o per estrarne uno. Le richieste di aggiunta di un carattere al buffer sono accettate se il
buffer non pieno; analogamente, le richieste di estrazione di un carattere dal buffer sono
onorate se il buffer non vuoto. Dal punto di vista del cliente, quando il buffer non n
pieno n vuoto, le richieste pendenti (se esistono) vengono gestite in maniera non-deterministica, come viene suggerito dal costrutto s e l e c t . . . o r . . . e n d s e l e c t della Figu-

loop
select
when

NOT_FULL

accept

PUT

(C:

in

Q u e s t o il corpo
come se fosse
una

CHAR);
di

PUT;
il cliente
procedura norma 1 e

lo

chiama

lo

ehiama

end ;
or
when

NOT_EMPTY

accept

GET

(C: out

Questo
il corpo
come se fosse
una

CHAR);
di

GET; il cliente
procedura normal e

end ;
end
end

select;

loop;

Figura 4.21

Struttura interna tipica di un task guardiano.

ra 4.21. Si noti che non abbiamo specificato cosa succede nel caso in cui diverse richieste
dello stesso tipo (ad esempio GET) vengano fatte allo stesso guardiano. Anche in questo caso, possiamo assumere che la scelta di quale richiesta debba essere eseguita sia fatta nondeterministicamente 8 . Similmente, nell'approccio basato su monitor, diversi processi potrebbero essere in attesa che la condizione di mutua esclusione venga rilasciata. Quale tra questi viene realmente risvegliato quando il monitor si libera? E, infine, se diversi processi sono sospesi su una clausola r e q u i r e s e la condizione dovesse diventare vera, quale verrebbe scelto?
In tutti questi casi, il comportamento del modulo, dal punto di vista del cliente, risulta non-deterministico. Ovvero, il modulo non rivela come prende realmente le sue decisioni. Il non-determinismo una propriet importante a livello di specifica, in quanto
indipendente dalle particolari implementazioni di concorrenza. Il nostro progetto, dunque, non risulta sensibile al modo in cui verr risolto il non-determinismo. Il linguaggio
di programmazione che usiamo per implementare il sistema potrebbe operare delle scelte
precise per i punti in cui abbiamo lasciato il non-determinismo, mentre altre scelte potrebbero venire prese dalla macchina astratta che supporta l'esecuzione del linguaggio di
programmazione. Qualunque siano le scelte fatte durante l'implementazione, il sistema
sar corretto e varieranno solo le sue prestazioni. Evitare il non-determinismo a livello di
specifica, costringe talvolta i progettisti a sovra-specificare inutilmente il comportamento
di un sistema.
Nella progettazione di sistemi concorrenti, necessario essere particolarmente attenti,
in modo da prevenire situazioni anomale durante l'esecuzione, che potrebbero causare il
blocco, per un tempo indefinito, dell'intero sistema (o di un sotto-sistema). Questa situazione anomala viene chiamata deadlock. Ad esempio, consideriamo il caso in cui un
processo A venga sospeso su una clausola r e q u i r e s X di un monitor. Supponiamo che
l'unico modo affinch X diventi true che un altro processo B esegua una determinata
parte di codice. Ma ipotizziamo che il processo B sia a sua volta bloccato su una clausola
r e q u i r e s Y di un monitor, e che l'unico modo per cui Y diventi true che il processo
A termini la sua chiamata al monitor ed esegua un certo pezzo di codice successivo. I processi A e B sono bloccati indefinitamente. Ciascuno aspetta che l'altro proceda nelle sue
computazioni. I Capitoli 5 e 6 spiegheranno come sia possibile individuare situazioni
anomale di questo tipo. L'individuazione pu essere fatta fornendo un modello formale
per l'architettura software e applicando poi metodi di analisi adeguati al modello formale. Ad esempio, illustreremo le reti di Petti, una notazione formale con cui possibile modellizzare architetture concorrenti di questo tipo, e vedremo come, mediante l'analisi del
modello delle reti di Petri, sia possibile individuare potenziali deadlock.
Esercizio
4.40

Considerate un ambiente di programmazione c o m p o s t o da un linguaggio di programmazione sequenziale (ad esempio C) e un sistema operativo (ad esempio Unix). Fornite linee guida
per la sua progettazione basata su monitor e su rendezvous.

In realt Ada dice che queste richieste debbano essere gestite in una maniera first in, first out.

4.5.2

Software real-time

Nel paragrafo precedente abbiamo affrontato il problema dell'accesso concorrente a dati


condivisi assumendo che si potesse sospendere l'esecuzione dei processi in competizione per
un determinato periodo di tempo. Per esempio, nella progettazione basata su monitor, i produttori potevano essere sospesi qualora il buffer cui accedevano fosse pieno.
Sfortunatamente, non sempre possibile sospendere i processi. Per esempio, un'operazione invocata su un oggetto astratto potrebbe appartenere al flusso di esecuzione di un
processo che non pu essere sospeso, in quanto il processo un'attivit fisica, la cui evoluzione temporale non sotto il controllo del sistema. Supponiamo, ad esempio, che in un
impianto chimico un produttore sia un sensore che raccoglie dati da spedire a un controllore (un computer). In un caso come questo, potrebbe non esistere un modo di rallentare o
sospendere l'impianto. Un dato inviato dall'impianto e non accettato in tempo utile dal controllore andrebbe irrimediabilmente perso. compito del controllore provvedere a soddisfare i requisiti di velocit dell'impianto in modo che i dati inviati sulla linea vengano accettati senza perdite. Problemi di questo tipo caratterizzano i sistemi real-time, che possono
essere definiti come sistemi per i quali la correttezza del funzionamento dipende dalla velocit di esecuzione dei processi che compongono il sistema stesso. Quando progetteremo sistemi di questo tipo dovremo soddisfare i requisiti che specificano i limiti di tempo entro i

concurrent module REACTIVE_ CHAR_BUFFER


Questo
un oggetto
simile a un monitor
real-time.
uses . . .

che

lavora

in

un

ambiente

exports
reactive

p r o c d u r e PUT

(C:

in

CHAR);

PUT viene
utilizzato
da processi
esterni
e due
richieste
consecutive
a PUT devono
arrivare
distanziate
da pi
di 5 millisecondi;
altrimenti,
alcuni
caratteri
potrebbero
andare
persi
procedure

end

GET

(C:out

CHAR);

REACTIVE_CHAR_BUFFER
(a)

Modulo
PUT

REACTIVE_CHAR_BUFFER

GET

(b)

Figura 4.22

Rappresentazione (a) testuale e (b) grafica della notazione per la d e s c r i z i o n e


di eventi.

quali devono essere eseguite determinate operazioni. Se alcune operazioni non dovessero essere eseguite entro i limiti (ad esempio se dovessero essere completate o troppo presto o troppo
tardi) il sistema risulterebbe non corretto.
I vincoli di rispetto dei limiti di tempo mostrano la differenza fondamentale tra un sistema puramente concorrente e un sistema concorrente real-time. Un sistema concorrente
viene progettato ignorando la velocit dei processi. Applicando adeguati principi di progettazione, si pu assicurare che il sistema sia corretto indipendentemente dalla velocit dei processi che lo compongono. I processi possono essere esplicitamente sospesi (oppure possono
essere rallentati) in modo da assicurare la validit di determinate propriet logiche. Per esempio, nel caso delle soluzioni basate su monitor discusse nel paragrafo precedente, siamo in
grado di dire: "Nel momento in cui il produttore avr il permesso di eseguire un'operazione di PUT il buffer avr lo spazio libero necessario per immagazzinare il valore fornito dal
cliente". Tutto ci viene affermato dalla clausola r e q u i r e s . Tali affermazioni non hanno
senso nel caso di un sistema real-time. Se i segnali in ingresso dovessero arrivare con una frequenza, supponiamo, di 5 millisecondi e, per ragioni di sicurezza, nessun segnale in ingresso deve esser perso, sapere che "prima o poi i segnali saranno inseriti in un buffer" non risolve il problema: il segnale deve essere inserito nel buffer entro 5 millisecondi (ovvero prima che arrivi il segnale successivo); altrimenti il segnale andrebbe perso.
Per affrontare le questioni real-time non proponiamo alcun costrutto speciale nella nostra notazione di progetto, ma piuttosto suggeriamo di utilizzare commenti per aggiungere
i requisiti necessari. Per esempio, si potrebbe usare un commento per dire che il tempo di
esecuzione di una funzione esportata possiede limiti inferiori e superiori.
I sistemi real-time molte volte interagiscono con un ambiente esterno che produce autonomamente stimoli, in momenti non prevedibili. Tali sistemi possono essere visti, dunque, come sistemi reattivi, che rispondono agli stimoli in ingresso forniti dal mondo esterno9. E quindi utile disporre di un modo per specificare che una data funzione rappresenta
la risposta a una richiesta proveniente dall'ambiente esterno. In T D N specificheremo questo fatto con la parola chiave r e a c t i v e ; in GDN lo indichiamo mediante una freccia a zig
zag (vedi Figura 4.22).
Se un'operazione dovesse essere classificata come r e a c t i v e , vuol dire che la sua esecuzione non potr essere ritardata, ad esempio, sospendendo il richiedente per riattivarlo pi
tardi, in un momento pi conveniente. In pratica, le operazioni reattive vengono specificate indicando limiti per i loro tempi di esecuzione (ad esempio: "L'operazione pu essere eseguita ogni x millisecondi, con 5 s x s 15"). E responsabilit del progettista assicurare che,
quando sopraggiunge un richiesta per tale operazione, non vi siano altre operazioni del modulo in esecuzione. Altrimenti il risultato sarebbe imprevedibile.
L'esperienza pratica ha mostrato che le questioni riguardanti il tempo sono estremamente
critiche, e sono ci che rende i sistemi real-time difficili da progettare e verificare. La complessit della progettazione e della verifica cresce sempre pi, man mano che ci spostiamo da
sistemi puramente sequenziali a sistemi concorrenti, e da sistemi concorrenti a sistemi realtime; ci che fa la differenza il tempo. Nel caso dei sistemi sequenziali, il tempo riguarda

II fatto che l'ambiente attivi un'operazione a intervalli n o n determinabili tipico, anche se non esclusivo, dei sistemi real-time.

solo le prestazioni del sistema. Nel caso dei sistemi concorrenti possibile organizzare il sistema in modo che un'adeguata sincronizzazione assicuri la correttezza, indipendentemente
dal tempo. Ancora una volta, dunque, il tempo incide solo sulle prestazioni del sistema. Nel
caso dei sistemi real-time, invece, il tempo incide sulla correttezza. Viene dunque introdotta
un'ulteriore dimensione (la dimensione temporale) che occorre prendere in considerazione al
momento della progettazione, dell'implementazione e della verifica dei sistemi.
Oltre a essere intrinsecamente complessi, i sistemi real-time offrono spesso funzioni critiche, dove errori possono avere conseguenze disastrose, causando non solo gravi perdite finanziarie, ma addirittura di vite umane. Una delle qualit richieste in molti sistemi real-time,
dunque, l'affidabilit. La progettazione e la verifica di sistemi real-time affidabili sono attualmente oggetto di un'attiva ricerca.

4.5.3

Software distribuito

Una classe importante di sistemi concorrenti costituita dai sistemi distribuiti, nei quali attivit concorrenti vengono eseguite su computer differenti connessi da una rete di comunicazione. Ad esempio, i computer di un'azienda sono spesso connessi per mezzo di una LAN
{locai area network) che consente agli utenti dei diversi computer di comunicare (ad esempio, tramite posta elettronica) e di condividere risorse (ad esempio, stampanti e file): in altre parole, di cooperare. Un insieme di LAN geograficamente distribuite possono essere connesse mediante una WAN {wide area network). Un sistema risultante dal collegamento delle diverse reti detto inter-rete {internet). Nel caso in cui un'inter-rete appartenga o sia sotto il controllo di una singola organizzazione viene detta intranet. Una intranet pu supportare diverse applicazioni distribuite, incluso il servizio interno di posta o il servizio interno
basato sul web per distribuire informazioni ai dipendenti.
In questo paragrafo forniremo una visione d'insieme delle questioni che hanno a che
fare con la gestione di software distribuito. Commenti aggiuntivi verranno forniti dopo che,
nel prossimo paragrafo, avremo introdotto la progettazione orientata agli oggetti. Inizieremo
osservando come la distribuzione imponga ulteriori requisiti ai concetti di moduli e di relazioni tra moduli studiati fino a ora. I moduli guardiani di risorse (Paragrafo 4.5.1) sono
direttamente utilizzabili in un'applicazione software distribuita e potrebbero rappresentare
un'unit di distribuzione. Dobbiamo, tuttavia, imporre determinate restrizioni alla relazione U S E S tra due moduli che risiedono su due differenti macchine. In particolare, dato che
i moduli su macchine diverse hanno spazi di indirizzi indipendenti, non possiamo consentire a un modulo di accedere direttamente alle variabili definite in altri moduli. Permetteremo,
comunque, l'accesso indiretto a tali variabili attraverso procedure di accesso esportate dal modulo, descritte nel Paragrafo 4.6.3.3.
Con il software distribuito dovremo considerare, al momento della progettazione, tre
nuove questioni.

I vincoli modulo-macchina. A volte richiesto che un modulo sia eseguito su una


particolare macchina. Ad esempio, se lo scopo del modulo fornire un servizio di
stampa, dovr essere eseguito su un computer cui collegata una stampante. In altri
casi, il modulo potrebbe essere eseguito su un'ampia classe di macchine (ad esempio, i
moduli che hanno una connessione a un gateway, in modo che possano raggiungere le
reti esterne all'azienda).

La comunicazione tra moduli. Se due moduli risiedono su macchine differenti, come possono comunicare? Abbiamo visto che tutti i moduli che risiedono sulla stessa
macchina possono comunicare mediante l'uso di un'area globale condivisa: un modulo vi registra le informazioni mentre un altro le legge. Questo approccio, che funziona sia per programmi sequenziali che per programmi concorrenti, non pu essere
esteso direttamente a un sistema distribuito visto che i moduli si trovano su macchine diverse. Un altro approccio alla comunicazione tra moduli in ambiente sequenziale
consiste nel passaggio di parametri nella chiamata a una procedura e nei valori che la
stessa procedura restituisce. Il meccanismo di chiamata alle procedure stato esteso
ai sistemi distribuiti attraverso il meccanismo di remote procedure cali (RPC, chiamata a procedura remota) in cui non viene richiesto che il chiamante e il chiamato siano sulla stessa macchina. Il linguaggio Java ha introdotto la nozione di remote method
invocation (RMI, invocazione di metodo remoto), la quale permette che un oggetto
chiami una procedura in un oggetto che risiede su un'altra macchina 10 . Un ulteriore
approccio per affrontare la comunicazione intermodulare in un ambiente distribuito
consiste nell'uso di messaggi. Esistono diverse librerie e diversi servizi forniti dai sistemi operativi per il supporto allo sviluppo di applicazioni che facciano uso di RPC o
messaggi.

L'accesso efficiente agli oggetti astratti. Abbiamo identificato gli oggetti astratti come tipi di moduli che si propongono in maniera del tutto naturale durante la progettazione di un sistema. In un sistema centralizzato, non incontriamo costi elevati per
incapsulare i dati necessari al modulo MT in un oggetto astratto M2. In un sistema distribuito, se i due moduli si trovano su macchine diverse, l'accesso ai dati contenuti in
M2 da parte di M! richieder molto pi tempo (a causa della chiamata a procedura remota) rispetto all'accesso a dati contenuti in Mj. I tempi d'accesso non locali possono
essere diversi ordini di grandezza pi alti per dati remoti. Due approcci possibili per
rendere pi efficienti gli oggetti astratti in un ambiente distribuito sono la replicazione e la distribuzione.

Esamineremo queste questioni in maggiore dettaglio pi avanti. Ma prima discuteremo brevemente un modello particolare per la strutturazione di un sistema distribuito: il modello
client-server.
4.5.3.1

II modello client-server

Abbiamo detto che il ruolo dei moduli quello di fornire servizi agli altri moduli, detti suoi
client. Questo modello direttamente applicabile alle architetture distribuite. L'architettura
pi popolare per le applicazioni distribuite consiste in moduli client e moduli server che risiedono su macchine diverse. Per esempio, consideriamo un servizio di stampa fornito a un'intera rete di computer. In questa rete, alcuni computer saranno connessi con stampanti, altri no. Possiamo progettare il servizio di stampa in modo che consista di moduli client e di
moduli server. Il server riceve un file e lo stampa su una stampante. Il client accetta un no-

10

I termini oggetto e metodo sono definiti nel Paragrafo 4.6, che si occuper della progettazione orientata
agli oggetti.

me di file dall'utente e spedisce i contenuti del file a un modulo server, insieme alle informazioni riguardanti l'utente che ha richiesto l'operazione di stampa.
Alcuni dei moduli che abbiamo incontrato possono essere considerati, in modo del tutto naturale, moduli server in un'architettura di tipo client-server. Per esempio, un modulo
simile all'esempio BUFFER del Paragrafo 4.5.1 potr essere utilizzato dai moduli client del
servizio di stampa per depositarvi i file da stampare. I moduli client sono i produttori, mentre il modulo server il consumatore. Anche i moduli guardiani di risorse del Paragrafo 4.5.1.2
possono modellare i server in un'applicazione distribuita.
Esercizio
4.41

Lo stesso m o d u l o pu essere client in un contesto e server in un altro. Per esempio, consideriamo un servizio di stampa che consiste di un numero di moduli client eseguiti su macchine senza stampanti, un certo numero di moduli BUFFER eseguiti su macchine qualsiasi e un
certo numero di moduli server eseguiti su macchine che invece possiedono stampanti. Verificate
se il BUFFER client o server.

4.5.3.2

II vincolo modulo-macchina

Come abbiamo detto, una questione che occorre affrontare nelle architetture software distribuite quella del vincolo tra moduli e macchine. A volte, come nell'esempio del server
di stampa, il vincolo imposto dall'ambiente fisico o dall'infrastruttura sottostante. In altri
casi, esiste la possibilit di scelta e questa scelta potr essere guidata da diverse considerazioni. Per esempio, per poter ridurre il costo della comunicazione potremmo volere vincolare i moduli server ad alcune macchine vicine ai propri client, eventualmente sulla stessa
macchina, qualora fosse possibile.
Un'altra questione se il vincolo debba essere statico o dinamico. Un vincolo statico
pi semplice, ma la possibilit di scegliere il luogo di esecuzione di un modulo in maniera
dinamica ci permette, ad esempio, di scegliere un computer con un basso carico di lavoro,
al fine di migliorare le prestazioni dell'applicazione. Questa possibilit essenziale anche per
il supporto di sistemi altamente affidabili, visto che il malfunzionamento di una macchina
potr essere tollerato spostando i moduli ivi allocati su un'altra macchina. Questo spostamento dinamico di processi viene detto migrazione. Per un approfondimento dei dettagli su
questo argomento rimandiamo alla letteratura specializzata.
Un modulo pu essere, o meno, stanziabile (cio, pu essere creato) dinamicamente.
Alcuni sistemi supportano la creazione di processi a run-time, mentre altri non lo fanno.
Nel caso in cui i processi possano essere creati dinamicamente l'applicazione potr determinare a run-time quanti esemplari del processo debbano essere eseguiti. E anche possibile, in
alcuni casi, definire su quale macchina il processo creato debba andare in esecuzione. Diversi
linguaggi e librerie che supportano il software distribuito offrono una grande variet di opzioni per il progettista.
Esercizi
4.42

Spiegate per quale motivo la creazione dinamica di processi potrebbe non essere desiderabile
in un sistema real-time.

4.43

Considerate un'applicazione che dovrebbe essere accessibile da qualsiasi macchina di una rete. Ci sono migliaia di macchine sulla rete ma ci aspettiamo che l'applicazione n o n sia eseguita da pi di 10 utenti alla volta. Progettate una soluzione a questo problema. Poter vincolare dinamicamente moduli alle macchine risulta utile per questo esempio? C o m e si potrebbe utilizzare la migrazione dei processi in quest'applicazione? Quale sarebbe la vostra soluzione nel caso venisse richiesto l'uso di vincoli statici tra i moduli e le macchine?

4.5.3.3

Comunicazione tra moduli

Nelle applicazioni distribuite vengono utilizzati due modelli di comunicazione: la chiamata a procedure remote e la spedizione di messaggi.
Il meccanismo della chiamata a procedure remote un'estensione della chiamata a procedura tradizionale, che consente ai moduli chiamanti e chiamati di risiedere su macchine differenti. Sono disponibili diversi pacchetti commerciali che supportano questo tipo di interazione sotto diversi sistemi operativi. Questi pacchetti offrono un IDL (interface definition
language, linguaggio di definizione di interfacce) e un compilatore. Usando IDL il progettista
definisce un'interfaccia per qualsiasi procedura che possa essere chiamata da client remoti. Il
compilatore elabora le definizioni e genera (o include) file header (di intestazione) che i client
e i server includeranno al momento della loro compilazione; questi file forniranno l'accesso alle procedure stub che supportano la comunicazione intermodulare. Per via della similitudine
tra le chiamate a procedure remote e le chiamate a procedure tradizionali possibile progettare le applicazioni senza distinguere tra servizi richiesti locali e remoti: qualsiasi interfaccia di
modulo scritta in termini di chiamate a procedure potr essere supportata in un'applicazione
distribuita. In pratica per ci sono molti dettagli che ostacolano questo approccio.
La prima differenza tra chiamate a procedure locali e remote risiede nelle prestazioni.
Dato che i parametri della chiamata devono essere trasmessi sulla rete, l'overhead di RPC
un ordine di grandezza maggiore di una chiamata locale. Cambiare da una chiamata locale
a una remota potr avere un impatto significativo sulle prestazioni di un'applicazione, considerando, inoltre, che un compilatore potrebbe a volte generare codice in-line per le chiamate locali in modo da migliorare le prestazioni, mentre la cosa non possibile per le chiamate remote.
Un'altra differenza sostanziale tra le chiamate a procedure locali e remote risiede nelle
modalit del passaggio dei parametri. Anche se i concetti di chiamata e risposta di una procedura possono essere implementati in maniera semplice, non tutte le modalit di passaggio dei parametri possono essere supportate in remoto. Per esempio, quando i moduli chiamanti e i chiamati risiedono all'interno di due spazi di indirizzamento separati, due moduli non possono comunicare in termini di puntatori. Questo significa che il passaggio di parametri per riferimento, o il passaggio di strutture dati concatenate, potrebbe risultare molto problematico, sempre che risulti possibile. Di conseguenza, i sistemi commerciali di chiamate a procedure remote generalmente non supportano il passaggio di strutture a puntatori.
Il paradigma della spedizione di messaggi per l'interazione intermodulare pu essere
pensato in termini di cassette postali. Si pu considerare che ogni modulo abbia una cassetta della posta in cui riceve messaggi dagli altri moduli. I moduli client possono spedire
messaggi alla cassetta della posta di un modulo server. Un server raccoglier una richiesta
dalla propria cassetta della posta, agir di conseguenza e, se necessario, spedir una risposta

alla cassetta della posta del client. Le considerazioni pi importanti da fare nell'invio di messaggi riguardano le dimensioni delle cassette della posta (quanti messaggi possono essere inseriti nel buffer), se la spedizione di messaggi debba essere sincrona o asincrona e se un modulo possa scegliere una cassetta della posta piuttosto che un'altra in modo dinamico, o se
la scelta debba rimanere statica.
Anche se i due paradigmi di chiamata a procedure remote e di spedizione di messaggi
hanno la stessa potenza (possono simularsi a vicenda), rappresentano soluzioni appropriate
per architetture software differenti. La differenza pi consistente risiede nel fatto che la chiamata a procedure remote di per se stessa una modalit di interazione sincrona, mentre la
spedizione di messaggi asincrona. Ci significa che un modulo che fa una richiesta a una
procedura remota deve necessariamente aspettare la risposta, mentre un client che spedisce
un messaggio pu continuare con il suo thread di controllo. L'esistenza di diversi thread di
controllo significa che il progettista dovr affrontare i thread concorrenti in maniera esplicita.
Esercizi
4.44

Considerate un'applicazione in cui u n m o d u l o sensore legge una serie di valori su una linea
di ingresso e li spedisce a un m o d u l o di registrazione, per ulteriori elaborazioni. Se questi due
moduli dovessero essere distribuiti su due differenti macchine, quale forma di comunicazione inter-modulare scegliereste di utilizzare? Perch?

4.45

Per l'esercizio precedente, abbozzate ciascun m o d u l o in un'estensione adeguata di T D N , usand o una volta la chiamata a procedura remota e una volta la spedizione di messaggi.

4.5.3.4

Replicazione e distribuzione

La considerazione finale inerente la progettazione di software per un ambiente distribuito


riguarda la necessit di rendere efficiente l'accesso ai dati. In particolare, abbiamo enfatizzato come l'oggetto astratto sia un tipo di modulo utile, che fornisce ai moduli client un accesso a una struttura dati incapsulata al proprio interno. Ci significa che un modulo client
potr inviare una richiesta, solitamente attraverso una chiamata a una procedura, per i dati
cui deve accedere. Il costo dell'accesso ad alcuni dati attraverso una chiamata a procedura
invece che direttamente nella memoria (come succede nel caso di dati locali) considerato
eccessivo addirittura in alcune applicazioni centralizzate. Il costo cresce considerevolmente
nel caso in cui l'oggetto astratto si trovi su una macchina remota. Il costo dell'accesso remoto sulle reti pi veloci circa quattro volte il costo di un accesso locale e pu incrementare fino a un ordine di grandezza pi alto. Abbiamo, dunque, necessit di un modo per
rendere efficiente l'accesso a oggetti astratti, se vogliamo utilizzarli all'interno di applicazioni distribuite. Esistono due metodi generali per riuscirci.
Il primo approccio quello di replicare l'oggetto distribuito su diverse macchine, addirittura su ogni macchina, se necessario. Nel secondo caso, ciascun cliente dovrebbe avere
accesso all'oggetto astratto localmente. Il problema ora diventa che se un cliente modifica
una copia dell'oggetto, tutte le copie dell'oggetto devono essere mantenute coerenti, in modo che i diversi clienti continuino a osservare lo stesso oggetto piuttosto che tanti oggetti
diversi. Sono state sviluppate numerose tecniche per risolvere il problema della coerenza dei
dati, sia nell'area dei sistemi operativi che in quella dei database.

Un'altra soluzione per velocizzare l'accesso ai dati remoti quella di distribuire l'oggetto astratto su macchine differenti. Ovvero, anche se l'oggetto, dal punto di vista logico,
rimane uno solo, possiamo partizionarlo fisicamente per poi vincolare diverse partizioni a
diverse macchine, mettendo ogni partizione vicino ai clienti che, con maggiore probabilit,
avranno bisogno di accedervi.
Per ogni particolare oggetto astratto che deve essere utilizzato in un'applicazione distribuita dobbiamo considerare se ha senso replicarlo, partizionarlo, realizzare entrambe le
cose o nessuna delle due.
Esercizi
4.46

Estendete sia T D N che G D N perch possano gestire problematiche di allocazione dinamica,


comunicazione intermodulare, replicazione e distribuzione.

4.47

Considerate un servizio di stampa. U n m o d u l o BUFFER immagazzina le richieste di stampa.


D o v r e m m o partizionare o replicare BUFFER? Perch? Perch no?

4.48

Considerate un'applicazione per la gestione di conti bancari. U n oggetto astratto rappresenta


tutti i clienti della banca, che ha molte sedi distribuite sul territorio nazionale. Ciascuna sede
dispone di un computer per l'accesso all'oggetto dei conti degli utenti. Replichereste o partizionereste l'oggetto? Perch?

4.49

Abbozzate la progettazione di un'applicazione per la prenotazione di stanze per conferenze.


Centinaia di stanze devono poter essere prenotate per un qualsiasi arco di tempo. Migliaia di
macchine sulla rete dovranno poter accedere all'applicazione.

4.50

Considerate un'applicazione che si aspetta di ricevere in ingresso i dati riguardanti i titoli di


borsa su un servizio di linea per renderli disponibili a tutti i computer di una rete, mediante
diverse tipologie di query. Decidiamo di usare u n oggetto astratto per rappresentare i dati dei
titoli di borsa. Partizionereste o replichereste i dati? Perch?

4.5.3.5

Middleware

La proliferazione di reti, inter-reti e intranet ha causato lo sviluppo di numerose applicazioni software distribuite, che si affidano a molti servizi comuni per eseguire i propri compiti. Per esempio, tutti devono poter trovare i servizi sulla rete, come quello di stampa, e devono potere trovare diversi processi e comunicare con loro. Il riconoscimento di tali servizi
comuni ha portato a un nuovo strato di software detto middleware. Lo strato di middleware
risiede tra lo strato di rete del sistema operativo e lo strato dell'applicativo. Proprio come i
sistemi operativi forniscono ai programmi servizi, ad esempio, per la gestione di file e direttori, il middleware fornisce alle applicazioni distribuite servizi di distribuzione. Tipicamente,
il middleware fornisce le seguenti due operazioni:

servizi basati sui nomi: per trovare processi e risorse sulla rete;

servizi per la comunicazione: varie forme di comunicazione tra processi, come la spedizione di messaggi o le chiamate a procedure remote.

Questi servizi sono utilizzati da quasi tutte le applicazioni distribuite. I servizi di comunicazione forniscono importanti operazioni per l'impacchettamento dei parametri e il trasporto attraverso macchine eterogenee. Senza tali servizi, lo sviluppatore dell'applicazione

dovrebbe occuparsi della conversione tra tipi di dati quando comunicano processi allocati
su due diversi computer.
Attualmente, i sistemi di middleware forniscono servizi per costruire applicazioni distribuite su reti locali e se ne sta studiando l'estensione per sistemi distribuiti su Internet. Le
sfide principali di questi sistemi di middleware sono la scalabilit e l'affidabilit. Le applicazioni su Internet devono essere in grado di gestire milioni di clienti e di affrontare parziali
malfunzionamenti della rete.
Grazie al middleware, chi progetta un sistema distribuito non deve cominciare da zero, ma pu fare affidamento e, quindi, riusare componenti preesistenti. I sistemi di middleware
forniscono molti altri servizi oltre a quelli associati ai nomi e alla comunicazione. Servizi comuni potranno essere la creazione di log, la gestione delle transazioni, la notifica di eventi,
la sicurezza e cos via. Nel Paragrafo 4.7 illustreremo CORBA, uno specifico standard per il
middleware.

4.6

Progettazione orientata agli oggetti

La progettazione orientata agli oggetti (OO, object-oriented) estremizza l'approccio alla progettazione basato sui tipi di dati astratti, e si sempre pi diffusa man mano che i linguaggi
O O (Smalltalk, C++, Java e altri) hanno incontrato un ampio impiego nella pratica. Nella progettazione O O esiste un solo tipo di modulo: il modulo di tipo di dato astratto. Usando la terminologia propria della progettazione O O chiameremo tali moduli classi. Una classe esporta
le operazioni che possono essere utilizzate per manipolare i suoi esemplari (istanze). Tali operazioni vengono definite da funzioni, solitamente chiamate metodi nella terminologia OO. Le
classi possono anche rivelare parte dei loro segreti interni, attraverso l'esportazione di attributi". Gli oggetti sono istanze di classi e le variabili sono riferimenti agli oggetti12.
Modificheremo T D N in modo che esprima il fatto che tutti i moduli implementano
tipi di dati astratti. Invece di usare la notazione " t y p e X = ?" nell'interfaccia di un modulo X per introdurre il nome del tipo, lasceremo che i moduli clienti usino direttamente il
nome della classe. Quindi, invece di dichiarare un riferimento a un oggetto del tipo di dato astratto XX esportato dal modulo X come "a : XX . x", scriveremo "a : x" per esprimere che a un riferimento all'istanza di un oggetto del tipo di dato astratto implementato
dalla classe x.
Un altro cambiamento sostanziale verr apportato per quanto riguarda la sintassi delle operazioni che vengono invocate sugli oggetti istanziati. Nel caso del modulo di tipo di
dato astratto X che esporta XX, l'invocazione dell'operazione op che manipola l'oggetto riferito da a veniva scritta come
op(a,

altri_parametri )

Nel caso della progettazione O O scriveremo


a.op(altri_parametri)

11
12

Un attributo di sola lettura come una funzione esportata con il fine di fornire il valore dell'attributo.
Ci riferiremo implicitamente al modello di dati supportato da Java.

per indicare l'invocazione dell'operazione op fornita dall'istanza a della classe X. Tutte le operazioni esportate da un modulo OO, dunque, opereranno su un oggetto istanziato.
La progettazione O O insiste nell'identificare le classi e le relazioni tra classi. Le relazioni vengono usate da O O in maniera molto ampia e astratta. Discuteremo tra breve le varie tipologie di relazioni e introdurremo una notazione grafica che specializza e rimpiazza il
nostro GDN nel caso della progettazione OO. Questa notazione, chiamata UML, viene comunemente usata per la descrizione di progetti OO. UML verr ulteriormente trattato nel
Paragrafo 4.6.4.

4.6.1

Generalizzazione e specializzazione

La progettazione O O permette l'organizzazione di tipi di dati astratti in maniera gerarchica mediante le relazioni di generalizzazione e specializzazione. Tale gerarchia definisce uno
schema per la classificazione per i tipi di dati astratti. Se la classe B specializza la classe A (e
pertanto, A generalizza B) allora il tipo di dato astratto implementato da B definisce oggetti che si comportano come le istanze di A, ma che possono offrire pi metodi e pi attributi. Di conseguenza tutti i metodi e gli attributi definiti per A possono essere utilizzati per
manipolare gli oggetti di B (i quali possono essere manipolati anche mediante l'uso dei metodi e degli attributi definiti specificatamente per B). B viene detto sottoclasse di A, mentre
A superclasse di B.
La relazione di generalizzazione-specializzazione pu essere implementata attraverso
l'uso del meccanismo di ereditariet fornito dai linguaggi di programmazione. per questo
che molte volte diciamo che "B eredita da A" per indicare che "B specializza A". Possiamo
anche dire che B erede della classe A e che A la classe padre di B.
Come esempio, si consideri la classe E M P L O Y E E definita in T D N della Figura 4.23. La
classe EMPLOYEE definisce le propriet comuni a qualsiasi tipo di impiegato. Tutti gli esemplari di EMPLOYEE (che rappresentano singole persone) sono caratterizzate dalle operazioni fornite dalla classe per la manipolazione delle istanze. Ad esempio, un impiegato pu essere assunto con uno stipendio iniziale, ricevendo di conseguenza un identificatore univoco; potrebbe essere licenziato, e quindi perderebbe l'identificatore univoco; potrebbe essere assegnato a
una filiale dell'azienda; potrebbe ricevere una promozione; potrebbe essere ricercato in funzione
del suo nome, della sua et, del suo stipendio, del suo identificatore univoco, etc.
Alcuni impiegati sono membri di uno staff tecnico, altri sono membri dello staff amministrativo mentre altri ancora non appartengono a nessuna delle due categorie. Per questo
motivo definiamo nella Figura 4.24 due sottoclassi: T E C H N I C A L _ S T A F F e A D M I N I S T R A TIVE_STAFF. Un membro dello staff amministrativo gode di tutte le propriet degli impiegati: a tutti gli effetti un impiegato. Da un punto di vista del tipo di dato astratto, ci
significa che gli oggetti corrispondenti possono essere manipolati da tutte le operazioni
definite dalla classe padre E M P L O Y E E e da tutte le altre che caratterizzano il modulo erede. Secondo la terminologia O O diremo che A D M I N I S T R A T I V E _ S T A F F eredita automaticamente tutti i metodi e gli attributi definiti da EMPLOYEE. Ovvero, i membri dello
staff amministrativo possono essere assunti, licenziati, etc. In aggiunta, potrebbero ottenere
del lavoro da svolgere mediante il passaggio di una pratica. Quest'ultima operazione specifica della classe erede; non viene ereditata da alcuna classe padre. In maniera simile, i membri dello staff tecnico, a parte i metodi e gli attributi ereditati da EMPLOYEE, sono caratterizzati da metodi aggiuntivi che rendono possibile la definizione e la richiesta della loro

class

EMPLOYEE

exports
function

FIRST_NAME():

function

LAST_NAME():

string_of_char;
string_of_char;

function

AGE():

function

WHERE( ) : SITE;

function

SALARY:

procedure

HIRE

naturai;
MONEY;

(FIRSTN:
LAS T _ N :

string

of

char ;

string_of_char;

INIT_SALARY: MONEY);
Questa
operazione
inizializza
un n u o v o
un nuovo
identificatore
univoco.
procedure FIRE();
procedure

ASSIGN

(S:

EMPLOYEE,

assegnandogli

SITE);

Non possibile
assegnare
un impiegato
a un sito se vi sia gi
stato
assegnato
in precedenza
(ad esempio,
WHERE deve essere
diverso
da S).
responsabilit
del cliente
assicurare
che questa
propriet
risulti
vera. L'effetto
quello
di cancellare
l'impiegato
da quelli
in WHERE,
aggiungere
1 ' impiegato
a quelli
in S,
generare
una nuova
scheda
identificativa
per l_impiegato
con il
codice
di sicurezza
per l'accesso
al sito durante
le ore
notturne
e aggiornare
WHERE.
end

EMPLOYEE

Figura 4.23

D e f i n i z i o n e d e l l a c l a s s e EMPLOYEE i n T D N .

principale abilit. Infine gli individui che non sono n membri dello staff tecnico n membri dello staff amministrativo vengono rappresentati da istanze della classe EMPLOYEE e non
appartengono ad alcuna delle sue sottoclassi.
Dal punto di vista della progettazione del software, la generalizzazione-specializzazione pu essere utilizzata per fattorizzare le parti comuni di diversi componenti all'interno della classe padre per poi evidenziare le variazioni all'interno delle classi eredi. Questo approccio ha il potenziale necessario per migliorare la riusabilit. Infatti, possiamo tentare di raggruppare in un modulo tutte le caratteristiche abbastanza generali da essere riutilizzabili. Le
caratteristiche aggiuntive necessarie in determinate applicazioni potranno essere aggiunte successivamente per mezzo di moduli eredi.
Possiamo anche vedere l'ereditariet come un modo per costruire il software in maniera
incrementale. L'ereditariet facilita, dunque, l'evoluzione dei sistemi man mano che emergono nuovi requisiti e pu rendere la manutenzione pi facile da eseguire. Infatti, ogni volta che sorge la necessit di modificare un modulo esistente M[ per ottenere un nuovo modulo M2, invece di modificare Mj possiamo estenderlo e applicare i cambiamenti necessari a
trasformarlo in M2. I tipi di cambiamenti che abbiamo fin qui analizzato consistono esclusivamente nell'aggiunta di nuove operazioni ai tipi di dati astratti. Esamineremo altri tipi di
cambiamenti a breve.
Abbiamo utilizzato l'incrementalit per definire due eredi di EMPLOYEE. I due moduli
eredi sono stati definiti semplicemente elencando le differenze rispetto al modulo padre. Per

class

ADMINISTRATIVESTAFF

inherits

EMPLOYEE

exports
procedure

end

DOTHIS

(F:

FOLDER);

u e s t a un'operazione
specifica
altre operazioni
possono
essere
ADMINISTRATIVE_STAFF

class

TECHNICAL_STAFF

inherits

degli
amministratori;
aggiunte.

EMPLOYEE

exports
function
procedure

end

GET_SKILL():
DEFSKILL

SKILL;

(SK:

SKILL);

Queste
sono operazioni
aggiuntive
altre operazioni
possono
essere
TECHNICAL STAFF

Figura 4.24

specifiche
aggiunte.

dei

tecnici ;

D e f i n i z i o n e di s o t t o c l a s s i in T D N .

essere pi precisi, il modulo erede ottenuto dal modulo padre come una copia della sua
implementazione con l'aggiunta di alcune nuove caratteristiche.
Un altro modo per concepire la gerarchia di generalizzazione-specializzazione quello
di vedere una classe erede come l'implementazione di un sottotipo del tipo definito dalla superclasse. Un elemento di un sottotipo dovrebbe poter apparire in qualsiasi contesto in cui
possa apparire un'istanza del tipo padre. Questo viene sovente detto principio di sostituibilit. Siccome tutte le istanze di una sottoclasse ereditano gli attributi e i metodi della sua
classe padre, il principio di sostituibilit soddisfatto in maniera banale. La progettazione
O O aggiunge, per, ulteriori caratteristiche alla relazione di generalizzazione-specializzazione. Una sottoclasse non solo pu aggiungere nuovi attributi e metodi ma pu anche ridefinire i metodi definiti nella propria classe padre.
Ad esempio, supponiamo che E M P L O Y E E fornisca un metodo per la promozione di
ruolo, che aumenta lo stipendio di un impiegato. Le classi T E C H N I C A L _ S T A F F e
A D M I N I S T R A T I V E _ S T A F F potrebbero ridefinire il metodo in modo che gli aumenti siano
diversi. Supponiamo di considerare un programma che manipola un oggetto X del tipo
E M P L O Y E E . Secondo il principio di sostituibilit, tale programma dovrebbe funzionare
altrettanto bene nel caso gli venisse fornito un sottotipo di E M P L O Y E E (ad esempio,
T E C H N I C A L _ S T A F F ) . Se dovesse essere invocato su X il metodo per la promozione, qualora X fosse legato a un'istanza della classe TECHNICAL_STAFF, verrebbe chiamato il metodo per la promozione ridefinito nella classe T E C H N I C A L _ S T A F F . I concetti fondamentali
collegati a questo approccio sono il polimorfismo e il binding (legame) dinamico. Dato che x
un oggetto del tipo EMPLOYEE, possiamo legarlo a oggetti di uno qualsiasi dei suoi sottotipi (polimorfismo) e i metodi che verranno invocati dipenderanno dal tipo dell'oggetto
legato a x al momento dell'esecuzione.
Concludiamo la discussione in merito a generalizzazione-specializzazione fornendone
la descrizione attraverso una notazione grafica. Come precedentemente accennato, in questo paragrafo introduciamo gradualmente gli elementi della notazione UML, in cui le clas-

Figura 4.25

Rappresentazione UML di una generalizzazione.

si sono rappresentate da scatole divise in tre parti, corrispondenti al nome della classe, agli
attributi e ai metodi, mentre la relazione per la generalizzazione-specializzazione viene rappresentata da un connettore triangolare tra classi.
Nella Figura 4.25 viene mostrata una descrizione della rappresentazione testuale presentata nella Figura 4.23 e nella Figura 4.24. Si osservi che la relazione USES tra classi non
viene indicata esplicitamente. Piuttosto, implicita nel fatto che i tipi di certi attributi o parametri per i metodi non sono elementari, ma sono definiti da altre classi (che, pertanto, sono utilizzate).

4.6.2

Associazioni

Le associazioni rappresentano relazioni tra istanze di classi che si richiede siano supportate
dall'implementazione. Per esempio, i membri dello staff tecnico potrebbero essere associati
al progetto cui stanno lavorando. (Ciascun tecnico lavorerebbe esattamente su un progetto,
ma diversi tecnici potrebbero lavorare sullo stesso progetto.)
La Figura 4.26 mostra come le associazioni possano essere rappresentate dal frammento
di un diagramma delle classi UML. Il frammento introduce un'altra sottoclasse di TECHN I C A L _ S T A F F , chiamato M A N A G E R , e un'ulteriore associazione tra manager e progetti. Il
diagramma mostra come i manager siano un tipo particolare di staff tecnico (ovvero, nel
mondo particolare di cui stiamo parlando, un membro dello staff amministrativo non pu
essere un manager) e come un manager possa essere associato a uno o pi progetti. Per semplicit, la Figura 4.26 non fornisce i dettagli dell'interfaccia della classe. (Per le classi EMPLOYEE
e T E C H N I C A L _ S T A F F il lettore pu fare riferimento alla Figura 4.24.)
Le associazioni in UML vengono rappresentate da collegamenti, etichettati con il nome dell'associazione, tra le scatole che rappresentano le classi. Le associazioni possono coinvolgere pi classi. Nella maggior parte dei casi, tuttavia, sono relazioni binarie (ovvero coinvolgono due classi); nel prosieguo, pertanto, assumeremo implicitamente che le associazioni siano binarie. Inoltre, le associazioni possono essere descritte specificando vincoli di molteplicit, i quali indicano quanti oggetti possono partecipare alla relazione. Per esempio, la
Figura 4.26 mostra come un numero qualsiasi di tecnici possano essere coinvolti in un progetto (indicato dal vincolo di molteplicit "*" alla fine del collegamento dalla parte di
T E C H N I C A L STAFF), mentre un tecnico pu essere coinvolto in un solo progetto (indicato dal vincolo di molteplicit "L" sul collegamento dalla parte di PROJECT). In generale, i

vincoli di molteplicit vengono dati specificando " l i m i t e i n f e r i o r e . . l i m i t e s u p e r i o r e " . L'abbreviazione "*" in realt significa "0. . i n f i n i t o ) , mentre " l " significa
" l . . l". Ad esempio, se dovesse essere richiesto che almeno un tecnico sia coinvolto in un
progetto, il limite di molteplicit "*" verrebbe sostituito da " l . . *". I vincoli di molteplicit assegnati all'associazione tra MANAGER e PROJECT mostrano come un manager possa
gestire uno o pi progetti. La specifica che i manager non gestiscano pi di tre progetti, richiederebbe la sostituzione del vincolo di molteplicit " l . . *" con " l . . 3".
La specifica delle associazioni in un diagramma delle classi, come quello mostrato nella
Figura 4.26, non fornisce informazioni sufficienti per derivarne un'implementazione. Ad esempio, specifica che i manager sono associati ai progetti che gestiscono e che i progetti sono associati ai tecnici che vi prendono parte. Ma l'implementazione che ne deriver dovr supportare la navigazione sia dai progetti ai loro manager che dai manager ai progetti che gestiscono?
Per navigazione dai manager ai progetti intendiamo che, dato un manager, siamo in grado di
determinare tutti i progetti che gestisce. Questioni simili possono essere poste per l'associazione
tra TECHNICAL_STAFF e PROJECT. Per rispondere a tali domande UML permette al progettista di decorare le associazioni con una freccia di navigabilit. Per esempio, il frammento di
progetto illustrato nella Figura 4.26 indica che l'associazione tra MANAGER e PROJECT tale
che sufficiente poter navigare da un manager ai progetti di cui responsabile. Invece, se nessuna freccia di navigabilit viene fornita per guidare l'implementazione, dovremmo assumere
che la navigazione debba essere possibile in entrambe le direzioni. Quindi, per esempio, l'implementazione dovrebbe supportare la navigazione da un tecnico ai progetti che gli sono stati
assegnati e da un progetto ai tecnici che vi partecipano.
La discussione precedente dimostra che le associazioni che introduciamo al momento
della progettazione pongono obblighi all'implementazione per quanto riguarda la navigabilit tra le classi. Per esempio, una possibile implementazione dell'associazione tra MANAGER
e PROJECT potrebbe consistere nell'avere, in ogni istanza di MANAGER, una variabile di tipo array di riferimenti a oggetti della classe PROJECT. Inoltre, la discussione illustra come
un'associazione tra classi definisca implicitamente una relazione USES; ad esempio, nel caso di Figura 4.26, MANAGER USES PROJECT.
Osserviamo infine che, durante la progettazione, la distinzione tra attributi (o metodi) e associazioni non sempre risulta ovvia. Per esempio, nella Figura 4.26, potremmo decidere che il metodo DO_THIS venga utilizzato per assegnare una cartella a un membro dei-

Figura 4.26

Rappresentazione di associazioni in UML.

lo staff amministrativo. In alternativa, potremmo definire una pratica FOLDER per rappresentare la pratica e un'associazione tra A D M I N I S T R A T I V E _ S T A F F e F O L D E R per descrivere il vincolo tra un membro dell'amministrazione e la pratica cui sta lavorando. Ovviamente,
la differenza consisterebbe nel fatto che l'associazione esplicita implicherebbe un supporto
alla navigazione dalle cartelle ai membri dell'amministrazione.

4.6.3

Aggregazione

Nel descrivere una classe, potrebbe essere utile definire gli oggetti di tale classe come composizione di componenti pi semplici. Tale operazione avviene attraverso la relazione
P A R T _ O F . Ad esempio, possiamo definire una classe TRI A N G L E e la sua relazione con la
classe POI NT come un'aggregazione (Figura 4.27). Per semplicit, la figura non fornisce i
dettagli (metodi e attributi) delle interfacce delle classi, ma illustra i limiti di cardinalit per
la relazione di aggregazione: tre punti costituiscono un triangolo.
Si noti come la relazione P A R T OF differisca dalla relazione IS C O M P O S E D OF, introdotta nel Paragrafo 4.2.1.2. Infatti, il componente che comprende le parti ha diverse propriet che non sono direttamente fornite dalle parti. Piuttosto, il componente usa le sue parti per fornire i suoi comportamenti (ad esempio, attributi e metodi).
Un'implementazione, ad esempio in Java, del frammento di progetto mostrato nella
Figura 4.27 richiederebbe che la classe T R I A N G L E fornisse i metodi e gli attributi necessari per manipolare triangoli, rappresentati da tre riferimenti a oggetti della classe POINT.
Implicitamente, dunque, questo indica che T R I A N G L E U S E S P O I N T .

Figura 4.27

4.6.4

Esempio di aggregazione.

Ulteriori nozioni sui diagrammi delle classi UML

I diagrammi delle classi UML possono essere visti come un'evoluzione della semplice notazione GDN precedentemente introdotta per la documentazione dei progetti. La relazione
USES introdotta per il GDN viene rimpiazzata da una serie di relazioni: generalizzazionespecializzazione, associazioni di vario tipo e aggregazione. La generalizzazione-specializzazione
viene implementata attraverso l'ereditariet dei linguaggi OO. Gli altri tipi di relazioni possono essere implementati direttamente includendo, all'interno di un oggetto, i riferimenti
agli oggetti con cui in relazione. La relazione USES risulta, in un certo senso, implicita. Se
una classe B eredita dalla classe A, B USES A. Se esiste un'associazione tra le classi A e B,

package_name
Classe 1
Classe 3
Classe 2

Figura 4.28

Package U M L per la relazione IS_COMPONENT_OF.

con un vincolo di navigabilit che indica la direzione da B ad A, allora B USES A13. Se


descritta come un'aggregazione di A allora, ancora una volta, B U S E S A. In un certo senso, possiamo concludere che UML introduce una serie di relazioni pi specifiche rispetto
alla relazione USES utilizzata per le notazioni T D N e GDN. Queste relazioni sono pi specifiche in quanto descrivono semanticamente concetti relazionali pi ricchi di quelli eventualmente descrivibili nei termini della relazione U S E S .
UML fornisce anche una notazione per la descrizione della relazione I S C O M P O NENT OF: il package. Il package raggruppa diverse classi o ulteriori package (Figura 4.28).
E anche possibile disegnare link di dipendenza tra package per indicare che le entit racchiuse in un package dipendono in qualche modo dalle entit definite in un altro package.
A parte fornire notazioni per la descrizione delle strutture statiche di un'architettura,
UML fornisce notazioni che possono essere utilizzate per complementare i diagrammi delle classi mediante la descrizione degli aspetti dinamici di un'architettura: diagrammi degli
stati e diagrammi delle attivit. I diagrammi degli stati descrivono tutti i possibili stati che
oggetti di una data classe possono assumere e come lo stato di un oggetto possa cambiare
come risultato di operazioni eseguite sull'oggetto. I diagrammi delle attivit descrivono flussi di lavoro che operano attraverso le esecuzioni di metodi di diversi oggetti; tali flussi possono procedere in parallelo. I diagrammi degli stati e i diagrammi delle attivit saranno illustrati nel Paragrafo 5.7.

4.7 Architettura e componenti


L'architettura di un sistema descrive l'organizzazione generale e la struttura del sistema nei
termini dei suoi costituenti principali e delle loro interazioni. Per esempio, per un sistema
di amministrazione di un moderno ospedale, la descrizione architetturale potrebbe illustrare come il sistema consista di molti sotto-sistemi: le apparecchiature per il monitoraggio dei
pazienti, le stazioni di lavoro per le infermiere, i dispositivi mobili per l'inserimento di dati da parte dei medici, il database dei pazienti, e cos via. L'architettura un primo progetto di alto livello. Per trovare l'architettura corretta il progettista considera molte opzioni, tiene conto dei vincoli e deve trovare diversi compromessi. I compromessi determinano mol-

13
Si noti che se la navigabilit permessa in entrambe le direzioni, la relazione USES che ne risulta non
potr essere una gerarchia.

te delle propriet generali di un sistema, come le sue prestazioni, la sua affidabilit e la sua
sicurezza. L'architettura, dunque, fornisce il mezzo per analizzare le propriet globali del sistema, visto che queste sono determinate non dai componenti individuali ma dall'interazione dell'intero insieme di componenti.
Nel progettare l'architettura, il progettista deve considerare molti requisiti funzionali
oltre ai requisiti non funzionali, come il costo e l'affidabilit. Esistono alcuni principi strutturali che governano la progettazione dell'architettura e, a seconda dei requisiti del sistema, alcune modalit di scomposizione del sistema in componenti e alcune tipologie di interazione tra questi componenti possono risultare particolarmente appropriate. Abbiamo
gi visto un esempio di struttura particolarmente adatta ai sistemi distribuiti: l'architettura client-server, che fornisce una linea guida a tutti i progettisti di sistemi distribuiti,
consistente nell'identificare insiemi di fornitori di servizi e insiemi di clienti che ricercano tali servizi.
Numerosi sono i benefci che derivano dallo sviluppo e dallo studio di tali architetture. In primo luogo, la conoscenza di architetture gi testate in sistemi precedenti consente
al progettista di intraprendere il progetto in modo rapido e con maggiore fiducia.
Un'architettura di questo tipo rappresenta le esperienze maturate da progettisti precedenti e
ad esse sono associate le decisioni di progettazione che devono essere intraprese. In secondo
luogo, dato che un'architettura stabilisce le modalit di comunicazione tra i componenti del
sistema, definisce un'interfaccia generica, punto di incontro dei vari componenti. L'esistenza
delle specifiche di una tale interfaccia favorisce lo sviluppo di componenti standard, che possono essere utilizzati in sistemi che utilizzano quella architettura. Infine, un'architettura serve come piattaforma di integrazione tra i diversi sottosistemi. Alcuni di questi sottosistemi
possono essere sviluppati per il particolare sistema che si sta progettando, oppure possono
essere sistemi software gi esistenti, come un database. Nei paragrafi che seguono affronteremo questi aspetti in dettaglio.

4.7.1

Architetture standard

Studiando i sistemi esistenti, i progettisti e i ricercatori hanno trovato come alcune architetture si ripetano con maggiore frequenza. Forniremo, quindi, di seguito un elenco di quelle di maggiore rilievo. Inoltre, nel Paragrafo 4.7.4, esamineremo le architetture per i sistemi
distribuiti.
Architetture "pipeline". A volte, i sottosistemi possono essere organizzati in modo da formare una pipeline (letteralmente, conduttura) di elementi di elaborazione. Ogni sottosistema ricever in ingresso un input proveniente dal sottosistema precedente, processer l'input
e passer il proprio output al sottosistema successivo. Il primo sottosistema legge l'input del
sistema, mentre l'ultimo sottosistema produce l'output del sistema. Una tale architettura pu
risultare utile, ad esempio, per la parte di un sistema di monitoraggio di un impianto in cui
i sensori leggono i dati ambientali per poi passarli a un altro sottosistema per ulteriori trattamenti. Un'architettura a pipeline viene chiamata anche architettura pipe-and-flter (conduttura e filtro) in quanto ogni sottosistema pu essere visto come filtraggio dei dati che riceve, mentre i dati fluiscono all'interno della conduttura da filtro a filtro. Cominciando da
un'architettura a pipeline, il progettista pu concentrarsi immediatamente su questioni quali i requisiti per le prestazioni del flusso dei dati lungo le pipe, i requisiti di sincronizzazio-

(c)

Figura 4.29

Relazioni tra componenti in varie architetture: (a) a pipeline; (b) a lavagna;


(c) basata su eventi.

ne tra filtri adiacenti, i possibili colli di bottiglia che si possono presentare nel flusso, e cos
via. Un'interpretazione grafica dell'architettura illustrata nella Figura 4.29a.
Architetture "blackboard". In un'architettura a pipeline la comunicazione tra due filtri avviene localmente. A volte diventa necessario per un sottosistema essere in grado di comunicare con altri sottosistemi che non siano loro vicini. Se molti sottosistemi hanno bisogno di
comunicare tra di loro, allora potrebbe risultare appropriata un'architettura a blackboard (lavagna). In una tale architettura uno dei sottosistemi viene eletto a "lavagna" e serve come
mezzo di comunicazione tra gli altri sottosistemi. Essenzialmente, la lavagna un'interfaccia per scrivere informazioni e per ricevere delle richieste di lettura. Un sistema per mercati azionari o per aste, ad esempio, pu essere facilmente strutturato con questa architettura,
con richieste e offerte rese disponibili su una lavagna, cui i clienti possono rivolgere delle richieste. Un'interpretazione grafica di questa architettura illustrata nella Figura 4.29b.
Architettura basata su eventi. Nelle architetture tradizionali, i componenti comunicano e
invocano operazioni mediante le chiamate a procedure. In un'architettura basata su eventi,
invece, i componenti risponderanno a determinati eventi. Un evento potrebbe essere la ricezione di un segnale da parte di un sensore o l'arrivo di un messaggio. I componenti vengono progettati per creare eventi o per iniziare le loro operazioni alla ricezione di un evento. Architetture di questo tipo sono appropriate quando i componenti devono rimanere in
attesa di un input dall'ambiente, oppure quando non sono definibili chiaramente relazioni
di tipo client-server. Per esempio, le interfacce utenti sono in genere strutturate in modo da
utilizzare clic o trascinamenti del mouse come eventi. Concettualmente possiamo immaginare un bus su cui vengono annunciati e propagati gli eventi. Diversi modelli di sistemi basati su eventi supportano operazioni per i componenti come la pubblicazione di eventi o la
sottoscrizione a eventi. I tipi di eventi dipendono dall'applicazione.
Le architetture basate su eventi soddisfano un paradigma o un modello publish-subscribe (pubblicazione-sottoscrizione). I componenti pubblicano eventi che vengono consegnati ai componenti che si erano, precedenza, ad essi iscritti. Fondamentale, per questa ar-

chitettura, il distributore di eventi, ovvero il responsabile della distribuzione a run-time


degli eventi, da chi li pubblica a chi iscritto. Il distributore di eventi pu essere fornito come parte del middleware. Un'interpretazione grafica di questa architettura illustrata nella
Figura 4.29c.
Architetture specifiche al dominio. Le architetture a pipeline, a lavagna e quelle basate su
eventi, codificano un certo insieme di componenti, unitamente alle loro relazioni e ai modelli di comunicazione. In pratica stanno emergendo molte architetture di questo tipo.
Queste architetture vogliono astrarre le propriet strutturali comuni alle classi dei sistemi,
senza rivolgere particolare attenzione al dominio in cui verranno utilizzati tali sistemi. Esiste
un'altra classe standard di architetture, che prova a sfruttare le propriet comuni di un dato dominio applicativo. Queste architetture vengono dette architetture specifiche al dominio. Per esempio, sono state sviluppate architetture specifiche per il dominio dei sistemi realtime e per il dominio dei sistemi di interfacce utente. Le architetture specifiche al dominio
prendono in considerazione molte assunzioni circa il dominio, come il modo in cui i componenti comunicano, la velocit con cui devono comunicare e l'esistenza di meccanismi di
time-out per la comunicazione dei messaggi. Le architetture specifiche al dominio velocizzano lo sviluppo dei sistemi in determinati domini applicativi. Inoltre, incoraggiano e supportano lo sviluppo di componenti riusabili in molti sistemi all'interno del dominio. Infine,
permettono lo sviluppo di strumenti quali editor, generatori e analizzatori, specifici per il
supporto del dominio. Per esempio, generatori di interfacce utente possono essere basati su
un'architettura standard di interfacce utente.
ESEMPIO 4 . 1 2

Un'architettura specifica al dominio molto conosciuta per il software che ha interazioni significative con l'utente l'architettura "model-view-controller" (modello-visualizzazione-controllo). L'architettura composta da tre componenti separati: il modello, che vuole essere un modello del mondo reale, la visualizzazione, che mostra il modello all'utente, e il controllore, che
comunica con l'utente e controlla gli altri due componenti. Come esempio di utilizzo dell'architettura "model-view-controller" si consideri un editor di file, che immagazzina i dati dell'utente per poterli mostrare successivamente in diversi formati, testuali o grafici. Il modello
gestirebbe archiviazione dei dati, la visualizzazione richiederebbe i dati al modello per poi mostrarli, e il controllore interagirebbe con l'utente per decodificarne i comandi e aggiornare i dati nel modello. Fornendo diversi componenti per la visualizzazione, il sistema potr supporta-

Figura 4.30

Architettura

"model-view-control".

re diverse opzioni di visualizzazione in maniera modulare. Per esempio, un componente di visualizzazione potrebbe fornire prima un profilo e poi i dati completi su un'altra pagina. Ogni
componente specializzerebbe la propria semantica o nasconderebbe le proprie informazioni di
formattazione dei dati. La Figura 4.30 illustra la struttura dell'architettura modello-visualizzazione-controllo. Le frecce nella figura rappresentano le richieste di servizi.
Molte librerie per lo sviluppo delle interfacce utenti, come le librerie Java Swing, implementano l'architettura "model-view-controller".

4.7.2

Componenti software

Nelle discipline ingegneristiche, i prodotti sono quasi sempre costruiti a partire da parti, o
componenti. La progettazione basata su componenti stata un obiettivo dell'ingegneria del
software fin dagli albori della sua storia. Molta ricerca stata fatta per rendere possibile l'utilizzo di componenti standard nello sviluppo di prodotti software. Di conseguenza la tecnologia del software evoluta in modo che i linguaggi e le metodologie supportino lo sviluppo e l'uso di componenti.
La domanda fondamentale circa i componenti software riguarda la forma che questi
dovrebbero assumere. Ovvero, quale deve essere l'unit di impacchettamento da usare come entit indipendente per un componente software? Fino ai primi anni Novanta, le sole
unit di impacchettamento a incontrare un certo successo nell'ingegneria del software sono
state le funzioni e le librerie di funzioni. Ad esempio, sono diffusamente commercializzate
le librerie di funzioni scientifiche per la manipolazione delle matrici. Tre le svariate motivazioni che hanno sovrinteso al successo di queste librerie, tre possono essere indicate come le
pi significative.

Interfaccia chiara. La specifica del componente definita in modo preciso nelle sue propriet matematiche. Inoltre, le propriet sono puramente funzionali, il che rende il componente pi facile da descrivere, capire e integrare con altri componenti.

Servizio utile e separabile. Il servizio fornito da un componente chiaramente identificabile come utile da molti clienti ed separabile dalle funzionalit del cliente stesso.

Dominio di applicabilit evidente. I programmatori e gli ingegneri che scrivono programmi scientifici sono profondi conoscitori del dominio matematico interessato e dei
confini dell'applicabilit di tali componenti all'interno di quel dominio.

Date queste propriet, facile per gli ingegneri individuare le circostanze in cui potrebbero
aver bisogno di un componente, esaminarne e capirne l'interfaccia e, quindi, utilizzarlo nei
loro applicativi.
Negli anni Novanta, con i passi in avanti delle tecnologie dei linguaggi di programmazione, sono diventati possibili ulteriori meccanismi di impacchettamento di funzioni. Tra
questi meccanismi ci sono (1) i costrutti generici in linguaggi come Ada e C++ e (2) gli oggetti e i framework nei linguaggi di programmazione a oggetti. Esamineremo un esempio
per ciascuno di questi meccanismi.
STL. La libreria dei templare standard una collezione di componenti software progettati
per C++, che stata incorporata all'interno della libreria standard C++. La libreria consiste
di strutture dati comuni, come liste e stack, e algoritmi frequentemente utilizzati, come

quelli per l'ordinamento o per la ricerca. Definendo un'interfaccia uniforme, sia per gli algoritmi sia per le strutture dati, STL ottiene uno stile di progettazione che consente l'applicazione della maggior parte degli algoritmi alla maggior parte delle strutture dati. Per esempio, un singolo algoritmo f i n d per la ricerca di un elemento in una collezione pu essere
applicato agli array, alle liste concatenate semplici e alle liste concatenate doppie. Algoritmi
e strutture dati sono impacchettati come template C++ all'interno di STL. Ci significa che
il codice sorgente nella libreria STL deve essere disponibile ai programmatori in modo che
possa essere compilato insieme al programma C++.
STL adotta un principio uniforme per la struttura delle interfacce dei componenti. La
maggior parte degli algoritmi che operano su collezioni di oggetti prende come input i riferimenti al primo e all'ultimo elemento della collezione. Per esempio, l'interfaccia per l'algoritmo f i n d , una funzione che cerca un elemento particolare all'interno di una sequenza
di elementi,
template

< class

Inputlterator
const

Ti

Inpu111erator , class

find(InputIterator

T >

first,

InputIterator

last,

value);

L'algoritmo generico definito per una sequenza qualsiasi di elementi del tipo T. Le strutture dati (collezioni), sono in grado di restituire riferimenti al loro primo e ultimo elemento. Questa struttura uniforme per le interfacce stabilisce linee guida per i progettisti di nuovi algoritmi e strutture dati. Se un algoritmo ricalca queste linee guida, potr essere combinato con una qualsiasi delle strutture dati di STL. Allo stesso modo, se una struttura dati
ricalca le linee guida, allora potr essere combinata con uno qualsiasi degli algoritmi contenuti in STL. Ci significa che con m strutture dati ed n algoritmi abbiamo m x n combinazioni tra componenti possibili.
Caratteristica unica di STL il suo utilizzo dei template C++. I template permettono
l'espressione di algoritmi e strutture dati generici, preservando la possibilit da parte del compilatore di effettuare controlli di tipo. STL include pi di 100 componenti. Un tipico componente piuttosto piccolo in termini di numero di righe di codice. I componenti derivano il proprio potere dal loro carattere generale e dal fatto che possono essere combinati con
altri componenti.
JavaBeans. I linguaggi di programmazione a oggetti promuovono componenti costituiti come classi e oggetti. Il linguaggio Java offre anche pacchetti e file di archiviazione (JAR,
Java archive files, file di archiviazione Java) per aumentare le funzionalit a disposizione
per lo sviluppo basato su componenti. Il framework dei componenti JavaBean promuove l'approccio visivo allo sviluppo del software in un ambiente in cui i componenti sono
rappresentati da icone che possono essere trascinate e posizionate sullo schermo e connesse ad altre icone. Un framework una collezione di classi relazionate che vengono progettate per essere utilizzate insieme nello sviluppo di applicazioni all'interno di un determinato dominio. Il framework dei JavaBean definisce un insieme di metodi che devono
essere supportati da ciascun componente. Questi metodi assicurano che i componenti possano essere composti visivamente. Il framework definisce la semantica che ogni metodo
deve fornire affinch sia assicurato che le connessioni tra componenti funzionino
correttamente.

Swing. Java ha una serie di librerie per il supporto allo sviluppo di software per diverse applicazioni. Per esempio, ci sono librerie per la rete e per la sicurezza. La libreria Swing supporta lo sviluppo di interfacce utenti grafiche. Swing un insieme di librerie di componenti specifici al dominio, progettati per interfacce utenti grafiche. Dato che la rappresentazione di tali interfacce alquanto standardizzata (un'interfaccia composta tipicamente da finestre, bottoni, menu e cos via) le librerie come Swing forniscono funzionalit per la costruzione di interfacce utenti mediante la combinazione di componenti elementari detti
widget. Il paradigma orientato agli oggetti perfettamente appropriato per questo dominio.
Ogni widget rappresentato come un oggetto che supporta metodi per la specifica di cosa
deve fare quando viene puntato dal mouse, quando viene trascinato, cliccato e cos via.
Come abbiamo detto prima, Swing segue lo stile architetturale "model-view-controller".
Oggetto di ricerca nello sviluppo di componenti software la granularit che dovrebbero
avere i componenti. I componenti STL sono componenti di granularit fine, mentre i componenti Swing e JavaBean sono di granularit media. anche possibile usare componenti
di granularit grande come i DBMS. Infatti, visto che molte applicazioni impiegano database, poter utilizzare i DBMS come componenti in un'applicazione presenta numerosi benefici. Per poter utilizzare gli attuali DBMS come componenti, lo standard ODBC (Open
Data Base Connectivity, connettivit a database aperta) definisce un insieme di interfacce per
un database relazionale. Queste interfacce sono state mappate su quasi tutti i sistemi per la
gestione di database relazionali esistenti. La disponibilit di queste corrispondenze, significa che un progettista pu assumere l'esistenza di un database relazionale come componente dell'architettura di un sistema. Ai livelli di progettazione e implementazione, si potr selezionare un DBMS sulla base del costo, delle prestazioni, della compatibilit con altri prodotti e cos via.

4.7.3

L'architettura come piattaforma


per l'integrazione dei componenti

Lo sviluppo di software basato su componenti presuppone un processo in due passi.


Innanzitutto viene sviluppata un'architettura o un progetto di alto livello del sistema, identificando i componenti che devono essere combinati per realizzare il sistema. Successivamente,
viene operato un tentativo finalizzato a reperire i componenti necessari, che si trovano, gi
pronti, sul mercato. Ovviamente, questi due passi si influenzano reciprocamente. Da una
parte, i progettisti sono motivati a optare per architetture che traggano vantaggio da componenti gi noti e disponibili. Dall'altra parte, la conoscenza delle architetture concepite dai
progettisti motiva gli sviluppatori a realizzare componenti il cui impiego possa risultare utile per le architetture pi diffuse.
Man mano che maturano determinati domini, si rendono disponibili componenti per
quei domini. In questi casi, progettare un'architettura per un'applicazione si riduce all'assemblaggio di insiemi di componenti esistenti, in modo da raggiungere gli scopi prefissati.
Possiamo, dunque, interpretare un'architettura come un quadro finalizzato a integrare un
insieme di componenti. L'architettura specifica il modo in cui i componenti dovrebbero essere organizzati e connessi, cos da soddisfare i requisiti dell'applicazione.
CORBA (Common Object Request Broker Architecture) un esempio di un'architettura standard che pu essere interpretata come una piattaforma di integrazione. CORBA

assume un paradigma client-server in un ambiente distribuito. Assume che i client e i server risiedano su una rete e che stabiliscano reciproche connessioni mediante l'uso di un intermediario, chiamato ORB (Object Request Broker). I server informano ORB della loro disponibilit, mentre i client interrogano ORB per conoscere la disponibilit dei server. Una
volta che un cliente trova un server mediante ORB, pu comunicare direttamente con il server. Gli ORB che si trovano su reti diverse possono comunicare tra di loro (attraverso un
protocollo Inter-ORB) in modo da fornire accesso ai server attraverso differenti reti. ORB,
dunque, svolge la funzione di server dei nomi all'interno di un'architettura CORBA. Nel
Paragrafo 4.5.3 abbiamo accennato al fatto che uno dei compiti necessari nel software distribuito vincolare un modulo a una macchina. ORB rende possibile cercare questo collegamento a run-time. Una volta che stato definito lo standard CORBA sono diventati disponibili una serie di ORB commerciali, oggigiorno largamente utilizzati.
Lo standard CORBA definisce essenzialmente l'architettura di un generico sistema distribuito basata su client e server. La Figura 4.31 illustra l'architettura CORBA. Compito
del progettista di un particolare sistema rendere disponibili client e server adatti a questo
framework, ovvero in grado di comunicare con gli ORB.
Uno dei contributi pi importanti dello standard CORBA il suo IDL (Interface
Definition Language, linguaggio di definizione delle interfacce). Il progettista di un server
impiega questo linguaggio per definire le interfacce fornite dal server. I client usano queste
interfacce per compilare e collegare i propri programmi. Il linguaggio fornisce un insieme
di tipi di dati che possono essere utilizzati nelle definizioni delle procedure. Le interfacce
possono ereditare da altre interfacce. Da un punto di vista dell'ingegneria del software l'esistenza di IDL piuttosto significativa. Le specifiche IDL separano chiaramente le responsabilit dei progettisti da quelle dei programmatori dei client e dei server. La specifica IDL
serve da specifica modulare dei server.
CORBA fornisce una piattaforma per la costruzione di applicazioni distribuite. Dovendo
costruire un nuovo sistema distribuito, possibile produrre nuovi componenti e integrarli nella piattaforma CORBA. In molti casi, tuttavia, potremmo voler integrare componenti gi esistenti. Per esempio, potremmo avere un sistema per il personale o un altro "software legacy"

Oggetti
applicativi

Interfacce
di dominio

Servizi C O R B A

Figura 4.31

Architettura CORBA.

Funzionalit
di C O R B A

che vorremmo rendere disponibile sulla rete distribuita. Un modo per integrare sistemi di questo tipo in una piattaforma CORBA quello di scrivere specifiche IDL per i loro servizi, per
poi scrivere programmi che traducano l'interfaccia in interazioni con il software legacy. Un tale software di traduzione viene detto software wrapper (software che avvolge), in quanto avvolge il software legacy creando un pacchetto che possa essere utilizzato nel nuovo ambiente.
Questa operazione, per, non sempre possibile. Ad esempio, un software legacy interattivo
non pu essere facilmente "avvolto", in modo che operi in un ambiente client-server.
Nel mondo Microsoft, DCOM (Distributed Component Object Model) stato progettato appositamente per l'integrazione di software legacy e, in particolare, per l'integrazione
di codice binario. DCOM , sotto molti aspetti, simile a CORBA, ma una piattaforma
software proprietaria.

4.7.4 Architetture per sistemi distribuiti


Uno dei principali vantaggi del modellare un sistema a livello architetturale che diventa
possibile capire la struttura generale del sistema e analizzare le sue propriet globali. Possiamo
addirittura scoprire modelli strutturali ricorrenti che risultino pi utili del sistema che si sta
in quel momento progettando. Per esempio, abbiamo visto che il paradigma client-server
stato codificato nello standard CORBA come un'architettura standard per sistemi distribuiti.
Con la proliferazione delle reti e la disponibilit crescente di ambienti distribuiti cresciuta l'importanza delle architetture standard per applicazioni distribuite. In questo paragrafo prenderemo in visione due architetture per i sistemi distribuiti: l'architettura a tre livelli (three tier) e gli application server.
L'architettura a tre livelli un'estensione dell'architettura client-server, la quale a tutti gli effetti a due livelli. In un'architettura client-server esistono infatti due livelli di componenti: il livello client e il livello server. Il livello client fa affidamento sui servizi del livello server. Per esempio, l'architettura del W W W (World Wide Web) ha una struttura di questo tipo: il browser web risiede sul client e comunica con il server web che risiede su un altro computer. Il browser invia al server richieste di pagine che poi visualizza sullo schermo
quando le riceve. Si veda nella Figura 4.32a l'illustrazione di questa architettura.
In molte applicazioni distribuite, invece, possiamo distinguere un terzo strato di funzionalit. Supponiamo che la richiesta del browser non sia per una pagina semplice ma per
un servizio pi elaborato, come l'accesso a un database. In questo caso il server web dovr
spedire la richiesta alla macchina che ospita il database (la quale potrebbe essere, ovviamente, la stessa macchina su cui risiede il browser), ricevere i risultati e rispedirli al cliente.
Possiamo dire che le applicazioni di questo tipo siano a tre livelli, un livello client, il quale
esegue l'interfaccia, uno strato di logica di business, il quale interpreta le richieste dell'utente
e determina cosa debba essere fatto, e un livello applicativo, il quale esegue il servizio richiesto.
L'applicativo spesso, ma non sempre, un server di database. Un'architettura a tre livelli
illustrata nella Figura 4.32b.
Con la diffusione sempre pi ampia di architetture a tre livelli diventa possibile identificare applicazioni specifiche, frequentemente richieste al livello applicativo. Per questo
possibile costruire application server che forniscano una specifica applicazione. Per esempio,
un'intera applicazione come un server di posta potrebbe essere fornita da un'application server di posta. L'approccio basato su application server promuove la creazione di server altamente specializzati e ottimizzati per la soluzione di problemi specifici. Tali server possono

Browser web
(client)

Richieste
di servizio

Server web
(server)

(a)

Figura 4.32

Architetture (a) a due e (b) a tre livelli.

essere visti come componenti a granularit alta da inserire all'interno di architetture distribuite. Il progettista deve solo scegliere, tra i diversi application server disponibili, quello che
meglio si integra con il sistema che sta costruendo.

4.8

Osservazioni conclusive

In questo capitolo abbiamo esaminato i vari aspetti della progettazione di software e delle
architetture. Molti dei principi generali per il software che abbiamo presentato nel Capitolo
3 sono stati qui esaminati in maggiore dettaglio, nel contesto della progettazione del software.
In particolare abbiamo enfatizzato la modularit, vera essenza e tema portante dell'intero capitolo. Anche i principi di separazione degli interessi, astrazione, generalit e incrementalit
sono stati discussi in maniera estesa. In particolare, sono stati visti come benefici derivanti
da tecniche appropriate di modularizzazione. Infine, stato mostrato come il rigore e la formalit siano obiettivi essenziali della nostra documentazione di progettazione, e come abbiano ispirato la definizione di una notazione di progettazione presentata sia in forma testuale (TDN) che in forma grafica (GDN). Tale notazione fornisce un modo chiaro per documentare la progettazione del software, per facilitare la comunicazione tra progettisti software
e futuri mantenitori del sistema.
La progettazione un'attivit critica e creativa. Pu essere ispirata da principi e linee
guida generali, ma non pu essere resa un'operazione meccanica mediante regole fisse o teoremi. Abbiamo mostrato come la progettazione consista di (1) la definizione di un'architettura in termini di un insieme di relazioni tra moduli e (2) la definizione di interfacce tra
moduli. Queste attivit possono essere guidate dai principi generali secondo i quali l'architettura dovrebbe avere un basso livello di accoppiamento e un alto grado di coesione, e secondo i quali le interfacce dovrebbero imporre l'information hiding. Mettere in pratica tali principi, comunque, richiede perspicacia, maturit ed esperienza da parte del progettista.
I principi non sono ricette!

Si assunto che l'information hiding fosse la pietra angolare su cui basare un progetto solido. Abbiamo fatto molta attenzione, quindi, alla questione di come progettare interfacce che rispettassero l'information hiding. In particolare abbiamo identificato diverse categorie di moduli che possono essere utilizzate come linee guida durante la progettazione. I
pi importanti sono gli oggetti astratti e i tipi di dati astratti.
Tra le qualit del progetto software abbiamo incentrato l'attenzione sull'evolvibilit e
l'affidabilit. L'evolvibilit viene raggiunta mediante la progettazione per il cambiamento.
L'affidabilit deriva, come risultato di un approccio disciplinato alla progettazione, dall'applicazione di metodi che aiutano a superare la complessit della progettazione e riducono la
probabilit della presenza di errori nel progetto. Ma abbiamo anche affrontato la questione
della progettazione difensiva, mostrando come possibili anomalie dovrebbero essere prese attentamente in considerazione durante la fase di progettazione e descritte nella documentazione.
Abbiamo affrontato i problemi specifici della progettazione dei sistemi concorrenti, realtime e distribuiti, estendendo ad essi i principi e le notazioni di tipo generale. Il nostro obiettivo era di mostrare come i principi e gli approcci presentati per la progettazione di software
sequenziale potessero essere estesi per affrontare la concorrenza, il real-time e la distribuzione.
L'information hiding, gli oggetti astratti e i tipi di dati astratti hanno portato al concetto di progettazione orientata agli oggetti, un approccio alla progettazione di software che
diventato dominante nell'ultimo decennio, grazie all'avvento di linguaggi di programmazione, come C++ e Java, che lo supportano mediante caratteristiche linguistiche che mancavano nei linguaggi tradizionali. La progettazione orientata agli oggetti, insieme ai suoi linguaggi, porta l'idea dell'information hiding alla sua logica conclusione, aiutando i progettisti di software ad avvicinarsi di pi agli obiettivi di progettazione per il cambiamento, progettazione di famiglie di prodotti, sviluppo incrementale, produzione di componenti riusabili e facilit di manutenzione. La notazione di progettazione UML stata introdotta per la
progettazione orientata agli oggetti. UML viene adottata sempre di pi come notazione
standard per la documentazione di architetture di applicazioni orientate agli oggetti.
Infine abbiamo esaminato un numero di questioni inerenti allo sviluppo di software
basato su componenti. Queste questioni riguardano architetture di riferimento e interfacce
che permettono lo sviluppo di componenti standard e la loro integrazione in un'applicazione. Abbiamo concluso il capitolo con una discussione dell'evoluzione delle architetture a due
e a tre livelli, per sistemi distribuiti.
Ulteriori esercizi
4.51

Indicate alcune utili relazioni tra m o d u l i , diverse d a quelle presentate in questo capitolo.

4.52

Classificate i c a m b i a m e n t i discussi nel Paragrafo 4 . 1 . 1 . 1 , s u d d i v i d e n d o l i in interventi di m a n u t e n z i o n e perfettiva, adattiva o correttiva.

4.53

C o n s i d e r a t e u n p r o g r a m m a scritto in linguaggio Ada. (L'esercizio p u c o m u n q u e essere adattato p i u t t o s t o facilmente a n c h e ad altri linguaggi quali M o d u l a - 2 , C o Pascal.) Si considerin o le unit della libreria Ada c o m e m o d u l i , s e c o n d o la descrizione fornita in questo capitolo.
Inoltre, definite u n a relazione CHIAMA tra d u e m o d u l i qualsiasi c o m e MI CHIAMA Ms s e e
solo se u n a c h i a m a t a a u n a p r o c e d u r a o f u n z i o n e di Mj proviene da Mi.
a.

C o n t r a r i a m e n t e all'assunzione fatta nel Paragrafo 4.2.1, avrebbe senso definire C H I A M A


c o m e u n a relazione riflessiva?

b. Cosa consegue dal richiedere che CHIAMA sia una gerarchia?


c.

Si disegni il grafo per la relazione CHIAMA e si controlli se risulta essere un D A G oppure no.

4.54

Dimostrate che l'inverso di una gerarchia un'altra gerarchia.

4.55

Facendo riferimento alla Figura 4.4, possiamo dire che


{MJ}

INCAPSULA

{M,,

M5,

M6,

{M2}

INCAPSULA

{M7,

M,

M9>

{M3>

INCAPSULA

{MS,

M6}

M,,

M,

M9>

La relazione INCAPSULA, d u n q u e , mette in relazione ciascun m o d u l o con l'insieme di moduli elementari da cui composto. Definite formalmente INCAPSULA.
4.56

Spiegate come mai un progetto con u n livello basso di accoppiamento aiuta la manutenibilit.

4.57

Studiate il costrutto COMMON del F O R T R A N e discutete la differenza tra COMMON con etichetta e COMMON senza etichetta, e il loro uso. Inoltre, discutete i meccanismi messi a disposizione da F O R T R A N per inizializzare le aree COMMON.

4.58

Descrivete come utilizzare C per definire un area c o m u n e per l'immagazzinamento dei dati.
Fate lo stesso per Pascal.

4.59

Abbiamo criticato l'interfaccia di P O P _ 2 nello stack dell'Esempio 4.7 per il fatto che risultava troppo specializzata. A b b i a m o detto che la sua generalizzazione di P O P fornisce ai clienti
una maggiore flessibilit nel suo impiego. Tuttavia la specializzazione risulta a volte necessaria per motivi di efficienza. Per esempio, si consideri uno stack che implementato su un nodo di u n sistema distribuito. C o n f r o n t a t e P O P e P O P _ 2 in termini di efficienza dell'interfaccia per i clienti.

4.60

Secondo la definizione del linguaggio di programmazione Ada, qual la differenza tra i tipi
private elimited private esportati da un package, per quanto riguarda i moduli clienti

4.61

L'Esempio 4.8 fa uso di oggetti del tipo FIFO_CARS per rappresentare una coda qualsiasi alla stazione di servizio. Vorremmo, per, poter trattare diversamente una coda di auto che aspettano il rifornimento di benzina rispetto a una coda di auto in attesa, ad esempio, di un controllo meccanico. In particolare, se dovesse finire la benzina, n o n v o r r e m m o che le diverse code venissero accorpate. Nell'Esempio 4.8, la gestione corretta delle code viene lasciata come
compito ai moduli client. I clienti scelgono nomi appropriati per gli oggetti come g a s o l i n e _ l
o c a r _ w a s h in m o d o da evitare di unire code eterogenee. Suggerite una soluzione migliore
usando i moduli generici.

4.62

Nel Paragrafo 4.2.4.3 abbiamo descritto i moduli generici parametrizzati per tipo. Studiate la
specifica del linguaggio Java. Java consente l'uso di moduli parametrizzati? E C++?

4.63

N o n abbiamo fornito un arricchimento di G D N per la descrizione di eccezioni e della loro


gestione. Proponete una notazione per descrivere il fatto che un'eccezione possa essere sollevata da un m o d u l o durante l'esecuzione di un servizio offerto. La notazione dovrebbe fornire anche un m o d o per mostrare c o m e un'eccezione possa essere propagata d o p o che stata segnalata a u n utente.

4.64

Supponete di voler modellare u n sistema per u n ascensore di un palazzo a pi piani. Il sistema composto da m piani e n ascensori. O g n i ascensore dispone di una pulsantiera dove ciascun tasto corrisponde a un piano. Q u a n d o viene premuto, il tasto si illumina, e provoca lo
spostamento dell'ascensore al piano corrispettivo. Una volta raggiunto il piano la luce si spe-

gne. Inoltre, ad ogni piano (eccetto il piano terra e l'ultimo piano) sono collocati due tasti:
uno per la richiesta di un ascensore in salita e uno per u n ascensore in discesa. Il tasto si spegne nel m o m e n t o in cui giunge u n ascensore che viaggia nelle giusta direzione o che risulta
libero da richieste. Infine, ogni ascensore dispone di u n tasto di emergenza che, se premuto,
invia un messaggio di allarme alla sede del gestore, e segnala che l'ascensore "fuori servizio".
Segnalazione che pu, a sua volta, essere disattivata tramite un apposito meccanismo.
a.

Modellate il sistema secondo u n o stile orientato agli oggetti.

b. Supponete che gli ascensori siano divisi in due insiemi, il primo che fornisce il servizio ai
piani compresi tra il piano t e r r a e il piano ni!, il secondo che fornisce il servizio ai piani compresi tra ni! e m. Cosa cambiereste nel progetto per tener c o n t o di questa nuova caratteristica?
4.65

D o p o il primo passo di progettazione descritto all'inizio del Paragrafo 4.4, il progettista del
m o d u l o SYMBOL TABLE anticipa che immagazziner le informazioni c o n t e n u t e nei vari
blocchi in locazioni contigue. Gli algoritmi RETRIEVE e LEVEL saranno quasi identici: per
cercare il valore di una variabile, innanzitutto si cercherebbe nell'ultimo blocco inserito, e poi,
se n o n venisse trovata la variabile, si cercherebbe nel blocco precedentemente inserito, e cosi
via. Il progettista p r o p o n e ai suoi colleghi di approfittare dell'estrema somiglianza tra gli
algoritmi e di modificare l'interfaccia. Pertanto, invece di avere le procedure RETRIEVE e
LEVEL, fornir una procedura (RETRIEVE

L E V E L ) c h e r i u n i s c e le d u e . L ' i n t e r f a c c i a p r o -

posta :
procedure

RETRIEVE_LEVEL

DESCR:

out

(ID:

DESCRIPTOR;

L:

in

IDENTIFIER;

out

integer);

D o p o una discussione, i colleghi del progettista lo convincono che riunire le due procedure
in una sola n o n una b u o n a idea. Sei d'accordo con questa decisione? Perch? Perch no?
4.66

Facendo riferimento al m o d u l o QUEUE_OF_CARS discusso nel Paragrafo 4.5, ci possiamo


aspettare che due esecuzioni concorrenti di NOT EMPT Y e N O T F U L L richiedano l'esecuzione
in m u t u a esclusione? Perch? Perch no? E d u e esecuzioni concorrenti di NOT_EMPTY e GET?

4.67

corretto dire che la clausola r e q u i r e s definita nel Paragrafo 4.5.1.1 parte dell'interfaccia di un m o d u l o concorrente? Perch? Perch no?

4.68

C o n s i d e r a t e il m o d u l o della Figura 4 . 1 9 e a s s u m e t e che le operazioni NOT FULL e


NOT_EMPTY siano esportate dal m o d u l o . Le operazioni esportate nella Figura 4 . 1 9 fornirebbero un'astrazione utile al m o m e n t o di essere chiamate dai m o d u l i client? Perch? Perch
no?

4.69

Quali difficolt si potrebbero incontrare se venisse data la possibilit a un'operazione di monitor di usare un'altra operazione di monitor (magari esportata dallo stesso monitor)?

4.70

Studiate le caratteristiche della concorrenza presenti in Ada e fornite una descrizione dettagliata del caso in cui un n u m e r o qualsiasi di consumatori (definiti da un tipo di compito) e
un produttore possano accedere a un dato buffer per inserirvi e rimuovere caratteri.

4.71

Al passaggio del secolo, si diede particolare rilievo al cosiddetto problema Y2K ("year 2000").
Molti software, progettati per gestire date impiegando due cifre per identificare l'anno, avrebbero p o t u t o causare problemi. Ad esempio, "00" avrebbe p o t u t o essere interpretato come
1900. Discutete questo aspetto come un problema di evoluzione del software. In particolare,
affrontate le seguenti questioni. Quale era la sorgente del problema? Il problema poteva essere previsto? Perch n o n fu previsto? C o m e si possono trovare gli errori all'interno di u n programma e come si potrebbe risolverli?

4.72

Esaminate il caso di studio nell'Appendice A. Mostrate come l'azienda avrebbe potuto gestire i diversi fabbisogni dei diversi clienti s f r u t t a n d o alcune delle tecniche illustrate in questo
capitolo.

4.73

Modificate i diagrammi delle classi nelle Figure 4.25 e 4 . 2 6 specificando un'associazione che
descriva la squadra che lavora al progetto. La squadra composta da un manager, un amministratore e u n g r u p p o di tecnici.

Suggerimenti e tracce di soluzioni


4.9

I S COMPOSED OF e IMPLEMENTS n o n possono essere definite come relazioni matematiche su S visto che m e t t o n o in relazione u n elemento di S con u n sotto-insieme di elementi
di S.

4.38

Osservate che le due affermazioni vengono, a tutti gli effetti, realizzate come una sequenza di
pi azioni elementari. Potrebbe accadere, d u n q u e , che entrambi i processi leggano TOT, che
PRODUCER_1 aumenti il valore letto e lo salvi n u o v a m e n t e in TOT e che poi CONSUMER_2
decrementi il valore e lo salvi ancora in TOT!

4.51

U n a relazione di equivalenza semantica tra moduli utile per decidere se un m o d u l o possa


rimpiazzarne u n altro durante l'evoluzione del software. Per q u a n t o riguarda la compilazione,
ci possono essere relazioni di ordine tra moduli, del tipo "M, deve essere compilato prima di
M 2 ". Nei sistemi distribuiti potrebbe esistere una relazione che definisce che due moduli debbano essere allocati sulla stessa macchina.

4.65

Un'implementazione differente potrebbe mettere in cache i descrittori di tutti gli identificatori all'ingresso nel blocco, senza mettere in cache il livello. Ci renderebbe n o n valida l'interfaccia proposta

4.67

U n m o d u l o concorrente pu essere utilizzato correttamente anche senza conoscere la clausola requires. Questa clausola, per, fornisce informazioni utili per una migliore comprensione degli effetti delle operazioni associate. H a u n impatto anche sull'analisi delle prestazioni, in q u a n t o aiuta a trovare i ritardi dovuti alla m u t u a esclusione.

4.68

I clienti n o n possono fare affidamento sui risultati restituiti in q u a n t o altri processi potrebbero averli cambiati nel frattempo.

4.69

Una tale caratteristica potrebbe far crescere il rischio di deadlock.

4.72

Usando una progettazione orientata agli oggetti, l'azienda avrebbe p o t u t o prima raggruppare
tutte le informazioni e le operazioni necessarie a tutti gli uffici legali. Pi tardi, gli uffici legali avrebbero p o t u t o essere specializzati applicando l'ereditariet. Q u e s t o approccio avrebbe
anche supportato una costruzione e una consegna incrementale del sistema. Inoltre, seguend o l'approccio suggerito nell'Esempio 4.5, l'azienda avrebbe p o t u t o concentrare gli sforzi sia
di progettazione che di promozione sulle caratteristiche innovative del sistema.

Note bibliografiche
I lavori di D.L. Parnas sono la sorgente principale che ha ispirato la visione della progettazione del
software presentata in questo libro.
Dijkstra [1968a, 1968b e 1971] fu il p r i m o a insegnare a usare i principi di "separazione degli
interessi" e i livelli di astrazione per poter affrontare la complessit della progettazione.
Parnas fu pioniere di t u t t o il successivo lavoro fatto sulla progettazione del software. Parnas
[1972b] introdusse il concetto di information hiding, la pietra angolare della "buona" progettazione
del software, mentre Parnas [ 1972a] introdusse la nozione di specifica dei moduli, che affronteremo
nel prossimo capitolo. I lavori successivi a p p r o f o n d i r o n o le questioni riguardanti le famiglie di prodotti (Parnas [1976]) e la modifica dei programmi (Parnas [1979]). Parnas [1974] discute il concetto di sistema gerarchico. Britton et al. [1981] discute le interfacce astratte per i moduli di interfaccia
per dispositivi. Hester et al. [1981] illustra l'uso di una b u o n a documentazione di progetto. Gli articoli originali di Parnas sono stati collezionati in H o f f m a n e Weiss [2001].
H o f f m a n [1989] discute la questione della progettazione di interfacce e specifiche in una maniera pratica, ma rigorosa. H o f f m a n [1990] discute i criteri per la progettazione di interfacce per moduli.
Il lavoro sui linguaggi di interconnessione di m o d u l i affronta la questione dei meccanismi linguistici per descrivere le interconnessioni di m o d u l i software; Prieto-Diaz e Neighbors [1986] ne presenta una rassegna.
La nozione di tipo di dato astratto ha le sue radici nel lavoro di Dahl et al. [1972] e Liskov e
Zilles [1974]. Liskov e G u t t a g [1986] illustra u n approccio metodologico alla costruzione di software
basato sul riconoscimento di astrazioni.
Lientz e Swanson [1980] discute le cause del cambiamento nel software e riportano dati che
mostrano l'influenza dei diversi fattori (per esempio, il c a m b i a m e n t o delle strutture dati).
La notazione T D N usata per illustrare i progetti basata sui linguaggi di programmazione Ada
(AJPO [1983]) e Modula-2 (Wirth [1983]). La rappresentazione grafica che abbiamo usato ricorda la
notazione H O O D , definita dall'Agenzia Spaziale Europea ( H O O D [1989]). Wasserman et al. [1990]
descrive un'altra proposta; Buhr [1984] fornisce una notazione grafica per descrivere i progetti Ada.
La questione dei generatori di applicazioni discussa in Software [1990c]. Per la compilazione
condizionale, vedi Babich [1986], che illustra il concetto nel contesto del configuration management.
La progettazione orientata agli oggetti illustrata da Booch [1986, 1987a e 1987b], nel contesto del linguaggio di p r o g r a m m a z i o n e Ada, e da Meyer [2000], nel contesto del linguaggio di
programmazione Eiffel. Wegner [1987] presenta una spiegazione e una classificazione delle questioni riguardanti la progettazione orientata agli oggetti. Szyperski [1998] fornisce u n o studio approfondito delle varie tecnologie e soluzioni offerte per supportare la progettazione per c o m p o n e n t i .
La progettazione di software concorrente fu affrontata da Brinch Hansen [1977], che defin il
linguaggio di programmazione C o n c u r r e n t Pascal. Meccanismi basati su rendezvous f u r o n o ispirati
da C S P (CommunicatingSequentialProcessesi,
definito da Hoare [1974], e trovarono la loro applicazione sistematica nel linguaggio di programmazione Ada. Weihl [1989] discute l'uso di tipi di dati
astratti in un ambiente concorrente.
I problemi specifici dei sistemi real-time sono discussi da Wirth [1977] e Stankovic [1988], Kopetz
[1977] presenta un trattamento ampio dei problemi dei sistemi real-time, con particolare attenzione
al rispetto dei vincoli temporali. La progettazione di sistemi distribuiti discussa da Shatz e W a n g
[1989].
II concetto di guardiano nel contesto dei sistemi distribuiti dovuto a Liskov e rappresenta la
base per la progettazione del sistema A R G U S (Liskov [1988]).
Un aspetto della progettazione n o n affrontato specificatamente in questo libro riguarda le interfacce utente. Il lettore interessato potr fare riferimento a Schneiderman [1988] e al n u m e r o speciale di IEEE Software [1989a],

Per u n q u a d r o sui linguaggi di programmazione e sul loro s u p p o r t o alla progettazione del


software, si faccia riferimento a Ghezzi e Jazayeri [1998].
Altri approcci alla progettazione sono descritti in vari libri sulla "progettazione strutturata" come Yourdon e Costantine [1979] e Myers [1978]. Questi approcci sono basati sulla nozione di scomposizione di u n sistema in moduli funzionali. Questi metodi sono parte di una metodologia pi ampia, chiamata SA/SD (Structured Analysis/Structured Design)-, affronteremo questa metodologia nel
Capitolo 7. Per una discussione dettagliata dei concetti di coesione e accoppiamento si faccia riferim e n t o a Yourdon e Costantine [1979] e a Myers [1978],
U n a rassegna di tecniche di progettazione presentata da Bergland [1981] e Yau eTsai [1986],
Ci sono molti libri e articoli sulla progettazione orientata agli oggetti. D u e idee che h a n n o ricevuto
molta attenzione nel contesto della progettazione orientata agli oggetti sono i design pattern e le piattaforme. I design pattern sono strutture ricorrenti che consistono di diversi componenti che appaiono nella progettazione di molti sistemi. G a m m a et al. [1994] la sorgente originale per quest'idea e
contiene 2 3 pattern. I framework sono un insieme correlato di classi che costituiscono l'ossatura per
una particolare area applicativa o dominio. Abbiamo usato la parola "framework" in questo capitolo
in senso generico. Nel m o n d o O O ha una definizione rigorosa. A proposito di framework si veda il
n u m e r o speciale di Communications of the ACM curato da Fayad e Schmidt [1977].
Anche se il termine "architettura" veniva utilizzato da Parnas e Brinch Hansen nei primi anni
O t t a n t a , lo studio sistematico dell'architettura software come oggetto indipendente di ricerca attrasse l'attenzione solo negli anni Novanta. Perry e Wolf [1992] argomenta in favore dello studio sistematico del soggetto. Shaw e Garlan [1996] rappresenta il primo studio sistematico del settore ed esplora la nozione di c o m p o n e n t i e connettori come le astrazioni strutturali di base per le architetture
software. Rechtin [1991] un'eccellente sorgente di idee pratiche per la strutturazione dei sistemi, di
cui enfatizza l'importanza della semplicit come principio architetturale.
Numerosi libri riportano esperienze diverse con le architetture software. Tra questi ricordiamo
Bass et al. [1999], Hofmeister et al. [1999] e Jazayeri et al. [2000], che si concentrano su architetture per famiglie di prodotti. Kruchten [1995] un articolo influente, che introduce l'importanza dei
diversi punti di vista dell'architettura software. Garlan [2000] recensisce le questioni pi rilevanti nell'ambito delle architetture software e "predice" sviluppi futuri.
Nuovi paradigmi architetturali sono stati esplorati in molte pubblicazioni. Wolf e Rosenblum
[1997] discute le architetture basate su eventi, Hauswirth e Jazayeri [1999] fornisce una rassegna dei
sistemi basati su "push" e li confronta con i sistemi basati su eventi.
S T L viene descritto in dettaglio da Musser e Saini [1996] e viene analizzato da un p u n t o di vista dell'ingegneria del software da Jazayeri [1995].
Fowler e Scott [1998] presenta una sintetica introduzione a U M L . Booch et al. [1999] il testo di base sull'argomento.
Ci sono molti libri su C O R B A , D C O M e altri middleware. Per esempio, si veda Emmerich
[2000] e Orfali et al. [1997]..

CAPITOLO

Specifica

Ogni sistema ingegneristico non banale deve essere specificato. Per esempio, si potrebbe stabilire che un ponte debba sostenere un peso fino a 1000 tonnellate, che debba essere largo almeno 30 metri, etc. In questo senso, la specifica un'affermazione precisa sui requisiti che il
sistema, in questo caso il ponte, deve soddisfare. Ovviamente, potremmo specificare, oltre ai
requisiti del sistema finale, anche quelli dei sottosistemi e dei componenti che verranno utilizzati. Il progettista del ponte, quindi, specificher i requisiti dei pilastri, dei cavi, delle viti, e
cos via. Ovviamente, andranno specificati anche il progetto e l'architettura.
Nelle discipline ingegneristiche tradizionali, la parola specifica ha un significato preciso. Nell'ingegneria del software, invece, il termine viene utilizzato in diversi contesti con
significati diversi. Noi stessi lo abbiamo utilizzato informalmente diverse volte nei capitoli precedenti.
In generale, possiamo interpretare una specifica come un'affermazione riguardante l'accordo raggiunto tra il produttore e il consumatore di un servizio o tra un implementatore e
un utente. A seconda del contesto, l'implementatore e l'utente saranno diversi, proprio come lo la natura delle specifiche. La specifica dei requisiti un accordo tra l'utente finale e
lo sviluppatore del sistema. La specifica di un progetto, per esempio in termini di una gerarchia U S E S , un diagramma UML o di un'interfaccia IDL, un accordo tra l'architetto (o
progettista) del sistema e gli implementatori. La specifica di un modulo un accordo tra i
programmatori che dovranno usare il modulo e il programmatore che lo implementa. Per
esempio, la clausola e x p o r t s dei moduli in una descrizione T D N pu essere interpretata
come una specifica (sintattica) di quei moduli. In maniera simile, i diagrammi delle classi
possono essere arricchiti elencando la segnatura di tutte le operazioni esportate.
Come dimostrano questi esempi, il termine viene usato in diversi stadi dello sviluppo
di un sistema. Inoltre, una specifica a un determinato livello definisce i requisiti per l'implementazione dei livelli sottostanti. Siccome la specifica un accordo tra l'utente e l'implementatore, possiamo interpretarla come una definizione di ci che l'implementazione deve
riuscire a ottenere.
Questa relazione tra la specifica e l'implementazione viene molte volte spiegata in termini della dicotomia "what versus how' (ovvero, "che cosa" rispetto a "come") descritta nel
Capitolo 1. La specifica afferma che cosa deve fare un sistema; l'implementatore decide come deve farlo. In pratica, per, la distinzione non poi cos evidente. Per esempio, la decisione se distribuire un sistema bancario a tutte le filiali della banca o se mantenerlo centra-

lizzato e utilizzare terminali remoti pu essere considerata una questione di progettazione


(ovvero un come). Si potrebbe allora affermare che la distribuzione fisica del sistema non sia
un requisito, ma una questione riguardante l'implementazione. In altri casi l'utente potrebbe richiedere esplicitamente un'architettura distribuita, che diventerebbe cos parte della specifica del sistema. Un'implementazione che realizzasse tutte le funzionalit richieste, mediante
l'uso di una singola macchina, sarebbe inaccettabile.
Inoltre, a volte, un modo semplice per descrivere che cosa si vuole quello di fornire
un esempio di come potrebbe essere realizzato. Questo non implica che debba essere realizzato esattamente in quel modo, ma che si dovr comportare come se fosse stato implementato cos. Per esempio, si potrebbe affermare che l'esecuzione di diverse transazioni concorrenti in un sistema informativo debba essere eseguita come se ogni transazione avvenisse in
un modo non interrompibile. Ci non significa che l'implementatore debba necessariamente lasciare che ogni transazione termini prima di eseguirne un'altra, il che sarebbe altamente inefficiente nel caso di transazioni lunghe; piuttosto, l'implementatore sar libero di
eseguire le transazioni contemporaneamente, a patto che l'utente le percepisca come eseguite
senza interruzioni.
In teoria, bisognerebbe specificare tutte le qualit desiderate, mentre l'implementazione dovrebbe assicurare che tutte le qualit descritte vengano effettivamente ottenute (ad
esempio in termini di funzionalit, usabilit, prestazioni, portabilit, etc.). In questo capitolo, tuttavia, ci concentreremo principalmente sulla specifica di funzionalit software (ovvero sulle specifiche funzionali). Affronteremo brevemente la specifica dei requisiti non funzionali nel Paragrafo 5.2.
L'attivit di specifica una parte critica dell'intero processo di progettazione. Le specifiche sono il risultato di un'attivit di progettazione complessa e creativa; sono soggette a
errori, proprio come i risultati di altre attivit (ad esempio, come la programmazione). Di
conseguenza, tutti i principi discussi nel Capitolo 3 dovrebbero essere applicati anche al processo di specifica.
In questo capitolo analizzeremo innanzitutto i possibili usi della specifica. Poi, illustreremo le qualit principali delle specifiche, che dovrebbero essere tenute presenti durante la stesura delle specifiche stesse. Poi analizzeremo alcune tecniche rilevanti per la stesura
di specifiche, classificandole secondo diversi stili di specifica. Discuteremo l'applicabilit di
ogni classe a diversi ambiti applicativi. Il ruolo della specifica lungo il processo di sviluppo
verr trattato nel Capitolo 6. Infine, discuteremo i problemi - e le possibili soluzioni della gestione delle specifiche di sistemi reali, i quali tendono a essere estremamente complessi.

5.1

I possibili usi delle specifiche

Le specifiche possono essere create e utilizzate per diversi motivi.


Descrizione dei requisiti degli utenti. Lo scopo primario di un prodotto quello di soddisfare i requisiti dell'utente ma questi, a volte, non vengono compresi a fondo dallo sviluppatore. Per evitare ci, diventa necessaria un'attenta analisi, accompagnata da frequenti
interazioni con l'utente, finalizzata a chiarire e documentare i requisiti, in modo da individuare ed evitare possibili fraintendimenti. A volte, all'inizio di un progetto, l'utente non sa

chiaramente quale sia il prodotto desiderato. Per esempio, un utente con poca esperienza di
prodotti software potrebbe non essere in grado di capire esattamente il livello al quale spingere l'automazione. probabile che la descrizione iniziale fornita da un utente con poca esperienza non contenga informazioni precise sui requisiti funzionali e prestazionali.
In altri casi, i requisiti potrebbero essere invece molto chiari e la loro specifica semplice. Nell'ingegneria tradizionale, infatti, esistono specifiche standard per i comuni oggetti utilizzati nelle costruzioni, quali chiodi, viti e mattoni. Tali standard permettono a differenti
produttori di realizzare lo "stesso" prodotto. Nell'ingegneria del software un linguaggio di
programmazione (e l'architettura del processore) possono essere interpretati come una specifica standard per un compilatore per quel linguaggio, che generi codice per quell'architettura. Ci permette a diversi produttori di creare prodotti basati sulle stesse specifiche.
In genere, i progetti falliscono a causa di incomprensioni tra il produttore e il consumatore. E pi probabile che tali fraintendimenti accadano quando la cultura e il "linguaggio" dei due sono molto diversi. Il caso di studio A in Appendice un esempio di un problema di questo tipo.
Quando si verificano fraintendimenti, sfortunatamente, ritornare al documento di specifica dei requisiti solitamente rivela un'ambiguit che supporta sia l'interpretazione dell'utente che quella del produttore. Questa situazione dimostra la necessit di poter verificare le
specifiche, ad esempio, poter controllare se definiscono in modo adeguato che cosa debba
essere il prodotto prima di implementarlo. Per esempio, sottoporre un documento di specifica all'utente finale potrebbe aiutare a individuare fraintendimenti circa le vere necessit dell'utente, evitando cos di produrre definizioni improprie.
Descrizione dell'interfaccia tra la macchina e l'ambiente controllato. I computer interagiscono con l'ambiente esterno ricevendo input (ad esempio, i comandi di un utente o i
segnali dai sensori di una centrale controllata) e fornendo output (ad esempio, dati di controllo per gli attuattori o risposte ai comandi degli utenti). Una specifica errata o un fraintendimento tra gli ingegneri e gli esperti del dominio applicativo (i quali conoscono tutti i
vari fenomeni che potrebbero influenzare la funzione di controllo da implementare), potrebbe
avere conseguenze indesiderate, e richiedere una riprogettazione o reimplementazione di
una grossa parte dell'applicazione, facendo cos crescere i costi di sviluppo. Nel caso di un
sistema critico, se questi problemi non venissero catturati e si dovessero propagare all'implementazione, potrebbero causare disastri o situazioni impossibili da recuperare. quindi
necessario specificare l'interfaccia tra la macchina e l'ambiente controllato, descrivendo in
maniera precisa gli input, gli output e le relazioni previste, inclusa la specifica dei limiti di
tempo che il controllore dovr soddisfare.
Tutti gli esempi illustrati condividono un'essenza comune. L'obiettivo della specifica
una descrizione precisa del confine tra la macchina e il mondo esterno con il quale la macchina interagisce. Nel caso di sistemi che devono interagire con gli utenti, il mondo esterno pu essere descritto e capito in termini di che cosa si aspetta l'utente finale. Nel caso di
sistemi embedded, l'ambiente esterno sar l'insieme di periferiche controllate dalla macchina.
Descrizione dei requisiti implementativi. Le specifiche possono essere usate come punto
di riferimento durante lo sviluppo del prodotto. Infatti, lo scopo ultimo dell'implementazione quello di costruire un prodotto che soddisfi le specifiche. Pertanto, l'implementato-

re usa le specifiche durante la fase di progettazione per prendere decisioni e durante l'attivit di verifica per controllare che l'implementazione soddisfi i requisiti.
Abbiamo gi sottolineato come l'intero processo di progettazione consista in una catena di definizione-implementazione-verifica. E quindi probabile che, in un dato momento, esistano diversi documenti di specifica. Una specifica che definisca il comportamento esterno di un sistema viene detta specifica dei requisiti, mentre una specifica dell'architettura
software, possibilmente a diversi livelli di astrazione, viene detta specifica di progetto.
In generale, i diversi usi della specifica sottolineano le qualit richieste in modi diversi, a volte anche contrastanti. Per esempio, se le specifiche dovranno essere utilizzate come
parte di un contratto, allora tutte le parti dovranno essere in grado di capire le specifiche.
Ci potrebbe porre restrizioni al linguaggio utilizzato per scriverle, visto che la terminologia
e la notazione tecnica potrebbero non essere accettabili per molti utenti. D'altra parte, la
specifica di un'interfaccia di un modulo risulter pi utile, ai fini dell'implementazione, se
scritta in una notazione formale (vedi Capitolo 4).
Descrizione di riferimento durante la manutenzione di un prodotto. Nei Capitoli 2 e
4 abbiamo visto le diverse tipologie di manutenzione che possono nascere durante il ciclo
di vita di un prodotto. Tutte hanno a che vedere con le specifiche.
Nel caso della manutenzione correttiva, solitamente viene cambiata solo l'implementazione. Le specifiche sono, quindi, necessarie per controllare se la nuova implementazione
risolve o meno gli errori presenti nella versione precedente del prodotto. Un'eccezione potrebbe essere il caso in cui l'errore si trovi nella specifica stessa, ma generalmente questo non
viene scoperto fino a quando non viene utilizzato il prodotto. In questo caso, si dovr prima correggere la specifica, per poi modificare di conseguenza l'implementazione.
La manutenzione adattativa si verifica a causa di cambiamenti nei requisiti. Tra questi cambiamenti ci sono le modifiche alle funzionalit del prodotto, ad esempio per rispettare un cambiamento di legge riguardante l'ambito applicativo del prodotto, o dovute a modifiche all'ambiente operativo, ad esempio un cambiamento nei meccanismi degli
sportelli Bancomat delle banche. In questi casi, le specifiche originali dovranno essere
adattate ai nuovi requisiti. Di conseguenza si dovr controllare se la nuova implementazione soddisfa in maniera corretta i requisiti. A volte, gli ingegneri tentano di ridurre i
tempi di sviluppo cambiando l'implementazione, senza prima modificare le specifiche.
Questo modo di procedere, per, crea inconsistenze tra le specifiche e l'implementazione, e pu portare a problemi ancora pi grandi in futuro. Uno degli esempi peggiori di
tale approccio quando diverse modifiche (in gergo, patch) vengono applicate al codice
oggetto, come nel caso di studio A. Queste patch producono incoerenze addirittura tra il
codice sorgente e il codice oggetto.
Nella manutenzione perfettiva, a volte, i requisiti funzionali non cambiano. Per esempio, si potrebbe ristrutturare il progetto di un prodotto nel tentativo di ottenere un miglioramento nelle prestazioni. In altri casi, invece, come l'inclusione di una nuova funzione o la
modifica di una funzione esistente, cambieranno anche i requisiti funzionali. Ancora una
volta, l'importante che le specifiche vengano utilizzate per capire in maniera chiara l'impatto di un cambiamento e in modo da poter portare a termine il cambiamento in modo
affidabile. Considerazioni simili possono essere applicate ai cambiamenti che devono essere
apportati per i moduli che compongono il prodotto. Se disponibile una specifica precisa

di un modulo, sar possibile capire se un cambiamento avr ripercussioni solo sulla sua implementazione (nel qual caso i moduli client risulteranno inalterati) o se avr ripercussioni
anche sull'interfaccia. Nel primo caso l'onere del cambiamento sar interamente dello sviluppatore del modulo; nel secondo, invece, saranno coinvolti tutti i moduli utenti del
modulo.

5.2

Qualit delle specifiche

ovviamente possibile scrivere specifiche buone o specifiche cattive. La maggior parte delle qualit elencate nel Capitolo 2 contribuisce a produrre specifiche di buon livello. Per esempio, l'usabilit una caratteristica rilevante per le specifiche, cos come lo per un prodotto software. Il concetto di usabilit, applicato alle specifiche, pu avere diverse accezioni a
seconda di chi sia effettivamente l'utente delle specifiche (ad esempio, l'utente finale o l'implementato re). Un'altra qualit desiderabile, nel caso delle specifiche, la manutenibilit, in
quanto le specifiche tendono a cambiare durante il ciclo di vita di un prodotto, di pari passo con i cambiamenti cui va incontro il prodotto stesso.
In questo paragrafo discuteremo tre insiemi di qualit particolarmente rilevanti per le
specifiche. Il primo insieme di qualit richieste per le specifiche che esse siano chiare, non
ambigue e facilmente comprensibili. Questa affermazione potrebbe sembrare ovvia ma non
mai troppo enfatizzata. In particolare, probabile che le specifiche informali, scritte in un
linguaggio naturale, contengano sottili ambiguit.
Si consideri l'esempio di un programma per la videoscrittura che fornisca il comando
s e l e c t , specificato nel seguente modo1:
La selezione il processo di designazione di aree del d o c u m e n t o su cui si vuole lavorare. La
maggior parte delle azioni di modifica e di formattazione richiede due passaggi: necessario
prima selezionare ci su cui si vuole lavorare, ad esempio un testo o un'immagine; poi si pu
iniziare l'azione appropriata.

Questa definizione non specifica esattamente che cosa si intende con il termine "area". Nella
maggior parte dei tool, la definizione assume implicitamente che un'area sia una "sequenza
contigua di caratteri". Un utente che non sia a conoscenza di questa assunzione potrebbe
interpretare il termine "area" come una collezione di frammenti di testo non necessariamente
contigui, tali per cui un utente pu selezionare diverse parole in punti diversi all'interno di
un testo e poi formattare i caratteri in corsivo con un comando solo. Ci non risulta possibile in un programma di videoscrittura tradizionale, anche se la specifica originale non chiara su questo fatto.
Un altro esempio potrebbe essere il seguente frammento di specifica, estratto dalla documentazione di un progetto reale per un sistema critico:
Il messaggio dovr essere triplicato. Le tre copie dovranno essere spedite attraverso tre canali fisici diversi. II ricevitore dovr accettare il messaggio sulla base di una politica di votazione "due
su tre".

La specifica proviene dal manuale di Microsoft Word 4.0.

Intuitivamente, la specifica afferma che, per motivi di affidabilit, i messaggi vengono triplicati. Al momento di ricevere le tre copie del messaggio, il ricevitore determiner i contenuti confrontando le copie: se due di esse dovessero essere uguali il contenuto verr assunto come corretto. Non chiaro, per, se il messaggio possa, o debba, essere considerato ricevuto non appena ne sono state ricevute due copie identiche, senza aspettare la terza, o se
il ricevitore debba aspettare tutte e tre le copie prima di eseguire il confronto dei loro contenuti. Siccome stiamo parlando di un sistema real-time, la questione pu fare la differenza
quando cambiano i tempi di risposta per il trattamento dei messaggi2.
L'applicazione dei principi di rigore e di formalit pu aiutare significativamente nel
raggiungere questa e molte altre qualit nelle specifiche. Per esempio, l'ambiguit nella politica di votazione appena discussa fu scoperta quando si decise di formalizzare la specifica
informale. Pi avanti, in questo capitolo, vedremo anche come formalizzare la politica di
votazione, in modo da rimuovere tutte le ambiguit.
La seconda qualit fondamentale richiesta per le specifiche la coerenza. Per esempio,
sempre nel caso di un programma di videoscrittura, si potrebbe affermare che:

l'intero testo deve essere mantenuto su linee di uguale lunghezza, specificata dall'utente;

a meno che l'utente non inserisca esplicitamente nel testo un trattino, il comando per
andare a capo dovrebbe essere possibile solo alla fine di una parola.

Queste due affermazioni sono contraddittorie nel caso in cui una particolare parola sia pi
lunga della lunghezza specificata per una linea. In tale caso, la specifica incoerente, e nessuna implementazione la potr mai soddisfare. La probabilit di introdurre inavvertitamente un'incoerenza all'interno di una specifica cresce con la lunghezza e la complessit dei documenti di specifica, cosa comune nei progetti reali.
La terza qualit richiesta per le specifiche che queste siano complete. Esistono due aspetti di completezza. In primo luogo, si richiede che la specifica sia completa internamente. Ci
significa che la specifica deve definire tutti i nuovi concetti o i nuovi termini di cui fa uso.
Solitamente, per questo motivo, utile servirsi di un glossario. Per esempio, se la specifica
di un ascensore dovesse affermare che "in assenza di richieste l'ascensore entra in uno stato
di attesa di richiesta", la specifica dovr anche definire il significato dello stato di "attesa di
richiesta".
Il secondo aspetto (completezza esterna) si riferisce alla completezza dei requisiti: la specifica dovrebbe documentare tutti i requisiti richiesti. Nell'esempio dell'ascensore, se dovesse essere richiesto che un ascensore libero (senza richieste pendenti) vada al primo piano e
apra le porte, questo requisito dovrebbe essere reso esplicitamente, e non lasciato alla discrezione del progettista. La completezza esterna si riferisce implicitamente ai requisiti funzionali. Anche se in genere i requisiti funzionali sono fondamentali, non sono gli unici requisiti importanti. Sono importanti anche i requisiti non funzionali o qualitativi (usabilit,
affidabilit, prestazioni, etc.). Per esempio, le prestazioni in termini di tempi di risposta non

Esaminando la documentazione di progetto abbiamo scoperto che in realt nessuna delle due possibilit era stata scelta dall'implementatore. Infatti, il ricevitore controllava periodicamente i tre canali in modo da definire un time-out implicito: se le tre copie venivano ricevute entro un periodo di tempo determinato, venivano confrontate tutte; se, invece, dopo un determinato periodo, erano state ricevute solo due
copie e i loro contenuti erano identici, allora il messaggio veniva accettato senza attendere la terza trasmissione.

vengono molte volte specificate per sistemi non real-time (come ad esempio un programma
di videoscrittura). Usando il prodotto, tuttavia, l'utente potrebbe lamentarsi del fatto che
questo sia troppo lento. In genere, anche i cosiddetti "casi eccezionali" - che possono essere molto rilevanti - vengono frequentemente ignorati: si pensi per esempio agli inconvenienti
che possono sorgere se un salto di tensione nell'impianto elettrico causasse la perdita di tutti i file aperti in quel momento.
Spesso irrealistico esigere specifiche complete in senso assoluto. Molti requisiti potrebbero essere identificati chiaramente solo dopo che si acquisisce una certa esperienza con
il sistema, e inoltre dovrebbero essere specificati troppi dettagli. Pi realisticamente, alcuni
requisiti possono essere considerati comuni a ogni sistema e quindi assunti implicitamente
per ogni specifica. Questa assunzione potrebbe quindi portarci ad accettare un certo livello
di imprecisione e ambiguit. Per esempio, capiter pi volte di accontentarsi di frasi del tipo "i tempi di risposta dovrebbero essere di circa due secondi" o "il sistema dovrebbe essere robusto rispetto ai guasti del sistema di alimentazione". Lo scopo, comunque, quello di
mantenere tali imprecisioni entro i limiti necessari per evitare il rischio di ambiguit pericolose. E responsabilit sia dell'utente che del produttore decidere quando una determinata imprecisione possa essere accettata sulla base del buon senso e quando invece vada evitata.
Si potrebbe argomentare che in molti casi pratici, inclusi gli esempi che abbiamo discusso in questo paragrafo, le specifiche vengano dichiarate appositamente in modo informale, in quanto la scelta di quale interpretazione dare per rimuovere le ambiguit ritenuta una decisione implementativa: dal punto di vista dell'utente ugualmente accettabile qualsiasi modo di togliere le ambiguit. Secondo questo punto di vista, fornire una specifica precisa sovraspecificherebbe il sistema e porrebbe limitazioni all'implementatore. Il punto debole di questa posizione che solitamente non si sa se la mancanza di precisione in una specifica sia dovuta a una scelta precisa o a una distrazione. Inoltre, l'uso di un linguaggio informale non aiuta a sottolineare i punti in cui si nascondono le ambiguit: le domande nascono solo quando si tenta di descrivere i requisiti in modo formale. A quel punto, si pu operare una scelta consapevole ed esplicita: o la specifica esprime in modo preciso che cosa va
fatto, o la risoluzione dell'ambiguit viene rimandata all'implementazione.
L'uso del principio di incrementalit estremamente importante nel derivare le specifiche, proprio a causa delle difficolt nell'ottenere specifiche complete, precise e non ambigue. Si potrebbe, quindi, cominciare abbozzando un documento di specifica per poi
espanderlo attraverso diverse iterazioni, magari dopo aver accumulato una certa esperienza con alcuni prototipi. Vedremo diversi esempi di questa strategia pi avanti in questo
capitolo. Per motivi analoghi, anche il principio di modularit risulta essere importante
per le specifiche.
Esercizi
5.1

Ripassate le qualit del software elencate nel Capitolo 2 ed evidenziate quali siano rilevanti
per le specifiche e quali no.

5.2

Fornite una specifica precisa per la funzione "giustifica a margine" per un p r o g r a m m a di videoscrittura.

5.3

Classificazione degli stili di specifica

Possiamo classificare i vari stili di specifica secondo due criteri indipendenti.


Le specifiche possono essere poste in maniera formale o informale. Le specifiche informali sono scritte in un linguaggio naturale e possono utilizzare anche figure, tabelle e altre
notazioni per fornire una struttura migliore e agevolare la comprensione. Possono anche essere strutturate in maniera standard. Una notazione dalla sintassi e dal significato precisamente definiti viene chiamata formalismo. I formalismi sono usati per creare specifiche formali. E utile parlare anche di specifiche semiformali, in quanto, nella pratica, capita frequentemente di impiegare una notazione senza insistere su una semantica precisa. Le notazioni T D N e GDN introdotte nel Paragrafo 4.2.3.1 sono un esempio di notazione semiformale, visto che uniscono una descrizione sintattica formale per le interfacce dei moduli alle descrizioni informali del loro significato. Vedremo come i commenti informali delle nostre specifiche di moduli con T D N possano essere resi precisi formalizzando la semantica delle operazioni. Anche i diagrammi delle classi UML possono essere usati come una
notazione semiformale per descrivere un progetto, ma anche disponibile un linguaggio formale chiamato OCL, utile per fornire alle classi una semantica precisa.
La seconda principale distinzione tra i diversi stili di specifica quella tra specifiche
operazionali e descrittive. Le specifiche operazionali descrivono il sistema desiderato specificando il suo comportamento desiderato, solitamente fornendo un modello del sistema (cio
una "macchina astratta" in grado di simulare il comportamento desiderato). Le specifiche descrittive, invece, esprimono le propriet desiderate del sistema in modo puramente dichiarativo.
Per esempio, si supponga di fornire la seguente specifica della figura geometrica E: la
figura geometrica E pu essere disegnata nel seguente modo:
1. Selezionare due punti P, eP 2 su un piano.
2. Prendere una cordicella e legare i due capi rispettivamente a Pj e P 2 .
3. Posizionare una matita come illustrato nella Figura 5.1.
4. Muovere la matita in senso orario, tenendo la cordicella tesa, fino a quando non viene raggiunto il punto di partenza del disegno.
Quella che abbiamo fornito una definizione operazionale della curva nota in geometria
con il nome di ellisse, con fuochi in Pj e P2. Una definizione alternativa della stessa curva
poteva essere data sotto forma della sua equazione, ax 2 + by 2 + c = 0, dove a, b e c
sono costanti.
L'esempio mostra che, mediante una definizione operazionale, possibile controllare
se la specifica descrive il tipo di curva che avevamo in mente al momento di fornirne la specifica. Infatti, molto semplice disegnare la curva con una matita su un foglio, seguendo la
specifica, e poi controllare se la curva soddisfa i requisiti che avevamo in mente. Certo, l'implementazione della curva potrebbe risultare completamente diversa da quanto descritto
nella specifica; per esempio, potrebbe essere una curva mostrata su un terminale.
Ciononostante, la sperimentazione ci aiuta a capire se la specifica data corretta. Se volessimo controllare se un dato punto P nella posizione ( x ^ y j giace sulla curva, potremmo
farlo con pi facilit riferendoci all'equazione. La curva potrebbe essere la traiettoria di un

5.3

Figura 5.1

Classificazione degli stili di specifica

185

Costruzione di un'ellisse.

robot, il punto P potrebbe rappresentare il punto in cui lavora una persona, e per motivi di
sicurezza potrebbe essere necessario richiedere che il robot non passi mai per il punto P.
Come esempio relativo al software, si consideri la seguente specifica operazionale informale dell'ordinamento di un array:
Sia a un array di n elementi. Il risultato dell'ordinamento di a un array b che p u essere costruito nel m o d o seguente:
1. Trovare l'elemento pi piccolo c o n t e n u t o in a e assegnarlo come primo elemento di b .
2. Rimuovere l'elemento trovato nel passo 1 da a e trovare l'elemento pi piccolo tra quelli rimanenti. Assegnare questo elemento come secondo elemento dell'array b .
3. Ripetere i passi 1 e 2 fino a q u a n d o tutti gli elementi n o n sono stati rimossi da a .

Questa specifica suggerisce un modo semplice e naturale (anche se non molto efficiente)
per implementare l'ordinamento di un array. Il suggerimento, tuttavia, non implica che
un algoritmo di ordinamento debba ordinare a in quel modo: dovr solo produrre lo stesso risultato. Quicksort, per esempio, un'implementazione perfettamente adeguata della
specifica. Il problema con questo tipo di specifica che risulta difficile per il lettore determinare esattamente che cosa sia prescritto, che cosa debba essere implementato e che
cosa no.
Una possibile specifica descrittiva dell'ordinamento di a potrebbe essere la seguente:
Il risultato dell'ordinamento di a un'array b ordinato che anche una permutazione di a .

Se i concetti di permutazione e di ordinamento non dovessero essere considerati abbastanza


chiari, potrebbero essere specificati ulteriormente. Se, d'altra parte, dovessero essere ritenuti concetti primitivi, la specifica si potrebbe considerare completa.
Esistono diversi compromessi nello scegliere una specifica descrittiva piuttosto che una
specifica operazionale. In genere si ritiene che le specifiche descrittive abbiano un maggiore
livelb di astrazione rispetto a quelle operazionali, visto che non indirizzano il lettore verso
alcuna implementazione particolare, ma aiutano a focalizzare l'attenzione sulle propriet essenziali del sistema, senza modellare il comportamento di alcuna implementazione. Anche
se si tratta di un'affermazione largamente condivisibile, dobbiamo riconoscere che in ogni

specifica presente uno schema implementativo nascosto. Per esempio, la specifica descrittiva dell'ordinamento di un'array suggerisce la seguente implementazione banale: "enumerare tutte le permutazioni dell'array originale. La prima permutazione ordinata rappresenta
un output adeguato per l'algoritmo di ordinamento".
Si potrebbero scrivere anche specifiche che in qualche modo rappresentino una via di
mezzo tra l'operazionale e il descrittivo. Per esempio, si potrebbe definire una nuova operazione da applicare a un array a nel seguente modo:

Innanzitutto, a deve essere ordinato, dove la definizione di "ordinato" viene fornita in


modo descrittivo.

Poi, tutti gli elementi presenti in copia multipla devono essere rimossi dall'array.

Questa specifica operazionale nel senso che definisce una sequenza di due operazioni da
eseguire in modo da ottenere il risultato desiderato. Ma una parte di questa, ovvero il significato di "ordinato", viene fornita in modo descrittivo.
Per concludere, la distinzione tra specifiche operazionali e descrittive non pu essere
sempre precisa e, a volte, risulta soggettiva. per una distinzione sostanzialmente adeguata per la categorizzazione di diversi stili di specifiche.
Nelle prossime pagine approfondiremo i diversi stili di specifica, valutando criticamente alcuni esempi di notazioni. Anche se queste sono state scelte in quanto costituiscono esempi importanti di diversi stili di specifiche, non sar la scelta delle notazioni l'argomento principale. Dato che non esiste lo stile perfetto per tutte le circostanze, il nostro
obiettivo sar aiutare il lettore a sviluppare la capacit di analizzare una notazione di specifica in maniera critica, per poi sceglierla o rifiutarla a seconda della situazione. Lo stile o notazione appropriata pu aiutare un progettista a esprimere con chiarezza gli aspetti del progetto. Una notazione inadeguata, invece, rende tutto pi difficile. Alcune notazioni, comunque, stanno guadagnando una larga diffusione, anche nella pratica. Di conseguenza, sono diventate oggetto di standardizzazione. La notazione UML, un esempio di notazione standardizzata introdotta nel Capitolo 4, verr qui ulteriormente approfondita.
Nessuno stile o notazione, formale o informale che sia, pu comunque garantire che
il progettista riesca a concepire un buon progetto. Una buona specifica e progettazione richiedono capacit adeguate, esperienza, giudizio e anche creativit.
Poich l'attivit di progetto e sviluppo dipende fortemente da criteri soggettivi, risulta importante sviluppare spirito critico e intuizione in quanto aiuteranno ad adattare stile,
tecnica e notazione di specifica al problema in esame.
Esercizi
5.3

Fornite una specifica descrittiva completa dell'esempio precedente, che faceva uso di un insieme di stili descrittivi e operazionali per descrivere la costruzione di un array ordinato senza elementi duplicati.

5.4

Considerate le specifiche operazionali e descrittive dell'operazione di o r d i n a m e n t o fornite


in questo paragrafo. Vi sono delle ambiguit? C o m e d o v r a n n o essere trattati gli elementi
duplicati?

5.4 Verifica delle specifiche


Un utilizzo importante delle specifiche quello di costituire il punto di riferimento in base
al quale verificare un'implementazione. Tuttavia, anche le specifiche necessitano di una verifica. Nel Capitolo 2 abbiamo visto che la correttezza di un'applicazione non garantisce che
le funzioni messe a disposizioni coincidano necessariamente con quelle richieste dal cliente.
La stessa cosa pu dirsi per tutte le qualit del software. Anche se l'implementazione dovesse eventualmente soddisfare la specifica, potremmo sempre avere tra le mani un prodotto
che non soddisfi il cliente. quindi importante che le specifiche siano verificate prima di
cominciare l'implementazione, in modo da controllare che siano corrette.
Ci sono due modi generali per verificare una specifica. Uno consiste nell'osservare il
comportamento dinamico del sistema specificato in modo da controllare se sia conforme all'idea intuitiva, che ci eravamo fatti, del comportamento del sistema. L'altro consiste nell'analizzare le propriet del sistema specificato che possono essere dedotte a partire dalla specifica. E possibile infatti confrontare le propriet dedotte con le propriet attese del sistema.
L'efficacia di entrambe le tecniche cresce in funzione del grado di formalit della specifica. Infatti, in un quadro interamente formale, un modo per osservare il comportamento
dinamico del sistema specificato potrebbe essere quello di fornire un'interpretazione del linguaggio formale usato per le specifiche per poi eseguire le specifiche formali con un input
di dati di esempio. In maniera simile, possibile dedurre automaticamente le nuove propriet da quelle indicate come parte della specifica (descrittiva) in un quadro di logica
formale.
Nell'esempio di un'ellisse, mostrato nella Figura 5.1, l'osservazione del comportamento dinamico del sistema pu anche essere detta simulazione. La simulazione ottenuta eseguendo la specifica formale, e quindi fornendo un prototipo del sistema prima ancora di cominciare l'implementazione. In un quadro meno formale, l'esecuzione potrebbe essere simulata a mente, piuttosto che meccanicamente. Analogamente, l'analisi delle propriet pu
essere eseguita mediante ispezione umana, soprattutto se le specifiche non sono completamente formali.
Potrebbe essere utile confrontare l'ingegneria del software con i settori ingegneristici
pi tradizionali. In questi, le specifiche descrittive sono frequentemente fornite in termini
di equazioni matematiche che modellano il sistema. Si pensi a un ponte di una data forma
che unisce due sponde di un fiume: il modello matematico fornito dall'ingegnere supporter un'analisi delle propriet, in termini di possibilit del ponte di supportare una data distribuzione di forze statiche e dinamiche. Un modello operazionale del sistema, invece, viene in genere fornito come prototipo, e non visto tanto come una specifica ma piuttosto
come un aiuto nella verifica della specifica. Sia nella costruzione di ponti che nella costruzione di software, una specifica inadeguata pu portare al fallimento del sistema. E noto che
alcuni ponti sono crollati durante tempeste perch, al momento della specifica, non era stata presa in considerazione la possibilit di venti anomali.
Fino ad ora, abbiamo preso in considerazione solo la verifica di specifiche funzionali.
importante verificare anche la completezza e la coerenza delle specifiche. Ancora una volta, con le specifiche formali, una parte di queste verifiche pu essere eseguita in maniera automatica (ad esempio verificando che tutti i termini utilizzati nella specifica siano stati definiti); una parte della specifica potrebbe invece necessitare di prove pi sofisticate. Le spe-

tifiche informali sono pi diffcili da verificare automaticamente, ma anche per loro esistono, e dovrebbero essere usati, alcuni controlli meccanici.
Affronteremo la questione della verifica dei requisiti pi avanti, sia in questo capitolo
che nei Capitoli 6, 7 e 9.

5.5

Specifiche operazionali

In questo paragrafo descriveremo alcune notazioni molto usate e applicate per le specifiche
operazionali. Cominceremo con alcune notazioni semiformali il cui utilizzo estremamente diffuso nella pratica per la descrizione di sistemi informativi, e passeremo poi ad illustrare notazioni formali adatte alla descrizione di aspetti di controllo nella modellazione di
sistemi.

5.5.1

Diagrammi di flusso di dati: la specifica delle funzioni


dei sistemi informativi

I DFD (data flow diagram, diagramma di flussi di dati) sono una notazione molto conosciuta e usata per la specifica di sistemi informativi e per la descrizione di come i dati fluiscono di funzione in funzione. Descrivono i sistemi come collezioni di funzioni che manipolano dati. I dati possono essere organizzati in diversi modi: possono essere immagazzinati in archivi di dati, possono fluire lungo flussi di dati e possono essere trasferiti da o verso
un ambiente esterno.
Uno dei motivi del successo dei DFD che possono essere espressi mediante notazioni grafiche che li rendono facili da usare.
Gli elementi di base dei DFD sono3:

Le funzioni, rappresentate da bolle.

I flussi di dati, rappresentati da frecce. Le frecce entranti in una bolla rappresentano valori di input facenti parte del dominio della funzione rappresentata dalla bolla. Le frecce uscenti invece rappresentano i risultati della funzione, ovvero, valori appartenenti al
codominio della funzione.

Gli archivi di dati, rappresentati da scatole aperte. Frecce entranti (o uscenti) nelle scatole aperte rappresentano dati che vengono inseriti (o estratti) nell'archivio di dati.

Gli input/output, rappresentati da tipi speciali di scatole I/O che descrivono acquisizioni e generazioni di dati durante l'interazione con gli utenti.

La Figura 5.2 fornisce alcuni esempi di questi simboli grafici. La Figura 5.3 illustra invece
come i simboli possano essere combinati per formare un DFD. Il DFD descrive la valutazione dell'espressione aritmetica
( a + b )

* (c + a * d)

La notazione D F D non uno standard. In letteratura esistono diverse definizioni leggermente diverse.

Simbolo per le funzioni

Simbolo per l'inpul rl.l periferica

Simbolo per il flusso di d.ili

Simbolo per gli archivi di dati

Figura 5.2

Simbolo per l'outpul verso periferiche

Simboli grafici di base usati per costruire i diagrammi di flussi di dati.

assumendo che i dati a, b, c e d vengano letti da un terminale e che il risultato venga stampato. La figura mostra come le frecce possano essere "separate" per rappresentare il fatto che
gli stessi dati vengono usati in posti diversi.
ESEMPIO 5.1

La Figura 5.4 descrive un sistema informativo semplificato per una biblioteca pubblica. I
dati e le funzioni presenti non sono necessariamente parte di un sistema computerizzato. Il
DFD pu descrivere oggetti fisici, come libri e ripiani, oltre ad archivi di dati che, con buona probabilit, saranno realizzati come file di computer. Prendere un libro da un ripiano pu
essere fatto sia automaticamente (da un robot) sia manualmente. In entrambi i casi questa
azione pu essere rappresentata da una funzione raffigurata da una bolla. La figura, che potrebbe anche rappresentare una biblioteca senza alcuna procedura computerizzata, descrive
anche il fatto che, per poter prendere un libro, occorrono:

una richiesta esplicita da parte dell'utente, che consiste del titolo, il nome dell'autore
del libro e il nome dell'utente;

un accesso al ripiano su cui sta il libro;

b
+

Figura 5.3

d
*

c
+

Diagramma di flusso di dati per la specifica del calcolo di un'espressione aritmetica.

Figura 5.4

D F D per la descrizione di un sistema informativo semplificato per biblioteche.

un elenco di autori;

un elenco di titoli;

per catturare le informazioni necessarie per trovare il libro.


La modalit precisa con cui viene ottenuto il libro, tuttavia, non espressa interamente
nella figura. Senza ricorrere alle nostre esperienze passate riguardanti il prelievo di un libro
in biblioteca, non ci sarebbe alcun modo per dedurre queste informazioni dalla figura.
Dovremmo, quindi, considerare questo DFD come una prima approssimazione della descrizione di un sistema informativo per biblioteche. Una descrizione pi precisa, data nella
Figura 5.5, pu essere vista come un raffinamento della Figura 5.4. La Figura 5.5 risulta tuttavia ancora un po' imprecisa, in quanto non specifica se siano necessari sia il titolo che il
nome dell'autore per identificare un libro o se basti uno dei due. Sappiamo che, in generale, uno dei due sufficiente, anche se occasionalmente sono necessari entrambi. Questa distinzione per non spiegata nella
figura.

Nell'Esempio 5.1, le Figure 5.4 e 5.5 forniscono una descrizione intuitiva del sistema, ma
difettano di precisione. Ci risulta essere vero in generale per tutti i DFD, i quali possono
avere un significato ambiguo, fondamentalmente per le seguenti ragioni:

Libro

Figura 5.5

R a f f i n a m e n t o p a r z i a l e d e l l a f u n z i o n e " C o n s e g n a un libro" d e l l a Figura 5 . 4 .

1. La semantica dei simboli utilizzati specificata solo dai nomi scelti dall'utente. A volte un nome pu definire un concetto in modo sufficientemente preciso; per esempio,
il simbolo "+" denota chiaramente la funzione "somma", senza che siano necessarie ulteriori spiegazioni. Altre volte, invece, pu non bastare. Per esempio, la funzione "Trova
la posizione del libro" (Figura 5.5) possiede un significato intuibile ma non specifica
che cosa succede se manca qualche informazione necessaria. Una definizione realistica
(anche se ancora informale) potrebbe essere la seguente:
if

viene

elseif

f o r n i t o sia il n o m e d e l l ' a u t o r e (o d e g l i a u t o r i )
e il t i t o l o d e l l i b r o t b e n
t r o v a r e la p o s i z i o n e d e l l i b r o (se il l i b r o
n o n e s i s t e r e s t i t u i r e un m e s s a g g i o a d e g u a t o )
viene fornito solo l'autore tben
f o r n i r e un e l e n c o
da q u e l l ' a u t o r e e
una scelta

elseif

end

viene

fornito

solo

di t u t t i
chiedere
il

titolo

i libri scritti
a l l ' u t e n t e di o p e r a r e
then

. . .

if

2. Gli aspetti di controllo non sono definiti dal modello. Per esempio, la Figura 5.6 mostra
un semplice DFD in cui gli output di tre bolle A, B e C sono input di D, mentre i due
output di D sono input delle bolle E ed F. Preso singolarmente, questo diagramma non
specifica chiaramente il modo in cui sono utilizzati gli input e come vengono prodotti

Figura 5.6

D F D ambiguo nel suo modo di usare gli input e gli output.

gli output dalla funzione D. In particolare potrebbero esserci numerose, differenti alternative, tutte ugualmente plausibili, sia per l'input che per l'output. Per gli input,

D potrebbe avere b i s o g n o di A, B e C; ovvero D p o t r e b b e n o n essere in grado di eseguire, a m e n o che n o n siano presenti tutti e tre i valori.

D p o t r e b b e avere b i s o g n o s o l o di u n o tra A, B e C per eseguire; ovvero, la trasform a z i o n e dei dati p o t r e b b e avvenire a n c h e in presenza di u n o s o l o dei tre.

Per gli output,

D p o t r e b b e produrre u n o u t p u t per una sola delle bolle tra E ed F, in maniera n o n


deterministica m a esclusiva.

D potrebbe produrre lo stesso o u t p u t sia per E c h e per F.

D p o t r e b b e produrre o u t p u t diversi per E e per F.

Anche altre interpretazioni degli input e degli output di D potrebbero essere compatibili con la Figura 5.6.
Un altro caso in cui i DFD non specificano interamente la sincronizzazione tra componenti di un sistema mostrato nella Figura 5.7, dove due bolle A e B sono connesse da un singolo flusso di dati. L'output di A viene spedito a B come nuovo input. Ci
sono almeno due interpretazioni possibili per questo DFD:
A produce un dato e poi aspetta che B lo consumi (questo potrebbe essere il caso in
cui A e B denotano operazioni aritmetiche su dati semplici).
A e B sono attivit autonome che hanno velocit diverse, ma esiste un meccanismo
di buffer tra loro (come ad esempio una coda limitata o una "pipe" illimitata) che
assicura che non si verifichino perdite o duplicazioni di dati (per esempio, A potrebbe
occuparsi del calcolo del tempo impiegato dai dipendenti nel loro lavoro, e B del
calcolo degli stipendi).

Figura 5.7

D F D che non specifica la sincronizzazione tra moduli.

Concludendo, i DFD sono una notazione grafica adatta a una descrizione immediata e intuitiva del flusso dei dati e delle operazioni coinvolte in un sistema informativo. Purtroppo,
i DFD non possiedono una semantica precisa. Anche se la notazione potrebbe essere interpretata come operazionale, il comportamento della macchina astratta corrispondente non
interamente specificato, ma sono possibili diverse interpretazioni per il regime di controllo
associato a un DFD.
Questi inconvenienti implicano conseguenze negative. In primo luogo, una descrizione
sommaria del sistema modellato non sufficiente: abbiamo bisogno di definizioni precise e
dettagliate che i DFD non sono in grado di fornirci. In secondo luogo, immaginiamo di voler costruire una macchina che simuli il sistema modellato in modo da poter controllare che
le specifiche riflettano il volere degli utenti. Tale macchina non pu essere derivata direttamente
dal DFD visto che nessuna esecuzione automatica possibile senza una semantica precisa per
la notazione. Un lettore umano potrebbe essere in grado di riempire i vuoti concettuali grazie
ai significati intuitivi degli identificatori, ma la macchina, che manca di intuito, non sar mai
in grado di interpretare una notazione come quella fornita nella Figura 5.7.
Per questi motivi, diciamo che i DFD tradizionali sono una notazione semiformale. La
loro sintassi, il modo in cui si compongono bolle, frecce e scatole, definita in modo preciso, ma la loro semantica non lo .
Sono stati progettati diversi metodi per ovviare a queste difficolt, che possono essere
sommariamente classificati nel seguente modo.

Usare una notazione complementare per descrivere quegli aspetti del sistema non definiti
in maniera adeguata dai DFD. La specifica intera del sistema consister nell'integrazione di diverse descrizioni fornite con differenti notazioni. Vedremo esempi di questa
tecnica pi avanti.

Migliorare il modello DFD in modo da poter affrontare aspetti non definiti nella sua versione tradizionale. Per esempio, potremmo gestire aspetti di controllo introducendo frecce di controllo delflusso. Una freccia per il controllo del flusso entrante in una bolla significherebbe che la computazione della funzione associata alla bolla possibile solo
nel momento in cui appare un segnale sulla freccia. La Figura 5.8 mostra la notazione
modificata e un esempio di uso delle frecce di controllo del flusso.

Figura 5.8

D F D parziale con l'aggiunta di frecce per il controllo del flusso. Il trigger


una freccia di controllo del flusso tratteggiata. La funzione "somma" associata
con la bolla applicata a tutti i dati esistenti nelle scatole non appena si presenta
il trigger.

Revisionare la definizione tradizionale di DFD in modo da renderlo interamente formale. Per esempio, si potrebbe definire un modello D F D per rendere possibile l'espressione di tutte le interpretazioni desiderabili dei D F D originali in maniera non ambigua. Si potrebbero utilizzare differenti notazioni per distinguere il caso in cui una freccia tra due bolle rappresenta il flusso di un dato singolo dal caso in cui rappresenta una
pipe. Oppure si potrebbe annotare il diagramma in modo da indicare la necessit di
avere tutti i dati di flusso in input o uno solo. Infine, si potrebbe fornire una notazione per la specifica formale della funzione eseguita da una bolla.

Esercizi
5.5

Fornite una descrizione pi dettagliata di u n sistema i n f o r m a t i v o per biblioteche, includ e n d o ulteriori operazioni c o m e la restituzione, la p r e n o t a z i o n e e la ricerca di un libro.
Raffinate le operazioni fino a u n livello in cui sono t u t t e spiegate in m o d o sufficientemente
dettagliato.

5.6

Fornite una specifica dettagliata di una funzione realistica che permetta di trovare la posizione di u n libro. Riflettete su quanti dettagli sono coinvolti nella specifica completa di una parte cosi piccola e apparentemente semplice di u n sistema informativo.

5.7

Fornite una descrizione D F D su c o m e richiedere ed effettuare l'iscrizione all'universit.

5.5.2 Diagrammi UML per comportamenti specifici


UML una collezione di linguaggi che forniscono notazioni per la specifica, l'analisi, la visualizzazione e la documentazione di sistemi software. Queste notazioni vengono utilizzate
dai progettisti per produrre progetti standard composti da diversi diagrammi, ognuno dei
quali finalizzato ad esprimere un diverso aspetto del sistema software. Illustreremo, in questo paragrafo, gli use case diagram, i sequence diagram e i collaboration diagram; ognuno di
questi pu essere utilizzato per modellare gli aspetti dinamici di un sistema.
Gli use case diagram forniscono un'astrazione globale degli "attori" coinvolti in un sistema e delle azioni eseguite dal sistema che, a loro volta, forniscono un risultato visibile di
interesse per gli attori. Gli use case diagram descrivono il contesto globale di un sistema partizionando le funzionalit offerte in transazioni utili agli attori e mostrando come gli attori
possono interagire con esse. Gli attori sono un'astrazione di entit esterne che interagiscono col sistema, quali persone e processi, e sono legati agli use case da associazioni che rappresentano il percorso comunicativo tra un attore e il caso d'uso al quale partecipa.
Per esempio, si consideri la descrizione di una biblioteca fornita nella Figura 5.9. Il sistema permette agli utenti di prendere in prestito e restituire libri. Queste azioni coinvolgono sia gli utenti che i dipendenti della biblioteca. I bibliotecari possono aggiornare la biblioteca inserendo copie di libri nuovi e eliminando copie di libri obsoleti.
I sequence diagram e i collaboration diagram sono due notazioni equivalenti che possono essere utilizzate per descrivere le interazioni tra oggetti mediante la spedizione di messaggi. Forniscono una visione dinamica di un sistema, mostrando graficamente gli scenari
che possono esistere a run-time quando gli oggetti interagiscono per portare a termine un
determinato compito.

Figura 5.9

Use case diagram.

Il sequence diagram della Figura 5.10 illustra un frammento della specifica di un sistema bibliotecario; lo fa descrivendo uno tra gli scenari che si possono configurare qualora un
cliente prenda un libro in prestito. In questo scenario, il cliente inizialmente esibisce la propria tessera e il bibliotecario provvede a controllarne la validit. Se valida, controlla il catalogo per verificare se il libro richiesto disponibile. In caso affermativo, il cliente pu prenderlo in prestito. Il sequence diagram indica, visivamente, la progressione del tempo lungo
l'asse verticale, in modo da mettere in evidenza la sequenza temporale dei messaggi scambiati tra oggetti (e cio, il cliente, il bibliotecario e il catalogo).
La Figura 5.11 descrive lo stesso scenario mediante un collaboration diagram. Questo
indica quali siano gli oggetti coinvolti nell'interazione e descrive la sequenza temporale degli eventi mediante la numerazione degli archi che legano gli oggetti che collaborano. I sequence diagram e i collaboration diagram sono semanticamente equivalenti, ma diversi sintatticamente. La scelta di quale dei due usare una questione di gusto personale. I collabo-

Clienle

Catalogo

Bibliolecario

Tessera
bibliotecaria +
richiesta libro

Tessera
valida

Richiesta libro
Libro disponibile
Libro preslalo

Figura 5.10

Sequence diagram.

Tempo

1 : Tessera
bibliotecaria +
richiesta libro

Cliente

5: Libro prestato

Figura 5.11

2 : Tessera
valida
3: Richiesta
libro
Bibliotecario

Catalogo
4 : Libro disponibile

Collaboration diagram.

ration diagram evidenziano meglio le propriet strutturali di una collaborazione, mentre i


sequence diagram evidenziano meglio l'evoluzione temporale dello scenario.
Le specifiche degli use case e di possibili scenari di comportamento del sistema sono molto utili nella fase di specifica dei requisiti, quando il progettista deve interagire con i clienti per
capire quali siano le loro aspettative. I diagrammi descrivono alcuni casi rappresentativi del comportamento del sistema in maniera molto intuitiva. Analizzando questi diagrammi, l'utente
pu confermare se le specifiche catturano con successo i comportamenti attesi.

5.5.3 Macchine a stati finiti: descrizione del flusso di controllo


Nella descrizione dei sistemi informativi, l'enfasi sull'organizzazione di funzioni e flussi di
dati. Per rendere le specifiche pi precise per necessario prestare attenzione anche agli aspetti di controllo. Per esempio, prima o poi, si potrebbe voler specificare in un DFD se l'esecuzione di una funzione debba attendere tutti gli input o se possa iniziare non appena ne arrivano alcuni. Proprio per questo motivo, anche i linguaggi di programmazione possiedono
costrutti per la descrizione di dati organizzati e del flusso di controllo.
Nella specifica di sistemi diversi, l'equilibrio tra la necessit di descrivere il flusso di dati e il flusso di controllo pu essere diverso. Per esempio, in un sistema per la comunicazione potrebbe essere preferibile specificare i requisiti nel seguente modo:

Figura 5.12

Macchina a stati finiti.

Premi tasto

Premi tasto

Figura 5.13

Descrizione di una lampada mediante l'uso di una macchina a stati finiti.

non si deve poter scrivere in un buffer pieno o leggere da un buffer vuoto;

non si deve poter accedere a un buffer mentre un altro processo lo sta utilizzando in
scrittura;

la lettura da un buffer deve avere priorit pi alta rispetto alla scrittura;

ogni messaggio deve essere rispedito attraverso un canale entro 2 millisecondi dal momento del suo arrivo.

Si potrebbero affrontare anche aspetti funzionali:

per ogni messaggio ricevuto deve essere controllata la parit;

dopo aver ricevuto 10 messaggi, deve essere sintetizzato un nuovo messaggio concatenando i 10 messaggi ricevuti e deve essere aggiunta un'intestazione contenente l'indirizzo della stazione che ha ricevuto i 10 messaggi originali.

Perci, sia i sistemi informativi sia quelli di controllo - cos come ogni altro tipo di sistema - presentano aspetti funzionali di trattamento dei dati unitamente ad aspetti di controllo. Tuttavia, i modelli utilizzati possono, a seconda della natura dei sistemi, porre in rilievo i due aspetti in modo differente.
Le FSM (finite state machine, macchina a stati finiti) sono un'importante notazione formale, semplice e molto conosciuta, per la descrizione di aspetti di controllo. Una FSM consiste di4:
1. un insieme finito di stati Q;
2. un insieme finito di input I;
3. una funzione di transizione 5: Q x I 8 pu essere una funzione parziale; ovvero,
pu non essere definita per alcuni valori del dominio.
Una FSM pu essere illustrata mediante un grafo i cui nodi rappresentano gli stati; un arco etichettato i porta da q j in q 2 se o solo se 8 ( q j , i ) = q 2 . La Figura 5.12 fornisce
un semplice esempio di FSM.

Pi precisamente, questa la definizione di una macchina a stati finiti deterministica.

Come suggerisce il termine stesso, le FSM sono molto adatte alla descrizione di sistemi che possono trovarsi in un insieme finito di stati e che sono in grado di spostarsi di
stato in stato in seguito a determinati eventi, modellati come valori di input. Per esempio,
una lampada pu essere spenta o accesa e pu cambiare il suo stato in seguito a un'azione
esterna, che pu consistere nella pressione di un tasto. Una seconda pressione del tasto pu
causare la transizione opposta. Questo sistema molto semplice descritto dalla FSM nella
Figura 5.13.
ESEMPIO 5 . 2

Si consideri il controllo di una parte di un impianto chimico. Per motivi di sicurezza, i livelli di temperatura e pressione devono essere monitorati costantemente, ed esistono sensori installati per segnalare il superamento di determinati livelli. Ecco una politica banale per
la gestione dell'impianto: quando viene generato un segnale da parte di uno dei sensori, il
sistema di controllo spegne l'impianto e attiva un segnale di allarme; il sistema viene reinizializzato manualmente quando il malfunzionamento stato eliminato. Tutto ci descritto nella FSM della Figura 5.14.
Questa semplice politica ovviamente inadeguata. Un modo migliore per gestire l'impianto il seguente: quando viene generato uno dei due segnali provenienti dai sensori di
controllo della temperatura e della pressione, il sistema viene portato in uno stato in cui si
tenta un'azione di recupero. Se l'azione di recupero ha successo, il sistema viene riportato
automaticamente allo stato "normale" e viene inviato un appropriato messaggio all'ambiente esterno. Altrimenti, si attiva il segnale di allarme e l'impianto viene spento. Il sistema viene spento anche nel caso in cui, trovandosi nello stato di recupero, venga generato un secondo segnale. Si assume che i due segnali non possano essere generati contemporaneamente. Questa seconda politica illustrata nella Figura 5.15.

Le FSM sono frequentemente utilizzate per specificare insiemi di sequenze accettabili di segnali di input (ad esempio, linguaggi formali). In questo caso, vengono arricchite definendo uno stato iniziale q0 e Q e un sottoinsieme F di Q, chiamato insieme degli stati finali, i
quali vengono specificati mediante l'uso di un nodo doppiamente cerchiato. L'insieme I

Allarme "pressione troppo elevata"

Allarme "temperatura troppo elevata"

Riavvia

Figura 5.14

Descrizione di un impianto chimico mediante l'uso di una FSM.

Suonale pressione

Attivit di recupero
ha successo

Attivit di recupero
fallimentare

Attivit di recupero
ha successo

Attivit di recupero
tallimentare

Segnale temperatura

Figura 5.15

Segnale temperatura

Segnale pressione

Politica riveduta per il controllo di un impianto chimico descritta mediante l'uso


di F S M .

un insieme di caratteri usati per comporre le stringhe di input. Una stringa di input accettata dalla FSM se e solo se esiste un percorso nella sua rappresentazione grafica che porti da q 0 in uno qualsiasi degli stati finali; il concatenamento delle etichette sugli archi del
percorso deve corrispondere alla stringa di input. Per esempio, la macchina nella Figura 5.16
accetta le parole b e g i n ed e n d , quella nella Figura 5.17 accetta gli identificatori validi di
un linguaggio di programmazione.
A volte, le FSM vengono arricchite mediante la possibilit di produrre segnali di output.
In questo caso, la transizione 8 diventa:
8:

q x i ->q x o,

dove 0 l'insieme finito di simboli di output. Graficamente, viene associata l'etichetta < i / o >
a un arco che porta da q j in q 2 se e solo se 5 ( q ! , i ) = < q 2 , o > .
Le FSM sono un modello semplice e ampiamente utilizzato. Le sue applicazioni variano dalla specifica di sistemi di controllo alla compilazione, dal riconoscimento di sequenze alla progettazione di protocolli e di hardware e addirittura ad applicazioni che non
riguardano il mondo informatico. La semplicit del modello, tuttavia, pu diventare una pec-

Figura 5 . 1 6

FSM che accetta le parole chiavi b e g i n ed e n d .

e
<digit>
Legenda: <letter> Abbreviazione di un insieme di frecce
etichettate rispettivamente
a,b,...,z, A , . . . , Z
<digit>

Figura 5.17

Abbreviazione di un insieme di frecce


etichettate rispettivamente 0 , 1 , . . . , 9

FSM che accetta identificatori di un linguaggio di programmazione.

ca in alcuni casi pi intricati. Discuteremo le pecche pi rilevanti dal punto di vista della
specifica di sistemi, facendo riferimento principalmente ai sistemi di controllo, uno dei pi
importanti campi applicativi delle FSM.
Innanzitutto, le FSM hanno una memoria finita, il che rende il loro potere espressivo
molto limitato. Cos, nell'Esempio 5.2, si supponga che, in risposta a una temperatura anomala, si voglia tentare un'azione raffreddante proporzionale a Dif fTemp, la differenza tra
la temperatura e un valore di riferimento. Questo tentativo non pu essere modellato da una
macchina a stati finiti in quanto gli stati di raffreddamento risultano essere infiniti (uno per
ogni valore possibile di Dif fTemp).
Anche quando l'insieme di valori possibili finito, come in molti casi di interesse pratico, la descrizione delle risposte pu diventare molto complicata. Una descrizione in linguaggio naturale risulterebbe notevolmente migliore rispetto alla FSM, che fa uso di un arco per ogni temperatura distinta in un insieme di 50 valori. Allo stesso modo, la memoria
di un computer, anche se pur sempre finita, consiste di un numero ingestibile di stati: descrivere un registro a 8 bit mediante un FSM richiederebbe 28 stati diversi!
Quando si presentano situazioni del genere, possiamo affrontarle in diversi modi.

Possiamo rinunciare alla descrizione di tutti i dettagli del sistema e accontentarci di un'approssimazione che ignori i requisiti del tipo appena discusso. Dopo tutto, la Figura 5.15
fornisce comunque informazioni utili nonostante non specifichi in maniera precisa
l'entit del tentativo di raffreddamento.

Possiamo completare il diagramma con commenti informali in linguaggio naturale.

Possiamo cambiare il modello. In realt sono stati proposti molti modelli per superare le difficolt delle FSM. Alcuni sono modifiche delle FSM originali, altri invece sono modelli completamente diversi.

Possiamo arricchire il modello aggiungendo nuove caratteristiche alla descrizione per


poter affrontare il nuovo requisito.

Cos, nel sistema dell'Esempio 5.2, possiamo affermare, formalmente o informalmente, che
la transizione dallo stato "normale" allo stato di "attivit di recupero del livello normale di
temperatura" debba essere accompagnata da un'azione descritta nel modo seguente:
C o o 1 i n g _ e f f o r t : = k"

(present_temperature-standard_value)

Inoltre, si potrebbero aggiungere predicati alle transizioni. Un predicato dovr essere o vero
o falso in modo che la transizione vada a buon fine. Per esempio, si potrebbe aggiungere alla macchina a stati finiti della Figura 5.15 una transizione dallo stato "normale" allo stato
"impianto spento" con predicato:
temp

>

verydangerousvalue

Inoltre, il seguente predicato potrebbe essere aggiunto alla transizione dallo stato "normale"
allo stato di "attivit di recupero del livello normale di temperatura":
temp

<

very_dangerous_value

In realt, se continuiamo ad arricchire il modello in questo modo, giungeremo alla definizione completa di un nuovo modello FSM-like. Ci stato fatto molte volte in passato, sia
per le FSM che per molti altri modelli, in modo da fornire nuovi linguaggi di specifica formale ad hoc''.
Le FSM hanno un altro difetto, se vogliamo, tipico della descrizione di sistemi di controllo. A questo proposito, vediamo un nuovo esempio.
ESEMPIO 5 . 3

Un produttore genera messaggi e li inserisce in un buffer con due posizioni. Un consumatore legge i messaggi e li rimuove dallo stesso buffer. Se il buffer pieno, il produttore deve
aspettare fino a quando il consumatore non libera una posizione. Ovviamente, se il buffer
vuoto, il consumatore deve aspettare fino a quando il produttore non inserisce un nuovo
messaggio. I due processi e il buffer possono essere descritti dalle FSM rappresentate nella
Figura 5.18.
Anche se pu essere utile esaminare i tre componenti separatamente, chiaro che i due
processi, insieme al buffer, rappresentano un sistema solo, sincronizzato, che deve essere descritto come tale.
Un modo naturale di affrontare il problema quello di comporre le diverse FSM per
ottenere una nuova FSM che descriva il sistema nella sua interezza. Intuitivamente, l'insieme degli stati risultante deve essere il prodotto cartesiano degli insiemi degli stati componenti; inoltre, gli archi che denotano la stessa azione devono diventare archi singoli. Applicando
questa composizione alla Figura 5.18, si ottiene il risultato mostrato nella Figura 5.19. In
questa FSM, uno stato indicato come <0, p 2 , c 2 > corrisponde allo stato in cui il buffer
vuoto, il produttore si trova nello stato p2 e il consumatore nello stato c 2 .

Vedremo pi avanti come il linguaggio Statecharts supporti la definizione di predicati e azioni associate a transizioni di una macchina a stati finiti.

202

Capitolo 5

Specifica

write

Pi )

consume

(C2 j

produce

read

(a)

(b)
write

empty ( 0 )

(C1 )

(P2J

write
2

LjL/
read

*f u l 1

read
(c)

Figura 5.18

Tre FSM separate per la descrizione di un sistema produttore-consumatore,


(a) Produttore, (b) Consumatore, (c) Buffer.

Quest'approccio, tuttavia, presenta alcuni inconvenienti. Innanzitutto, anche nel caso


piuttosto semplice che stiamo analizzando, la cardinalit dello spazio degli stati cresce drammaticamente: se componiamo n sottosistemi, ognuno contenente kj. stati, il sistema risultante avr k! k2 . . . k stati.
Questa obiezione per pu essere mitigata considerando che ci di cui abbiamo bisogno non una FSM che descriva il sistema ma di una descrizione completa del sistema. Tutte
le informazioni necessarie al sistema possono essere ricavate dai tre componenti della Figura
5.18, debitamente completati con regole precise, anche algoritmiche, per la composizione
delle FSM, senza dover produrre a tutti i costi la descrizione fornita nella Figura 5.19. Da
un certo punto di vista, stata costruita una specifica modulare del sistema, lasciando la sua
integrazione a un "compositore" di FSM.

write

Figura 5.19

write

FSM integrata di un sistema produttore-consumatore.

Nonostante ci, esiste un'obiezione pi seria all'uso di FSM per la descrizione di sistemi che consistono di diverse unit concorrenti. Analizzando la Figura 5.19 possiamo vedere come questa sia in effetti una descrizione adeguata del sistema, date alcune assunzioni
piuttosto restrittive. Fondamentalmente, il sistema descritto si trova sempre in un solo stato ed esegue esattamente un'azione sola in un qualsiasi dato momento. Concettualmente non
necessario imporre una serializzazione tra l'operato del produttore e quello del consumatore; le due azioni sono assolutamente indipendenti.
Una possibile risposta a questa obiezione che, comunque, la figura rimane una specifica adeguata del sistema concorrente in quanto l'effetto di due azioni concorrenti e compatibili (ad esempio una w r i t e e una consume) identico all'effetto di una serializzazione
delle due azioni. Questa risposta, per, vera solo in parte. Va bene se possiamo assumere
che il tempo necessario per una qualsiasi delle transizioni sia abbastanza breve per cui, ad esempio, in qualsiasi dato istante t possiamo dire che "lo stato attuale del sistema <1, P i , c 2 >".
In questo caso, possiamo definire nuove transizioni che rappresentino "l'esecuzione parallela" di transizioni elementari, in modo da rendere gli eventi veramente simultanei.
Supponiamo invece che le varie operazioni necessitino di tempi di esecuzione molto
diversi; ad esempio, che consumare sia molto pi laborioso che produrre e, a sua volta, produrre sia molto pi laborioso che leggere e scrivere nel buffer. In questo caso, potrebbe accadere che il produttore e il consumatore inizino insieme. Dopo un po' il produttore che ha
finito di produrre inizier a scrivere sul buffer. Potrebbe finire di scrivere prima che il consumatore completi la sua operazione originale e cominciare una nuova operazione di produzione. Le transizioni tra diversi stati, quindi, avvengono in maniera asincrona, ma ci non
pu essere descritto adeguatamente da una FSM come quella della Figura 5.19.

L'Esempio 5.3 dimostra come le FSM siano essenzialmente un modello sincrono. Esistono,
invece, altri modelli che sono pi adatti alla descrizione di sistemi composti da moduli concorrenti e asincroni, a maggior ragione nel caso in cui gli aspetti temporali siano importanti (ad esempio, nella definizione di vincoli temporali per il completamento di diverse transizioni). Nel prossimo paragrafo, presenteremo e valuteremo un modello operazionale esplicitamente indirizzato alla descrizione di sistemi concorrenti.
Esercizi
5.8

Usando le FSM, descrivete un sistema di illuminazione composto da una lampada e due tasti. Se la lampada spenta (o accesa), la pressione di u n o qualsiasi dei due tasti la accende (o
la spegne).

5.9

Descrivete un sistema con due lampade e un tasto. Q u a n d o le luci sono spente, la pressione
di un tasto accende la prima lampada. Una seconda pressione del tasto provoca l'accensione
della seconda lampada e lo spegnimento della prima. Un'ulteriore pressione del tasto causa
l'accensione di entrambe le lampade, mentre un'ultima pressione le spegne entrambe.

5.10

Modificate la specifica fornita mediante FSM nella Figura 5.15 in m o d o da poter gestire segnali simultanei.

5.11

Modificate la specifica fornita mediante FSM nella Figura 5.15 considerando il caso in cui la
temperatura e la pressione sono associate a due segnali diversi, il p r i m o che indica una deviazione minima dal valore accettabile e il secondo che indica una deviazione pericolosa. Nel secondo caso, il sistema deve essere spento immediatamente.

5.5.4 Le reti di Petri: specifica di sistemi asincroni


Le reti di Petri sono un formalismo per la specifica di sistemi che contengono attivit parallele o concorrenti. Sono definite come una quaterna (P, T, F, w), dove
1. P un insieme finito di posti
2. T un insieme finito di transizioni
3.

P H T 0

4. F c {p x T> u {T x P} la relazione del flusso


5. W: F >N {0} la funzione peso, che associa un valore naturale non nullo a ogni
elemento di F. Se non viene esplicitamente associato alcun peso a un elemento di flusso, assunto il valore di default "1".
Una PN (Petri Net, rete di Petri) pu essere descritta usando una rappresentazione grafica, che rende la specifica comprensibile intuitivamente. I posti sono rappresentati come
cerchi, le transizioni come sbarre e gli elementi di flusso come frecce. Quando necessario,
possibile unire un posto p e una transizione t con una freccia bidirezionale, andando
cos a sostituire le due frecce monodirezionali equivalenti, una da p in t e una da t in p.
La Figura 5.20 illustra un esempio di rete di Petri.
A una PN viene associato uno stato marcando i suoi posti. Formalmente, una marcatura una funzione M che associa ai posti un numero naturale:
M: P

-> N

Una marcatura rappresentata graficamente inserendo un numero x di gettoni in ogni posto della rete, in modo che x = M(p). La Figura 5.21 (a) mostra una marcatura della PN
illustrata nella Figura 5.20. L'evoluzione di una PN regolata nel modo seguente. Una transizione pu avere uno o pi posti di input o di output. Se una freccia porta da un posto a
una transizione, il posto viene detto uno dei posti di input della transizione; se una freccia
porta da una transizione in un posto, questo viene detto uno dei posti di output della transizione. Un posto pu essere sia di input che di output per una transizione. Una transizione

Figura 5.20

Esempio di rete di Petri.

Pi

Pi

Figura 5.21

P2

Pi

P2

Pi

P2

Evoluzione di una rete di Petri. (a) Marcatura iniziale, (b) Esecuzione di t t a partire
dalla marcatura iniziale, (c) Esecuzione di t 2 a partire dalla marcatura iniziale,
(d) Esecuzione di t t e t 2 a partire dalla marcatura iniziale.

che possiede in ogni posto di input un numero di gettoni maggiore o uguale al peso dell'elemento di flusso che li unisce si dice abilitata. Di default il peso dell'elemento di flusso
"1", quindi ogni posto di input dovr avere almeno un gettone. Una transizione senza posti di input viene ritenuta sempre attivata.
Una transizione abilitata si dice che pu scattare. Lo scatto di una transizione t rimuove
da ogni posto di input Pi un numero di gettoni pari al peso dell'elemento di flusso che unisce Pi con t e inserisce in ogni posto di output qi un numero di gettoni pari al peso dell'elemento di flusso che unisce t con qi. Nella Figura 5.21(a), sia t i che t 2 sono abilitate;
nessun'altra transizione attivata. In questo caso, la marcatura della PN pu evolvere in almeno due modi: effettuando la transizione t i o effettuando la transizione t 2 . Il modello ,
dunque, non deterministico, nel senso che, data una marcatura iniziale, sono possibili diverse evoluzioni della PN. Nel caso della Figura 5.21 (a) la scelta della transizione t i produce
la marcatura illustrata al punto (b), mentre la scelta della transizione t 2 produce la marcatura illustrata al punto (c). Si osservi che dopo la scelta della transizione t t 2 rimane abi-

litata e pertanto pu scattare. Anche t j rimane abilitata nel caso in cui t 2 scatti per prima.
Le transizioni t , e t 2 possono anche scattare in parallelo. In ogni caso, possibile raggiungere la marcatura illustrata nella Figura 5.21 (d). A questo punto sia t 3 che t , sono abilitate; qualsiasi delle due transizioni pu scattare, in maniera non deterministica. Questa volta,
per, quando scatta una delle due, l'altra si disabilita. Per esempio, se scatta t 3 , t 4 non pu
pi scattare.
Una sequenza di scatti di una PN viene indicata con una stringa di etichette di transizione < t u t 2 , . . . , t n >, secondo la quale t j la prima transizione, t 2 la transizione
che scatta dalla marcatura raggiunta grazie a t x e cos via.
Prima di addentrarci nell'analisi dei comportamenti delle PN, studieremo l'uso possibile per la descrizione di sistemi concorrenti. In una PN, una transizione rappresenta generalmente un evento o un'azione, e la sua esecuzione il fatto che l'evento capiti o che l'azione venga eseguita. Una transizione viene abilitata se sono soddisfatte le condizioni necessarie perch capiti l'evento o venga eseguita l'azione. La presenza di un gettone in un posto
denota l'esistenza di una determinata condizione o stato. Per esempio, un posto pu modellare una risorsa, mentre la presenza di uno o pi gettoni nel posto rappresenta la disponibilit di una o pi istanze di quella risorsa.
Osservando la PN illustrata nella Figura 5.21 (a) possiamo notare come sia composta
da due parti, una contenente le transizioni t t 3 , t 5 e l'altra le transizioni t 2 , t 4 e t 6 . Le
due parti possono essere considerate due attivit indipendenti che evolvono per effetto degli eventi modellati dalle transizioni. Le due attivit per condividono una risorsa comune,
modellata dal posto p 3 . Potrebbe trattarsi di due programmi che usano la stessa CPU, due
studenti che condividono uno stesso libro, etc.
Inizialmente le due attivit possono procedere in maniera indipendente e asincrona.
Infatti, t [ e t 2 sono entrambe abilitate e l'esecuzione di una delle due non impedisce all'altra di essere eseguita successivamente. In questo caso diciamo che le due transizioni sono
concorrenti. Riconducendosi a casi di questo tipo, possibile modellare la modifica indipendente di due programmi a due terminali diversi o la lettura di appunti da parte di due
studenti. Dopo che sono state eseguite entrambe le transizioni6, invece, tutte e due le attivit possono continuare la loro esecuzione in mutua esclusione. Ci viene mostrato al punto (d) della Figura 5.21. La risorsa modellata da p3 disponibile, ma solo per una delle due
attivit, la cui scelta non deterministica. In questo caso, diciamo che le due transizioni sono in conflitto.
Si supponga che la risorsa venga data all'attivit di sinistra. La rete pu procedere attraverso t 3 e t 5 , lasciando l'altra attivit temporaneamente bloccata. Lo scatto di t 5 , per,
libera nuovamente la risorsa, che diventa cos disponibile. A questo punto pu scattare t 4 .
anche possibile, per, che scatti ancora una volta t j e che la scelta tra t 3 e t , venga risolta a favore di t 3 . Questa sequenza di eventi, in pratica, pu ripetersi all'infinito.
Il modello non impone alcuna politica per risolvere conflitti di questo genere. Nella terminologia propria dei sistemi concorrenti, la politica di schedulazione precedente viene detta
non fair (non giusta). Un processo che non ha mai la possibilit di accedere a una risorsa

Si noti che non necessariamente avviene quanto descritto. Per esempio, dopo lo scatto di t, potrebbe
scattare t 3 . Ci disabiliterebbe t 2 fino allo scatto di t 5 .

Figura 5.22

Rete di Petri che pu entrare in uno stato di deadlock.

necessaria si dice soggetto a starvation (letteralmente "destinato a morire di fame"). Una sequenza di scatti contenente solo t M t 3 e t 5 porta l'attivit destra della PN a "morire di fame".
Si assuma che la marcatura iniziale della PN abbia due gettoni in p 3 invece di uno
solo. Ci significa che sono disponibili due risorse indistinguibili. Di conseguenza, t 3 e
t 4 non sono pi in conflitto ma concorrenti. Se le due attivit dovessero rappresentare
processi, e dovessero essere disponibili due CPU, i due processi potrebbero essere eseguiti in parallelo.
La Figura 5.22 una modifica della Figura 5.21 (a) che modella il caso in cui le due attivit necessitano di due copie identiche di una risorsa per proseguire. Queste copie sono
modellate come due gettoni presenti nel posto R. Dopo che un'attivit, ad esempio quella
di sinistra, comincia l'esecuzione di t pu ottenere una qualsiasi delle risorse disponibili
eseguendo 1 P u tentare successivamente di ottenere anche l'altra risorsa mediante l'esecuzione di 1 3 '. Una volta che l'attivit ha ottenuto entrambe le risorse, l'esecuzione pu procedere, liberando entrambe le risorse mediante l'esecuzione di t 5 .
Si consideri, per, la sequenza di esecuzione < t 1 ( t i , t 2 , t j > . Questa sequenza
porta a una marcatura dove non possibile alcuna ulteriore transizione. Ciascuna delle due
attivit ha ottenuto una risorsa ma necessita dell'altra per proseguire.
Questa una tipica situazione di deadlock (punto morto), situazione modellabile molto bene con le PN. Formalmente, una PN con una data marcatura detta in deadlock se e
solo se nessuna transizione risulta abilitata in quello stato. Una PN in cui una situazione di
deadlock non pu mai presentarsi data una marcatura iniziale detta live (viva).

I deadlock portano aJ "congelamento" del sistema. I progettisti cercano ovviamente di


evitare i deadlock ma individuarli spesso molto difficile. I formalismi di modellazione come le reti di Petri, per, rendono possibile l'analisi del sistema. In questo esempio, siamo
stati in grado di derivare la propriet di deadlock di un sistema manualmente, analizzando
la PN che modella il sistema.

Figura 5.24

Rete di Petri soggetta a starvation parziale.

Esercizi
5.12

D i m o s t r a t e che la modifica della P N della Figura 5.22 fornita nella Figura 5 . 2 3 live. C o m e
si p u interpretare la modifica introdotta?

5.13

Considerate la P N della Figura 5.24. La P N c h i a r a m e n t e live. Le attivit modellate dalle


transizioni t e t , , per, possono andare in starvation. Infatti, la rete p u raggiungere u n a
marcatura dalla quale le d u e transizioni n o n p o t r a n n o mai essere abilitate. C o m m e n t a t e brev e m e n t e la differenza tra questo tipo di starvation e quello illustrato p r e c e d e n t e m e n t e .

ESEMPIO 5 . 4

Torniamo al sistema produttore-consumatore modellato mediante una FSM nell'Esempio


5.3. Possiamo usare le PN della Figura 5.25 per descrivere i tre componenti separati del
sistema.
Graficamente, la composizione dei tre sottosistemi in una PN illustrata nella Figura
5.26. La figura mostra che gli svantaggi della rappresentazione mediante FSM (Figura 5.19)
possono essere ora risolti in maniera soddisfacente. Innanzitutto la complessit della figura non causata dalla moltiplicazione del numero degli stati dei componenti, ma semplicemente additiva. Infatti, nella Figura 5.19 il numero dei nodi coincide con il nume-

Consume

Write

- 0

Produce

Read

Read

Write

Figura 5.25

Tre reti di Petri separate per la descrizione di un sistema produttore-consumatore.

Figura 5.26

Rete di Petri integrata per la descrizione di un sistema produttore-consumatore.

ro di stati. Nella Figura 5.26 il numero degli stati viene fornito dal numero di possibili
marcature. Il lettore invitato a confrontare una PN che descrive due produttori e tre consumatori usando un buffer a quattro posizioni con la corrispondente rappresentazione mediante FSM.
Inoltre, nella Figura 5.26 la concorrenza di attivit indipendenti descritta in maniera corretta. Infatti se il sistema si trova nello stato <1, p j , c 2 > (ovvero, un gettone presente in ognuno di quei posti), entrambe le transizioni p r o d u c e e consume sono abilitate. Ovvero, le due transizioni sono concorrenti. Possono essere eseguite in parallelo senza
che lo scatto dell'una impedisca lo scatto dell'altra. La sequenza di scatti
produce,

write,

produce,

read,

consume,

write,

read,

consume>

mostra immediatamente quali azioni possono essere eseguite concorrentemente e quali devono essere serializzate in quanto la fine di una necessaria per l'inizio di un'altra.

Esercizi
5.14

Fornite esempi di sequenze di esecuzione per la rete della Figura 5.21(a).

5.15

Descrivete, mediante l'uso di PN, alcuni dei sistemi precedentemente presentati con
le FSM e confrontate le diverse specifiche.

5.5.4.1

Limitazioni ed estensioni delle reti di Petri

Nonostante le PN modellino piuttosto bene certi aspetti dei sistemi, il loro uso ha rivelato
alcune debolezze nell'utilizzo per la specifica di software. Innanzitutto, come le FSM, sono
un modello orientato al controllo. I gettoni rappresentano in genere il flusso di controllo
dell'esecuzione di diverse azioni. Essi, per, sono anonimi. Per esempio, la presenza di un
gettone in un posto pu indicare solo la presenza di un messaggio in un buffer, non il suo
contenuto.
Questa semplicit pu tuttavia rivelarsi utile. Spesso (ad esempio quando siamo interessati ad analizzare il flusso di messaggi all'interno di una rete) la questione importante
se un messaggio, prodotto da qualche parte, giunga o meno a destinazione. In questi casi,
il contenuto del messaggio pu essere considerato irrilevante.
Ma non sempre cos. Si supponga, ad esempio, di voler specificare un sistema in cui
un messaggio debba essere spedito lungo uno tra due differenti canali: viene scelto c h a n n e l ,
se il messaggio ben formato, mentre viene scelto c h a n n e l 2 (il canale di errore) se il messaggio scorretto. Il messaggio definito ben formato se contiene un numero pari di 1 (ovvero, se la sua parit corretta).
Nella Figura 5.27 viene illustrata una PN che specifica un sistema di questo tipo.
Questa rete, tuttavia, fa s che quando un messaggio pronto per essere spedito (ovvero, quando un gettone si trova nel posto P), la scelta tra i due canali sia non deterministica. Pertanto,
la figura non una descrizione adeguata del sistema che abbiamo descritto a parole, visto
che la scelta tra i due canali dovrebbe dipendere dal contenuto del messaggio.
Nell'esempio, lo scatto di una transizione dovrebbe dipendere dal valore del messaggio,
ma siccome il messaggio rappresentato da un gettone "anonimo", ci chiaramente impossibile. Il gettone pu infatti indicare solo la presenza di un messaggio. Dovremmo essere in grado di associare ai gettoni i valori dei messaggi, e dovremmo essere in grado di calcolare il valore di ciascun gettone. La stazione ricevente dovrebbe poi modificare il contenuto del messaggio prima di spedirlo (ad esempio, aggiungendo un nuovo indirizzo).
Un'altra debolezza delle PN dovuta al fatto che, in generale, non possibile specificare
una politica di scelta tra diverse transizioni abilitate. Per esempio, tornando alla Figura 5.21 (a),

channel 1

Figura 5.27

channel 2

Porzione di una rete di Petri che descrive la spedizione di messaggi su diversi canali.

le d u e s e q u e n z e di e s e c u z i o n e .

abbiamo visto come sia possibile che la sequenza di esecuzione < t 1 ( 1 3 , 1 5 > venga attivata continuamente, mandando in starvation la sequenza < t 2 , t 4 , t 6 > . Per evitare la starvation potremmo imporre una politica per alternare le due sequenze, modificando leggermente la rete secondo quanto mostrato nella Figura 5.28. Questa rete impedisce l'esecuzione ripetuta di t 3 , senza che venga eseguita prima t.
In generale si pu dimostrare matematicamente che le PN non possiedono l'abilit di
descrivere una politica del tipo seguente:
if

transizione
esegui

t abilitata

then

t;

else

end

e s e g u i la p r i m a
una d e t e r m i n a t a
i f;

transizione
p o l i t i c a di

abilitata secondo
ordinamento

La temporizzazione un altro aspetto critico di alcuni sistemi. Come abbiamo visto nei
Capitoli 2 e 4, per alcuni sistemi real-time, non riuscire a calcolare una risposta entro un
dato periodo temporale pu avere gli stessi effetti negativi di non calcolarla affatto o calcolarla scorrettamente. Inoltre, il risultato di un calcolo pu dipendere anche dalla velocit di
esecuzione di determinate azioni.

Per esempio, si supponga che una linea esterna invii messaggi a un computer a una determinata velocit. Ogni messaggio ricevuto viene inserito in un buffer e poi elaborato. Un
messaggio, se non viene tolto dal buffer prima che arrivi il messaggio successivo, viene perso. I risultati dell'esecuzione possono quindi dipendere dalla velocit di arrivo dei messaggi.
Sfortunatamente, la maggior parte dei modelli di sistemi, incluse le PN, non prende
esplicitamente in considerazione il tempo. Per esempio, si consideri nuovamente la Figura
5.21 (a). Si assuma che le azioni siano eseguite non appena diventano attive. Si assuma anche che lo scatto di una transizione accada quando terminano le azioni corrispondenti, modellate dalle transizioni. Se le azioni modellate da t i , t3 e t5 necessitano di 1 secondo per
essere completate e l'azione modellata da t2 necessita di 5 secondi, la sequenza di transizioni <tlf t2, t3, t5, t4> non ovviamente possibile, contrariamente a quanto suggerito dalla rete. Infatti, si supponga che al momento 0 sia ti che t2 vengano eseguite. Al
momento 1 pu essere eseguita t3, ma t2 non ancora finita. Quindi, l'esecuzione di t2
non pu avvenire prima dell'esecuzione di t3.
Fortunatamente, la flessibilit del modello delle reti di Petri ha consentito la sua estensione in diverse direzioni, mantenendone le caratteristiche originali. Affronteremo qui alcune modifiche piuttosto comuni delle PN, che si sono dimostrate utili in diverse circostanze.
Per semplicit, assumeremo il caso di default, in cui il peso di tutti gli elementi della relazione di flusso sia 1.
Assegnare valori ai gettoni. I gettoni possono essere modificati per avere un valore di un
tipo appropriato: un intero, un array di byte o addirittura un intero ambiente, costituito da
diverse variabili con i relativi valori. Anche le transizioni possono essere modificate in modo da essere associate a predicati e funzioni. La regola di scatto di una transizione sar, di
conseguenza, basata sui valori delle variabili, oltre che sulla presenza o meno di gettoni. Una
transizione con k posti di input e h posti di output verr abilitata se sono presenti k gettoni, uno per ogni posto di ingresso, tali per cui il predicato associato nella transizione sia soddisfatto dai valori dei gettoni. L'insieme di questi gettoni viene detto tupla pronta.
Si osservi che il predicato valutato su un gettone per ciascun posto di input. Di conseguenza, potrebbe esistere pi di una tupla pronta per una transizione; ovvero, lo stesso gettone potrebbe appartenere a diverse tuple pronte. Una transizione abilitata che scatta comporta alcune conseguenze:

la cancellazione di tutti i gettoni che appartengono a una tupla pronta dai posti di ingresso (se esiste pi di una tupla pronta la scelta non deterministica);

la valutazione di h nuovi gettoni sulla base dei valori della tupla pronta applicando la
funzione associata alla transizione (la funzione avr un dominio di tuple di dimensione k e un codominio di tuple di dimensione h);

la produzione di un gettone per ciascun posto di output, il cui valore calcolato dalla funzione associata alla transizione in questione.

Per esempio, si consideri la PN nella Figura 5.29, in cui si assume che i gettoni abbiano valori interi. La notazione autoesplicativa: il nome di un posto in un predicato o in una funzione rappresenta un gettone in quel posto. Le transizioni t , e t ; sono abilitate. La transizione ti ha due tuple pronte, <3,7> e < 3 , 4 > , visto che entrambe le tuple soddisfano il

Figura 5.29

Rete di Petri i cui gettoni portano valori. Il predicato p 2 > P ! e la funzione


p , : = p 2 + P i sono associati alla transizione t , ; il predicato p 3 = p 2
e le funzioni p 4 : =p 3 p 2 e p 5 : = p 2 + p 3 sono associati alla transizione t 2

predicato P2>Pi- La transizione t2 ha una tupla pronta, <4 , 4>, la quale soddisfa il predicato P 3 =P 4 . Il gettone con valore 1 in p2 non appartiene ad alcuna tupla pronta. Lo scatto
di t! mediante l'uso della tupla <3, 4> produrrebbe un gettone con valore 7 in p, e disabiliterebbe t2, visto che i gettoni 3 e 4 scomparirebbero da Pj e P2. Invece, lo scatto mediante l'uso di <3, 7> produrrebbe un valore 10 in P4, lasciando t2 ancora abilitate. Nel
caso scattasse successivamente t2, verrebbe prodotto il valore 0 in P4 e 8 in p5.
Questo primo arricchimento del modello PN fornisce una soluzione semplice al problema illustrato nella Figura 5.27. Infatti, sufficiente considerare i gettoni come portatori
di un valore del tipo "messaggio", ovvero una sequenza di bit. Il predicato "p ha un numero pari (risp. dispari) di l " pu essere associato alla transizione c h a n n e l t (risp. c h a n n e l 2 ) .
Inoltre, il fatto che un messaggio debba essere modificato prima di essere spedito su uno dei
canali pu essere indicato mediante l'aggiunta alle transizioni di apposite funzioni.
Esercizio
5.16

Usando l'estensione alle P N illustrata in questo paragrafo, descrivete un m o d u l o addetto alla


spedizione di messaggi. Il m o d u l o riceve messaggi su due canali diversi e controlla la parit di
ciascun messaggio. Se la parit scorretta, spedisce un "nack" (negative acknowledgment, riconoscimento errato) su un canale di risposta (esiste u n canale di risposta per ogni canale in ingresso); se la parit corretta sposta il messaggio ricevuto in un buffer. Il buffer pu contenere fino a 10 messaggi. Q u a n d o il buffer pieno, il m o d u l o spedisce tutti i contenuti del buffer
a un'unit di calcolo su un altro canale. N o n si possono inserire messaggi in un buffer pieno.

Specifica di politiche di schedulazione. Quando il non determinismo delle PN non adeguato, necessario affrontare il problema della specifica di una politica per la selezione di una
transizione da eseguire tra tutte quelle abilitate in quell'istante. Un modo semplice per raggiungere questo scopo quello di associare priorit alle transizioni. Formalmente, ci pu essere specificato mediante una funzione p r i che associa un numero naturale a ogni transizione:
pri : T

> N

poi possibile modificare la regola di scatto di una transizione nel seguente modo: se diverse transizioni sono abilitate, possono scattare solo quelle con priorit massima.
Secondo questa definizione, le priorit sono statiche. Se per i gettoni hanno un valore, potremmo pensare di definire priorit dinamiche, i cui valori dipendano dai valori dei
gettoni dei posti di input delle transizioni.
Esercizio
5.17

Aggiungete priorit alla P N costruita per la risoluzione dell'Esercizio 5.16. Se il m o d u l o per


la spedizione dei messaggi in una condizione per cui p u (a) ricevere un messaggio da un
canale di input, (b) spedire una risposta "nack" o p p u r e (c) spedire i contenuti del messaggio
al buffer, allora deve ordinare le priorit nel seguente m o d o : prima ricevere il messaggio, poi
spedire il "nack" e infine spedire i contenuti del messaggio al buffer.

Reti di Petri temporizzate. Quando si tenta di introdurre il tempo nei modelli computazionali formali, sorgono problemi teorici delicati, che vanno al di l degli scopi di questo libro. Qui ci limiteremo a illustrare il modo pi semplice e naturale di introdurre il tempo
nelle PN.
Le PN temporizzate sono PN in cui due costanti < t m i n , t m a x > vengono associate a ogni
transizione. In modelli pi sofisticati possibile definire i limiti temporali in modo che siano calcolati come funzioni dei valori dei gettoni nei posti di input. Una marcatura iniziale
viene fornita all'istante t = 0. Una volta che una transizione abilitata, deve aspettare almeno t m i n prima di poter scattare. Inoltre deve scattare entro t, ax , a meno che non venga
disabilitata dall'esecuzione di un'altra transizione. Una PN normale pu essere rappresentata da una PN temporizzata dove, per ogni transizione, t m i n = 0 e t m a x =
Le estensioni di natura temporale possono essere applicate in combinazione anche con
altre estensioni. Se applichiamo sia le estensioni temporali che l'aggiunta di priorit alle
transizioni, occorre porre molta attenzione nella determinazione di quale transizione possa
o debba scattare in un dato momento. Una regola naturale che se possono scattare diverse transizioni, data la disposizione di gettoni e considerato l'intervallo [ t m i n , t m a x ] , solo
quelle con priorit massima possono effettivamente scattare, entro un periodo di tempo minore o uguale al proprio t m a x .
Per esempio, si consideri la rete nella Figura 5.30, con la sua marcatura iniziale al tempo zero. Potrebbe accadere che t [ scatti entro 2 unit di tempo. Se non dovesse scattare in
quell'intervallo, non potr pi scattare, visto che a t = 2, anche t 2 pu scattare e ha una
priorit pi alta di t ^ Se al momento t = 1 viene prodotto un gettone in p, allora nell'intervallo l < t < 2, sia t 3 che t j potranno scattare, ma t^ non potr scattare prima
di t 3 visto che ha una priorit pi bassa.
Torniamo al problema di attribuire un preciso significato alla specifica informale:
Il messaggio deve essere triplicato. Le tre copie devono essere spedite su tre canali diversi. Il ricevitore accetta il messaggio in base a una politica di tipo due su tre

che abbiamo trattato nel Paragrafo 5.2. Le PN temporizzate, con gettoni portatori di valori riguardanti i messaggi, possono fornire in maniera facile una descrizione precisa delle possibili interpretazioni di questa specifica informale.

priority = 1
Figura 5.30

priority = 3

priority = 2

Rete di Petri temporizzata.

Original message

Message

triplication

Message copies

Message copies transmission

vot ing3

for all three transitions

Forwarded

Figura 5.31

Message

Possibile formalizzazione per la replicazione e selezione di messaggi mediante


l'uso di reti di Petri modificate. Il predicato PC, = PC 2 associato a t v o t i n 9 l .
Il predicato PC, = PC 3 associato a t v o t i n g 2 . Il predicato PC 2 = PC 3 associato
a tvotinga- H predicato true associato a tutte le altre transizioni. La funzione
d'identit associata a tutte le transizioni, c ^ k j c ^ k ; ) sono i limiti inferiori
(superiori) della durata dell'operazione Message triplication (Message
copies

t r a n s m i s s ion).

Figura 5.32

Formalizzazione alternativa della replicazione e selezione di messaggi.


Il predicato PC 3 = PC 2 or PC 2 = PC 3 or PC, = PC 3 associato
con t v o t i n g l . La funzione " i f PC, = PC 2 theo PC, elsif PC 2 = PC 3
then PC 2 elsif PC! = PC 3 then PC ; else " E R R 0 R " endif"
associata a t v o t i n g . Il predicato true associato a tutte le altre transizioni
cos come la funzione d'identit.

La prima interpretazione suggerita nel Paragrafo 5.2 quella secondo la quale il messaggio debba essere considerato ricevuto non appena sono state ricevute due copie identiche. Questa interpretazione pu essere formalizzata dalla PN illustrata nella Figura 5.31. Con
questa formulazione, non appena due gettoni dai valori identici sono presenti in Pj, P2 o
P3, scatta la transizione corrispondente. Un'interpretazione diversa dei requisiti informali,
basata sulla decisione di aspettare fino a quando tutte e tre le copie sono state ricevute prima di effettuare il confronto, formalizzata dalla rete della Figura 5.32.
Questo esempio dimostra che l'uso di un modello formale ci permette di fornire un
significato preciso alla specifica del sistema. Inoltre, il modello formale pu rappresentare la
base per un'analisi rigorosa. Per esempio, se siamo interessati a determinare il tempo massi-

mo necessario a rispedire un messaggio ricevuto, possiamo notare che risulta le, + k 2 in


entrambi i casi. Se assumiamo, per, che la trasmissione delle copie attraverso i tre canali
necessiti di un tempo compreso tra c 2 e k 2 , possiamo notare che la probabilit di ricevere
un messaggio nel posto F o r w a r d e d M e s s a g e entro l'istante t (con
+ c 2 t s ki
+ k 2 ) pi alta nel caso illustrato nella Figura 5.31 rispetto al caso della Figura 5.32.
Si assuma ora che ogni canale di transizione possa avere un malfunzionamento. Questo
caso pu essere modellato aggiungendo altre tre trasmissione, ognuna connessa ai posti in
input "message copies", il cui scatto distruggerebbe i gettoni che rappresentano i messaggi.
Il modello della Figura 5.31 avrebbe una probabilit inferiore di malfunzionamento globale (il mancato inoltro del messaggio al posto F o r w a r d e d M e s s a g e ) rispetto al modello
della Figura 5.32. Siccome la triplicazione di un messaggio viene fatta solo per rendere la
PN pi tollerante ai guasti, chiaro che un'analisi della differenza tra le due interpretazioni della specifica informale utile.
L'analisi precedente poteva essere resa pi precisa arricchendo ulteriormente il modello
PN con caratteristiche stocastiche, come la distribuzione probabilistica di tempi di esecuzione o la distribuzione probabilistica dell'esecuzione di transizioni attive. Il lettore interessato
potr trovare modelli di questo genere nella letteratura suggerita nelle note bibliografiche.
Esercizi
5.18 Fornite una formalizzazione in termini di PN estese della terza interpretazione vista
per il Paragrafo 5.2, in cui il ricevitore controlla periodicamente i tre canali. Se vengono ricevute tre copie in un dato arco di tempo, vengono confrontate tutte e tre. Se
ne vengono ricevute solo due, e sono identiche, il messaggio accettato.
5.19 La formalizzazione della Figura 5.31 ha un piccolo difetto che pu diventare rilevante
se la PN diventa parte di un sistema ciclico. Si individui e si elimini l'errore (ad esempio, modificando la PN in modo tale che si comporti correttamente anche se ripetuta ciclicamente).
5.5.4.2

Un esempio di uso delle reti di Petri

Applicheremo ora il modello delle reti di Petri, e alcune delle sue variazioni, al caso della descrizione di un sistema realistico e complesso, come quello di un ascensore. Si consideri la
seguente specifica informale, proposta in letteratura come banco di prova dell'applicabilit
di tecniche di specifica.
U n sistema composto da n ascensori deve essere installato in un palazzo a m piani. I costruttori forniscono gli ascensori e i meccanismi di controllo. Il problema concerne la logica necessaria per spostare gli ascensori tra i piani secondo le seguenti regole:
1. O g n i ascensore possiede un insieme di pulsanti, uno per piano. Q u a n d o premuti, i pulsanti si illuminano e causano lo spostamento dell'ascensore al corrispondente piano. Le luci si
spengono q u a n d o il piano desiderato raggiunto.
2. Ciascun piano, a parte il p r i m o e l'ultimo, possiede due tasti, u n o per richiedere un ascensore che sale e u n altro per richiedere u n ascensore che scende. Anche questi tasti si illuminano alla pressione. Le luci si spengono q u a n d o arriva un ascensore che sta viaggiando nella direzione desiderata o p p u r e che libero. Nel secondo caso, se entrambi i tasti di richiesta

sono stati premuti viene annullata una sola delle richieste. L'algoritmo per decidere quale delle richieste soddisfare per prima mira a minimizzare i tempi di attesa di entrambe le richieste.
3. Q u a n d o u n ascensore n o n richiesto da nessuno rimane fermo al piano che rappresenta l'ultima destinazione richiesta, con le porte chiuse e in attesa di ulteriori richieste.
4. Tutte le richieste di ascensori provenienti dai piani devono essere soddisfatte, prima o poi, e
tutti i piani h a n n o la stessa priorit.
5. Tutte le richieste interne agli ascensori diretti ai piani devono essere soddisfatte, prima o poi,
servendo i piani sequenzialmente nell'ordine di percorrenza dell'ascensore.
6. O g n i ascensore possiede un tasto di emergenza che, alla pressione, causa l'invio al supervisore di un segnale di allarme. L'ascensore viene successivamente segnalato come "fuori servizio". O g n i ascensore possiede u n meccanismo per cancellare lo stato di "fuori servizio".

Prima di tradurre queste specifiche in un modello formale, esaminiamole attentamente.


Nonostante questo sistema sia di uso comune, vale la pena di ponderare le specifiche con
una certa attenzione. Potrebbe anche essere interessante per il lettore posporre la lettura dei
commenti che seguono a un'analisi e a un eventuale formalizzazione personale delle specifiche medesime.
Focalizziamo la nostra attenzione sul punto 2. La descrizione informale prescrive che
ciascun piano, eccetto il primo e l'ultimo, possegga due pulsanti. Di per s non esiste alcuna implicazione secondo la quale il primo piano non possa possedere nove pulsanti e l'ultimo quattro. Questa osservazione pu per essere vista come eccessivamente critica: esiste
un'interpretazione "ovviamente corretta", ovvero che il primo piano possegga un solo pulsante per richiedere un ascensore per salire e che l'ultimo ne abbia uno solo per richiedere
un ascensore in discesa. Possiamo scegliere questa interpretazione in quanto fa parte della
nostra conoscenza degli ascensori e possiamo integrare questa conoscenza nel nostro progetto del sistema, specificando questi requisiti in maniera esplicita. Infatti, possiamo anche
usare questo caso come esempio a favore dell'informalit delle specifiche: una descrizione
formale richiederebbe una specifica pienamente dettagliata anche nell'eventualit di aspetti
perfettamente owii, causando cos uno spreco di energie.
In generale, il formalismo uno strumento per essere precisi quando serve. La precisione assoluta pu essere inutile e addirittura noiosa se il lettore finale una persona e non
una macchina. responsabilit di chi scrive le specifiche scegliere un livello di formalit adeguato. A volte, la semplicit, l'immediatezza e la generalit del linguaggio naturale possono
essere preferibili al rigore semantico di un formalismo matematico. In altri casi, invece, una
notazione semiformale, magari grafica, pu dare un'idea veloce e sufficientemente chiara del
sistema desiderato. In altri ancora, soprattutto se il sistema complesso o critico, pu valer
la pena compiere lo sforzo di una formalizzazione completa. In generale, la formalit richiesta quando non possiamo permetterci di essere fraintesi.
Affronteremo ora un'analisi pi approfondita del punto 2. La regola afferma che:
le luci si spengono q u a n d o arriva u n ascensore che sta viaggiando nella direzione desiderata oppure che libero.

Questa frase pu essere interpretata in almeno due modi. Si consideri un ascensore che sta
salendo, il caso di un ascensore che scende simmetrico. La regola potrebbe essere interpretata in qualsiasi delle seguenti due maniere:

La luce viene spenta non appena un ascensore che arriva dal basso giunge al piano dove
stata effettuata la richiesta (questa interpretazione ammette un'eccezione per il primo
piano).

La luce viene spenta dopo che l'ascensore giunge al piano e comincia la sua salita
verso i piani superiori (questa interpretazione ammette un'eccezione per l'ultimo
piano).

Esaminando diversi ascensori reali possiamo vedere come entrambe le interpretazioni siano
state implementate in passato. Come abbiamo visto prima, si potrebbe discutere che l'ambiguit sia stata lasciata appositamente in modo da consentire all'implementatore la possibilit di scegliere la soluzione migliore senza imporre ulteriori vincoli. Questa affermazione
pu essere accettabile nel caso in cui una soluzione o l'altra non faccia differenza. In generale, comunque, l'ambiguit non viene colta fino a quando non viene costruita una versione formale della specifica.
Si noti, infine, l'imprecisione presente nel requisito:
l'algoritmo per decidere quale delle richieste soddisfare per prima mira a minimizzare i tempi
di attesa di entrambe le richieste.

Cosa significa "minimizzare i tempi di attesa di entrambe le richieste"? Queste sono due interpretazioni possibili:

Non deve essere possibile servire alcuna delle due richieste in un tempo minore. Questa
interpretazione potrebbe non essere praticabile: minimizzare il tempo di attesa per una
richiesta potrebbe richiedere un tempo di attesa pi lungo per l'altra;

la somma dei due tempi di attesa dovrebbe essere minimizzata. Ma perch la somma?

Ancora peggio, il tempo di attesa previsto al momento di effettuare la richiesta potrebbe


essere alterato da una richiesta fatta durante il servizio. Per esempio, si immagini che l'ascensore viaggi dal secondo piano per rispondere a una richiesta fatta al sessantesimo piano. Mentre sale, l'ascensore si ferma al quarantesimo piano per rispondere a una richiesta. Questa chiamata potrebbe non essere stata presa in considerazione per la "minimizzazione" del tempo di attesa al momento della scelta dell'ascensore per rispondere alla richiesta originale.
Esercizio
5.20

Proseguite l'analisi delle specifiche precedenti, cercando di scoprirne i punti ambigui.

Vediamo ora di fornire una specifica del sistema di ascensori mediante l'uso di reti di Petri.
La Figura 5.33 fornisce una bozza iniziale del sistema. Questa descrizione intuitiva utile in quanto fornisce un'illustrazione grafica della posizione dell'ascensore e degli eventi che determinano il movimento degli ascensori. Sottolinea il fatto che, perch l'ascensore possa spostarsi da un piano a quello adiacente, un pulsante debba essere illuminato e
che lo spostamento, a sua volta, risulta essere la conseguenza della pressione del tasto in
questione.

Figura 5.33

Pressione
del pulsante
interno
corrispondenic
il piano j + 1

Prima bozza della descrizione mediante reti di Petri dell'alternarsi dello stato
di un pulsante dell'ascensore.

La descrizione della Figura 5.33, tuttavia, ben lontana dall'essere soddisfacente. Ecco
alcuni dei suoi difetti:
1. La descrizione terribilmente incompleta: molte altre questioni devono essere prese in
considerazione. Ci sono tasti interni, ma anche esterni. Lo spostamento dell'ascensore pu essere causato dalla pressione di uno qualsiasi dei pulsanti: anche un ascensore
fermo al primo piano pu spostarsi quando, al quarantesimo piano, viene premuto il
tasto per richiedere un ascensore per scendere. La rete inoltre non spiega come vengono spente le luci che illuminano i pulsanti. Inoltre, cosa succede se viene premuto il
tasto esterno di richiesta di salita al piano 20 nel momento preciso in cui un ascensore sta passando a quel piano sul suo tragitto dal piano 4 al piano 27? Qual l'ultimo
istante accettabile di chiamata in questi casi?
2. La descrizione mostra immediatamente che la formalizzazione completa del sistema sar
enorme e difficilmente gestibile. Si pensi ad un sistema composto da 100 piani e da
sette ascensori!
3. La descrizione ovviamente errata in molti dei suoi dettagli. Per esempio, la figura suggerisce che un pulsante si illumina quando viene premuto; ci viene modellato dalla
presenza di un gettone nel posto "illuminazione del pulsante". Se il pulsante viene premuto due volte avremo due gettoni in quel posto. Quando la richiesta sar soddisfatta, verr consumato solo uno dei gettoni; l'altro gettone rimarr, indicando erroneamente che il pulsante ancora illuminato.
Esercizio
5.21

Individuate ulteriori inadeguatezze e punti problematici della formalizzazione iniziale.

Nonostante le sue manchevolezze, la Figura 5.33 pu essere assunta come punto di partenza per ottenere una specifica completa e corretta del sistema mediante l'uso di reti di
Petri.
Innanzitutto, affrontiamo il problema della gestione della complessit del sistema.
Ricordiamo che la specifica un'attivit di progettazione e che un documento di specifica
completa , in generale, il risultato di molti tentativi e correzioni. Non un documento da
elaborare da zero senza mai modificarlo. Dobbiamo, quindi, applicare anche alla specifica
tutti i principi di progettazione visti nei Capitoli 3 e 4.
In particolare, molto utile in questo caso definire moduli di specifica, intesi come componenti della PN finale.
Ciascun modulo descrive un componente del sistema e la descrizione completa il risultato dell'integrazione di tutti i moduli. In questo caso, risulta naturale usare diversi moduli per rappresentare, da una parte, le posizioni degli ascensori e, dall'altra, gli stati dei pulsanti interni ed esterni agli ascensori.
Inoltre possiamo osservare che, con l'eccezione del primo e dell'ultimo piano, la
descrizione di cosa succede al piano j identica alla descrizione di cosa succede al piano
k. Visto che lo stesso vale sia per gli ascensori che per i pulsanti, possiamo pensare di utilizzare una specifica parametrizzata che faccia riferimento al piano generico j , all'ascensore generico m, al pulsante generico h, etc. Otterremo cos la seguente struttura della
specifica.
Descrizione del sistema. La specifica completa scomposta in moduli. Ci sono n moduli di specifica del tipo E L E V A T O R (ascensore) e M moduli di specifica del tipo F L O O R
(piano). Ogni modulo descritto da una PN estesa, con interconnessioni appropriate.
Ogni modulo di tipo E L E V A T O R scomposto in due sottomoduli, uno di tipo E L E VATOR_POSITION, che rappresenta la posizione di un ascensore, e l'altro di tipo ELEVATOR_BUTTONS, che rappresenta lo stato dei pulsanti interni all'ascensore. Pi precisamente, il secondo pu essere scomposto in m moduli di tipo B U T T O N , ciascuno dei quali
rappresenta uno degli m pulsanti interni a ogni ascensore.
Ciascun modulo del tipo FLOOR, a sua volta, scomposto in due moduli di tipo B U T TON, i quali rappresentano i pulsanti per richiedere un ascensore per salire o per scendere. I moduli che rappresentano il primo e l'ultimo piano sono un'eccezione, visto che sono composti da un solo modulo di tipo B U T T O N , il quale permette l'invio di una richiesta per salire o scendere, rispettivamente.
Affronteremo ora le problematiche di descrizione delle diverse parti, una alla volta.
Cominceremo con le regole per l'illuminazione dei pulsanti, facendo uso di PN temporizzate con priorit.
Descrizione dei pulsanti. I moduli di tipo B U T T O N possono essere descritti come nella
Figura 5.34. La pressione di un pulsante viene rappresentata dall'esecuzione della transizione Push, sempre attiva per rappresentare che un pulsante pu essere premuto in qualsiasi momento. Se il pulsante spento (ovvero, un gettone presente in Off ) e viene eseguita Push, allora viene immediatamente eseguita anche Set (ovvero, t, in (Set) =
t n a x ( Set ) = 0) e il pulsante viene acceso (ovvero, un gettone viene inserito nel posto
On). Per impedire l'inutile accumularsi di gettoni in P si pu impostare t m i n ( P u s h ) =

Figura 5.34

Accensione dei pulsanti.

0,1 e t m i n ( c ) = t m a x ( c ) = 0 , 0005 (la transizione c funge da consumatore di gettoni) 7 . In questo modo, un pulsante acceso pu essere premuto molte volte (con un tempo minimo di ritardo pari a 0 , 1 ) senza provocare conseguenze rilevanti.
L'esecuzione della transizione Reset rappresenta la reinizializzazione del pulsante. Pi
avanti verr illustrato come altri moduli reinizializzano i pulsanti. Ci significa che altre frecce, qui non indicate, verranno connesse a Reset per altri moduli di specifica.
Esercizio
5.22

Fornite una specifica alternativa per l'illuminazione di un pulsante usando le P N estese con
il concetto di priorit, invece che con il concetto di tempo. Discutete le differenze tra le due
rappresentazioni.

Descrizione della posizione di un ascensore e del suo spostamento. In prima approssimazione, ogni modulo del tipo E L E V A T O R _ P O S I T I O N pu essere rappresentato come nella Figura 5.35. Intuitivamente, la figura descrive come un ascensore pu spostarsi da un piano all'altro in entrambe le direzioni. Un gettone nel posto F i , 1 < i < m, rappresenta
un ascensore fermo al piano i. Un gettone nel posto DF ^ ( UFA ) , 2 < i < m-1, rappresenta un ascensore che sta passando per il piano i , lungo il suo tragitto di discesa (salita). Si dovrebbero associare tempi adeguati alle transizioni in modo da prendere in considerazione le velocit degli ascensori.
Insistiamo sull'importanza di descrivere questioni complesse in maniera incrementale.
La Figura 5.35 fornisce un'idea di spostamento degli ascensori, distinguendo tra ascensori

II tempo viene indicato di default in secondi. Si noti che un qualsiasi valore arbitrario inferiore a 0,1
pu sostituire 0,005.

Figura 5.35

Prima descrizione dello spostamento dell'ascensore.

fermi e ascensori in movimento. In una descrizione pi dettagliata, le condizioni che spingono un ascensore a spostarsi possono essere espresse da un frammento di rete come quello
illustrato nella Figura 5.36, il quale fa riferimento a un ascensore fermo al piano Fi. (Le condizioni che determinano il movimento dell'ascensore verso il basso possono essere modellate alla stessa maniera).
Sia h un numero intero maggiore di j + 1 e minore o uguale a m. Un ascensore fermo
al piano j pu spostarsi verso l'alto se viene fatta una richiesta interna per andare al piano
j + 1 o un qualsiasi piano h o se viene fatta una richiesta esterna di servizio al piano j +1 o
a un qualsiasi piano h. Tali richieste sono modellate dalla presenza di un gettone nel posto
On nelle reti di tipo BUTTON che rappresentano pulsanti interni ed esterni. Nella figura, I L B j + 1
e l L B h sono reti di tipo BUTTON che rappresentano i pulsanti interni per le fermate ai piani j + 1 e h, rispettivamente. UP j + 1 , DOWNJ + 1 , UP h e DOWNh rappresentano pulsanti per chiamate esterne dai piani j + 1 e h , per salire o scendere, come indicato dal loro nome8.

Il primo e l'ultimo piano sono un'eccezione, visto che hanno rispettivamente solo i pulsanti UP, e DOWN.

Figura 5.36

Descrizione pi precisa dello spostamento dell'ascensore.

La specifica della Figura 5.36 usa due posti intermedi, F e F tra F.j e la coppia Fj+1
L'esecuzione delle transizioni comprese tra t x e 1 6 rappresenta la risposta dell'ascensore
a una richiesta di salita; di conseguenza, un gettone viene collocato nel posto F . L'esecuzione
della transizione t tra F J e F " rappresenta il tempo necessario per spostarsi dal piano j al
piano j + l ; infatti, definiamo t m i n ( t ) =t m a x ( t ) =AT il tempo necessario per spostarsi da un
piano a quello adiacente. Per semplificare le cose ignoriamo il fatto che tale tempo non sia
costante a causa dell'accelerazione. Per ogni altra transizione presente nella Figura 5.36 impostiamo t m l n =t m a i I =0. Assumiamo anche che il tempo necessario per prendere la decisione se fermarsi o meno a un determinato piano possa essere ignorato. L'assunzione un'astrazione della situazione reale, in cui il meccanismo che governa il sistema (ad esempio, un
microprocessore) possiede tempi di reazione trascurabili rispetto ai tempi richiesti dal sistema meccanico.
Le transizioni comprese tra t 7 e t 1 2 rappresentano la scelta (non deterministica) tra le
richieste di servizio. In questo modo, modelliamo il fatto che l'ascensore possa servire richieste
in arrivo dal piano j +1, a patto che accadano durante il tempo previsto per salire dal piano j al piano j + l .
La descrizione di un ascensore in transito dal piano j + l (rappresentato da un gettone presente nel posto UFj + 1) pu essere simile (ma non identica) a quella appena fornita. Si osservi che un gettone appare nel posto UFj + 1 solo se ci sono richieste di servizio
eUF j t l .

non soddisfatte (interne o esterne) provenienti da un piano j + 1 che si trova pi in alto del piano.
Nella nostra descrizione, fino ad ora, non abbiamo fatto assunzioni riguardo alla politica decisionale per la scelta di quale richiesta servire, nel caso in cui ve ne siano pi di una
in attesa. Il modello pertanto non deterministico. Per esempio, il modello non richiede
che l'ascensore si fermi al piano j + 1 se una richiesta interna viene fatta prima di passare da
quel piano. La nostra scelta quella di concentrare tutte le politiche decisionali in un modulo S C H E D U L E R che discuteremo tra breve.
Prima di procedere in questo senso finiremo di inquadrare gli aspetti pi salienti del
sistema di ascensori.
Spegnimento dei pulsanti. La Figura 5.37 modella lo spegnimento di un pulsante interno
iLBj nel momento in cui l'ascensore giunge al piano j . Abbiamo disegnato una scatola tratteggiata intorno ai componenti di I L B j in modo da evidenziarne i confini. La transizione
R e s e t ha t m i =t m a x =0 e la priorit pi alta; abbiamo, dunque, la certezza che la luce venga spenta non appena l'ascensore giunge al piano corrispondente. Ancora una volta, si noti
come la specifica sia fatta di parti che si aggregano (incrementalmente o in maniera modulare). Nella figura, infatti, facciamo riferimento al posto Fj senza ripetere tutte le connessioni che vi giungono (e che abbiamo gi visto).
La Figura 5.38 descrive come i pulsanti UPj (1 < j < m-1) vengono spenti. La
transizione 11 la duplicazione di una qualsiasi transizione t i (1 < i < 6) della Figura
5.36 (sono trattate tutte allo stesso modo). La transizioni t 1 hanno anch'esse t,in=tmax=0,
ma hanno priorit pi alta rispetto a ti; in questo modo viene scelto lo scatto di t [ rispetto
a ti; se il pulsante deve essere spento. In altre parole entrambe le transizioni tA e t [ modellano la "decisione" dell'ascensore di salire. In pi, per, la seconda transizione modella
lo spegnimento del pulsante. La transizione Reset invece viene eseguita per spegnere un
tasto quando non ci sono richieste pendenti. Definiamo t m i n ( R e s e t ) =tmaiI ( R e s e t ) =dp,
dove dp il tempo necessario per modellare una persona che entra in un ascensore e preme un pulsante. I tasti DOWNi (2 < i < m) sono modellati allo stesso modo. Di con-

Figura 5.37

Spegnimento dei pulsanti interni. Il riquadro tratteggiato indica i confini


del m o d u l o I L B J .

Figura 5.38

Spegnimento dei pulsanti esterni del piano.

seguenza, entrambi i tasti esterni vengono spenti se nessuna richiesta interna viene fatta nel
tempo previsto. Si osservi che in questo caso stiamo alterando leggermente le specifiche informali. Si osservi, per, che la Figura 5.38 toglie ogni ambiguit dall'affermazione informale riguardante lo spegnimento delle luci dei pulsanti, scegliendo la seconda delle interpretazioni ipotizzate quando abbiamo analizzato le carenze dei requisiti informali.
Esercizi
5.23

Formalizzate la prima delle due interpretazioni della regola per lo spegnimento della luce del
pulsante, discusse nella nostra analisi delle specifiche informali.

5.24

Formalizzate la regola originale espressa nelle specifiche informali, ovvero "nel secondo caso,
se entrambi i tasti di richiesta sono stati premuti viene annullata una sola delle richieste", invece della scelta attuale che spegne le luci di entrambi i pulsanti.

Politiche decisionali. Il modello fino ad ora descritto altamente non deterministico. In


molti casi, il non determinismo pu essere una propriet importante della specifica, per esempio, quando si sceglie di specificare un insieme di comportamenti accettabili senza restringere il modello a un comportamento specifico, lasciando che questo venga scelto successivamente, in base a considerazioni di diversa natura. A volte, per, il non determinismo pu
causare comportamenti indesiderati, che vorremmo poter escludere al momento di effettuare
la specifica. Nel nostro esempio, fino ad ora, abbiamo affrontato solo la specifica dei meccanismi che governano gli spostamenti degli ascensori, senza considerare le politiche coinvolte. Queste politiche devono, comunque, essere specificate se vogliamo fornire le prove
che tutte le richieste, prima o poi, verranno soddisfatte dagli ascensori. Si veda il requisito
informale 4. Abbiamo deciso di incapsulare tutte le politiche decisionali in un unico modulo chiamato SCHEDULER. Si tratta di un'applicazione dei principi di information hiding

alle specifiche: incapsulare le politiche decisionali ci consente di cambiarle senza causare ripercussioni sui meccanismi descritti dal resto della rete; per esempio, saremmo in grado di
raffinare le prestazioni del sistema a livello di specifica dei requisiti simulando diverse politiche.
Tratteggeremo qui solo una specifica molto semplice del modulo SCHEDULER. La politica scelta differisce dai requisiti ambigui delle specifiche informali: garantisce correttezza
nei confronti delle richieste di servizio; ovvero, garantisce che ogni richiesta sar servita prima o poi (ma non prova a "minimizzare i tempi di attesa", secondo una qualunque interpretazione). Nessuna richiesta soffrir mai di starvation.
Assegniamo a ciascun ascensore un "direction state" (stato di direzione), che pu essere o U (up) o D {down). (Figura 5.39.) Un gettone in u ( D ) significa che la direzione di spostamento dell'ascensore verso l'alto (verso il basso). La politica scelta consiste nel tenere la
direzione di spostamento il pi a lungo possibile uguale, fino a quando non vengono soddisfatte tutte le richieste pendenti di spostamento in quella direzione. Altrimenti, si eseguono
le transizioni U_D e D_u per invertire la direzione di spostamento. uk denota una qualsiasi
transizione delle rete che indica uno spostamento verso l'alto (ad esempio, t , , . . . , t 1 2 );
mentre, DK denota una qualsiasi transizione della rete che indica uno spostamento verso il
basso. Si ricordi che queste transizioni hanno t m i n =t m a x =0 ; ovvero, vengono eseguite non
appena sono rese attive. Le transizioni U_D e D_U, invece, rappresentano operazioni che hanno un tempo di esecuzione non nullo; ad esempio, hanno t m i n =t m a x =x msec, con x diverso da zero. Questa durata rappresenta il tempo necessario allo S C H E D U L E R per controllare se esistono richieste pendenti di salita o discesa. Inoltre, nella porzione di rete illustrata nella Figura 5.36, e in reti simili, viene data priorit pi alta alle transizioni t 7 , t 8 e t 9
rispetto a t j o . t n e t i j . I n questo modo, un ascensore costretto a fermarsi a ogni piano
per cui c' una richiesta di servizio interna o esterna. Come si pu notare, un ascensore continua a spostarsi verso l'alto fino a quando non ci sono richieste pendenti di salita. Se non
vi sono pi richieste di servizio in quella direzione, dopo un certo periodo di tempo l'ascensore
si porta in uno stato di discesa, se vi sono richieste pendenti di discesa. Se non ci sono richieste
di questo tipo, l'ascensore continua a controllare fino a quando non diventano disponibili.
Un modo pi generale per modellare le politiche di schedulazione consiste nell'introdurre un posto, S C H E D U L E R , che contiene un gettone particolare che conosce lo stato globale del sistema. Si possono poi predisporre predicati adeguati da associare alle transizioni
in modo che vengano rese attive le transizioni che possono essere eseguite secondo la politica di schedulazione. Ne viene fornito un esempio nella Figura 5.40.
Una volta terminata la formalizzazione del sistema di ascensori in termini di PN, possiamo analizzarlo per verificare se definisce il comportamento desiderato in maniera correr-

SCHEDULER

Figura 5.40

Modo pi generale per la rappresentazione di politiche di schedulazione.


Ogni transizione ha un predicato del tipo 0K ( S c h e d u l e r ) (insieme ad altre
possibili condizioni). Il gettone in SCHEDULER registra tutte le informazioni
circa lo stato del sistema utili per la selezione di quale transizione eseguire.
Il gettone "permanente", in quanto viene sempre riprodotto dopo l'esecuzione
di una qualsiasi delle transizioni e dopo il suo aggiornamento.

ta. Come anticipato nel Paragrafo 5.4, un modo per verificare l'adeguatezza di una specifica quello di simularla. In questo caso, la simulazione delle PN risulta piuttosto naturale:
sufficiente applicare le regole di transizione del modello, partendo da uno stato iniziale e
osservandone il comportamento.
Per esempio, potremmo considerare una marcatura iniziale secondo la quale un ascensore si trova al primo piano (ovvero, un gettone presente nel posto F J e tutti i pulsanti
interni ed esterni sono spenti. Assumiamo ora che qualcuno entri nell'ascensore e prema il
pulsante 2. Ci corrisponde all'esecuzione della transizione P u s h nella porzione della rete
che descrive il pulsante (Figura 5.34). Poi viene eseguita immediatamente la transizione
S e t , che corrisponde all'accensione della luce del pulsante. Viene, quindi, resa attiva la transizione t 4 , presente nella rete del tipo rappresentato nella Figura 5.36, la quale viene eseguita immediatamente. Dopo un determinato tempo At sar presente un gettone nel posto
F", abilitando t 7 . Questa transizione verr eseguita immediatamente. Subito dopo, viene
eseguita la transizione Reset per il pulsante interno all'ascensore corrispondente al secondo piano (Figura 5.37). Questa simulazione ci permette di concludere che, se un ascensore
si trova al primo piano e nessun pulsante illuminato quando viene premuto il pulsante interno corrispondente al secondo piano, allora, dopo At secondi, l'ascensore giunger al secondo piano e il pulsante verr resettato. In maniera del tutto simile, si potrebbero simulare le chiamate esterne usando le regole formalizzate nelle Figure 5.34 e 5.38. Ci renderebbe evidente la scena interpretativa del requisito informale. L'illuminazione cancellata quando l'ascensore visita il piano e/o si muove nella direzione desiderata.
Se i risultati della simulazione dovessero non corrispondere all'interpretazione del cliente, la specifica deve essere modificata (si veda l'Esercizio 5.23).
Abbiamo constatato una motivazione essenziale dell'uso di modelli formali per le specifiche: le specifiche formali possono essere simulate automaticamente con l'aiuto di un interprete del modello. I benefici di questo approccio dovrebbero risultare evidenti. L'utilit
di una simulazione dipende, tuttavia, dal modello. Interpretare una FSM, anche una piuttosto complessa, facile ed efficiente. Interpretare una PN concettualmente semplice, ma
la sua efficienza inferiore per via della natura intrinsecamente non deterministica del modello, il che potrebbe costringere all'uso di tecniche laboriose di backtracking. Infatti, si supponga di eseguire una PN per vedere se, da una determinata marcatura m possa essere rag-

giunta una marcatura diversa m '. Durante l'interpretazione vengono fatte delle scelte non
deterministiche quando sono abilitate pi transizioni. Nel caso queste scelte non portassero al risultato desiderato (ovvero, la marcatura m '), diventa necessario tornare indietro per
tentare altre scelte. Commenteremo ulteriormente l'uso di modelli per la verifica delle specifiche nei Paragrafi 5.6.2.4 e 5.7.3.
Esercizio
5.25

5.6

Fornite un'interpretazione ragionevole della frase "minimizzare i tempi di attesa", presente nei
requisiti informali del sistema di ascensori. Si formalizzi l'interpretazione data in termini di
funzioni di selezione da aggiungere a u n o schema P N del tipo rappresentato nella Figura 5.40.
Si controlli se la politica fornita continua a garantire che tutte le richieste vengano prima o
poi soddisfatte.

Specifiche descrittive

Come affermato nel Paragrafo 5.3, le specifiche descrittive cercano di esprimere le propriet
desiderate di un sistema piuttosto che il suo comportamento. Inizieremo ora un breve studio di
una notazione di specifica descrittiva semiformale molto diffusa, e affronteremo successivamente
le notazioni completamente formali. Illustreremo diverse notazioni che possono essere utilizzate per specificare sistemi lungo diversi stadi del processo di sviluppo e vedremo come alcune notazioni siano utili al livello dei requisiti, altre per la specifica della semantica delle interfacce dei moduli, mentre altre ancora per la specifica di frammenti di programma. Tutte le notazioni, comunque, possiederanno la stessa caratteristica comune: lo stile descrittivo.
Un modo naturale di fornire specifiche descrittive precise quello di usare formule matematiche. Contrariamente al linguaggio naturale, le formule matematiche possiedono una
sintassi e una semantica precisa. Inoltre, possono essere gestite da strumenti automatici oltre che da modelli operazionali formali.
Sono stati proposti molti formalismi matematici per la descrizione delle propriet dei
sistemi; in questo paragrafo, affronteremo lo studio di due approcci, uno basato sull'uso della logica matematica e l'altro sull'uso dell'algebra.

5.6.1 Diagrammi entit-relazione


Abbiamo visto che i DFD costituiscono una notazione utile per la descrizione di operazioni
usate per accedere e manipolare i dati di un sistema, in generale un sistema informativo.
Tuttavia, non sono sufficienti per specificare tutte le caratteristiche interessanti del sistema:
necessaria anche una descrizione concettuale della struttura dei dati e delle loro relazioni.
In realt non chiaro quale delle due descrizioni debba essere fornita prima: se quella
delle operazioni o quella delle strutture dati. Da un lato, capire le operazioni da fornire aiuta a capire la struttura logica dei dati; dall'altro, la struttura logica dei dati risulta stabile, indipendentemente dalle operazioni eseguibili su di essa. Si potrebbe addirittura affermare che
rappresenti la nostra conoscenza dell'area applicativa, che pi stabile delle operazioni fornite dall'applicativo.

I due punti di vista sono complementari ed entrambi utili. Cominceremo il nostro studio delle specifiche descrittive affrontando il modello ER (entity-relationship, entit-relazione), una notazione molto usata e adottata per la descrizione delle relazioni esistenti tra i dati di un sistema informativo.
II modello ER nasce dall'esigenza di disporre di un modello concettuale dei dati che
possa specificare i requisiti logici in un sistema informativo. Il modello basato su tre concetti primitivi: le entit, le relazioni e gli attributi. Il modello possiede anche un linguaggio
grafico particolarmente facile da capire; quando le descrizioni sono fornite nel linguaggio
grafico vengono dette diagrammi ER.
La Figura 5.41 mostra un esempio molto semplice di diagramma ER che descrive le
entit S T U D E N T (studente) e C L A S S (classe), con la relazione E N R O L L E D _ I N ( iscritto a)
che pu sussistere tra uno S T U D E N T e una CLASS. Un'entit, rappresentata da una scatola,
indica una collezione di oggetti che condividono propriet comuni; il concetto simile a
quello di tipo in un linguaggio di programmazione. Le propriet di un'entit sono i suoi attributi e le relazioni cui partecipa. Gli attributi vengono elencati vicini all'entit, mentre le
relazioni vengono rappresentate da scatole a forma di rombo. Nel nostro esempio, S T U D E N T
una collezione di individui; N A M E , AGE e SEX sono attributi di S T U D E N T : ogni S T U D E N T
caratterizzato da tre valori rappresentanti il suo nome, la sua et e il suo sesso. Una relazione tra due entit, come S T U D E N T e CLASS, un insieme di coppie <a,b>, dove a un
elemento di S T U D E N T e b un elemento di CLASS. La relazione mostrata nella Figura 5.41
rappresenta il fatto che uno studente a iscritto alla classe b.
I diagrammi ER non sono stati standardizzati in maniera rigorosa. Ci significa che
non esiste alcuna versione universalmente riconosciuta come standard; in pratica, ne esistono molte varianti. Alcuni linguaggi ER permettono relazioni n-arie (ovvero, che possono mettere in relazione n entit); altri supportano solo relazioni binarie. Inoltre, alcuni permettono alle relazioni di avere attributi, mentre altri no. Nel caso fossero permessi gli attributi sulle relazioni, potremmo definire l'attributo P R O F I C I E N C Y (competenza) da associare alla relazione E N R O L L E D _ I N . L'attributo associato a una coppia < a , b > della relazione E N R O L L E D _ I N rappresenterebbe la competenza di uno studente a iscritto alla classe b. Infine, alcuni linguaggi ER supportano qualcosa di simile all'ereditariet tra entit, spesso chiamata,
nel gergo ER, relazione IS_A (ovvero, " un"). Per esempio, si potrebbero definire UNDERG R A D U A T E e G R A D U A T E come due sottoentit di S T U D E N T , i quali ereditano le propriet
di S T U D E N T e ne aggiungono eventualmente di nuove in termini di attributi e partecipazioni in relazioni ( U N D E R G R A D U A T E IS A STUDENT).
Molti linguaggi ER permettono relazioni parziali; ovvero, non tutti gli elementi delle entit coinvolte devono partecipare alla relazione. Inoltre, spesso viene data la possibilit di specificare la relazione come one to one (uno a uno), one to many (uno a molti), many to one (molti a uno) o many to many (molti a molti). Se una relazione R tra A e B uno a uno, allora per
qualsiasi <a, b> in R non esiste alcun a ' in A tale che <a ', b> sia in R con a ' ^ a. Inoltre,
non esiste alcun b ' in B tale che <a, b ' > sia in R con b 1 * b. Se R molti a uno, si richiede che, per ogni <a,b> in R, non esista alcun b 1 in B tale che anche <a,b '> sia in R con
b ' * b, etc. Graficamente queste annotazioni vengono rappresentate come nella Figura 5.42.
Possiamo notare che la relazione E N R O L L E D _ l N nella Figura 5.41 molti a molti.
Le annotazioni presenti nella Figura 5.42 descrivono alcune semplici limitazioni della
relazione. In pratica, per, quando specifichiamo i requisiti, vorremmo essere in grado di

NAME

Figura 5.41

Diagramma ER che descrive la relazione tra studenti e una classe.

definire limitazioni pi generali e complesse circa i nostri dati, per rappresentare propriet
importanti del mondo che stiamo modellando. Sfortunatamente, il potere espressivo del linguaggio ER piuttosto limitato; concetti pi complessi devono essere espressi separatamente
usando un'altra notazione. Per esempio, non esiste un modo per specificare graficamente che
una classe pu esistere solo se il numero di studenti iscritti pi di cinque e non pi di
M A X _ E N R O L L M E N T , attributo di ogni classe. Se volessimo, questa limitazione potrebbe essere definita sotto forma di un commento in linguaggio naturale associato al diagramma come ulteriore documentazione.
Secondo la classificazione vista nel Paragrafo 5.3, i diagrammi ER sono una notazione
semiformale, in quanto la loro sintassi e la loro semantica non sono definite precisamente e
poich la mancanza di potere espressivo ci costringe ad aggiungere propriet sotto forma di
commenti non formali. Inoltre, sono una notazione descrittiva in quanto specificano cosa
sono le entit e quali sono le loro propriet in termini di attributi e partecipazioni a relazioni. Nel Paragrafo 5.6.2 commenteremo la relazione tra i diagrammi ER e la notazione
descrittiva fornita dalla logica.
Il lettore potrebbe aver notato che i diagrammi ER sono simili a una forma semplificata di diagramma delle classi. In realt, i diagrammi delle classi UML sono stati definiti come un'evoluzione dei diagrammi ER in cui le entit dei dati sono modellate insieme alle loro operazioni secondo uno stile object oriented. Adottando uno stile convenzionale, in cui le
funzioni e i dati sono definiti separatamente, i diagrammi ER possono essere utilizzati per
complementare i DFD. Adottando uno stile orientato agli oggetti, invece, i due aspetti possono essere raggruppati insieme attraverso il concetto di classe.
I diagrammi ER sono ancora oggi molto diffusi, soprattutto nelle applicazioni "dataoriented" (orientate ai dati). Per via della loro natura grafica sono semplici da capire anche

A R B e uno a uno

A R B uno a molti

A R B molti a uno

A R B molti a molti

Figura 5.42

Annotazioni che descrivono le qualifiche della relazione R.

per i non professionisti. Molti sostengono che possano essere efficaci anche per la specifica
dei requisiti, in quanto possibile insegnare agli utenti finali come capirli per verificare se
la descrizione fornita dall'ingegnere del software modella il dominio applicativo in maniera
adeguata in termini di entit rilevanti e loro relazioni.
Esercizi
5.26

Supponete che il vostro linguaggio E R non supporti l'associazione di attributi alle relazioni,
ma supporti le relazioni n-arie. Seguendo la discussione del Paragrafo 5.6.1, come rappresentereste i livelli di competenza in una data classe?

5.27

Arricchite il diagramma ER della Figura 5.41 introducendo l'entit PROFESSOR e le relazioni TEACHES (insegna) con l'entit CLASS E A D V I S E S (consiglia) con l'entit STUDENT.
C o m e si potrebbe specificare una limitazione del tipo "uno studente non p u iscriversi a u n
corso (CLASS) tenuto dal proprio advisor"?

5.6.2 Specifiche logiche


Una formula di una FOT ( first-order theory, teoria di primo ordine) un'espressione che
coinvolge variabili, costanti numeriche, funzioni, predicati e parentesi, come nell'aritmetica tradizionale. Vengono usati anche i soliti connettori logici, and, o r , n o t , i m p l i e s e =
(il quale denota equivalenza logica). Il tipo del risultato di una formula FOT deve essere
booleano. Contrariamente alle formule booleane di molti linguaggi di programmazione,
per, le formule FOT possono usare anche quantificatori, ovvero possibile applicare i simboli e x i s t s e f o r ali alle variabili.
Seguono alcuni esempi di formule FOT:
1. x

>

and

2. x

y s y

>

= x;

implies

>

z;

3. for

ali

4. x +

1 < x -

5. for

ali

6.

x,

y,

(x

> y and

y >

z implles

>z);

1 ;

(ezists

(y

= x

z) ) ;

x > 3 o r x < - 6 .

La semantica di queste formule dovrebbe essere sufficientemente chiara per via del significato intuitivo dei simboli usati.
Si noti che alcune delle formule riportate sono vere, altre sono false, mentre altre ancora sono o vere o false, a seconda dei valori delle variabili non quantificate. Le formule 1
e 2 sono vere indipendentemente dai valori di x, y e z, la formula 4 falsa indipendentemente dal valore di x, mentre la formula 6 vera per alcuni valori di x e falsa per altri valori.
Una variabile presente in una formula detta libera se non viene quantificata; una variabile non libera viene detta legata. Di conseguenza x libera nella formula numero 1 e legata nella formula numero 3. Se tutte le variabili di una formula sono quantificate, la formula detta chiusa. Di conseguenza, la formula numero 3 chiusa. Una formula chiusa
sempre o vera o falsa. La chiusura di una formula ottenuta quantificando tutte le variabili libere con il quantificatore f o r a l i . La chiusura della formula 4 , quindi, f o r a l i x
(x + 1 < x 1 ). Se una formula vera per tutti i possibili valori delle sue variabili libere, lo anche la sua chiusura. Quindi, la chiusura della formula numero 1 vera, mentre
la chiusura della formula 4 falsa.
In alcuni casi, il fatto che una formula sia vera pu dipendere dal dominio scelto per
le sue variabili. Per esempio, la formula
7. for

ali

(X

> 1) or

(x

< - 1 ) or

(x

0)

vera se x un numero intero e falsa se numero reale. In questo libro, ad ogni modo, tratteremo variabili il cui dominio dovrebbe risultare chiaro, senza ambiguit. In particolare, le
variabili numeriche saranno sempre numeri interi, a meno che venga specificato diversamente.
Abbiamo osservato in precedenza che i diagrammi ER possono essere classificati come
una notazione descrittiva semiformale. Possiamo sottolineare questa propriet dimostrando
che i diagrammi ER possono essere riscritti in termini logici; inoltre, la logica fornisce una
notazione formale per esprimere le limitazioni non esprimibili in termini di diagrammi ER.
Per esempio, nella discussione riguardante la Figura 5.41, la limitazione secondo la quale
"una classe pu esistere solo se il numero di studenti iscritti maggiore di 5 e minore di
MAX_ENROLLMENT" pu essere scritta come
for

ali

in

CLASS

5 < c a r d i n a l ity

{b|<

a,

b >

in

ENROLLED_IN}

<

a.MAX_ENROLLMENT

In questa formula, usiamo la dot notation per indicare il valore dell'attributo di un'entit. La
funzione c a r d i n a l i t y stata introdotta per indicare il numero di elementi presenti in
un insieme.
Dimostreremo ora l'uso delle formule logiche secondo lo stile della cosiddetta specifica di programma alla Floyd-Hoare. Mostreremo come interi programmi possano essere specificati usando formule logiche per la descrizione degli input e degli output del programma

stesso: le asserzioni di input-output. Affronteremo poi l'uso delle asserzioni intermedie, che
vengono utilizzate per specificare frammenti di un programma facendo affermazioni circa
lo stato di esecuzione del programma stesso; in seguito vedremo brevemente come lo stile
di specifica possa essere esteso alla progettazione orientata agli oggetti per specificare classi.
Infine, useremo entrambi i tipi di specifica per presentare lo studio di un sistema di ascensori in termini di specifiche logiche.
Esercizi
5.28

Costruite le chiusure delle sette formule precedentemente presentate.

5.29

La chiusura della formula 6 vera?

5.6.2.1

La specifica di programmi completi: le asserzioni input-output

Cominceremo usando formule matematiche molto semplici per esprimere le propriet del
software. Sia P un programma sequenziale. Sia < i x , i 2 , . . . , i n > la sequenza degli input
di P e <0!, o 2 , . . . , om> la sequenza degli output di P. Pi precisamente, si assuma che
P legga (scriva) tutti gli input (e output) da (su) file sequenziali; sia < i l f i 2 , , i n >
la sequenza di valori registrati sul file di input nell'ordine in cui vengono letti da P, e < o u
o 2 j . . . , om> la sequenza che P scrive sul file.
Una propriet, o requisito, di P viene specificata come una formula del tipo
{ Pr e ( i j, i 2 ,

. . . , i) }

P
{Post(olr

o2,

dovePre(i1,

...,

om,

i1(

i2,

...,

in)>

i n ) indica una formula FOT avente le variabili libere ij, i2,


o m , i l f i 2 , ..., i n ) indica una formula FOT
avente le variabili libere o x , o 2 i . . . , o m e i l f i2, ..., in. Pr e viene detta precondizione di P, e Post postcondizione. La formula precedente significa che se Pre vera per i
valori di input, prima dell'esecuzione di P, allora, dopo la sua esecuzione, dovr essere vera
Post per i valori di output e input.
Forniremo ora alcuni esempi di specifiche di programmi dati in termini di pre e postcondizioni:
...,

i2,

in, e P o s t ( o l f

1. { e x i s t s

z(ij

o2,

z*i 2 )>

(1 =

l/2>

Questo esempio indica che se il valore di input


multiplo del valore di input i 2 , allora
l'output dovr essere il risultato della divisione i i / i 2 - Un requisito pi forte per un programma di divisione pu essere il seguente:
2. {i, >
p
{i[ =

i2>
i 2 *Oi + o 2

and

o2

0 and

o2

<

i2}

Questo requisito pi forte nel senso che impone meno limitazioni ai valori di input e
maggiori limitazioni ai valori di output. Una precondizione { t r u e } non impone alcuna

limitazione ai valori di input; sempre vera, indipendentemente dai valori di input. Ci


implica che il programma raggiunger i risultati preposti per tutti i possibili valori di
input.
La specifica:
3.

{true}
p
{o

ii or

i 2 ) and

2 i t and

i2}

richiede che P produca il valore pi grande tra i i e i 2 . La specifica


4. {i, >

0 and

i2 >

0}

z2

(i, =

P
{(exists
and

z1(

* z, and

i2 = o

z2)

not

(exists

(exists

z,,

z2

(i, =

z, a n d

i2 =

* z 2 ) and

h >

o))>

richiede c h e P calcoli il m a s s i m o c o m u n e divisore di i j e i 2 .

Assumendo che n sia un valore positivo che indica la lunghezza della sequenza di input,
la specifica
5. <n

>

0}

{ .?. 4
richiede che P calcoli la somma della sequenza. Infine, la specifica
6.

{n > 0}
P
{for

ali

(1

i 5 n)

implies

(Oi =

i.itl) }

richiede c h e P produca la sequenza inversa rispetto alla sequenza di input, a s s u m e n d o che


la sequenza di i n p u t n o n sia vuota.

Esercizi
5.30

Fornite una specifica logica per un p r o g r a m m a che legge una sequenza di n + 1 valori e controlla se il p r i m o valore appare nuovamente nei seguenti n valori.

5.31

Fornite una specifica logica per u n p r o g r a m m a che legge d u e parole (ovvero, d u e sequenze
di caratteri alfabetici, separati da u n o spazio e terminati da un carattere speciale '#'). La sec o n d a parola p u essere nulla; la p r i m a no. Il p r o g r a m m a legge poi una sequenza di altre
parole, separate da spazi e t e r m i n a n t i con '#', e riscrive la sequenza, sostituendo tutte le occorrenze della p r i m a parola con la seconda. Al m o m e n t o fornite solo una bozza di soluzione senza a p p r o f o n d i r e tutti i dettagli. Riaffrontate poi il p r o b l e m a d o p o aver finito di leggere il seguito di questo paragrafo.

Gli esempi e gli esercizi precedenti hanno dimostrato che le formule necessarie per specificare anche problemi semplici possono richiedere molti dettagli e possono essere diffcili da

capire. Abbiamo gi affrontato questo inconveniente nei nostri esempi di specifiche operazionali. Nel Paragrafo 5.7 considereremo il problema della gestione di specifiche complesse
in generale. Anticiperemo ora, comunque, alcuni suggerimenti per migliorare la leggibilit
delle specifiche logiche, come abbiamo fatto quando abbiamo affrontato specifiche non banali con le PN e le FSM.
Si consideri, ad esempio, il problema fornito nell'Esercizio 5.31. L'origine dei problemi che anche concetti semplici e intuitivi, come "parola", non hanno un significato preciso nella sintassi FOT. Quindi, se vogliamo formalizzare una frase del tipo "due parole sono uguali" o "una parola sostituita da un'altra", dobbiamo affrontare molti dettagli. Il problema pu essere risolto una volta per tutte usando definizioni adeguate.
Per esempio, possiamo introdurre il predicato i n p u t _ w o r d ( m, n ) per affermare che
la sequenza di caratteri nello stream di input, compresa tra la m-esima e l'n-esima posizione, rappresenta una parola. Questa notazione formalizzata dalla formula
inputword

(m,n) =

(for

ali

(m < i < n )

implies

alphabetic(cL))

dove Ci rappresenta l'i-esimo carattere mentre a l p h a b e t i c ( c ) significa che c un carattere alfabetico. (La formalizzazione di questo predicato un esercizio banale).
Ora possiamo usare il predicato i n p u t _ w o r d come abbreviazione compatta e chiara ogni volta che risulti necessario. In particolare, possiamo definire il predicato
i n p u t _ t e x t ( m, n ) in modo che affermi che la sequenza di elementi del file di input
dalla m-esima all'n-esima posizione un frammento di testo, ovvero una sequenza di parole separate da spazi e racchiuse da una coppia di simboli '#'. Precisamente,
input

text(m,n ) =
(i= ' #'

and

(exists

i='#'
k

(for

(exists

and
ali

(1

< k)

hj, irij ( i n p u t _ w o r d
mi = m

(1

<

j <

iBj

hj +

1 and

mk

+ hk

k)implies
1= '

implies

(m j f

itij + hj)
+

(mjfi

1 =

and
and

= m 3 + hj +

and

'))))))

Una volta definita questa affermazione, si possono definire predicati adeguati per il file di
output e una relazione globale tra i file di input e di output basati sulle definizioni precedenti. Lasciamo questo compito al lettore.
Esercizio
5.32

Individuate qualche dettaglio della specifica informale dell'Esercizio 5.31 n o n definito precisamente, e confrontatelo con la definizione formale.

5.6.2.2

Specfica di frammenti di programma: asserzioni intermedie

Nel paragrafo precedente abbiamo usato le pre e le postcondizioni sui valori di I/O per
specificare programmi completi. Spesso, per, utile specificare solo porzioni dei programmi. Per esempio, se stiamo costruendo una libreria di moduli per uso generale non
possiamo conoscere a priori tutti i contesti in cui alcune delle procedure verranno eseguite.

Una tale generalizzazione piuttosto semplice da realizzare. Tutto quello che necessario fare permettere alle espressioni delle pre e postcondizioni di fare riferimento alle variabili dei programmi oltre che ai valori di I/O. Per esempio, si supponga di voler specificare una procedura s e a r c h con parametri in ingresso e l e m e n t , t a b l e (un array di interi)
e n, il numero di elementi immagazzinati in t a b l e . La procedura controlla se e l e m e n t
esiste in t a b l e . La specifica si pu presentare nel modo seguente9:
7. {n

>

0}

n un

procedure

valore

search

(table:
element:

{found

(exists

i(l

costante
in
in

integer

a r r a y ; n:

integer;

s i n and

found:

table(i)

in
out

integer;
Boolean);

element))}

In maniera simile, si potrebbe scrivere la seguente specifica di un sottoprogramma che inverte l'ordine del contenuto di un array di interi:
8.

{n

>

0)

procedure
{for

all

reverse
i

(1

(a:

< i s n)

in

out

integer

implies

(a(i)

a r r a y ; n:

in

= old_a(n

- i +

integer);
1))}

In questa specifica stato necessario indicare una relazione tra i valori delle variabili del programma prima e dopo l'esecuzione della procedura. Quindi, stata usata la variabile ausiliaria o l d _ a per indicare il valore di a prima dell'esecuzione della procedura.
La seguente , invece, la specifica di una procedura di ordinamento:
9. {n

>

0)

procedure

sort

(a:

in

out

integer

a r r a y ; n:

in

integer);

{sorted(a,n ) > ,

Come abbiamo fatto prima, definiamo il nuovo predicato


sorted(a,n)

(for

all

i(l

s i < n)

implies

a(i)

a(i+l))

Esercizio
5.33

La s p e c i f i c a 9 a d e g u a t a p e r u n a p r o c e d u r a d i o r d i n a m e n t o ? P e r c h ? Se n o n lo , f o r n i t e u n a
specifica adeguata.

5.6.2.3 Specifica di classi


La specifica di propriet dello stato di esecuzione di un programma, piuttosto che solo delle relazioni I/O, diventa ancora pi importante nel caso di linguaggi orientati agli oggetti,
quando vogliamo specificare una classe. Pi precisamente, quando vogliamo specificare il
comportamento degli oggetti di una data classe durante la loro vita, dalla loro creazione all'esecuzione di operazioni che ne alterano gli stati, alle operazioni chiamate per conoscere i
loro stati. In questo caso, infatti, non esiste la nozione di una computazione, con un punto
di inizio e uno di fine. Piuttosto, per capire l'effetto di un'operazione applicata a un ogget-

A s s u m i a m o i m p l i c i t a m e n t e c h e il l i m i t e i n f e r i o r e degli array sia 1.

to, dobbiamo poterne caratterizzare lo stato, che rappresenta il risultato delle sequenze di
operazioni ad esso applicate in precedenza.
Un modo per caratterizzare gli stati degli oggetti quello di usare un predicato invariante. L'invariante definisce una propriet che caratterizza l'oggetto dal momento della sua
creazione, durante tutta la sua vita. L'invariante deve essere preservato dalle operazioni. Per
esempio, se si utilizza un array I M P L di dimensione l e n g t h per implementare il tipo di dato astratto SET, l'invariante potrebbe affermare che non devono essere presenti in IMPL due
elementi uguali:
for

ali

i, j

(1 ^ i ^ l e n g t h

and

1 <

j < length

and

i*j)

implies

IMPL[i]*IMPL[ j ]

Inoltre, ogni operazione pu essere specificata fornendo una precondizione e una postcondizione. La precondizione un predicato sullo stato iniziale dell'oggetto (parametri in ingresso e variabili istanze dell'oggetto) che specifica un obbligo che i moduli clienti sono tenuti a soddisfare quando invocano l'operazione. La postcondizione , invece, un predicato
sullo stato dell'oggetto dopo l'invocazione dell'operazione. Per esempio, si supponga che l'operazione DELETE elimini un elemento x da un'insieme. Una precondizione per DELETE
potrebbe essere
exists

(1

i i i length

and

IMPL[i]=

x)

La postcondizione sarebbe
for

ali

for

(1

exists
and

i i s length

ali

i
j

((1
(1

IMP L [ j ] =

implies

IMPL[i]

i o1d_length

s i s

and

* x ) and

old I M P L [ i ] * x )

implies

length

oldIMPI[i]) )

Secondo questa specifica, se un modulo cliente assicura la verit delle precondizioni al momento di invocare l'operazione l'implementazione dell'operazione fornita dalla classe deve
assicurare che la postcondizione sar vera dopo la sua esecuzione.
In generale, assumiamo che INV sia un predicato invariante per una classe. Per ogni
operazione opi definita dalla classe, sia p r e i la sua precondizione e p o s t i l a sua postcondizione. La specifica completa del codice che implementa l'operazione opi definita dalla classe pu essere fornita nella seguente maniera
(INV

and

pre^} program

fragment

for

opi

{INV

and

posti)

Questa scrittura formalizza la propriet che INV un'invariante ( vera dopo l'esecuzione
dell'operazione, assumendo che sia vera prima dell'operazione) e che p r e i e p o s t i sono rispettivamente la pre e la postcondizione. Per completare la specifica dobbiamo fidarci del
fatto che l'oggetto, nel suo stato iniziale, soddisfa l'invariante. Assumendo che venga fornita un'operazione per costruire l'oggetto ( c s t r ) , richiediamo che
{true}

program

fragment

for

cstr

{INV}

Infatti, dato che all'inizio l'oggetto soddisfa l'invariante e siccome ogni operazione fornita
la preserva, possiamo concludere, per induzione, che l'invariante sempre vero durante la
vita dell'oggetto, prima e dopo l'esecuzione di una qualsiasi delle operazioni possibili.

5.6.2.4

La specifica di comportamenti che non terminano

Molto spesso necessario specificare sistemi i cui comportamenti sono descritti da programmi che non terminano. Un sistema di questo tipo potrebbe essere un sistema reattivo,
il quale aspetta continuamente segnali dall'ambiente esterno e fornisce risposte a questi segnali. Un sistema operativo un esempio tipico di sistema reattivo: risponde alle richieste
degli utenti, agli interrupt provenienti da periferiche, etc. I sistemi di controllo computerizzati, come il sistema per ascensori discusso nel Paragrafo 5.5.4.2, sono ulteriori esempi tipici. Come caso di studio, considereremo qui il classico sistema produttore-consumatore introdotto nel Paragrafo 4.5.1.1. Un requisito per questo sistema che la sequenza storica di
oggetti realizzati dal produttore (passati all'operazione PUT come parametri), in qualsiasi momento, coincida con gli oggetti consumati (ottenuti mediante l'operazione GET), eccetto per
gli elementi prodotti pi di recente, i quali sono contenuti all'interno del buffer. Questa propriet deve essere sempre vera; ovvero, deve rappresentare un invariante per l'intera esecuzione del sistema. Se i processi di produzione e di consumo, tuttavia, si dovessero trovare in
cicli del tipo r e p e a t - f o r e v e r (ripeti all'infinto), le due sequenze non sarebbero mai definite: una sequenza deve essere intrinsecamente finita. Quello cui siamo in realt interessati che, in qualsiasi momento, le due sequenze coincidano. Questa propriet deve essere vera infinite volte consecutive, indipendentemente dal numero di elementi prodotti e consumati. Ancora una volta, deve rappresentare un invariante per l'intera esecuzione del sistema.
Esaminiamo un sistema produttore-consumatore in maggiore dettaglio.
ESEMPIO 5 . 5

Si consideri il sistema produttore-consumatore del Paragrafo 4.5.1.1. Per specificare il comportamento desiderato, necessario formalizzare un predicato basato sulla sequenza di caratteri che viene letta e scritta. Queste sequenze non sono, in senso stretto, n variabili I/O
n variabili di programma: non esiste una dichiarazione di tali variabili. Possiamo farvi riferimento, tuttavia, come se fossero variabili di programma, nello stesso modo in cui abbiamo fatto riferimento ai valori della stessa variabile in diversi punti dell'esecuzione di un programma sequenziale.
Definiamo, dunque, le variabili I N P U T _ S E Q U E N C E (sequenza di input) e O U T P U T _ S E Q U E N C E (sequenza di output), e assumiamo che ogni operazione PUT di un produttore (in C H A R _ B U F F E R ) inserisca in coda a I N P U T _ S E Q U E N C E il carattere scritto e che ogni
operazione G E T di un consumatore inserisca in coda a O U T P U T S E Q U E N C E il carattere letto.
Ci rendiamo conto, per, che non del tutto vero che le sequenze debbano essere sempre uguali: possono, infatti, differire per quanto riguarda gli ultimi elementi ancora contenuti del buffer. Per questo motivo, la propriet invariante che pu essere assunta come specifica pu essere
INPUT_SEQUENCE

= append

(OUTPUT_SEQUENCE,

contents(CHAR_BUFFER))

dove il significato delle operazioni a p p e n d e c o n t e n t s dovrebbe risultare ovvio.


Analizzando ulteriormente questa propriet, per, ci rendiamo conto che la sua nuova
formulazione non pu essere detta una propriet invariante in senso stretto. Infatti, quando il
consumatore sta eseguendo la procedura G E T , i contenuti del buffer e di O U T P U T _ S E Q U E N C E

Line

Figura 5.43

Port

Producer

Buffer

Consumer

Semplice sistema produttore-consumatore.

non sono ben definiti. Ciononostante, non necessitiamo di un requisito cos forte per quanto
riguarda l'invarianza della propriet desiderata. Per i nostri scopi, perfettamente sufficiente
che la propriet invariante sia vera quando il sistema non sta eseguendo alcuna procedura del
monitor.
Quindi, i punti di esecuzione critici in cui dobbiamo assicurare la veridicit della propriet invariante sono l'ingresso e l'uscita delle procedure del monitor (infatti le variabili di
interesse vengono modificate solo dalle procedure del monitor).

Nell'esempio precedente, abbiamo visto che la specifica descrive propriet dello stato di esecuzione del sistema. Nel Capitolo 6 forniremo ulteriori esempi di queste specifiche.
Esercizi
5.34

5.35

Specificate una procedura che fonda il c o n t e n u t o di due array in un unico array. Si possono
distinguere due casi:

Elementi duplicati negli array di input devono apparire duplicati nell'array di o u t p u t .

Gli array di input e di o u t p u t n o n devono contenere elementi duplicati.

Considerate il sistema descritto nella Figura 5.43. Consiste di tre processi: Line, Producer
e Consumer. Line rappresenta l'ambiente esterno del sistema. Scrive caratteri singoli in Port,
da cui li ottiene Producer. Le due procedure sono sincronizzate mediante l'uso di u n m o nitor. P r o d u c e r inserisce il carattere in B u f f e r m e d i a n t e un altro m o n i t o r , da cui
Consumer ottiene pacchetti di n caratteri con n < s i z e ( B u f f e r ) . C o n s u m e r n o n deve attendere che il buffer sia pieno. Formalizzate un c o m p o r t a m e n t o corretto del sistema mediante un'appropriata asserzione invariante.

5.6.2.5

Caso di studio dell'uso di specifiche logiche

Per affrontare un problema pi complesso, applicheremo i formalismi logici al caso degli


ascensori studiato nel Paragrafo 5.5.4.2. Questo ci permetter di confrontare questo tipo di
formalismo con l'analisi fatta con le PN. Il primo passo della specifica quello di definire
un insieme di predicati elementari per la descrizione di propriet rilevanti circa lo stato e le
operazioni del sistema. Per esempio, at ( E, F, T ) potrebbe essere utilizzato per affermare che l'ascensore E (elevatoi) si trova al piano F ( floor) all'istante T (time). In maniera simile, start ( E, F, T, up) potrebbe indicare che l'ascensore E ha lasciato il piano F all'istante T per spostarsi verso l'alto.
Poi, si potrebbe scrivere un insieme di formule per descrivere le regole comportamentali del sistema. Per esempio,
(at(E,

F,

T)

and

o n(E B,

Flf

T)

and

F, >

F)

implies

start(E,

F,

T,

up)

s p e c i f i c a c h e , se nell'istante T l'ascensore E si trova al p i a n o F, e si a c c e n d e il s u o p u l s a n t e


i n t e r n o ( I B ) f a c e n t e r i f e r i m e n t o al p i a n o FJ, c o n F 3

>

F allora, n e l l o stesso istante, l'a-

s c e n s o r e c o m i n c i a a spostarsi verso l'alto.

Si noti che stiamo affermando propriet di oggetti di sistema come ascensori, piani,
etc., nello stesso modo con cui, negli esempi precedenti, avevamo affermato propriet per
le variabili di programma. Sono stati introdotti nuovi predicati (ad esempio, on e a t ) ed
stata specificata la loro semantica mediante l'uso di formule FOT nello stesso modo con cui
affermeremmo la propriet
x > y a n d y >

z implies x > z

per il predicato V nell'aritmetica.


Questo un buon esempio dei diversi usi che si possono fare delle specifiche. Quando
specifichiamo propriet per gli oggetti di sistema, come ascensori e pulsanti, stiamo specificando requisiti di sistema; quando specifichiamo propriet per le variabili di programma,
stiamo specificando la progettazione e Y implementazione dei programmi.
Per convenienza sintattica, useremo stringhe che cominciano con una lettera maiuscola
per indicare variabili e stringhe che cominciano con lettere minuscole per indicare predicati. Si noti che considereremo il tempo una variabile di sistema. Questo ci permetter di scrivere predicati riguardanti il tempo che intercorre tra diversi eventi (ad esempio, la richiesta
di servizio da parte di un ascensore e il suo arrivo al piano corrispondente).
Per formalizzare il sistema di ascensori seguiamo un approccio sistematico, che distingue l'insieme di predicati elementari, con i quali intendiamo descrivere l'evoluzione del sistema, in stati elementari ed eventi. Gli stati descrivono una condizione che ha una durata
non nulla nel tempo. La nozione globale di stato di un sistema, in un dato istante, l'insieme di tutti gli stati elementari verificati in quell'istante. Per brevit, in seguito faremo riferimento agli stati elementari semplicemente come stati. Il termine stato globale identificher
lo stato dell'intero sistema. Gli eventi descrivono le condizioni che si possono verificare solo in un determinato istante. Per esempio, l'evento
arrived

(E, F, T)

s i g n i f i c a c h e l'ascensore E g i u n t o al p i a n o F nell'istante T, m e n t r e lo stato


standing

(E, F, T w

T2)

s i g n i f i c a c h e l'ascensore E r i m a s t o f e r m o al p i a n o F dall'istante Tj all'istante T 2 .

In questa descrizione il tempo rappresentato esplicitamente da un singolo valore per


gli eventi, che sono per definizione istantanei, e da una coppia di valori per gli stati, dove la
coppia indica l'intervallo durante il quale rimasto invariato Io stato.
Il comportamento del sistema viene descritto usando formule di implicazione o regole, le quali sono un insieme di premesse, seguito dalla parola chiave i m p l i e s , seguita da
una conclusione, che deve discendere logicamente dalle premesse. Le regole sono implicitamente quantificate universalmente per simboli che appaiono alla sinistra di i m p l i e s e
quantificate esistenzialmente per i simboli presenti solo alla sua destra. Esse permettono di
dedurre le occorrenze di eventi o il cambiamento di uno stato in un determinato istante come conseguenza di un insieme di eventi o stati che si presentono in quell'istante o precedentemente, eventualmente soggetti a determinate condizioni. Dato che uno stato cambia

in seguito a un evento, un altro insieme di regole estende il periodo di tempo durante il quale rimane invariato uno stato, fino a quando non accade un evento che lo modifica.
Per non complicare eccessivamente lo studio faremo alcune semplici assunzioni.
Innanzitutto, assumeremo tempi decisionali nulli, come nel Paragrafo 5.5.4.2. Inoltre, escludiamo eventi simultanei generati dall'ambiente esterno. Queste decisioni non incidono in maniera sostanziale sulla specifica, ma permettono una semplificazione di determinate regole.
Se dovessero risultare non realistiche, le assunzioni possono sempre essere rimosse in maniera semplice, al prezzo di incrementare leggermente il numero di formule necessarie.
Forniamo ora un campione degli eventi, stati e regole che costituiscono la specifica del
sistema. Gli esercizi proposti guideranno il lettore nel completamento della specifica e nella modifica di alcune sue parti. Si ricordi che ci sono m piani e n ascensori. Il sistema viene
acceso all'istante t 0 .
Eventi

arrivai(E,
E

in

(t 0

F,

[1..n],

is

the

T)
F

in

[l..m],

initial

2 t0

time)

rappresenta l'arrivo dell'ascensore al piano F nell'istante T. Questo evento non indica se,
una volta arrivato, l'ascensore si ferma a quel piano o lo lascia immediatamente per un altro; n implica alcunch circa la direzione di spostamento dell'ascensore o il piano lasciato dall'ascensore.

departure(E,
E

in

F,

[l..n],

D,
F in

T)
[l..m],

D in

(up,

down),

T 2 t0

Descrive la partenza di un ascensore dal piano F nella direzione D all'istante T. Vedremo


nelle regole che questo evento pone l'ascensore in uno stato di moto dal piano F nella
direzione D.

stop(E,
E

in

F,

T)

[l..n],

F in

[1..m],

T 2 t0

Rappresenta l'arrivo e la fermata dell'ascensore E al piano F all'istante T. S t o p serve a soddisfare richieste di servizio interne ed esterne. Vedremo nelle regole che questo evento pone
l'ascensore E in uno stato di fermata al piano F.

new_list(E,
E

in

L,

[l..n],

T)
L

in

(l..m)*,

T t0

In ogni istante T, a ciascun ascensore associata una lista L di interi10 compresi tra l e m .
La lista rappresenta l'insieme dei piani cui l'ascensore si fermer, secondo le decisioni di schedulazione effettuate fino a quel momento dal componente di controllo del sistema. Una lista viene associata a ciascun ascensore affinch la strategia di schedulazione per le richieste
interne ed esterne possa essere rappresentata in maniera semplice da regole per la gestione
delle liste dei diversi ascensori. In ogni istante, ciascun ascensore "decide" che cosa fare se-

10

L'asterisco indica l ' o p e r a z i o n e d i c h i u s u r a transitiva; a * i n d i c a u n a s e q u e n z a di zero o p i a .

condo i contenuti della propria lista. N e w _ l i s t (E, L, T) significa che la lista associata all'ascensore E diventa L nell'istante T; quindi, il predicato rappresenta l'evento che trasforma lo stato di controllo dell'ascensore E impostando la sua lista di "prenotazioni" L.

cali(F,

D,

T)

request(E,

F,

in

[l..n],

T)
F

in

[l..m],

in

{up,

down),

2 t

Questi due predicati indicano gli eventi generati al di fuori del sistema: le chiamate esterne
dal piano F nella direzione D all'istante T e la "prenotazione" del piano F dall'interno dell'ascensore E all'istante T, rispettivamente. Entrambi gli eventi sono associati alla pressione
del corrispondente pulsante da parte di qualcuno che desidera inoltrare una richiesta. Se F = 1
o F = m, esiste un'ulteriore condizione sui valori dei parametri: D deve essere rispettivamente uguale a up o down.
Stati
Siccome gli stati sono propriet degli oggetti che hanno una durata, tutti i predicati che fanno riferimento agli stati hanno due parametri temporali che rappresentano i limiti dell'intervallo temporale durante il quale la propriet risulta vera. Useremo la convenzione che gli
intervalli di tempo associati ai predicati di stato siano chiusi a sinistra e aperti a destra; ovvero, il limite inferiore dell'intervallo di tempo sar incluso nell'intervallo, mentre quello superiore no (la notazione adottata per esprimere intervalli di tempo di questo tipo [T l f
T2 [). La motivazione di questa convenzione che gli effetti degli eventi devono essere intesi istantanei, in modo che un nuovo stato possa risultare vero dall'istante (incluso) in cui
accade l'evento che lo genera fino al momento (escluso) in cui accade l'evento che causa il
successivo istantaneo cambiamento di stato.
Dalla definizione di stato risulta chiaro che se una propriet che caratterizza uno stato risulta vera durante un determinato intervallo di tempo, sar vera durante qualsiasi intervallo di tempo contenuto nel primo. Inoltre, dato un intervallo durante il quale risulta
vero lo stato, lo stesso stato pu risultare vero anche in un intervallo pi ampio. Per esempio, se affermiamo che
moving(E,

(ovvero, lo

F,

D,

ali

T2)

stato di spostamento di u n ascensore E dal p i a n o F in direzione D) risulta vero

nell'intervallo [ T , ,
for

T,,

T 2 [ deve essere possibile dedurre

T 3 , T 4 ( T , TJ < T 4 S T 2 ) m o v i n g ( E ,

F,

D,

T31

T,)

Niente impedisce comunque che


moving(E,

F,

D,

T3,

T)

con T3 < Ti < T 2 < T 4 sia anch'essa vera. Forniremo pi avanti alcune regole specifiche
per definire queste assunzioni circa la semantica degli stati.
Forniamo ora un elenco di stati per gli ascensori:

standing(E,
E

in

[l..n[,

F,
F

T1(
in

T2 )
[l..m[,

t0

s T, <

T2

Specifica che nell'intervallo [ T l f T 2 [ l'ascensore E fermo al piano F. Come appena indicato, ci non implica necessariamente che l'ascensore E sia arrivato al piano F nell'istante Tj n che l'ascensore E lascer il piano F all'istante T2.

movingfE,
E

in

F,

D,

[l..n],

t < T, <

T
in

T2)

[ 1.. m ] , D

in

(up,

down),

T2

Significa che nell'intervallo [T x , T 2 [ l'ascensore E si sta muovendo in direzione D e che


l'ultimo piano da cui passato il piano F.

list

(E,

[ 1 . . n[ , L

in

L,

T j , T2)
in

[ 1 . . m [ * , t < T, <

T2

Specifica che nell'intervallo [T 1 ( T2 [ la lista associata all'ascensore E L. A differenza dei


predicati s t a n d i n g e moving, i quali indicano una propriet fsica dell'ascensore, il predicato l i s t indica una propriet di controllo.
Illustreremo ora le regole che descrivono il comportamento del sistema. Forniremo prima le regole per la descrizione dello spostamento degli ascensori e successivamente quelle
per la deduzione degli eventi e quelle per la deduzione degli stati.
Regole per mettere in relazione gli eventi e gli stati degli ascensori
Le regole che governano le relazioni tra stati ed eventi verranno, innanzitutto, descritte in
linguaggio naturale e successivamente nella loro versione formalizzata con espressioni
logiche.
Ri L'ascensore E, arrivando al piano F, lo lascia subito se quel piano non richiede servizio
e se la lista non vuota. Se il successivo piano da servire si trova pi in alto del piano F, la
direzione di spostamento sar verso l'alto; altrimenti, sar verso il basso. Adottiamo la convenzione che il piano cui fornire il servizio sia quello nella prima posizione della lista. Ci
viene indicato dalla funzione f i r s t applicata alla variabile L.
arrival(E,

F,

Ta)

and

list(E,

T,

Ta)

and

first(L)

L,
>

implies
departure(E,

F,

up,

Ta).

Una regola simile applicabile al caso in cui l'ascensore lascia un piano nella direzione opposta.
R 2 L'ascensore E, arrivando al piano F, si ferma se il piano in attesa di servizio, ovvero
se il numero del piano appare come primo elemento della lista associata all'ascensore.
arrival(E,

F,

Ta)

and

1i s t ( E ,

T,

Ta)

and

first(L)

L,

= F

implies
S top(E,

F,

Ta) .

R 3 L'ascensore E arriva al piano F con una lista vuota, e si ferma l. Assumendo che tutti gli ascensori siano in movimento per soddisfare le richieste di servizio dei piani che sono presenti nelle loro rispettive liste, dovrebbero partire da e arrivare ai piani con liste non
vuote. Questa regola diventa significativa se la politica di schedulazione permette la cancellazione degli elementi dalla l i s t di un ascensore in movimento (si vedano le regole di
controllo che verranno fornite tra poco).
arrival(E,
1i s t ( E ,

F,

Ta)

empty,

and

T,

Ta)

implies
s t o p ( E , F,

Ta).

R4 Assumiamo che gli ascensori abbiano un tempo di servizio costante e ben conosciuto At 3 . Se la lista dell'ascensore non vuota alla fine di questo intervallo, l'ascensore lascia il piano immediatamente.
stop(E,

F,

Ta )

1i s t ( E ,

L,

T,

first(L)

>

and
Ta

Atg)

and

imp1ie s
departure(E,

F,

up,

Ta

+ Ats | .

Una regola simile applicabile per le partenze dell'ascensore nella direzione opposta.
R 5 Alla fine del periodo di servizio se non ci sono piani da servire (ovvero, se la lista
dell'ascensore vuota) l'ascensore lascer il piano solo quando la lista cesser di essere
vuota.
stop(E,

F,

list(E,

empty,

Tp

+ At

>

Ta)

1ist(E , L , Tp,
first(L)

>

and
Ta

Ata,Tp)

and

and
T)

and

implies
departure(E,

F,

up,

Tp).

Come sempre, si pu fornire una regola simile anche per il senso di marcia opposto.
R i Come abbiamo fatto con le PN, assumiamo che il tempo At necessario perch un
ascensore si sposti da un piano al successivo, in una qualsiasi delle due direzioni, sia costante e conosciuto. L'arrivo a un piano avviene nell'istante At dopo l'istante di partenza
dal piano precedente.
departure

(E,

F,

up,

T)

i m p l i es
arrivai

(E,

1,

At).

Una regola simile pu essere espressa anche per il caso in cui il senso di marcia sia verso
il basso.

L ' e v e n t o d i f e r m a t a al p i a n o F n e l l ' i s t a n t e T a v v i a u n o s t a t o d i p e r m a n e n z a al p i a n o

che dura a l m e n o l'intervallo di t e m p o [ T ,


stop(E,

F,

Ats[.

T)

implie s
standing(E,

F,

T,

At3).

R 8 Alla fine di una permanenza della durata di A t s , se non ci sono altri piani da servire,
l'ascensore rimarr fermo al piano fino a quando la lista rimane vuota.
stop(E,

F,

1i s t ( E ,

empty,

Ts)

and
Ts

+ Ataf

T)

F,

T).

implies
standing(E,

T,

R 9 L'evento di partenza di un ascensore E dal piano F nella direzione D nell'istante T avvia uno stato di spostamento che dura un intervallo di tempo [ T, T + At [.
departure(E,

F,

D,

T)

moving(E,

F,

D,

implie s
T,

At).

R 1 0 Se uno stato rimane costante per l'intervallo di tempo [ T j , T2 [, allora rimarr costante anche per l'intervallo di tempo [ T 3 , T4 [, compreso in [ T i , T 2 [.
standing(E,

F,

Tt

Tj < T and

<

Tj and

TWT2)

and
T

T2

implies
standing(E,

F,

TJ,TJ).

Ci sono regole simili anche per tutti gli altri stati.


Regole di controllo
Le regole di controllo permettono di dedurre gli eventi del tipo n e w _ l i s t e gli stati del tipo l i s t ; in altre parole esprimono la strategia di schedulazione. Come esempio forniremo
le regole che esprimono la semplice strategia secondo la quale le richieste provenienti dall'interno dell'ascensore sono inserite immediatamente nella lista dell'ascensore in modo che
questa sia riordinata nella sequenza dal piano corrente all'ultimo se la direzione di marcia
verso l'alto o inversamente se la direzione di marcia verso il basso. Un elemento viene rimosso dalla lista non appena l'ascensore si ferma al piano corrispondente. Le chiamate esterne possono essere gestite in maniera simile (ad esempio, inserendo il piano richiesto nella lista dell'ascensore pi vicino, che si sta muovendo nella direzione giusta, o in quella di un
ascensore fermo).
R n L'evento per "prenotare" la fermata a un piano F dall'interno di un ascensore E che
non si trova gi a quel piano provoca un aggiornamento immediato alla lista L associata
a E, secondo la politica appena accennata. Questo evento pu essere descritto nel modo
seguente:

request(E,

F,

not

Ta (standing)E,

exists

list(E,
LF

L,

T)

and

Ta,TR)

F,

Ta,T)

and

and

insert_in_order(L,

F,

E))

implies
new_list(E,

LF,

T).

La relazione i n s e r t _ i n _ o r d e r una funzione che produce la nuova lista inserendo F sulla base della posizione e della direzione di marcia dell'ascensore E, secondo la politica definita. La formalizzazione dettagliata della funzione lasciata come compito al lettore. Come
abbiamo fatto con la specifica in PN, nascondiamo la politica di servizio in una sola funzione, in modo che un cambiamento della politica provochi ripercussioni solo sulla funzione i n s e r t i n o r d e r . Si tratta di un esempio del principio di progettazione in vista del
cambiamento applicato alla specifica.
R12

L'effetto dell'arrivo al p i a n o F di un ascensore E che si ferma la r i m o z i o n e del pri-

m o e l e m e n t o F dalla lista.
arrival(E,

F,

Ta)

and

1i s t ( E ,

T,

Ta)

and

F
Lt

=
=

L,

first(L)

and

tail(L)

implies
new_list(E,

Lt,Ta).

Il predicato t a i l ( L ) denota la parte rimanente di L dopo la cancellazione del primo elemento.


R 1 3 LO stato di controllo di un ascensore E rappresentato dalla lista che specifica tutte
le "prenotazioni" schedulate per R. La lista rimane uguale fino a quando scatta un evento di
tipo n e w _ l i s t .
new_list(E,

L,

T j)

and

not exists L,, T2 (new_list(E, L, T 2 ) and L, + L and T, < T 2 < T 3 )


implies
list(E,

L,

T,,

T3 ) .

Esercizi
5.36

Completate l'insieme di eventi, stati e regole fornito precedentemente. In particolare, descrivete gli eventi che accendono e spengono le luci dei pulsanti interni ed esterni dell'ascensore.
Definite la politica per la cancellazione delle "prenotazioni" esterne secondo entrambe le interpretazioni descritte nel Paragrafo 5.5.4.2.
Prestate attenzione alla coerenza tra i contenuti delle liste e l'accensione delle luci dei pulsanti. Se viene premuto un pulsante interno, il piano corrispondente appare nella lista dell'ascensore.
Inoltre, potrebbe nascere incoerenza tra la regola R ] 2 e la politica per l'accensione e lo spegnimento delle luci dei pulsanti esterni solo q u a n d o l'ascensore lascia il piano nella direzione
desiderata. Perch? C o m e si p u evitare il rischio di incoerenza?

5.37

La politica descritta per soddisfare le richieste interne ed esterne la stessa presente anche nel
Paragrafo 5.5.4.2?

5.38

Definite e formalizzate ulteriori politiche di servizio.

5.6.2.6

Verifica delle specifiche: un confronto tra lo stile descrittivo


e quello operazionale

Nel paragrafo precedente abbiamo usato la logica per specificare un sistema di ascensori. Sono
state fornite regole per descrivere il comportamento del sistema in termini di come questo
reagisce agli stimoli quando si trova in un determinato stato.
Analizzeremo ora la specifica, come abbiamo fatto nel caso delle reti di Petri. E possibile, infatti, "simulare" il sistema assumendo un dato stato in un determinato istante e controllando l'effetto dell'occorrenza di determinati eventi. Questo approccio formalizzato mediante l'uso di formule che a priori si assumono vere (e che quindi denotano fatti). Poi, le
regole fornite nel paragrafo precedente possono essere utilizzate per dedurre le conseguenza
di questi fatti.
Per esempio, possiamo definire lo stato dell'ascensore
standing

(2,

3,

5,

7) ;

Ovvero, l'ascensore 2 rimasto fermo al terzo piano almeno per il periodo che va dall'istante
5 all'istante 7; l'ascensore entra in questo stato come conseguenza di s t o p
(2,3,5).
Possiamo anche affermare
list

(2, e m p t y ,

request

(2,

5,

7) ;

8, 7) ;

Poi, applicando le regole della specifica, possiamo dedurre il fatto


new_list

(2,

{8},

7),

e, se escludiamo l'occorrenza di ulteriori eventi, possiamo dedurre anche i seguenti fatti


departure
arrivai

(2,

(2,

8,

3,

up,

7 +

7 + Ats

Ata);

+ Ata

(8-3))

mediante poche semplici deduzioni logiche.


Il formalismo logico descrive, quindi, l'operare del sistema mediante deduzioni. L'insieme
di formule una specifica del sistema nel senso che ogni implementazione deve garantire
che tutte le regole date siano vere. Ma se ci risulta vero, allora sono garantite anche tutte
le conseguenze delle regole.
La deduzione logica pu essere applicata, oltre che alla simulazione del comportamento
di un sistema, all'analisi delle propriet di un sistema. Infatti, in una specifica descrittiva come la descrizione logica del sistema di ascensori, una propriet di un sistema non altro che
una formula che prende in considerazione le variabili di sistema. Per esempio, la propriet
che tutte le richieste vengano soddisfatte prima o poi pu essere formalizzata come viene
mostrato qui di seguito. Si noti che in questa formula i simboli l j e t 3 si trovano solo sulla parte destra della formula e quindi, secondo le regole di quantificazione, sono assunte quantificate esistenzialmente.

newlist

(E,

L,

new_list

T)

and

(E,

Llf

T,)

implies

and

L, and

T, >

T2.

Se riuscissimo a dedurre la validit di questa formula dalle specifiche precedenti del sistema,
potremmo assumere che sia vera per qualsiasi implementazione valida del sistema. La dimostrazione di propriet di una specifica logica consiste, quindi, in un'attivit del tutto analoga alla deduzione della dinamica del sistema.
Possiamo notare un'importante differenza tra gli stili di specifica operazionale e di specifica descrittiva. Nelle specifiche operazionali, la descrizione dello stato di un sistema ben
diversa dalla descrizione delle propriet del sistema. Questo fatto ha un impatto enorme sul
modo in cui possiamo utilizzare un interprete (possibilmente automatico) per verificare le specifiche. Per esempio, potremmo usare un interprete di PN per verificare se, dopo avere inizializzato un sistema di ascensori con 30 piani, quattro ascensori, una determinata velocit e
una data sequenza di chiamate, tutte le richieste saranno soddisfatte entro un periodo di 30
secondi. Tuttavia, l'interprete non pu fornire prove per cui qualsiasi sistema con n piani, in
ascensori e velocit z, con qualsiasi sequenza con meno di y chiamate al minuto, il tempo
massimo di attesa tra una richiesta e il servizio sia w, espresso come una funzione di n, m, z
e y. In altre parole, un interprete di PN potrebbe essere utilizzato per simulare determinate
specifiche ma non per fornire prove circa la validit di determinate propriet.
Questi commenti si applicano in generale a tutti gli stili operazionali e descrittivi e a
tutte le propriet. Per esempio, si considerino le FSM. Se vogliamo conoscere quale sar lo
stato del sistema, iniziando da un determinato stato e applicando una data sequenza di transizioni, dobbiamo semplicemente "mettere in moto l'automa". Se invece vogliamo sapere se
il sistema si comporter in maniera ciclica, dobbiamo analizzare il grafo che rappresenta la
FSM alla ricerca di percorsi ciclici.
Come ulteriore esempio, si consideri la propriet di un sistema di assenza di deadlock.
Le PN hanno una definizione formale chiara di questa propriet, come abbiamo visto nel
Paragrafo 5.5.4. Un interprete di PN, per, non pu essere utilizzato per decidere l'assenza
di deadlock; pu solo, nella migliore delle ipotesi, "eseguire" il modello in diversi casi per
vedere se appare un deadlock. Pertanto, se vogliamo sapere se un sistema modellato da una
PN corre il rischio di incappare in un deadlock, dobbiamo analizzare il modello in un modo diverso da come lo eseguiamo".
Invece, si supponga di aver fornito una definizione formale dello stesso sistema usando le formule FOT. La propriet desiderata , ancora una volta, una formula FOT, quale
for

all

and

reachable

S,,

exists

S, ( ( s t a t e
(S 2 ,

(S,)

S, ) )

S] ( s t a t e

and

state

(S 2 )

implies
(S 3 )

and

S 3 * S, and

reachable

(S 3 ,

S2))

Ovvero, per qualsiasi stato s ^ e per qualsiasi stato S2 raggiungibile da s 1 ; il sistema pu evolvere verso uno stato s 3 , raggiungibile da s 2 . Di conseguenza, possiamo usare lo stesso interprete delle formule FOT per simulare un sistema o per decidere le propriet del sistema.
Ovviamente, una propriet indecidibile non diviene decidibile solo perch stato utilizza-

' 1 L'assenza di deadlock in una "PN pura" (ovvero una PN che rispetta la definizione originale) decidibile, anche se rappresenta, nel caso generale, un problema di complessit intrattabile.

to uno stile descrittivo per esprimerlo: semplicemente abbiamo un linguaggio omogeneo per
la descrizione del sistema e delle sue propriet.
D'altra parte, eseguire le specifiche logiche (ovvero costruire interpreti adeguati) potrebbe non essere semplice come per le specifiche operazionali. Per esempio, "interpretare"
un insieme di regole FOT richiede l'applicazione di regole di deduzione che, in generale,
sono caratterizzate da non determinismo: spesso, da un insieme di premesse, possono essere derivate diverse conclusioni parziali, di cui molte possono anche non portare alla conclusione finale desiderata.
utile ricordare che il problema di dimostrare teoremi nelle FOT indecidibile; in altre parole non possiamo decidere automaticamente se una data propriet (una formula FOT)
implicata da una data specifica (un'altra formula FOT). Linguaggi logici eseguibili come
il PROLOG, tuttavia, riescono ad approssimare piuttosto bene il potere deduttivo delle FOT
attraverso tecniche di interpretazione efficaci. Per questa ragione, possono essere utilizzati
come linguaggi di proto tipizzazione.
In conclusione, i formalismi operazionali sembrano pi orientati verso la simulazione
dei sistemi, mentre i formalismi descrittivi sono applicabili in maniera pi naturale all'analisi delle propriet.
Esercizio
5.39

Definite (non dimostrate!) una propriet del sistema di ascensori che afferma come il t e m p o
di attesa per ciascuna richiesta possieda u n limite superiore. Formalizzate questa propriet usand o una formula FOT.

5.6.3 Specifiche algebriche


Un altro stile di specifica descrittiva si basa sull'uso c\Y algebra, piuttosto che della logica,
come formalismo matematico sottostante. Essenzialmente, le specifiche algebriche definiscono
un sistema come uri algebra eterogenea, ovvero, come una collezione di diversi insiemi su cui
sono definite diverse operazioni.
Le algebre tradizionali sono omogenee. Un'algebra omogenea consiste in un unico insieme e diverse operazioni. Per esempio, gli interi, con le operazioni di addizione, sottrazione, moltiplicazione e divisione, sono un'algebra omogenea. Invece, le stringhe alfabetiche
(ovvero, le sequenze di caratteri), con le operazioni di concatenazione e calcolo della lunghezza non sono un'algebra omogenea, visto che il codominio dell'operazione di calcolo della lunghezza l'insieme dei numeri interi, non delle stringhe. Quindi, quest'algebra consiste di due insiemi, stringhe e interi, su cui sono definite le operazioni di concatenamento e
di lunghezza delle stringhe.
Molti sistemi software possono essere definiti in maniera naturale come algebre eterogenee. Dopo tutto, la definizione "collezione di insiemi e operazioni" molto vicina alla nozione di tipo di dato astratto introdotta nel Paragrafo 4.2.4.2. Nel Paragrafo 5.7.2.1 sottolineeremo alcune importanti connessioni esistenti tra le algebre eterogenee e i tipi di dati
astratti.
Vedremo ora le caratteristiche essenziali delle specifiche algebriche mediante alcuni
esempi. Come punto d'inizio, si consideri il semplice caso delle stringhe.

ESEMPIO 5 . 6

Supponiamo di voler specificare un sistema per la gestione delle stringhe. Il primo insieme
di fatti da registrare sono le operazioni necessarie e gli insiemi coinvolti da queste operazioni.
In questo caso, assumiamo di voler gestire le stringhe mediante le seguenti operazioni:

Creazione di nuove stringhe vuote (operazione new);

Concatenamento di stringhe (operazione append);

Aggiunta di un nuovo carattere alla fine di una stringa (operazione add);

Controllo della lunghezza di una data stringa (operazione l e n g t h ) ;

Controllo se una stringa vuota (operazione

Controllo se due stringhe sono uguali (operazione e q u a l ) .

isEmpty);

U n a breve i s p e z i o n e della lista indica c h e gli i n s i e m i c o i n v o l t i , oltre all'insieme delle stringhe, chiamato S t r i n g , sono:

Char: l'insieme dei caratteri alfabetici;


Nat: l'insieme dei numeri naturali;

B o o l : l ' i n s i e m e dei valori logici,

{true,

false}.

La collezione di insiemi che formano l'algebra eterogenea detta segnatura. Ogni insieme,
a sua volta, detto un sort (letteralmente "tipo") dell'algebra. Per definire un'algebra eterogenea , quindi, necessario specificare la sua segnatura, le operazioni coinvolte, e i loro domimi e codominii. Questa definizione viene chiamata la sintassi dell'algebra e pu essere espressa mediante diverse notazioni. Adotteremo una notazione basata sul linguaggio di specifica
Larch. La notazione piuttosto intuitiva e vicina ad altri linguaggi algebrici. Segue la sintassi dell'algebra per le stringhe in una notazione tipo Larch.
algebra

StringSpec;

introduces
sorts

String,

Char,

Nat,

Bool;

operations
new:

()

append:
add:

String;

String,

String,

length:

String

isEmpty:
equal:

String

Char

String

String,

>

String;

String;

> N a t ;

Bool;

String

Bool.

Il tipo s t r i n g solo uno degli insiemi coinvolti. II fatto che si sia interessati alla sua definizione e che, invece, i tipi Char, Nat e B o o l siano "tipi ausiliari" non esplicito nella notazione; solo evidenziato dal nome dato all'algebra stessa nella prima linea. Si noti che
l'operazione new non ha argomenti (ovvero, non ha alcun dominio). Questo un modo
convenzionale per indicare valori costanti. Infatti, una funzione senza argomenti deve avere un solo valore possibile. In questo caso la "stringa vuota".

I significati delle singole operazioni specificate sopra sono molto chiari in questo caso.
Ci nonostante, il significato deve essere specificato in maniera precisa e lo si fa mediante
la semantica dell'algebra, usando equazioni che sono intese a definire le propriet essenziali
che devono verificarsi quando vengono eseguite le operazioni. Per questo motivo, tali equazioni vengono dette anche assiomi dell'algebra.
Seguono alcune propriet ovvie delle operazioni dell'algebra S t r i n g : (1) la stringa creata dall'operazione new una stringa empty (vuota). In questo caso si tratta pi di una definizione che di una propriet. (2) Il risultato del concatenamento di una stringa vuota a
un'altra stringa corrisponde alla stessa stringa. (3) Il risultato dell'aggiunta di un carattere a
una qualsiasi stringa non pu mai corrispondere alla stringa vuota.
La formalizzazione di queste e di altre propriet nella nostra notazione la seguente:
constrains
for

ali

new,

append,

add,

length,

isEmpty(new() ) =

equal

so

tbat

true;

isEmpty(add(s , c )) =
length(new())

false;

0;

length(add(s,c))
append(s,new())

=
=

length(s)+l;
s;

append(s,,add(s2,c))
equa 1 ( n e w ( ) , n e w ( ) ) =

add(append(slfs2),c);
true;

equa 1 ( n e w ( ) , a d d ( s , c ) ) =

false;

equa 1 ( a d d ( s , c ) , n e w ( ) ) =

false;

equa 1 ( a d d ( s w c ) , a d d ( s 2 , c)
end

isEmpty,

[ s , s [, s 2 : S t r i n g ; c : C h a r ]

= e q u a 1(s^, s 2 ) ;

StringSpec.

Esaminiamo le equazioni fornite. Per cominciare, si osservi che stato fatto uso di cinque
simboli non dichiarati e non definiti, ovvero, 0, 1, +, t r u e e f a l s e . Gli altri simboli, incluse le parentesi e '=' (da non confondere con l'operazione "equal"), sono o simboli della notazione o sono stati definiti nella sintassi. Se avessimo voluto essere del tutto rigorosi,
avremmo dovuto dichiarare '+' come un'operazione dell'algebra Nat e 0,1, t r u e e f a l s e
come costanti (ovvero, funzioni con zero argomenti) dei rispettivi tipi. Inoltre, avremmo dovuto fornire assiomi per specificare le propriet di quelle algebre; in particolare, secondo l'assiomatizzazione classica dell'aritmetica di Peano, avremmo dovuto affermare che ' l ' il risultato dell'operazione s u c c e s s o r (successivo) applicato al valore costante 'o'. Per evitare
un numero eccessivo di equazioni molto ovvie, le abbiamo assunte come implicite.
Procediamo con l'analisi della specifica. Non c' dubbio che le equazioni di questa specifica descrivono "verit" riguardanti le stringhe. Quello cui siamo interessati, per, la possibilit di derivare ulteriori propriet a partire da queste, come abbiamo fatto con le specifiche logiche. E per questo motivo che le equazioni vengono dette axioms (assiomi).
Per esempio, la seguente propriet dovrebbe risultare vera per ogni carattere c:
a p p e n d ( n e w ( ) , a d d ( n e w ( ),c) ) =

add(new(),c)

Questa propriet pu essere derivata dagli assiomi nella seguente maniera: se s s 2 =


new( ) nell'assioma
append(s,,

add(s2,c))

add(append(st,

s2),c)

otteniamo
append(new(),add(new(), c )) =

add ( a p p e n d ( n e w ( ) , n e w ( ) ) , c )

(i)

Poi, sostituendo s = new( ) nell'assioma a p p e n d ( s , n e w ( ) )


= s , otteniamo
a p p e n d ( new( ) , new( ) ) = new( ). Sostituendo questo risultato in ( i ) , otteniamo
la propriet desiderata.
In maniera simile, sia < c t , . . . , c> un'abbreviazione per
add(add. . .(add(new( ), c,),...),

c)

pertanto facile dimostrare che


append

( <c3, c2> , <c3, c4> ) =

per ogni c 1 (

c2,

c3i

<c !, c 2 , c 3 , c 4 >

c4.

Si considerino le propriet
append(new(),

s)

(ii)

e
append

(s1(

a p p e n d ( s2, s3 ) ) =

append)append(s,,

) ,s,)

(iii)

Queste due propriet possono essere dimostrate per induzione. Mostriamo come, nel caso di (ii). Questa verificata per s = new( ) applicando l'assioma append(s,
new( ) ) = s con s = new( ). Si assuma ora che ( i i ) sia verificata per una stringa data
Si, e sia s add ( s !, c ). Di conseguenza a p p e n d ( n e w ( ) , s ) = a p p e n d ( n e w ( ) ,
add ( s !, c ) ) = add ( a p p e n d ( n e w ( ) , s 1 ) , c ) = a d d ( s 1 , c ) = s . Quindi, ( i i )
verificata anche per s.
Il precedente ragionamento intuitivo, per, assume che ogni stringa s, diversa da
new( ), sia del tipo add( s x , c ) per qualche s x , c. Le operazioni new e add vengono di
conseguenza chiamate generator (generatori) dell'algebra S t r i n g S p e c . Nonostante questo
fatto sembri ovvio, non esplicitamente espresso nella formalizzazione di S t r i n g S p e c . Per
specificare ci, modifichiamo l'intestazione della precedente definizione semantica nel seguente modo:
constrains

new,

append,

StringSpec
for

all

add,

generated

length,
by

[new,

[s , s t,s 2 : S t r i n g ;

c:

isEmpty,

equal

so

that

add]
Char]

Questo costrutto esprime esplicitamente che tutti gli elementi del sort S t r i n g possono
essere ottenuti come combinazione adeguata di operazioni new e add. In maniera simile,
tutti i numeri naturali possono essere generati a partire dalla costante 'o' e l'operazione
successor.
Ora, assumiamo che la precedente algebra venga arricchita introducendo caratteri costanti, come 'a', ' b ' , ... (dove le costanti sono racchiuse tra virgolette singole).
Formalmente, queste costanti sono introdotte definendole come funzioni senza argomenti,
come abbiamo fatto per l'operazione new; per esempio, a : ( )
Char. ' a ' verr utilizzata come abbreviazione di a ( ). Si consideri, poi, la propriet esprimibile con la formula
equal|add|s,'a'),add(s,'b'))

false

Intuitivamente, questa formula sembra essere vera. Non esiste, per, un modo per dimostrare che sia vera12 all'interno del sistema di equazioni.
L'impossibilit di dimostrare la formula in questione dimostra che gli assiomi forniti
per questo sistema sono incompleti, ovvero, non ci permettono di dedurre tutte le "verit"
desiderate della nostra algebra. Fortunatamente, in questo caso, le specifiche formalizzate dalle equazioni precedenti possono essere completate facilmente (non sempre, per, semplice
ottenere la completezza!) arricchendo la sintassi del linguaggio nel seguente modo:

Aggiungendo una nuova operazione e q u a l c , la quale definisce Vuguaglianza tra caratteri. Questa operazione dovrebbe essere definita da equazioni del tipo
equalC(

a' , 'a' ) =

equalC('a','b')

true;
false;

Sostituendo l'ultima equazione della semantica precedente con


e q u a 1 ( a d d ( s t , c , ) , a d d ( s 21 c 2 ) ) = e q u a l ( s , , s , )

and

equalC(c,,c2)

(j)

Chiaramente, durante la stesura delle specifiche algebriche, come con altri stili, l'incompletezza non l'unico rischio. Un altro pericolo quello di sovraspecificare (ovvero, fornire limitazioni eccessive) un sistema, come accadrebbe, per esempio, nel caso in cui aggiungessimo l'assioma
e q u a l ( add ( slf e, ), add ( s 2 , c 2 ) ) = e q u a l ( s w c 2 )
equalC(c,,c2)

and

not

and

equalC(clf'a')

(jj)

invece di ( j ). Infatti, ( j j ) afferma impropriamente che due stringhe possono essere uguali solo se non contengono il carattere ' a ' !
Potremmo anche scrivere specifiche contraddittorie o incoerenti, come nel caso aggiungessimo questo assioma
e q u a l ( a d d ( s , c ), s ) =

true

In generale, un insieme di equazioni algebriche contraddittorio, o incoerente, se consente di


dimostrare che t r u e = f a l s e .
Potremmo scrivere specifiche ridondanti, per esempio, aggiungendo all'insieme di
assiomi
append(new(),s)

= s

12
II lettore dovrebbe prestare attenzione a certe complicazioni delle formule matematiche. In questo caso vogliamo dimostrare una formula che afferma la falsit di un'altra formula. Dovremmo ricordarci che,
in generale, il fatto che non sussistano prove di una formula non significa che questa sia falsa. N o n esiste una dimostrazione per e q u a l ( a d d ( s , ' a ' ) , a d d ( s , ' b ' ) ) = t r u e . N o n esiste n e m m e n o la
dimostrazione per e q u a l ( a d d ( s , ' a ' ) , a d d ( s , ' b ' ) ) = f a l s e , ma "intuiamo" che questa formula dovrebbe essere vera e vorremmo poterla dimostrare. Ancora una volta, la nostra esposizione di questi argomenti teorici si affida all'intuito del lettore e lascia un approfondimento del trattamento matematico alla letteratura.

visto che questa formula pu gi essere derivata come conseguenza degli altri assiomi. Nella
pratica, tuttavia, la ridondanza nelle specifiche molto meno problematica dell'incoerenza
o dell'incompletezza e pu essere spesso ignorata.
In conclusione, una specifica algebrica definisce un sistema come una collezione di insiemi e operazioni, espresse nella segnatura della specifica, i cui elementi soddisfano le equazioni della parte semantica e, quindi, anche le equazioni derivabili.

Acquisiremo una conoscenza pi approfondita delle specifiche algebriche attraverso un esempio pi elaborato.
ESEMPIO 5 . 7

Supponiamo di voler specificare un editor di testi. La specifica deve indicare i tipi di dati su
cui deve poter operare l'editor (ovvero, i tipi, le operazioni disponibili e i loro significati).
Inizialmente, consideriamo un editor di testi molto semplificato, adatto alla gestione di file di testo molto semplici, dotato del seguente insieme di operazioni dove il suffisso F viene usato per indicare che l'operazione agisce sui file:
: crea

un nuovo

file

NewF

vuoto;

isEmptyF: indica se un file vuoto;

A d d F : a g g i u n g e u n a s t r i n g a d i c a r a t t e r i alla fine d e l

I n s e r t F : i n s e r i s c e u n a s t r i n g a i n u n a d a t a p o s i z i o n e d e l file. Il r e s t o d e l file v i e n e s p o -

file;

s t a t o alla p o s i z i o n e i m m e d i a t a m e n t e s u c c e s s i v a alla s t r i n g a i n s e r i t a ;

due file;

A p p e n d F : concatena

Altre operazioni che verranno discusse pi avanti o che possono essere immaginate dal
lettore come esercizio.

Grazie alle similitudini con l'esempio precedente, la seguente specifica algebrica per l'editor
di testi dovrebbe risultare chiara:
algebra

TextEditor;

introduces
sorts

Text,

String,

Char,

Bool,

Nat;

operations
newF:

( )

isEmptyF:
addF:

Text;

Text

Text,

> B o o l ;

String

>

Text;

insertF:

Text,

Nat,

String

appendF:

Text,

Text

>

deleteF:

Text

lengthF : Text
equalF:
addFC:

Text,
Text,

>

>

Text;

Text;

Text;

> N a t ;
Text
Char

>
>

Bool;
Text;

{Questa u n ' o p e r a z i o n e ausiliaria che sar necessaria


p e r d e f i n i r e a d d F e a l t r e o p e r a z i o n i sui f i l e . I n o l t r e ,

constrains
TextEditor
for

ali

[f,

si a s s u m e c h e t u t t e le o p e r a z i o n i p r e c e d e n t e m e n t e
i n t r o d o t t e in S t r i n g S p e c s i a n o a n c o r a d i s p o n i b i l i .
Per c h i a r i r e la d i s t i n z i o n e tra i d u e t i p i di o p e r a z i o n i ,
le s e c o n d e p o s s i e d o n o il s u f f i s s o 'S'. Si a s s u m e c h e
la s i n t a s s i e la s e m a n t i c a di q u e s t e a p p a r t e n g a n o
anche a TextEditor, senza copiarle e s p l i c i t a m e n t e )
n e w F , i s E m p t y F , a d d F , a p p e n d F , i n s e r t F , d e l e t e F so that
g e n e r a t e d by [ n e w F , a d d F C ]
fi,f ; :

Text;

isEmptyF(newF())=

s:

String;

isEmptyF(addFC(f,

c))=

a d d F ( f , news( ) )=

f;

addF(f , addS(s,

c))=

lengthF(newF())=

0;

lengthF(addFC(f,

c))=

appendF(f,
appendF(f,,

c:

Char;

cursor:

Nat]

true;

newF())=

false;

a d d F C ( a d d F ( f , s),

lengthF(f)

c);

1;

f;

addFC(f2,c))=

a d d F C ( a p p e n d F ( f , , f ) , c ) ;

equalF(newF(),newF())=

true;

equalF(newF(),addFC(f,

c))=

equalF(addFC(f , c),new())=

false;
false;

equalF(addFC(f1,c1),addFC(f2,c2)=
e q u a l F ( f 1 ( f 2 ) and
insertF(f,cursor , newS())=

equalC

(c,,c| ;

f;

((equa 1 F ( f , a p p e n d F ( f , , f 2 ) ) and

( l e n g t h F | ,)=

cursor

1))

imp1ie s
equalF(insertF
end

(f,cursor,s),

appendF(addF(f1,s),f2)))=

true;

TextEditor.

L'ultima equazione sembra piuttosto complicata. Per rendere le equazioni pi chiare, potremmo scriverle come
if

( e q u a l F ( f , a p p e n d F ( f ! , f 2 ) ) and
(insertF(f,cursor,s)

(lengthF(f,)=cursor

1))

then

= a p p e n d F ( a d d F ( f , s), f 2 ) )

Le equazioni di questo tipo vengono chiamate equazioni condizionali.


Esaminiamo il reale significato delle operazioni definite dalle equazioni precedenti. Le
equazioni algebriche esprimono relazioni tra gli elementi degli insiemi coinvolti, non relazioni riguardanti variabili che immagazzinano valori. Sarebbe quindi plausibile, ma non necessario, dedurre dalle equazioni precedenti che il risultato dell'applicazione di i n s e r t F ( f ,
c u r s o r , s) sia una modifica dello stato del file f che consiste nell'inserimento della stringa
s tra le porzioni f t e f 2 del file. Per sottolineare la differenza con i linguaggi di programmazione convenzionali, la sintassi per le specifiche algebriche usa la parola chiave o p e r a t i o n s
invece delle parole chiave tradizionali p r o c e d u r e e f u n c t i o n .
Entrambe le seguenti implementazioni dell'operazione di inserimento sarebbero adeguate
rispetto alla specifica fornita:

Implementazione 1 : l'operazione modifica f nella maniera specificata;

Implementazione 2: l'operazione crea un nuovo file il cui valore calcolato nel modo
specificato.

In generale, ovviamente, i file system ad accesso diretto tendono a lavorare secondo l'implementazione 1 (in assenza di comandi che impongono il contrario), mentre file system sequenziali lavorano necessariamente secondo l'implementazione 2.
Anche se la specifica contiene un numero considerevole di equazioni (il lettore non deve dimenticare che ne sono state lasciate molte implicite), ancora lontana dall'essere una
specifica per un editor di testi realistico. Il procedimento per completarla dovrebbe per ora
risultare chiaro.

Le specifiche algebriche sono una notazione utile per la specifica della semantica di moduli come i tipi di dati astratti. Possono, quindi, complementare le notazioni di progetto
come il T D N / G D N o i diagrammi delle classi UML con lo scopo di aggiungere informazioni semantiche ai moduli. Le specifiche algebriche si differenziano dalle pre/post
condizioni e dagli invarianti in quanto forniscono specifiche pi astratte della semantica
dei tipi di dati astracci. Le pre/post condizioni e gli invarianti sono infatti espresse in termini di implementazioni di tipi di dati astratti (ad esempio, in termini di variabili e parametri di classi).
Esercizi
5.40

Dimostrate che le seguenti propriet sono vere per l'algebra S t r i n g S p e c introdotta in questo paragrafo:
equal(<,a,i'c'/,a'fldl>
equal(< 'a' , ' c ' / a ' / d ^

, <'a','c,,,a,,,d,>)=
, <'a',,c,,,d,,,a,>)=

true
false

5.41

Se immaginiamo che le operazioni dell'Esempio 5.7 siano comandi interattivi richiesti dall'utente per manipolare file di testo, si p u notare che sono in definitiva troppo limitate. La
mancanza pi evidente che l'utente n o n ha a disposizione un m o d o per nominare i file. E
possibile solo costruire nuovi file e operarvi. Si estenda la specifica per includere la facolt di
n o m i n a r e i file. L'utente deve essere in grado di fornire identificatori ai file al m o m e n t o della creazione e usare un identificatore per specificare il file su cui deve agire una determinata
operazione.
Il n o m e o identificatore di u n file va considerato diversamente dai precedenti identificatori di
variabile f , f ! e f 2 di sort T e x t . In altre parole, il n o m e dovrebbe essere un attributo di un
file che lo identifichi univocamente.

5.42

Estendete la specifica fornita nell'Esempio 5.7 con le seguenti operazioni:

change (f, s

f i n d ( f , s ) : restituisce un valore booleano che indica se s in f e, se lo , restituisce la posizione del p r i m o carattere di s in f . (Ci sono u n paio di punti problematici in questa definizione n o n formale; riuscite a individuarli?)

a 2 ) : sostituisce tutte le occorrenze di s t in f con s 2 ;

5.7

Stesura e uso delle specifiche nella pratica

Nei paragrafi precedenti sono stati mostrati esempi di modelli e notazioni che possono essere usati per fornire specifiche di diversi tipi di sistemi. Nonostante molti degli esempi forniti fossero ispirati da problemi reali, i sistemi presi in considerazione erano molto semplici. In questo paragrafo, affronteremo il problema dell'applicazione delle tecniche di specifica a sistemi reali. Dopo aver esaminato cosa occorre per scrivere e usare le specifiche nella
pratica, studieremo alcuni modi per affrontare i problemi pratici per trattare sistemi complessi di grande dimensione.

5.7.1

Requisiti per le notazioni di specifica

Esaminiamo in maniera critica i benefici che si ottengono e i problemi che nascono dall'uso dei formalismi introdotti in questo capitolo. Certamente, applicando i principi di rigore
e di formalit, siamo stati in grado di chiarire e precisare molte specifiche che altrimenti avrebbero potuto rimanere vaghe o ambigue fino a un punto avanzato della progettazione o dell'implementazione.
Anche nella specifica di sistemi relativamente semplici, abbiamo osservato come dovessero essere presi in considerazione molti dettagli. Per esempio, una descrizione completa
di un sistema di ascensori per grattacieli in termini di PN produrrebbe una rete enorme, ingestibile e incomprensibile; anche la specifica algebrica di un editor di testi "giocattolo" ha
mostrato come siano richieste moltissime equazioni, sia esplicite che implicite.
Costruire specifiche dei requisiti per sistemi reali probabilmente un attivit tanto complessa quanto la progettazione dell'implementazione di questi sistemi: il documento di specifica risultante, sia che venga utilizzato un linguaggio formale sia che venga utilizzato un
linguaggio naturale, pu essere complesso quanto il documento di progettazione o il codice stesso! Di conseguenza, tutti i principi espressi nel Capitolo 3 dovrebbero essere applicati anche alla costruzione di specifiche complesse13. Anche molte tra le "tecniche di progettazione" esaminate nel Capitolo 4 risultano estremamente utili per la gestione della "progettazione" di specifiche complesse.
Avendo enfatizzato l'importanza del rigore e della formalit nelle sezioni precedenti,
esamineremo ora qualche altro principio. Tutti i "linguaggi di specifica" finora presentati (le
macchine a stati finiti, i DFD, le PN, le equazioni logiche e algebriche) sono in relazione ai
linguaggi di specifica reali come lo sono i linguaggi di programmazione "giocattolo", come
quelli utilizzati in molti corsi introduttivi di informatica, ai linguaggi di programmazione
reali. Permettono al lettore di cogliere le propriet essenziali dei linguaggi reali, ma sono adeguati solo all'espressione di semplici algoritmi, come algoritmi di ordinamento o funzioni
trigonometriche, e non alla descrizione di sistemi reali come quelli per il calcolo degli stipendi dei dipendenti o per la compilazione di codice sorgente. Dove perdono il confronto
con i linguaggi di programmazione reali nei meccanismi di astrazione e di modularizzazione. Il prossimo paragrafo illustrer come anche i formalismi per la specifica possano essere dotati di tali meccanismi.

13

Infatti, la nostra presentazione della specifica del sistema di ascensori stata sviluppata in maniera modulare, anche se non stata fornita la definizione formale di "modulo PN" o "modulo FOT".

Il principio della separazione degli interessi ha anch'esso implicazioni naturali nell'ambito delle specifiche. Per esempio, quando possibile, le specifiche funzionali (ovvero, la definizione di che cosa deve fare un sistema in conseguenza di un determinato input) devono
essere tenute separate dalle specifiche prestazionali (ovvero, la definizione dei requisiti di efficienza), dalle specifiche di interfaccia utente, etc.
A volte, la separazione degli interessi pu anche risultare nell'uso di diverse notazioni per
la specifica di differenti aspetti di un sistema. Per esempio, supponiamo di voler descrivere la
produzione di un documento in un sistema di automazione di un ufficio. Il DFD della Figura
5.44(a) descrive quali sorgenti di informazioni e quali tipi di dati sono necessari per produrre
il documento: una libreria di modelli predefiniti (Predef i n e d T e m p l a t e s ) , ovvero di contratti di diverso tipo in cui mancano dati specifici, come nomi, date, e quantit; un database
di clienti (customers) che sono gi entrati in contatto con l'ufficio; i clienti stessi; e un insieme
di formati predefiniti (Predef i n e d F o r m a t s ) tra cui scegliere per la stampa dei documenti.
La figura, per, non specifica l'ordine di esecuzione delle azioni che portano alla composizione e alla stampa del documento. Ci specificato dalla FSM della Figura 5.44(b), la
quale mostra che, dopo avere acquisito il nome del cliente, possibile ottenere ulteriori dati (l'indirizzo, la data di nascita, etc.) o dal database C u s t o m e r o attraverso l'interazione
con il cliente. Intuitivamente se nel database gi presente un record per il cliente, allora
viene seguito il ramo "prendi ulteriori dati dal database"; il ramo alternativo viene seguito
se nel database non ancora presente un record per quel cliente. Ci, per, non viene indicato esplicitamente nella figura. Se avessimo voluto essere pi precisi, avremmo potuto
espandere il modello aggiungendo etichette adeguate per specificare sotto quali condizioni
debba essere presa una via piuttosto che l'altra. Come alternativa alla FSM per illustrare i
requisiti di controllo possibile usare un sequence o un collaboration diagram.
In maniera simile, abbiamo gi notato nel Paragrafo 5.6.1 che i DFD e i diagrammi
ER possono complementarsi a vicenda fornendo "viste" diverse delle descrizioni del sistema: il primo pi adatto a fornire una specifica delle funzionalit del sistema e il secondo pi
adatto a fornire una specifica delle relazioni esistenti tra i dati.
L'incrementalit forse ancora pi importante per le specifiche che per l'implementazione. Raramente tutti i requisiti sono ben compresi all'inizio di un progetto software.
Solitamente, la formulazione finale il risultato di un lungo processo di tentativi, molti dei
quali falliscono. Inoltre, in ogni caso saggio focalizzare inizialmente l'attenzione sui requisiti
pi rilevanti e critici del sistema. Successivamente, possono essere presi in considerazione i
requisiti minori, che verranno espressi in documenti appropriati.
L'incrementalit dovrebbe essere applicata nella costruzione di specifiche, anche al livello di rigore e formalit, nel senso che, inizialmente, difficile procedere a formulazioni
precise. In molti casi, i concetti non sono precisi neanche nella mente della persona che espone i requisiti. Le formulazioni iniziali tendono, quindi, ad essere informali e piuttosto oscure, scritte in un linguaggio naturale o in una qualche notazione grafica. Pi avanti, le specifiche cambieranno; anche il linguaggio in cui sono espresse cambier, diventando da informale a semiformale e da semiformale a formale.
Molti degli esempi di specifica forniti nei Paragrafi 5.5 e 5.6 sono stati derivati in maniera incrementale, sia progredendo da un'esposizione informale a una formale (si ricordi il
sistema di ascensori, in particolare l'accensione e lo spegnimento delle luci dei pulsanti) sia
modificando le formulazioni precedenti, date nello stesso formalismo (ancora una volta, il

(a)

o
Get user name

0
Search in
Customers
Get other data from
the database

Get other relevant data


from user interaction

Get appropriate text


skeletons from
predefined text 1ibrary

Compose the document by choosing


formatting options
(this involves interaction with the user and
access to the Formats database)

Print document

(b)

Figura 5.44

D u e viste d e l l a p r o d u z i o n e di d o c u m e n t i .
(a) Vista dei dati m e d i a n t e D F D .
(b) Vista di c o n t r o l l o m e d i a n t e F S M .

caso dell'accensione e dello spegnimento delle luci dei pulsanti degli ascensori un buon
esempio).
Nello scrivere specifiche, una formalizzazione completa del documento non solitamente necessaria. gi stato sottolineato nel Capitolo 3 che anche i matematici, quando
formalizzano e cercano di dimostrare teoremi, procedono in modo incrementale, aggiungendo dettagli e formalit man mano che vengono effettuati passi critici di definizione e di

dimostrazione. Allo stesso modo, un documento di specifica potrebbe essere un insieme di


espressioni fornite in un linguaggio naturale non formale, figure semiformali e modelli pienamente formali, quando necessari, e sempre sotto la completa responsabilit del progettista.
Un altro motivo per adottare combinazioni di "linguaggi" di specifica risiede nel fatto
che le specifiche sono frequentemente utilizzate da persone diverse con differenti scopi. Per
esempio, alquanto improbabile che un contratto per un prodotto software sia firmato sulla base di un documento scritto in logica matematica, in quanto dovr essere compreso dagli utenti finali.
Insomma, parlare di linguaggi di specifica difficile almeno quanto parlare di linguaggi di programmazione. In genere, un documento di specifica non sar una PN enorme, ancorch ben strutturata e ben modularizzata. N sar una singola formula FOT. In molti casi, un documento d specifica utilizza diversi formalismi per ottenere differenti scopi.
L'estensore potr utilizzare specifiche non formali, semiformali e formali in un unico documento, ponendosi come obiettivo primario la chiarezza, la precisione e l'immediata comprensibilit. Spesso, una descrizione completa in un unico linguaggio formale pu sembrare pi un frammento di codice implementativo piuttosto che una definizione del sistema.
Quindi, il "linguaggio di specifica ideale" risulta ancora pi irrealistico del "linguaggio di
programmazione ideale". Ci non rappresenta, per, un buon motivo per non utilizzare alcun linguaggio di specifica!
Infine, il processo di stesura, analisi, modifica e uso delle specifiche per controllare un'implementazione pu trarre benefici dall'uso di strumenti appropriati. Nel caso pi banale, si
pu utilizzare un editor di testi per la stesura dei documenti in un linguaggio naturale.
chiaro che, per semplificare l'intero processo, si richiedono sempre nuovi supporti, molti dei
quali sono gi disponibili - quali editor grafici per aiutare nella stesura di DFD o PN e strumenti per esaminare le specifiche alla ricerca di diverse propriet - ed altri lo saranno in futuro. Nel prossimo paragrafo, vedremo come UML combini diverse notazioni in un linguaggio
di specifica inteso per l'uso pratico.
Esercizi
5.43

Le specifiche prestazionali possono essere separate da quelle funzionali nei sistemi real-time?
Argomentate la vostra risposta.

5.44

Fornite una vista di controllo espressa con una P N pe