Sei sulla pagina 1di 192

Software Engineering

Michele Del Regno, Luca Aliberti, Mario Boccia, Mario Amato

Anno Accademico 2021/2022

Contents
1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1 Brook’s Law . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

2 Modelli di Processo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2 Modello di Processo di Produzione del Software . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.1 Ruoli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.2 Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2.3 Linee Guida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 Utilità dei Modelli Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4 Un Semplice Esempio di Processo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.5 Principali Attività del Processo di Produzione del Software . . . . . . . . . . . . . . . . . . . 13
2.6 Modelli di processo tradizionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.6.1 Modello a cascata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.6.1.1 Vantaggi e aspetti positivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.6.1.2 Svantaggi e rischi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.6.1.3 Varianti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.6.1.4 Applicabilità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.6.2 Modello a V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.6.3 Modello incrementale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.6.4 Di↵erenze tra modello incrementale e iterativo . . . . . . . . . . . . . . . . . . . . . . 19

3 Ingegneria dei Requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20


3.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.2 Attività dell’Ingegneria dei Requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.3 Vantaggi dell’Ingegneria dei Requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4 Elicitazione e raccolta dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.4.1 Elicitazione ad alto livello dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.4.2 Elicitazione dettagliata dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.5 Analisi dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.5.1 Categorizzazione dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

1
3.5.1.1 Categorizzazione semplice dei requisiti . . . . . . . . . . . . . . . . . . . . . . 26
3.5.1.2 Categorizzazione dei requisiti tramite le 6 dimensioni . . . . . . . . . . . . . 26
3.5.1.3 Categorizzazione e analisi dei requisiti tramite VORD - Viewpoint-Oriented
Requirement Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.5.2 Prioritizzazione dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.6 Definizione, prototipizzazione e revisione dei requisiti . . . . . . . . . . . . . . . . . . . . . . . 29
3.6.1 Definizione dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.6.1.1 Linguaggio Naturale Strutturato . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.6.1.2 Data Flow Diagram (DFD) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.6.1.3 Entity Relation Diagram (ERD) . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.6.1.4 Use Case Diagram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.6.2 Tracciamento dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.6.3 Prototipazione dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.6.4 Specifica dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.6.5 Revisione dei requisiti e Sign-O↵ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

4 Software Project Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37


4.1 Planning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.1.1 Attività del Planning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.1.2 Fasi del Planning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2 Organizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.3 Monitoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.4 Adjusting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.5 Tecniche di Project Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.5.1 Planning - Project E↵ort Estimation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.5.2 Valutazione della produttività degli sviluppatori . . . . . . . . . . . . . . . . . . . . . 46
4.5.3 Planning e Organizing - Work Breakdown Structure . . . . . . . . . . . . . . . . . . . 47
4.5.4 Monitoraggio dello stato del progetto con “Earned Value” . . . . . . . . . . . . . . . . 49
4.6 Software Pricing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.7 Pricing Strategies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.8 Pricing to win . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

5 Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.1.1 Definizione di Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.1.2 Caratteristiche di Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.1.3 Funzionamento di Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
5.1.4 Ruoli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.1.5 Product Backlog: uno strumento per la pianificazione . . . . . . . . . . . . . . . . . . 56
5.1.6 Il processo Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.1.6.1 Pre-Game Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.1.6.2 Game Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.1.6.3 Post-Game Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.1.7 Monitoraggio dei progressi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

2
5.2 Ruoli di Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.2.1 Product Owner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.2.2 Scrum Master . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.2.3 Team di sviluppo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.3 Cerimonie di Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
5.3.1 Sprint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.3.2 Sprint Planning Meeting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.3.3 Daily Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.3.4 Sprint Review . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.3.5 Sprint Retrospective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.4 Artefatti Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.4.1 Product Backlog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.4.2 Sprint Backlog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
5.4.3 Burndown Chart . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
5.4.4 Doppio istogramma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
5.4.5 Incremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.5 Pianificazione Agile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.5.1 Stime della durata e dimensioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.6 User Story . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.6.1 Vantaggi nell’utilizzo delle user story . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.6.2 Formati di una user story . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.6.3 User Story: le tre C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5.6.3.1 Card . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5.6.3.2 Conversation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.6.3.3 Confirmation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.6.4 User Story: i criteri INVEST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.6.4.1 Indipendent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.6.4.2 Negotiable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.6.4.3 Valuable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.6.4.4 Estimable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.6.4.5 Small . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.6.4.6 Testable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.6.5 Di↵erenza tra task e user story . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.6.6 Tipologia di elementi del product backlog . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.7 Agile Estimation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.7.1 Principi dietro la stima Agile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.7.2 Story Points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.7.2.1 Planning Poker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.7.3 Value Point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.7.4 Bang for the Buck . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.7.5 Velocità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.8 Definizione di ”Fatto” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75

3
6 Progettazione: Architettura e Metodologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
6.1 Introduzione al Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
6.2 Design Architetturale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
6.3 Viste Architetturali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
6.4 Stili/Patterns Architetturali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.4.1 Pipe and Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
6.4.2 Client Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
6.4.3 Model View Controller (MVC) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.4.4 Layered . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
6.4.5 Database-Centric (Shared Data) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
6.4.6 Three-Tier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.4.7 Considerazioni sugli Stili Architetturali . . . . . . . . . . . . . . . . . . . . . . . . . . 87
6.5 Tattiche archietteturali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
6.6 Progettazione di dettaglio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
6.6.1 Decomposizione del sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.6.2 Decomposizione funzionale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.6.3 Progettazione Object Oriented e UML . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.6.3.1 Progettazione delle classi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
6.6.3.2 Relazioni tra classi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
6.6.3.3 Diagramma delle transizioni di stato . . . . . . . . . . . . . . . . . . . . . . . 93
6.6.3.4 Diagrammi di sequenza: interazioni tra classi . . . . . . . . . . . . . . . . . . 94
6.6.3.5 Mismatch tra modello relazionale e a oggetti . . . . . . . . . . . . . . . . . . 95
6.6.4 User Interface Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.6.4.1 Flusso di interazioni nell’interfaccia . . . . . . . . . . . . . . . . . . . . . . . 96
6.6.5 Trabocchetto comune . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6.7 ”Good” Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6.7.1 Coesione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
6.7.2 Accoppiamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
6.7.3 Accoppiamento e programmazione ad oggetti . . . . . . . . . . . . . . . . . . . . . . . 100
6.7.4 Legge di Demeter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

7 Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
7.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
7.2 Definizioni di Errori, Difetti e Problemi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
7.3 Definizione di Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
7.4 Black Box Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
7.4.1 Classi di equivalenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
7.4.2 Analisi dei casi limite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
7.5 White Box Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
7.6 Unit Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
7.6.1 Quando finiscono i test? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
7.7 Inspections and Reviews . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
7.8 Formal Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112

4
7.9 Static Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113

8 Test Driven Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114


8.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
8.2 Test Early . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
8.3 Test Often . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8.4 Test Automatically . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8.5 TDD e Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
8.6 Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116

9 JUnit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
9.1 JUnit use . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
9.2 JUnit assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
9.3 Esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
9.4 JUnit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
9.5 Fixtures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
9.6 Tips . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

10 Version Control Systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124


10.1 VCS Distribuito e VCS Centralizzato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
10.2 Concetti fondamentali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
10.2.1 Grafo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
10.2.2 Contenuto dei nodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
10.2.3 Comandi Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
10.2.3.1 Stato di un file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
10.2.3.2 Comandi principali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
10.2.4 Id e Tag . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
10.2.5 Branch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
10.2.5.1 Nodo HEAD e riferimenti a catena . . . . . . . . . . . . . . . . . . . . . . . . 129
10.2.5.2 Esempio di utilizzo dei branch . . . . . . . . . . . . . . . . . . . . . . . . . . 130
10.2.5.3 Esempio di merge tra due branch distinti . . . . . . . . . . . . . . . . . . . . 133
10.2.6 Branching review . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
10.3 Condivisione dei commit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
10.3.1 Esempio di gestione dei branch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
10.3.2 Buone pratiche da seguire quando si lavora in team con un remote . . . . . . . . . . . 139

11 Design Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140


11.1 Che cos’è un pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
11.1.1 Di↵erenze tra pattern e algoritmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
11.1.2 Aspetti essenziali di un pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
11.2 Classificazione dei Design Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

12 Creational Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143


12.1 Factory Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143

5
12.1.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
12.1.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
12.1.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
12.1.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
12.1.5 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
12.2 Abstract Factory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
12.2.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
12.2.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
12.2.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
12.2.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
12.2.5 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
12.3 Singleton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
12.3.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
12.3.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
12.3.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
12.3.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
12.3.5 Esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

13 Structural Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152


13.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
13.2 Adapter Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
13.2.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
13.2.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
13.2.3 Struttura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
13.2.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
13.3 Adapter Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
13.3.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
13.3.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
13.3.3 Struttura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
13.3.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
13.3.5 Esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
13.3.6 Soluzione Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
13.4 Decorator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
13.4.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
13.4.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
13.4.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
13.4.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
13.4.5 Esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
13.5 Proxy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
13.5.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
13.5.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
13.5.3 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
13.5.4 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

6
13.5.5 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
13.6 Relazioni tra Structural Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162

14 Behavioral Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163


14.1 Command . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
14.1.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
14.1.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
14.1.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
14.1.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
14.1.5 Esempio: gestione di conti correnti bancari . . . . . . . . . . . . . . . . . . . . . . . . 165
14.2 Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
14.2.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
14.2.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
14.2.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
14.2.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
14.3 Observer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
14.3.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
14.3.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
14.3.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
14.3.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
14.3.5 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
14.4 State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
14.4.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
14.4.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
14.4.3 Struttura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
14.4.4 Esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
14.4.5 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
14.5 Strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
14.5.1 Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
14.5.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
14.5.3 UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
14.5.4 Conseguenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
14.5.5 Esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
14.6 Di↵erenza tra State e Strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191

7
1 Introduzione
Negli anni 60 si verificò la cosidetta ”crisi del software” che venne annunciata dalla NATO. In particolare,
si evidenziò come lo sviluppo software fosse un campo ancora recente e immaturo causato, dalla diverse
problematiche riscontrate durante un progetto software (es. over-budget, overtime, qualità decrescente). In
questo periodo i casi di fallimento dei progetti registrati erano dell’84, dovuto in parte dalla crescente difficoltà
nel rapportarsi con computer sempre più potenti e principalmente da tre problemi principali:

• mancanza di input dell’user;

• requisiti e specifiche incomplete

• requisiti e specifiche che cambiavano nel tempo

1.1 Brook’s Law


Durante la pianificazione di un progetto software le aziende misurano il costo del software in mesi d’uomo.
Questa unità di misura indica, generalmente, il quantitativo di lavoro che un singolo dipendente può svolgere
in un mese lavorativo. Nell’e↵ettuare questa moltiplicazione (tra tempo e uomo) si sta supponendo che
queste due quantità siano intercambiabili, per esempio se per completare un progetto ad una solo persona
occorrono 10 mesi allora per completare lo stesso progetto a 10 persone basta 1 mese. Tuttavia, questa
assunzione è fallace, basti pensare che il bambino non nasce in un mese se metto incinta nove donne. Questo
accade perchè, i compiti che sono necessari per il completamento di un progetto non possono essere sempre
divisi in parti uguali tra i membri infatti esisteranno compiti che dovranno essere eseguiti in sequenza. La
legge di Brook a↵erma che aumentando il numero di persone in un progetto, aumenterà anche il tempo
per completarlo. Questo accade perchè tanto piu sarà grande il numero di persone in un team tanto più
sarà grande la comunicazione necessaria tra di loro e questa avrà un costo siccome non sarà automatica.
Facendo un asssunzione semplificata, il numero di comunicazioni possibili cresce con il quadrato del numero
di persone.

8
Figure 1.1: Esempio Legge di Brook

Come viene mostrato nella figura 1.1 nel caso si voglia calcolare quanto tempo occorre per ultimare un
progetto in base al numero di persone che ci lavorano dentro, noteremo che inizialmente il tempo diminuirà,
tuttavia, arrivati ad una certa soglia di persone, che dipenderà anche dal tipo di progetto che stiamo af-
frontando, constateremo che continuando ad aggiungere personale il tempo anzichè dimunuire, aumenterà.
Questo si verifica siccome, il guadagno dall’avere più manodopera è minore rispetto al tempo speso per
far comunicare il team.

9
2 Modelli di Processo
2.1 Introduzione
Durante un processo di produzione del software, soprattutto quando sono molteplici le persone coinvolte, è
importante che ci sia uniformità nello svolgere le varie attività, al fine di evitare inconsistenze e confu-
sione nel codice. Per essere sicuri che tutti i membri del team che stanno partecipando svolgano le attività
nello stesso modo, si usano i processi di produzione del software.

Un processo di produzione del software è un insieme di regole che gli sviluppatori possono seguire,
al fine di dare uniformità alla produzione del software.

2.2 Modello di Processo di Produzione del Software


La parola modello, come riscontrato durante la carriera accademica, sta ad indicare un’astrazione, che
serve appunto ad avere una visione della nostra realtà di interesse, focalizzandoci solo su alcune parti, ed
ignorando volutamente altre parti o particolari dettagli, è anche detta infatti ignoranza selettiva.

Un modello di processo è un’astrazione di un processo di produzione del software, che si concentra quindi
su alcuni elementi del processo, ma ne trascura altri. Questo viene descritto in termini di:

• Ruoli

• Tasks

• Linee guida

2.2.1 Ruoli

I ruoli rispondono alla domanda: “Chi fa che cosa?”, nel momento in cui vi sono più persone coinvolte
nel processo, risulta necessario definire delle caratteristiche con cui una persona partecipa alla
creazione del software, non è infatti assolutamente vero che le varie persone partecipano al processo
svolgendo tutti esattamente le stesse attività. Un processo software ha molti partecipanti, che hanno di↵erenti:

• Abilità/Conoscenze

• Attività da svolgere

• Responsabilità

In base a queste loro caratteristiche, in un processo si andranno a definire i ruoli che le persone andranno a
ricoprire all’interno del progetto
Una cosa importante, riguarda il fatto che non vi è una corrispondenza biunivoca tra ruoli e persone.
Una stessa persona può infatti avere più ruoli all’interno di un progetto, e non è detto che un singolo ruolo
sia ricoperto solo da una persona.
Ovviamente bisogna tenere in conto che, soprattutto su progetti di lunga durata, il team e quindi le persone
coinvolte potrebbero cambiare nel tempo.

10
2.2.2 Tasks

Un altro elemento importante per descrivere un modello di processo sono le attività (tasks) che sono le unità
di lavoro assegnate ai ruoli. Essenzialmente un task ha degli input e degli output, che possono essere di
varia natura:

• Codice, parte indispensabile di un processo software.

• Documentazione, serve appunto a documentare il prodotto.

• Modelli del Sistema che vogliamo realizzare, ovvero un’astrazione del software che vogliamo realizzare.
Essendo un modello, conterrà alcune informazioni e ne ignorerà altre, quindi magari possiamo avere un
diagramma delle classi, ma senza il codice associato. Ad esempio un automa a stati finiti è un esempio
di modello utile da cui “partire”.

Per questi task, vi saranno sempre delle entry condition e delle exit condition, ovvero quando comincio
una certa attività, e quando posso considerarla terminata. Il quando si comincia dovrebbe essere abbastanza
chiaro ed intuitivo, appunto il “quando si inizia a scrivere codice?” può cambiare a seconda del processo,
ma in linea generale si scrive codice nel momento in cui si finisce la parte di progettazione, tipo ad esempio
quando è pronto il Diagramma delle Classi. Risulta però meno intuitivo dire “quando finisce la scrittura
del codice?”, alcuni potrebbero considerare il caso in cui siano state implementate tutte le classi, altri oltre
all’implementazione esigono anche una fase di testing, altri ancora anche la documentazione e cosı̀ via.
Risulta quindi importante che tutti i partecipanti al progetto seguano le stesse regole per capire
quando un’attività deve essere iniziata e quando può essere terminata.

2.2.3 Linee Guida

Questo è un elemento “meno formalizzato” e serve a dare delle indicazioni agli sviluppatori, su come
svolgere determinate attività, per esempio potrebbero esserci:

• Best Practices da seguire

• Far capire i tuoi Obiettivi (Goal), come ad esempio, quando si scrive una documentazione l’obiettivo è
far capire quello che si vuole e si può fare.

• Far capire come scegliere le priorità da dare durante il processo.

• Dare indicazioni sugli errori comuni da evitare.

2.3 Utilità dei Modelli Software


Se prima abbiamo parlato di modello di processo di produzione del software, ora andremo a accennare qualcosa
riguarda i modelli software. Ricordiamo infatti che un modello software è un’astrazione grafica del sistema
dove si andranno a descrivere dettagli di questo, ovviamente a seconda del modello scelto ci si focalizzerà su
dettagli piuttosto che su altri.
Alcuni esempi di Modelli di Sistema già incontrati, possono essere sicuramente i Diagrammi delle Classi, i
Flow Chart, gli Automi a Stati Finiti etc.
I vantaggi principali apportati dall’utilizzo di un modello sono sicuramente:

11
• Migliorare la comunicazione

• Verificare alcune proprietà del sistema prima di costruirlo

In particolare per il primo vantaggio, sappiamo che la comunicazione è uno dei tanti problemi con cui avere a
che fare durante un processo di produzione del software. Nel momento in cui vi è una divisione del lavoro, è
sicuramente più facile farla con un modello alla mano, come ad esempio un diagramma delle classi, potendo
dividere appunto il lavoro in termini delle classi e di come comunicano tra loro. Ma si va inoltre anche a
migliorare la comunicazione con il cliente, se appunto si va a discutere con il cliente con un modello a stati
finiti sotto mano, potrà essere sicuramente meglio per illustrare il funzionamento e capire se procedere in una
direzione piuttosto che in un’altra.

Il secondo, serve invece a capire se il sistema è stato progettato o organizzato male, senza aver già iniziato
o magari ultimato la realizzazione, evitando quindi un dispendio di energie e soldi. Un esempio lampante,
riguardante la progettazione di un Database, può essere sicuramente la normalizzazione. Quest’attività, ri-
cordiamo che può essere fatta senza dover disporre di una Database implementato, ma è sufficiente farla in
fase di progettazione, andando quindi a verificare se quella particolare progettazione fa sı̀ che il Database
che si vuole sviluppare goda di alcune proprietà che garantiscono una non ridondanza dei dati ed una pro-
gettazione di qualità. Quindi questo è un ulteriore caso in cui si è dimostrato utile avere un modello, servito
appunto per verificare che il sistema finale goda di alcune proprietà, prima ancora di realizzarlo.

Alla luce di ciò, nel momento in cui stiamo producendo un modello software e notiamo che questo non è
utile né per il primo vantaggio né per il secondo, allora significa che il modello prodotto, o in fase di pro-
duzione, sarà solamente una perdita di tempo. Quindi non serve rispettare il processo, come nel caso
della creazione di un diagramma delle classi, solo perché bisogna farlo, ma bisogna farlo coscienziosi
del fatto che ci saranno dei vantaggi, altrimenti è inutile.

2.4 Un Semplice Esempio di Processo


Quando si lavora, si segue un processo, che si sia consapevoli di farlo oppure no, ed in generale quando non
si è consapevoli di farlo quasi sempre questo processo non è ottimale e quindi non produrrà i risultati auspicati.

Figure 2.1: Un esempio di un Semplice Processo

12
Per essere precisi, quello in figura anziché un processo vero e proprio è più un modello di processo dato che
mancano una serie di dettagli, nonostante ci sono delle attività e delle transizioni per passare da una all’altra.

Quindi quasi sempre si parte da un’analisi del problema, per poi iniziare a scrivere codice e provarlo, se
ci sono errori il processo si ripeterà fino a trovare una soluzione, una volta assicurati della correttezza, si
rilascerà il prodotto software, con conseguente fine del processo.

Sicuramente questo è un processo molto semplice, che potrebbe magari andare bene nel momento in cui
si parla di programmi amatoriali, ma non è assolutamente adatto per sviluppo di software professionale.

Cosı̀ come per i giochi da tavolo, dove ci sono delle regole da seguire, anche nel processo di produzione
del software ci sono delle regole, nessuno ci obbliga a seguirle, ma il non rispettarle farebbe venire
meno il senso di quello che stiamo facendo, per i giochi si perderebbe il senso del gioco in sé, mentre
per il processo di produzione software verrebbero meno tutti i benefici sulla qualità del prodotto finale, che
sarebbe stata “garantita” se avessimo seguito il processo, ciò non significa che non si possa sviluppare software
senza un processo, ma quasi sicuramente il prodotto finale non sarà di qualità.

2.5 Principali Attività del Processo di Produzione del Software


Tre le principali attività riguardanti il processo di produzione del software vi sono:

1. Analisi dei Requisiti, ovvero capire quali sono le funzioni che il software dovrà mettere a disposizione,
quali sono i vincoli sotto cui dovrà operare e soprattutto capire se sono rispettate le richieste del cliente,
documentando i requisiti e facendoli validare dal cliente.

2. Design, ovvero progettare e definire la struttura del software, fornendo quindi vari modelli che servono
per analizzare il software che vogliamo implementare.

3. Implementazione, che viene spesso considerata come un’attività di basso profilo, ma in realtà non è
cosı̀ dato che non è un’attività meccanica altrimenti la farebbe un computer al posto di un umano.

4. Integrazione e Testing, soprattutto quando il codice viene scritto da persone di↵erenti, utile per
validare il corretto funzionamento del prodotto.

5. Rilascio e Manutenzione, quando il programma è pronto, deve poi essere distribuito sulle macchine
dei clienti, il che non è sempre una cosa banale, potrebbero esserci problematiche relative al fatto che
opererà su macchine nuove e di↵erenti, o magari con la migrazione di alcuni dati, soprattutto in caso
di applicativi di tipo client-server etc.

13
Figure 2.2: Costi per Attività Processo Produzione Software

È possibile notare dalla figura 2.2, tramite dati mediati tra un set di progetti software, i costi delle varie
attività del processo di produzione del software. Notiamo che la parte d’implementazione non pesa tanto
rispetto a quella di Rilascio e Manutenzione, proprio per le molteplici difficoltà a cui quasi sicuramente si
andrà incontro.

2.6 Modelli di processo tradizionali


2.6.1 Modello a cascata

Il modello di processo di sviluppo software a cascata è stato il primo modello di processo formalmente definito
e utilizzato. Il nome a cascata deriva dal fatto che le varie attività che lo compongono sono strettamente
sequenziali, una dopo l’altra, con l’output di una fase (documenti verificati e approvati, codice sorgente,
etc.) dato in input alla fase successiva, come mostrato in Figura 2.3.

14
Figure 2.3: Modello a cascata

In questo modello, le attività si svolgono nella loro interezza, quindi, prima di passare alla fase successiva,
la fase corrente deve essere completamente portata a termine.

Nel caso in cui nella fase finale (di mantenimento ed installazione) vengono riscontrati degli errori o dei
problemi relativi a fasi precedenti, il modello consente il backtracking, ovvero l’operazione di ritornare
indietro alla fase in cui si è generato l’errore o il problema, correggerlo e continuare con le fasi
successive. Ad esempio, se dopo aver installato il software il cliente si accorge che non vengono rispettati
alcuni requisiti richiesti, bisogna ritornare alla fase di definizione e analisi dei requisiti e continuare con le
fasi successive utilizzando la nuova documentazione. Oppure, se il cliente durante l’utilizzo si accorge di un
bug, si ritorna alla fase di implementazione per correggerlo.

Il problema relativo a questo tipo di attività è che se l’errore o il problema è stato commesso ad una
delle fasi iniziali, diventa relativamente costoso ritornare indietro e continuare lo sviluppo.

2.6.1.1 Vantaggi e aspetti positivi

Il modello a cascata o↵re diversi vantaggi, soprattutto dal punto di vista del project managing:

• Può essere utilizzato come tool di gestione del progetto da parte del manager. Dal momento che il
processo segue una rigida scaletta passando attraverso distinti stadi di sviluppo con documentazione
approvata e verificata ad ogni stadio, il manager è sempre al corrente dello stato del progetto.
Inoltre, essendo a conoscenza di ciò, potrebbe allocare risorse (testers, sviluppatori, etc.) in base alla
fase in cui si trova il progetto. In questo modo potrebbe anche gestire più di un progetto contempo-
raneamente, avendo conoscenza delle risorse di cui ha bisogno.

15
• O↵re una divisione semplice del lavoro, grazie alla divisione in fasi distinte.

2.6.1.2 Svantaggi e rischi

Il modello a cascata presenta anche diversi rischi e svantaggi:

• Poca flessibilità. Se ci sono dei cambiamenti ai requisiti (da parte degli stakeholder) durante una fase
avanzata di sviluppo, diventa complicato soddisfare le richieste, perchè bisognerebbe fare backtracking
e ritornare alla fase di definizione dei requisiti.

• Feedback assente da parte dell’utilizzatore. Soltanto alla fine del processo di sviluppo, durante la fase
di installazione e manutenzione del software sarà possibile avere un feedback da parte dell’utilizzatore.
Nelle fasi intermedie, errori nella definizione dei requisiti, relativi a bug, etc. possono non essere rilevati.

• Molta documentazione. A causa dell’elevato ammontare di documenti generato dalle varie fasi, il
processo è anche conosciuto come document-driven approach.

2.6.1.3 Varianti

Una variante del modello a cascata è il modello a cascata con feedback ad ogni fase.

Figure 2.4: Modello a cascata con feedback ad ogni fase

Questo modello mitiga parzialmente i problemi del modello precedente validando l’input di ogni fase (out-
put della fase precedente) controllando la presenza di errori o bug. Se vengono riscontrati errori, si ritorna
alla fase precedente, altrimenti si continua.

I problemi relativi all’alta documentazione, al poco feedback da parte dell’utente finale e della poca flessibilità
continuano però a rimanere.

16
2.6.1.4 Applicabilità

Il modello a cascata è applicabile nei seguenti ambiti:

• Progetti di grandi dimensioni. Questo tipo di modello, grazie alla facile divisione del lavoro tra i
membri del team e l’allocazione di risorse da parte del manager, trova una grande utilità in progetti
di grandi dimensioni. In ogni fase c’è una sola attività in corso e di conseguenza si possono allocare
risorse soltanto quando ce ne è bisogno.

• Sistemi in cui deve essere massimizzata la qualità del prodotto. Esistono sistemi (controllori
di un aereoplano, del funzionamento di treni, etc.) in cui non è accettabile la presenza di bug o
malfunzionamenti, deve essere garantita una probabilità di errore inferiore ad una certa soglia (molto
bassa). Infatti, un qualsisi malfunzionamento potrebbe costare delle vite umane.
Come si ottiene la certezza che la probabilità di errore sia inferiore ad una certa soglia? Non basta
testare il programma, ma è obbligatorio e↵ettuare molte verifiche alla fine di ogni fase. Queste
verifiche comprendono il controllo di ogni linea di codice, l’applicazione di automi a stati finiti sui
possibili stati dell’applicazione, la dimostrazione formale e matematica per la verifica dell’assenza di
malfunzionamenti in un certo regime di funzionamento, etc. Per questo tipo di sistemi, è vantaggioso
l’utilizzo del modello a cascata grazie alla presenza di rigidi controlli.

2.6.2 Modello a V

Il modello a V è una sorta di variante del modello a cascata. Come per il waterfall, infatti, è utilizzato in
sistemi ad alto rischio e sistemi in cui bisogna garantire un estrema qualità del prodotto. Il nome deriva
dalla struttura grafica del modello, come mostrato in Figura 2.5.

Figure 2.5: Modello a V

17
L’idea di base è la stessa: è costituito da una sequenza di fasi e↵ettuate una dopo l’altra, in cui l’output di
una fase è l’input della successiva. A di↵erenza del waterfall, ogni fase ha un’attività speculare dall’altro lato
della V che si occupa della verifica della suddetta fase. Per ogni fase che costruisce qualcosa, c’è una
fase speculare che verifica quello che è stato fatto.

Ad esempio, la fase di progettazione dei componenti ha una fase speculare rappresentata dalla verifica e
dallo unit testing dei singoli componenti progettati; la fase di progettazione dell’architettura ha una fase
speculare di testing e verifica dei vari sottosistemi, etc. Per verifica non si intende soltanto il testing, ma
anche metodi formali e altri tipi di verifica.

In settori automobilistici, ferroviari, aeronautici, etc. la normativa richiede di adottare un modello di


questo tipo, perchè garantisce che siano stati fatti tutti i passaggi di verifica necessari prima di arrivare al
prodotto finale.

2.6.3 Modello incrementale

Il modello incrementale è un processo che, a di↵erenza dei precedenti, dove il prodotto veniva rilasciato
esclusivamente una volta completato, consiste in più rilasci del software. In questo modello si possono avere
due modalità di rilascio:

• Si definisce il software come formato da più componenti (pezzi singoli) e si rilasciano separatamente uno
per volta. Per esempio, in un programma come Netbeans si rilascia prima l’editor, poi il compilatore e
cosı̀ via. Questa non è la soluzione più comune.

• Si rilasciano più release del prodotto, ciascuna delle quali implementa un sottoinsieme via via cresente
di funzionalità (prototipo). Per esempio, in un ambiente di sviluppo come Netbeans, nella prima release
si decide che l’utente potrà scrivere del testo ma non avrà la possibilità di evidenziarlo, oppure, si
implementa il compilatore ma non il debugger. Successivamente nella seconda release si aggiungeranno
funzionalità che non erano presenti nella prima fino al raggiungimento di tutte le funzionalità previste.
La di↵erenza in questo caso consisterà nell’unicità del mio programma.

Figure 2.6: Struttura del modello incrementale

18
Nella figura 2.6 vengono mostrate le varie attività che vengono eseguite nel modello incrementale. Nella prima
fase si definiscono i requisiti di massima in modo da pianificare cosa inserire in ciascuna release, pertanto non
avrà necessità di essere ripetuta. Dopodichè inizia la parte ciclica dove ad ogni iterazione si aggiungeranno
delle funzionalità al software già esistente, dettagliando la fase dei requisiti iniziali, validando e integrando
l’incremento e validando il sistema, ripetendo questo procedimento fino a quando il software non è completo.
In realtà esistono progetti in cui la condizione di ”progetto finito” non esiste, in quanto alcuni sono stati
pensati per essere costantemente aggiornati (es. Sistemi operativi). I vantaggi di questo modello sono:

• Fornisco immediatamente un prodotto funzionante al cliente dandomi la possibiltà di ricevere


feedback in modo da indirizzare il team di sviluppo verso un prodotto ideale per il cliente. In questo
modo il cliente comprende meglio le sue esigenze rendendolo più soddisfatto una volta che il lavoro è
finito.

• Riduce il rischio di completo fallimento del progetto, siccome nel caso in cui debba interrompere
il progetto improvvisamente (insufficienza di soldi o di tempo) io posso fornire una release contenente
una parte delle funzioni (se sono furbo realizzo prima le parti più importanti per il cliente).

2.6.4 Di↵erenze tra modello incrementale e iterativo

Alcuni testi fanno distinzione tra modello incrementale e modello iterativo definendoli come due modelli
separati. Il modello incrementale consiste in un rilascio di incrementi volta per volta mentre il modello
iterativo consiste nella ripetizione di attività più volte.
In teoria le due cose sono indipendendenti l’uno dall’altra, potrei avere un processo dove rilasico il mio
software a incrementi successivi, ma le attività le svolgo una sola volta o anche un processo software dove le
attività vengono ripetute, ma lo rilascio solo alla fine.
In realtà non esiste un processo che non sia sia incrementale sia iterativo pertanto, li useremo come sinonimi.

19
3 Ingegneria dei Requisiti
3.1 Introduzione
L’analisi dei requisiti serve in generale per capire che cosa deve fare il nostro sistema, ed anche quali sono
eventuali vincoli a cui deve sottostare al fine di soddisfare le esigenze del committente. Questa fase è una
delle più importanti, poiché errori qui, avrebbero un impatto molto grande sulla probabilità di fallimento
del nostro progetto, essendo estremamente difficili da riparare gli errori relativi a cattiva comprensione dei
requisiti. Bisogna infatti dedicare un tempo adeguato a questa fase, ed in diversi casi deve essere una frazione
significativa del tempo totale che si ha a disposizione per il progetto.

Il problema ora è, nel momento in cui un’azienda vuole affidarsi a noi per la realizzazione di un prodotto
software, vorrà anche avere un preventivo, in termini di tempistiche e di denaro, quindi dovremmo per forza
avere un’idea dei requisiti per poter fare tale preventivo. Già di per sé la stima dei costi e dei tempi è a↵etta
da errori, ma senza requisiti questa imprecisione viene esponenzialmente aumentata.

Il paradosso ora è, se investiamo troppo in termini di analisi dei requisiti per e↵ettuare un preventivo il
più preciso possibile, nel momento in cui l’azienda non decidesse di operare con noi, avremmo perso soldi
e tempo. Se invece non vi è una fase dei requisiti, si risparmiano soldi e tempo, ma la stima e quindi il
preventivo con alta probabilità sarà molto impreciso, quindi magari potremmo prenderci in carico un lavoro
che non era fattibile per quel budget/tempo.

Come si risolve tale paradosso?

• Considerare l’Analisi dei Requisiti come un’attività separata dallo sviluppo del software, facen-
dosi pagare per questa. In alcuni casi accade che addirittura i requisiti vengono elaborati da un’azienda,
e magari il software verrà sviluppato da un’altra. Nella pratica è spesso difficile far accettare al cliente
che deve pagare per ciò.

• Fare una stima dei requisiti preliminare e veloce, mantenendosi dei margini di sicurezza, e nel
momento in cui l’azienda accetta di delegarci il lavoro, si andrà ad affinare il tutto. In questo modo
eviteremo di investire troppo e rischiare di andare in perdita nel caso in cui poi non ci venga commis-
sionato il lavoro.

3.2 Attività dell’Ingegneria dei Requisiti


Le principali attività che fanno parte del processo di Ingegneria dei requisiti sono:

• Elicitazione

• Documentazione e Definizione

• Prototipizzazione

• Analisi

• Specifica

20
• Revisione e Validazione

• Consenso ed Accettazione
L’Elicitazione dei requisiti è l’attività che serve ad estrarre i requisiti sulla base delle volontà dell’utente.
Infatti notiamo che tra le varie attività non esiste la creazione dei requisiti, dato che l’azienda non può
inventarseli a suo piacimento, ma bisogna estrapolarli dai desideri del cliente.

Figure 3.1: Attività Principali del Processo di Ingegneria dei Requisiti

Notiamo dalla Figura 3.1 in che modo sono collegate queste attività tra di loro, in particolare l’Elicitazione,
che tira fuori requisiti “grezzi” ha un collegamento bidirezionale con l’Analisi, dove appunto verranno anal-
izzati e rielaborati al meglio. Il collegamento bidirezionale consiste nel fatto che il processo potrebbe essere
fatto più volte, magari dopo un primo incontro con il cliente potrebbero esserci dei dubbi, che potrebbero
essere estinti tramite un nuovo colloquio successivo.
I diversi colori nell’immagine hanno un significato ben preciso, ovvero quelle in grigio sono le attività svolte
dall’azienda dove vi è interazione con il cliente, mentre quelle in bianco sono quelle senza interazione.

In generale, questo insieme di attività porteranno ad avere come prodotto finale una Software Require-
ment Specification (SRS), un documento dove vi saranno tutti i requisiti ben elaborati, i quali dovranno
poi essere successivamente accettati dal cliente.

3.3 Vantaggi dell’Ingegneria dei Requisiti


Le motivazioni per cui le attività dell’Ingegneria dei Requisiti e la sua documentazione sono importanti sono
principalmente le seguenti:
• Abbiamo bisogno dei requisiti per progettare il nostro sistema, se non sappiamo cosa realizzare,
non sapremo nemmeno come farlo.

• Testare il nostro sistema, il che è un’attività molto importante, dove non basta solo avere gli input
ed il programma, ma bisogna capire soprattutto come si dovrà comportare il programma in risposta a
tali input. È quindi importante, al fine di realizzare il caso di test, avere una descrizione dei requisiti
(attenzione, i requisiti non devono essere solo chiari, ma anche documentati).

21
• Controllare lo Scope-Creep, creep dall’inglese significa “strisciare”, quindi per Scope-Creep si in-
tende capire che siamo partiti con delle intenzioni e degli obiettivi finali, e piano piano il programma è
“strisciato” verso un nuovo obiettivo. Bisogna quindi tenere sotto controllo ciò, per evitare di aggiun-
gere nuove funzionalità trascurando quelle realmente importanti. Il processo di Scope-Creep non è di
per se sbagliato, il cliente deve avere la possibilità di aggiungere cose, ma deve essere consapevole del
fatto che richiede tempo, è quindi meglio controllare l’aggiunta di nuove cose, poiché la somma di tante
piccole nuove cose potrebbe portare ad andare fuori budget.

• Sono la base su cui poi costruire la Documentazione per gli Utilizzatori del sistema,
solitamente le due cose (requisiti e manuale utente) non coincidono, dato che il linguaggio potrebbe
essere chiaro per gli sviluppatori e non per gli utenti finali. I Requisiti però restituiscono quali sono
tutte le funzionalità del sistema, sono quindi utili da seguire per la creazione del manuale utente.

• Aiutano la divisione del lavoro, infatti uno dei modi per dividere il lavoro è proprio quello di
assegnare i requisiti a parti diverse del team. Questa cosa va fatta ovviamente con un minimo di
consapevolezza della progettazione, poiché potrebbero esserci requisiti collegati e dipendente tra di
loro, che se vengono divisi in maniera poco intelligente non favoriscono il riutilizzo del codice.

3.4 Elicitazione e raccolta dei requisiti


Per l’attività di elicitazione dei requisiti, skills sia di comunicazione sia di conoscenza del dominio di busi-
ness sono fondamentali. Skills di comunicazione sono necessarie perchè clienti/utenti non sanno sempre come
esprimere i propri bisogni, mentre è anche essenziale avere conoscenza nel dominio di business perchè ogni
industria o↵re una sua terminologia unica.

Esistono due possibili scenari:

1. I requisiti sono già forniti all’azienda che produce il software.

• Spesso, il cliente ha già una prima idea e una consapevolezza tecnica adeguata per esprimerla in
termini di requisiti, quindi sono forniti dal cliente stesso.
• In alcuni casi, il progetto software consiste nel migliorare uno già esistente o continuarne il suo
sviluppo. In questi casi, esiste un set già stabilito dei requisiti del sistema, che può essere
documentato o meno. Le attività da svolgere da parte degli ingegneri del software quindi sono la
lettura e la comprensione di questi requisiti e contemporaneamente capire che cosa il cliente vuole
modificare rispetto ai requisiti già esistenti.
• Raramente, può capitare che l’azienda cliente ha commissionato l’elicitazione dei requisiti (o anche
l’analisi) ad un’altra azienda specializzata, diversa da quella che produce il software.

2. I requisiti devono essere determinati dall’azienda che produce software interagendo con il
cliente e gli stakeholders.

• Soprattutto nelle grandi aziende formate da diverse parti o settori, ogni settore ha una visione
ristretta di quello che il software farà, quindi ciascuna parte vede soltanto il suo punto di vista,
ignorando le altre parti. Per questo motivo serve qualcuno che armonizzi tutti i punti di vista per
ottenere un insieme di requisiti completi e consistenti.

22
Un esempio può essere un software che gestisce un magazzino automatico, come quello di Amazon.
Gli utilizzatori possono essere sia i lavoratori del magazzino, sia i manager che controllano il flusso
di lavoro dell’azienda, sia chi e↵ettua i rifornimenti delle merci, gli spedizionieri, etc. Tutti però
hanno punti di vista di↵erenti su quello che dovrà fare il software per portare beneficio e tutti
dovranno in qualche modo interagire con il software che si sta sviluppando.
• Bisogna capire a fondo l’obiettivo dell’azienda e i vari sotto-obiettivi che lo formano. Ciò va
capito attraverso l’attività di elicitazione ad alto livello descritta nel paragrafo successivo.
• Spesso può capitare che l’azienda descriva i suoi bisogni in maniera incompleta e contraddit-
toria. L’azienda cliente può dare per scontato delle cose che sono comuni a tutte le aziende del
suo settore ma non sono ovvie per il produttore del software (soprattutto quando il produttore del
software lavora per la prima volta in quel particolare mercato).

L’elicitazione dei requisiti è e↵ettuata in maniera gerarchica, si parte da un insieme di requisiti di alto
livello che si va via via a raffinare aggiungendo dettagli. Esistono infatti due livelli di elicitazione dei requisiti:
elicitazione ad alto livello dei requisiti ed elicitazione dettagliata dei requisiti.

3.4.1 Elicitazione ad alto livello dei requisiti

Ad alto livello, si deve indagare e capire la logica aziendale e la motivazione dello sviluppo del software.
In sostanza, capire la visione dell’azienda che richiede il software, ovvero come l’azienda vede se stessa,
quali sono i processi aziendali, quali sono i suoi valori chiave, i suoi obiettivi, gli indicatori di successo, etc.

La categoria di informazioni che contribuisce ad e↵ettaure questa analisi ad alto livello dei requisiti e stilare
un profilo aziendale è la seguente:

• Bisogni e opportunità. I bisogni e le opportunità chiariscono quali sono i problemi da risolvere ad


un livello alto di astrazione. Inoltre, vanno compresi anche gli obiettivi.

• Motivazione del progetto. Il motivo per cui il cliente ha richiesto e commisisonato lo sviluppo del
software. Per capire la motivazione, bisogna anche comprendere se il progetto può portare ad un certo
payback a livello di business, ovvero se può ripagare.

• Ambito. Capire quali sono i problemi più importanti aiuta l’azienda produttrice di software a delineare
un profilo preciso su quale saranno i campi di applicazione principali.

• Vincoli principali. Gli analisti devono essere capaci anche di comprendere i vincoli principali. Uno
dei vincoli principali è infatti il budget per un progetto software. Le informazioni su vincoli sul budget
sono importanti perchè contribuiscono al processo di decisione nel definire cosa è importante e cosa non
è importante durante la fase di prioritizzazione dei requisiti di dettaglio. Un altro vincolo aziendale è la
programmazione delle attività in termini di tempo di sviluppo, che deve essere chiarito durante questa
fase.

• Funzionalità principali. Ad alto livello, prima della elicitazione in dettaglio, attraverso queste in-
formazioni si può avere un’idea di quale sarà l’ambito del progetto e quindi dirigere l’elicitazione di
dettaglio dei requisiti lungo la giusta area di business.

23
• Caratteristiche degli utenti. Il successo di un sistema dipende molto da quanto gli utenti sono
addestrati/formati ad utilizzarlo. È importante quindi raccogliere piu informazioni possibili e stilare
un profilo dell’utilizzatore (titolo di lavoro, responsabilita, competente tecniche medie, etc.).

• Fattori di successo. Questi requisiti ad alto livello sono essenziali per il successo del progetto software
e possono essere usati come sorgente per formulare un obiettivo ad alto livello del progetto. Trasformare
i requisiti ad alto livello in obiettivi ad alto livello.

Avere un’idea di tutte queste cose aiuta a realizzare un prodotto software allineato con i bisogni e la
visione dell’azienda in modo che risulti il più possibile soddisfacente.

Questa parte di elicitazione dei requisiti è e↵ettuata da persone con una formazione particolare chiamate
business analysts, che non sono esperti di software ma di aziende e hanno una buona capacità di comuni-
care e relazionarsi con i clienti (aziende).

3.4.2 Elicitazione dettagliata dei requisiti

Una volta e↵ettuata l’elicitazione ad alto livello dei requsiti, bisogna e↵ettuare quella di dettaglio.

Figure 3.2: 6 dimensioni dei requisiti

Esistono 6 principali categorie per classificare i requisiti, dette le 6 dimensioni dei requisiti, come mostrato
in Figura 3.2. Ogni dimensione rappresenta un’insieme di informazioni da acquisire da un cliente circa
il sistema da realizzare. Ogni requisito raccolto si collocherà in una di queste dimensioni. Sono le seguenti:

• Funzionalità individuali. È il gruppo di requisiti più ovvio tra tutti ed è considerato un punto di
inizio naturale per l’acquisizione dei requisiti. Si chiede all’utente quali sono le singole funzionalità che
il software deve mettere a disposizione.

• Business flow. Ogni funzionalità va espressa nel contesto del business flow, ovvero, come la particolare
funzionalità del software si collega ai processi aziendali in cui è inserito. Capire come le funzionalità

24
del sistema si inseriscono nei processi aziendali può essere utile per realizzarle nel modo migliore. In
sostanza, come il software si colloca in una catena di operazioni che viene fatta all’interno
dell’azienda.

Una delle attività svolte, soprattutto per progetti software per grandi aziende, è quella di sviluppare
un modello del business process, ovvero uno schema attraverso cui si traccia il processo aziendale
in cui si colloca il software. Può essere utile per verificare se ci sono inefficienza nel processo aziandale
e magari apportare delle migliorie, dando indicazioni all’azienda cliente su come migliorare la produt-
tività, le attività, etc. individuando ridondanze, ritardi, flussi di attività sbagliati etc.

• Dati, formati e informazioni richieste. È importante capire i dati di input e output del sis-
tema, quali sono le informazioni che entrano nel sistema, per quale motivo, qual è il formato dei dati
sia in ingresso che in uscita, se si ha a che fare con numeri, etc.

• Interfaccia utente. Diversi utenti hanno diverse preferenze riguardo l’interfaccia utente. Bisogna
quindi definire la piattaforma, il sistema operativo supportato. Inoltre, bisogna fare chiarezza sulle
icone da utilizzare, i pulsanti, i controlli che l’applicazione deve avere, dal momento che il flusso di
un’applicazione è anche l’interfaccia grafica e generalmente simula il processo di business in qualche
modo.

• Interfacciamento con altri sistemi. È importante definire i possibili interfacciamenti con sistemi
già esistenti attraverso una rete informatica, Internet o tra due processi nella stessa macchina.

• Vincoli di performances, sicurezza e affidabilità. Questo insieme di informazioni aiuta a stabilire


tutti i requisiti non funzionali del progetto software. Comprendono informazioni relative ai vincoli sulle
prestazioni (temporali), sulla sicurezza (dei dati o di autorizzazioni ad e↵ettuare determinate azioni),
sull’affidabilità (se va garantito che in caso di crash l’app continui a funzionare e/o abbia bisogno di
fare un backup di tutti i dati).

3.5 Analisi dei requisiti


Una volta raccolti ed elicitati tutti i requisiti, si presentano come un insieme di dati disorganizzati. Devono
quindi essere analizzati.

L’analisi dei requisisi consiste in due attività: categorizzazione o raggruppamento dei requisiti (in
vari modi) e prioritizzazione dei requisiti.

3.5.1 Categorizzazione dei requisiti

L’attività di categorizzazione dei requisiti è utile per utilizzare il vecchio adagio del divide et impera:
dividendo i requisiti in gruppi è possibile concentrarsi più facilmente in un gruppo alla volta nelle fasi
successive. Durante la fase di raggruppamento è fondamentale controllare che i requisiti siano consistenti

25
(ovvero se non ci sono contraddizioni tra loro) e completi (se i requisiti del sistema sono stati completamente
descritti).

3.5.1.1 Categorizzazione semplice dei requisiti

Un semplice modo per categorizzare i requisiti cosiste nel dividerli in requisiti funzionali e requisiti non
funzionali.

I requisiti funzionali sono i requisiti che corrispondono alle principali funzionalità del sistema, ovvero
servizi che il sistema deve fornire, come il sistema dovrebbe reagire a particolari input e come il sistema
dovrebbe comportarsi in particolari situazioni

I requisiti non funzionali sono invece i vincoli sulle funzioni o servizi o↵erti dal sistema.

3.5.1.2 Categorizzazione dei requisiti tramite le 6 dimensioni

Un modo naturale per raggruppare i requsiti è attraverso le 6 dimensioni dei requisiti descritti nel
precedente paragrafo:

• Funzionalità individuali

• Business flow

• Dati, formato e informazioni richieste

• Interfaccia utente

• Interfaccia con altri sistemi

• Vincoli come performances, affidabilità e sicurezza

Queste categorie non sono tra di loro sempre mutuamente esclusive. Può infatti capitare che un requisito si
accavalli tra piú di una categoria. Un overlap di questo genere andrebbe chiarito in modo che non ci siano
duplicati (consistenza).

3.5.1.3 Categorizzazione e analisi dei requisiti tramite VORD - Viewpoint-Oriented Requirement


Definition

In contesti complessi, soprattutto quando il sistema è di grandi dimensioni, possono esistere diverse tipologie
di utenti che si interfacciano con esso. Il VORD è un metodo di analisi dei requisiti basato sul concetto che
diversi soggetti che interagiscono con il sistema possono avere punti di vista diversi su cosa dovrà fare/non
fare il software. I requisiti quindi non sono visti allo stesso modo da tutti gli stakeholders. Per un sistema
di grandi dimensioni che ha molti sotto-componenti, infatti, i diversi utenti articoleranno i requisiti facendo
enfasi su diverse specifiche, ovvero quelle che hanno a che fare con il loro campo di interesse. Ad esempio,
il cliente che pagherà il software spesso ha una prospettiva diversa sui requisiti rispetto a chi si interfaccerà
quotidianamente con il sistema.

Che cos’è uno stakeholder? Gli stakeholders sono tutte le figure influenzate dal sistema software che
si sta realizzando. Sono stakeholders sia le figure che utilizzeranno il sistema, sia le persone che non lo

26
utilizzeranno direttamente, ma il cui lavoro sarà in qualche modo influenzato dal modo in cui il
sistema funzionerà.

Il VORD fornisce una metodologia sia per l’analisi dei requisiti sia per la elicitazione.

La chiave è che esistono diversi punti di vista degli stakeholders rispetto ai requisiti del sistema. Questi
diversi punti di vista spesso sfociano in diverse prospettive dello stesso problema e questo può essere
utilizzato come vantaggio per categorizzare e strutturare i requisiti.

La metodologia VORD è divisa in 4 steps:

1. Identificare i diversi stakeholder, i loro diversi punti di vista (requisiti).

2. Strutturare e categorizzare punti di vista (requisiti), eliminando i duplicati e raggrup-


pandoli per punti di vista comuni. Dal momento che gli stakeholders possono avere punti di vista
di↵erenti, bisogna trovare un tradeo↵ tra questi.

3. Raffinare i punti di vista (requisiti) identificati.

4. Mappare i punti di vista (requisiti) al sistema e ai servizi che il sistema dovrà fornire.

3.5.2 Prioritizzazione dei requisiti

La categorizzazione dei requisiti è solo una parte dell’analisi che permette di identificare le inconsistenze
e le incompletezze tra i vari gruppi di requisiti.

Un problema ulteriore è rappresentato dal fatto che non tutti i requisiti identificati possono essere svilup-
pati e consegnati, a causa di vincoli come:

• Risorse limitate

• Tempo limitato

• Capacità tecniche limitate

Quando si hanno risorse limitate è necessario allocare risorse in maniera più efficiente possibile, sulla
base di quali requisiti sono più importanti.

Per questo motivo, la prioritizzazione dei requisiti è un’attività di vitale importanza per il successo di
un progetto software, in quanto permette di sviluppare per primi i requisiti che consegnano il valore più alto
possibile al cliente.

Tipicamente, si seguono approcci iterativi per lo sviluppo software. Uno dei vantaggi di questo approccio è
rappresentato dal fatto che allo scadere del tempo, si ha un pezzo di software funzionante e già consegnabile
al cliente che grazie alla prioritizzazione apporta il massimo valore possibile al cliente (anche se non è com-
pletamente terminato e tutti i requisiti sono stati aggiunti al sistema finale).

27
Un errore comune è quello di decidere per sè i criteri di prioritizzazione dei requisiti, senza coinvolgere il
cliente. Questo modo di fare è sbagliato, perchè potrebbe non riscontrare le reali esigenze del cliente. La
regola principale infatti è che il cliente detta le priorità dei requisiti.

Per far stabilire la priorità dei requisiti al cliente, ci si può basare sui seguenti criteri:

• Bisogni e desideri del cliente.

• Concorrenza e condizioni del mercato. Alcuni requisiti sono necessari perchè senza questi il sistema
non avrebbe senso.

• Bisogni dei suoi futuri clienti.

• Vantaggio rispetto alla concorrenza.

• Risoluzione di problemi nel sistema attuale, nel caso in cui si stia sviluppando una release di un software
gia esistente.

La priorità dei requisiti è qualcosa di soggettivo, infatti, in molti casi, può essere che diversi stakeholder
siano in contrasto tra loro. La risoluzione di questo contrasto non è però compito dell’ingegnere del soft-
ware ma degli stakeholders.

Un possibile output dellla fase di prioritizzazione potrebbe essere quello in Figura 3.3.

Figure 3.3: Formato di output della prioritizzazione dei requisiti

Il numero del requisito è un identificativo (numero, sigla, etc.) che identifica univocamente un singolo
requisito e rimane valido per tutta la durata del progetto. È importante che esso sia univoco perchè nel caso
in cui un requisito sia cancellato, il suo identificativo non è in nessun modo utilizzato per identificare un
requisito diverso (evitando le ambiguità nel caso si consultino documenti vecchi).

La breve descrizione rappresenta una descrizione ad alto livello del requisito. Non è ancora una rapp-
resentazione formale.

La sorgente indica lo stakeholder, chi ha espresso il requisito. A ciascun requisito si possono associare
uno o più stakeholders (più di uno nel caso in cui durante la fase di categorizzazione due stakeholders diversi
hanno espresso lo stesso requisito).

28
La priorità è il livello di priorità dettato dal cliente. È conveniente non avere un numero eccessivo di
livelli di priorità per evitare che il cliente perda tempo assegnando i livelli di priorità ai vari requisiti. Va
cercato di evitare che il cliente dia a tutti (o quasi) i requisiti un livello di priorità massimo, caso in cui si
dovrà scendere a compromessi.

Lo stato del requisito è un indicatore che ha senso soltanto nel caso in cui si ha a che fare con un
processo iterativo di sviluppo software.

Cosa succede nel caso in cui due requisiti hanno lo stesso livello di priorità? Quale viene implemen-
tato prima? A parità di priorità del cliente, si cerca di implementare prima il requisito più rischioso, ovvero
il requisito più complicato e complesso da implementare (che richiede uno sforzo maggiore). In questo modo,
si ha più tempo per lavorarci su e nel caso di processo iterativo si ha un feedback più immediato da parte
del cliente. In caso di feedback negativo, si ha un’altra release per svilupparlo.

3.6 Definizione, prototipizzazione e revisione dei requisiti


Dopo aver ottenuto l’elenco dei requisiti, ciascuno dei quali viene da uno o più stakeholder, averli prioritizzati
e dopo aver e↵ettuato un’operazione di armonizzazione dove sono stati eliminati i requisiti contraddittori,
questi vanno scritti in maniera formale. La scrittura di un documento formale dei requisiti è possibile tramite
3 attività spesso eseguite in concomitanza tra loro:

• La definizione

• La prototipazione

• La revisione

3.6.1 Definizione dei requisiti

Definizione dei requisiti significa descrivere il requisito, precedentemente individuato, in maniera formale.
La definizione dei requisiti può essere scritta usando diverse forme:

1. Simple Input/process/output (IPU) description in English, anche detto linguaggio naturale strut-
turato;

2. Dataflow Diagram (DFD);

3. Entity Relation Diagram (ERD);

4. Use Case Diagram from Unified Modeling Language (UML).

Anche se non esiste un unico modo corretto di scrivere i requisiti formalmente, è utile anche usare diversi
formalismi contemporaneamente per rappresentare la totalità delle informazioni estratte dai requisiti. Una
volta che i requisiti sono descritti è importante che ci sia un processo di revisione, idealmente eseguito da un
team diverso da quello che ha scritto i requisiti.

29
3.6.1.1 Linguaggio Naturale Strutturato

Quando si usa il linguaggio naturale strutturato, il testo che usiamo per descrivere il requisito ha una
struttura ben definita che deve essere rispettata per tutti i requisiti. Un esempio di struttura è la seguente:

Figure 3.4: Input/process/output (IPU) description

Una struttura di questo tipo è adatta quando si devono formalizzare dei requisiti funzionali (cosa fa il
sistema).

3.6.1.2 Data Flow Diagram (DFD)

I diagrammi del flusso dei dati (Data Flow Diagram) mostrano il flusso delle informazioni tra vari
enti a monte e a valle di una determinata operazione. Data la loro semplicità ed immediatezza, sono molto
di↵usi.
La sintassi è la seguente:

• I rettangoli rappresentano delle entità che possono essere persone o altri sistemi che generano o
ricevono dati;

• I rettangoli con i vertici smussati rappresentano l’operazione o la funzione;

• I rettangoli aperti sul lato rappresentano i sistemi nel quale vengono immagazzinate le informazioni;

• Le frecce rappresentano dei dati che fluiscono.

30
Figure 3.5: Data Flow Diagram Syntax

Di seguito si presenta un esempio di DFD che descrive parte di un sistema che deve gestire gli ordini di
un’azienda e le spedizioni degli articoli ordinati.

Figure 3.6: data flow diagram example

Si ha un attore che è il Customer, che trasmette gli ordini ad una funzionalità del sistema, cioè il processo di
ordine e riceve la fattura. Il processo di ordine riceve gli ordini dal cliente, prende dal database dell’inventario
le informazioni sui prodotti disponibili, dal database dei clienti le informazioni sul cliente che ha e↵ettuato
l’ordine e produce in output delle informazioni di spedizione. E informazioni di spedizione vengono trasmesse
al processo di packaging, che prende i dettagli da un altro database e produce l’etichetta di spedizione che

31
viene inviata all’addetto alle spedizioni.
Anche se un diagramma di questo genere è molto chiaro e da una buona visione d’insieme dei requisiti, non
dà una descrizione accurata di come vanno implementate le funzionalità dei vari blocchi.

3.6.1.3 Entity Relation Diagram (ERD)

I diagrammi Entità-Relazione vengono usati per descrivere le relazioni che sussistono tra i dati.

Figure 3.7: entity relation diagram

Attenzione: Ricordiamo che nella fase di analisi dei requisiti non si deve progettare il sistema, o il
database del sistema, ma descrivere le informazioni che arrivano dagli stakeholder, per questo i diagrammi
Entità-Relazione vengono usati per chiarire e formalizzare queste informazioni. In seguito, quando si passerà
alla progettazione vera e propria, si potrà utilizzare il diagramma Entità-Relazione prodotto in fase di analisi
dei requisiti (che non sarà uno schema completo), ad esempio, come base per la progettazione del database.
La rappresentazione delle entità può essere in formato: tabellare o grafico. La scelta però deve essere la
stessa per tutti gli ERD usati!

3.6.1.4 Use Case Diagram

Un caso d’uso (Use Case) rappresenta uno scenario in cui viene usata una certa funzionalità del sistema.
Descrivere uno Use Case significa descrivere tutte le sequenze di azioni e risposte da parte del sistema che
avvengono in un particolare scenario e deve contenere almeno le seguenti informazioni:

• Funzionalità di base;

• Pre-condizioni della funzionalità;

• Flusso di eventi o scenari;

• Post-condizioni;

• Condizioni di errore e scenari alternativi.

32
Di seguito un esempio di uso:

Esempio: Login di utente;

Funzionalità: l’utente deve autenticarsi;


Pre-condizioni : L’utente accede in maniera anonima alla mia applicazione; L’utente deve già essere registrato
e dunque avere un username ed una password;
Flusso di eventi o scenari: Inserisce lo username e la password e clicca su invio;
Post-Condizioni : se lo username e la password sono corretti, l’utente risulta autenticato e può accedere alle
parti riservate della mia applicazione;
Condizioni di errore e scenari alternativi : Se la password o l’username non sono corretti, viene stampato un
messaggio di errore che lo invita a riprovare a reinserirli.

Fine Esempio.

Come si può notare dall’esempio, con gli Use Case si presenta una funzionalità del sistema mostrando uno
scenario di utilizzo e il suo comportamento in relazione ad alcuni eventi o condizioni. In UML è possibile
rappresentare graficamente un Use Case usando gli Use Case Diagram. Gli Use Case presentano i seguenti
elementi:

• Gli Attori, che sono gli utilizzatori del sistema o altri sistemi esterni con cui il nostro interagisce;

• I nomi delle funzionalità che sono utilizzate dagli attori;

• Le relazioni tra le funzionalità.

Di seguito un esempio di diagramma:

33
Figure 3.8: use case diagram example

Uno Use Case Diagram però non è tanto accurato quanto la descrizione formale dello use Case! Per Questo
se si vogliono usare gli Use Case Diagram, questi devono essere accompagnati da una descrizione degli Use
Case.

3.6.2 Tracciamento dei requisiti

Una volta documentati individualmente i requisiti, usando una qualsiasi delle tecniche viste in precedenza
(Use Cases, IPO, ecc.) è importante gestire il tracciamento dei requisiti (Requirement Traceability).
È necessario tenere traccia di un requisito per assicurarsi che, alla consegna del prodotto finale, si siano
implementati tutti i requisiti ma anche che, in caso di modifica, siano aggiornate tutte le componenti che
dipendono da quel requisito.
Per evitare spiacevoli inconsistenze, bisogna tracciare:

• La sorgente da cui proviene un requisito (persone e documenti)

• Gli step successivi all’analisi di un requisito (design, implementazione, test e release)

• I requisiti che sono prerequisiti per l’implementazione di altri requisiti

Una Traceability Table può essere usata per tenere traccia delle dipendenze con i requisiti:

34
Figure 3.9: traceability table example

3.6.3 Prototipazione dei requisiti

Qunado si parla di requisiti, la prototipazione si riferisce quasi esclusivamente ai requisiti di interfaccia utente,
in termini di:

• Visual look, cioè mostrare come dovrà apparire l’interfaccia utente in relazione alle funzionalità imple-
mentate;

• Flow, come le funzionalità sono collegate tra loro.

I prototipi di interfaccia realizzabili possono avere diversi livelli di dettaglio. Possiamo avere:

• Prototipi a Bassa Fedeltà, che ad esempio possono essere dei disegni che in maniera schematica rapp-
resentano un’interfaccia;

• Prototipi ad Alta Fedeltà, che può essere qualcosa realizzato con uno strumento automatizzato oppure
si può codificare solo la GUI e non le funzionalità.

3.6.4 Specifica dei requisiti

Quando abbiamo definito e prototipato i requisiti, bisogna passare alla specifica dei requisiti, cioè la loro
aggiunta ad un documento. Documenti di questo tipo prendono il nome di Software Requirement Spec-
ification (SRS). Anche se ogni azienda può definire il proprio formato per questo documento, uno standard
che si può seguire è: IEEE /EIA Standard 12207.1-1997. Lo standard richiede:

1. Un’introduzione, che contiene alcune informazioni di corredo del tipo: Nome del progetto che stiamo
documentando, come si collega ad altri progetti, a cosa serve il documento, la versione del documento,
ecc.

35
2. Descrizione di alto livello dei requisiti, una descrizione che non entra nei dettagli tecnici ma per dare
una spiegazione generale del sistema da realizzare;

3. Descrizione dettagliata:

(a) Dei requisiti funzionali, che può essere fatta usando la tecnica Input-Output-Process;
(b) Delle interfacce, sia interfaccia utente che magari interfacce verso altri sistemi;
(c) Dei requisiti prestazionali;
(d) Di altri vincoli;
(e) Di altre proprietà come sicurezza, affidabilità, ecc.
(f) Di ogni requisito specifico che non trova posto nelle precedenti sottosezioni.

3.6.5 Revisione dei requisiti e Sign-O↵

La revisione dei requisiti è un processo che coinvolge sia il cliente che il team che ha prodotto i requisiti ed ha
lo scopo di verificare che il documento dei requisiti non abbia anomalie o omissioni. Se non sono state trovate
pecche nel documento questo deve essere accettato e firmato dal cliente. Questo serve per due motivi:

1. Chiudere questa fase iniziale del processo;

2. Il documento può essere preso come punto di riferimento per le modifiche successive.

36
4 Software Project Management
Tutti i progetti hanno bisogno di un certo grado di controllo, per assicurarsi che il piano di sviluppo stia
progredendo opportunamente.
Il Software Project Management è un processo che si occupa di monitorare che venga applicato un
corretto processo di ingegneria del software durante la realizzazione di progetti.
Tutti i progetti hanno bisogno di tale processo, che essi siano di grandi o di piccole dimensioni, ovviamente
maggiore è la grandezza maggiore sarà la complessità e l’importanza di tale processo.
Bisogna fare attenzione al fatto che la gestione dei progetti è essa stessa un processo, detto anche processo
di management, ed è una cosa di↵erente dal processo di ingegneria del software.
Infatti tipicamente il team si occupa del processo di produzione del software, mentre vi è una figura separata,
il Project Manager, che si occuperà della gestione del progetto.
Potrebbe capitare anche che il Project Manager non sia un Ingegnere Informatico che non ha le competenze
per capire cosa si sta facendo, poichè infatti lui è interessato al come ed al perseguire l’obiettivo finale.

Le quattro attività principali del processo di gestione del progetto, note anche come POMA, sono:
• Planning

• Organizing

• Monitoring

• Adjusting
Nonostante il fatto che la maggior parte di queste attività sembrano essere eseguite in maniera sequenziale,
come mostrato in Figura 4.1, queste quattro attività nella realtà spesso si sovrappongono, dovendo essere
ripetute ed aggiornate più volte.

Figure 4.1: Software Project Management Process

Le attività di Monitoring e di Adjusting, oltre a sovrapporsi, interagiscono ciclicamente, al fine di per-


mettere un corretto controllo del processo. Infatti, capita spesso che le informazioni sullo stato del progetto

37
portino a decidere di e↵ettuare cambiamenti su quest’ultimo.

Il Project Management si impegna quindi a garantire il raggiungimento dei seguenti obiettivi:

• Il risultato finale soddisfa le esigenze del cliente.

• Tutti i prodotti/attributi richiesti dal progetto (qualità, sicurezza, costi, etc.) sono stati soddis-
fatti.

• Il team lavora in modo efficiente e coordinato, con alti livello di morale.

• Materiale e tools di interesse sono reperibili ed utilizzati in maniera efficiente.

È importante ricordare che un Project Manager non può fare tutto da solo ma deve lavorare insieme ai
membri del team per realizzare questi target manageriali.

4.1 Planning
La fase di pianificazione o planning è la prima fase di ogni progetto, infatti, la sua riuscita dipende in
gran parte da una buona pianificazione iniziale.
Avere un piano ben concepito e documentato facilita ampiamente il lavoro e ci permetterà quasi sempre di
anticipare quello che succederà durante tutta la durata del progetto software.

4.1.1 Attività del Planning

Le attività che fanno parte della fase di pianificazione del progetto sono:

• Identificare i requisiti del progetto ed assicurarsi che siano accuratamente descritti, specifici e soprat-
tutto compresi, questa è una fase molto delicata, date anche le problematiche di cui abbiamo discusso
nel paragrafo 3.1.

• Stimare il carico del lavoro, tempi, costi e risorse necessarie al progetto per essere completato.

• Programmare le attività da svolgere.

• Definire e stabilire degli obiettivi realistici durante il progetto. Quest’attività non dovrebbe essere
solo a carico del Project Manager ma di tutto il team di sviluppo.
Il successo di un progetto, infatti, è fortemente determinato dal raggiungimento degli obiettivi pianificati
e concordati insieme. Dunque, qualsiasi goal deve essere misurabile in modo che durante la fase di
monitoraggio si possa avere una misura precisa di quanto un determinato obiettivo è stato raggiunto.
Dividere degli obiettivi grandi in più obiettivi minori, permette inoltre di monitorare tutto il processo
di produzione del software, evitando di incappare in problemi solo nella fase finale di un progetto.

• Determinare le risorse da allocare, dove per risorse si intende: persone, processi, attrezzature,
impianti etc. Le risorse non devono essere intese solo dal punto di vista quantitativo, ma anche dal
punto di vista qualitativo, come ad esempio le competenze del personale richieste.

38
• Identificare ed analizzare i rischi del progetto, ovvero tutti quei fattori che possono determinare
il non raggiungimento degli obiettivi del progetto, possono essere interni all’azienda, come il fatto di
non avere esperienza con una determinata tecnologia, oppure possono essere fattori di rischio esterni
all’azienda, come l’uscita di una soluzione concorrente migliore della nostra.
Data l’importanza, questa fase può essere divisa in 3 parti:

1. Identificazione dei Rischi : dove si considerano tutti i possibili rischi che possono avere un impatto
negativo sul progetto.
2. Prioritizzazione dei Rischi : dal momento che la lista di rischi può essere grande è utile dare delle
priorità e considerare necessario il tracciamento dei soli problemi ad alta priorità.
3. Mitigazione dei Rischi : la parte dedicata all’organizzazione di attività atte a mitigare i rischi
prioritari.

Una volta che i requisiti del sistema sono stati ben identificati, usando le tecniche viste in precedenza, le fasi
restanti della pianificazione del progetto sono molto più semplici da eseguire e completare.
La fase di pianificazione non si ferma solo alla prima parte di un progetto software ma può essere nec-
essaria anche nelle fasi avanzate, dato che quasi sempre ci sono dei requisiti o degli inconvenienti che non
sono stati precedentemente valutati. La fase di planning può essere ripetuta in più di un’occasione.

4.1.2 Fasi del Planning

Gli stage del planning si dividono generalmente in tre fasi:

1. Proposal Planning, è la fase pre-contrattuale per lo sviluppo di un sistema software. In questa fase
potrebbe non esserci una descrizione accurata di tutti i requisiti del sistema. L’obiettivo di questa fase
è dunque quello di dare informazioni che verranno usate per fissare un prezzo ai clienti.
La stima del prezzo di un sistema software prende in considerazione costo dello sta↵, dell’hardware, del
software in sè, etc.

2. Project Startup Planning, è la fase post-contrattuale, prima di inziare il progetto.


In questa fase si ha più conoscenza riguardo i requisiti del sistema ma non si conoscono ancora infor-
mazioni sul design o sull’implementazione. Dal momento che si ha più familiarità col sistema software
da sviluppare, lo Startup Planning viene generalmente utilizzato per definire un piano più accu-
rato su come svolgere tutte le attività descritte nei paragrafi precedenti: l’allocazione di risorse
all’interno del team di sviluppo, la scelta dei meccanismi di monitoraggio, la schedulazione dei tasks,
la definizione dei goals, l’individuazione dei principali rischi, etc.
In poche parole, metteremo in piedi tutto il necessario per partire con lo sviluppo.

3. Development Planning, ha luogo durante lo sviluppo del software, iterativamente.


Il programma del progetto dovrebbe essere regolarmente monitorato e revisionato durante la
fase di sviluppo, dal momento che si hanno più informazioni a riguardo. La schedulazione delle at-
tività, l’allocazione delle risorse, la stima dei costi, dei tempi e i rischi sono fattori che devono essere
regolarmente revisionati

39
4.2 Organizing
Una volta che la fase di pianificazione è stata pienamente, o al massimo parzialmente, formulata, si può
partire con la fase di organizzazione.

Figure 4.2: Comparazione della fase di organizzazione e pianificazione

Come si può notare dalla Figura 4.2, alcune delle attività di organizzazione sono strettamente collegate o
addirittura sovrapposte a quelle di pianificazione.
Ad esempio, non appena una specifica attività di pianificazione, come la pianificazione del rischio, è completa,
siamo già in grado stabilire i meccanismi per tracciare e mitigare i rischi. Per questo, un Project Manager
non deve necessariamente attendere il completamento della fase di pianificazione prima di in-
iziare la fase organizzativa.

Le attività principali relative alla fase di organizzazione sono:

• Design delle strutture organizzative;

• Assunzione delle risorse umane che devono iniziare ed essere completate, con l’acquisizione di tutte
le risorse necessarie;

• Formazione del personale deve essere portata a termine;

• Il meccanismo di tracciamento e monitoraggio deve essere ben definito. Indichiamo due categorie
principali:

– Monitoraggio del rischio e mitigazione


– Supervisione degli obiettivi di progetto(scheduling, costi, etc.)

In particolare, dato che i progetti di sviluppo software sono più dipendenti dalle risorse umane rispetto
alla gran parte dei progetti in altri campi, è di vitale importanza che il Project Manager ponga partico-
lare attenzione alle esigenze relative al personale disponibile, ed in particolare, l’organizzazione di
quest’ultimo, dovendo garantire una struttura organizzativa costruita in modo tempestivo e adatta alla
pianificazione del progetto.
Avere un’ottima pianificazione e non riuscire ad eseguirla per colpa del personale è uno dei più comuni e
scomodi problemi, dato che entra in gioco anche la componente emotiva del personale stesso.
Inoltre in questa fase vi è anche un’organizzazione al fine di gestire i problemi relativi alle risorse fi-
nanziarie e di reperimento delle risorse fisiche necessarie.

40
4.3 Monitoring
Una volta pianificato ed organizzato il tutto, non ci si può certamente aspettare che il processo sia perfetto
e sia possibile lasciarlo a sé stesso. Inevitabilmente ci saranno dei cambiamenti, magari anche dipendenti da
fattori esterni, come il licenziamento di personale o non conformità rilevate relativamente ad alcune proce-
dure.
Se il progetto non è correttamente monitorato, questi cambiamenti potrebbero essere percepiti quando
è ormai già troppo tardi, con elevati costi di aggiustamento.

A tal motivo necessitiamo di meccanismi per misurare in maniera regolare e continua se il progetto
sta procedendo secondo i piani.

Le tre componenti principali nel monitoraggio di un progetto sono:

• Reperimento delle informazioni relative allo stato del progetto.

• Analisi e valutazione delle informazioni collezionate sul progetto.

• Presentazione e comunicazione delle informazioni relative allo stato del progetto.

Come si è ben capito, l’obiettivo principale di questa fase è quindi quello di raccogliere informazioni
pertinenti al progetto. Oltre a capire quali sono le informazioni rilevanti da raccogliere, bisogna capire
anche come queste vengono raccolte.
La raccolta delle informazioni può avvenire principalmente in due modi:

• Canali Formali, come riunioni di revisione del progetto, dove le informazioni devono essere rese
disponibili e presentate senza eccezioni.

• Canali Informali, ad esempio i normali processi di socializzazione (tipo macchinetta del ca↵è), dove
magari è possibile reperire informazioni ancora più veritiere (un buon Project Manager riesce a sfruttare
in maniera ottimale questi canali)

Le informazioni possono essere riportate in più modi, come si nota dalla Figura 4.3

Figure 4.3: Software Project Management Process

41
4.4 Adjusting
Se dalla fase di Monitoraggio si deduce che il processo di produzione software non procede per il verso giusto,
risulta necessario che il team che si occupa della gestione del progetto proceda con la fase di aggiustamento
(Adjusting). Generalmente aspettare che i problemi si risolvano da soli internamente non è una buona
idea, e quasi sempre sancisce il fallimento del progetto. Risulta quindi opportuno intervenire senza paura
dei cambiamenti. Solitamente le aree interessate dagli aggiustamenti sono varie, ma in generale le aree
d’interesse principali sono:

• Risorse, se infatti magari stiamo andando troppo a rilento, il problema potrebbe derivare dal fatto
che non abbiamo abbastanza gente a lavorare, anche se bisogna fare attenzione a come aggiungiamo le
risorse (Legge di Brooks).

• Programmazione delle Attività, se infatti la stima delle tempistiche non ha tenuto conto di alcuni
fattori, potremmo rinegoziare la data di consegna e quindi agire sull’attuale programmazione delle
attività.

• Contenuti del Progetto, se ci rendiamo conto che non riusciamo a terminare in tempo ciò che era
stato pattuito, potremmo rinegoziare con il cliente quali requisiti è possibili omettere, magari alcuni
con priorità più bassa. E’ infatti preferibile rinunciare ad alcuni dei requisiti anzichè consegnare un
prodotto non di qualità, dove è venuto meno il processo di ingegneria del software pur di rientrare nelle
tempistiche.

Anche se sembrano non essere direttamente collegate, un aggiustamento in un’area d’interesse può
avere e↵etti anche sulle altre. Basti pensare alla legge di Brooks (ref), secondo la quale aggiungendo
risorse umane ad un progetto già avviato, per velocizzarlo, non si fa altro che ottenere l’e↵etto opposto.

Da notare, infine, che la qualità del software non è stata aggiunta tra le aree d’interesse degli
aggiustamenti del progetto, dato che non è un parametro che un buon Project Manager dovrebbe ridurre per
favorire la consegna in tempo del progetto finale.
Infatti una scelta del genere non farebbe altro che peggiorare le cose, poichè il costo che risparmiamo nel non
fare le cose con metodo e rispettando il processo, verrà ripagato il doppio quando poi dovremo correggere i
vari bug che verranno fuori nel momento in cui rilasceremo il prodotto al cliente, oltre ovviamente al fare
una pessima figura col cliente stesso.

4.5 Tecniche di Project Management


4.5.1 Planning - Project E↵ort Estimation

E↵ettuare una stima iniziale dell’impegno di un progetto software è sempre stato un compito difficile, spe-
cialmente per i nuovi manager che provengono da ranghi più tecnici. La stima dell’e↵ort e dei costi infatti è
un’attività che richiede molta esperienza ereditata da progetti passati, relativa al conoscere quali sono i fat-
tori da considerare, quante risorse devono essere messe in gioco e quante risorse extra possono essere tollerate.

Nello stimare l’e↵ort di un progetto software, ci devono essere degli input che descrivono in qualche modo
(anche sommariamente) i requisiti del progetto. Dal momento che i requisiti, all’inizio, sono anche essi delle

42
stime, diventa necessario convertirli in una forma numerica, come ad esempio i mesi-uomo.

In maniera più generale, la stima può essere vista come un insieme di fattori progettuali, che possono essere
combinati in qualche forma per fornire una stima dell’impegno. Le principali tecniche di stima sono:

• Metodo Algoritmico

• Giudizio degli Esperti.

• Stima per Analogia

• Legge di Parkinson

• Pricing to win

La maggior parte di queste tecniche di stima hanno un tratto in comune. Essi infatti si basano su un modello
generale che ha la seguente forma:

E↵ort = a + b(size)c + ACCUM(f actors) (4.1)

In particolare:

• La costante a tiene conto delle attività iniziali. Ogni progetto ha un minimo costo di base a prescindere
dalla sua dimensione, che include costi di supporto, amministrativi, di sta↵, di preparazione, di avvio,
etc.

• Il termine b(size)c dipende invece dalla dimensione del progetto. Nello specifico, b è una costante che
scala linearmente con la dimensione, size è una stima delle dimensioni del prodotto finale e c
è una costante che permette alla stima di influenzare l’e↵ort del progetto in una forma non lineare
(cosa che e↵ettivamente accade nella realtà, dal momento che più cresce la dimensione del progetto e
più velocemente cresce l’e↵ort per questo, in termini di comunicazione tra i membri, problematiche,
quantità di documentazione, etc.).

• Il termine ACCUM(f actors) è un accumulazione di diversi fattori che influenzano la stima del pro-
getto, valutati caso per caso. La funzione ACCUM potrebbe essere una somma aritmetica o una
produttoria di una lista di fattori come fattori tecnici, del processo utilizzato, dello sta↵ e altri
vincoli che in qualche modo influenzano il progetto.

Le costanti a, b e c sono calcolate sperimentalmente avendo come riferimento i progetti software passati che
hanno a che fare con lo stesso campo di interesse del progetto di cui si sta calcolando la stima dell’e↵ort.

43
Figure 4.4: Andamento della stima di un progetto software rispetto al suo valore e↵ettivo (asse x)

Nel complesso, costanti e variabili della 4.1 possono cambiare durante lo svolgimento del progetto. La stima
iniziale sarà infatti inevitabilmente a↵etta da errori e poco precisa, soprattutto nelle fasi iniziali del progetto
in cui si potrebbe arrivare a sovrastimare o sottostimare di 3 o 4 volte l’e↵ort e↵ettivo. Più si va avanti con le
successive fasi (raffinando i requisiti, progettando, etc.), più questa stima tende al valore e↵ettivo, riducendo
l’errore a zero soltanto al momento della consegna, come si nota in Figura 4.4.

Per quanto riguarda la 4.1, resta da stimare soltanto la dimensione del progetto. La variabile size può
essere calcolata in 2 modi di↵erenti:

• Lines of Code(LOC)

• Function Point.

Il metodo di stima della size tramite LOC, nonostante sia facile da calcolare, potrebbe presentare numerose
ambiguità. Infatti non è possibile fare un’analisi sulla qualità del codice scritto, due sviluppatori potreb-
bero aver scritto lo stesso numero di linee di codice ma prodotti qualitativamente di↵erenti, venendo valutati
allo stesso modo. Un altro problema relativo al calcolo della size tramite LOC è la scelta del particolare
linguaggio di programmazione utilizzato. In ultima parte, essendo una quantità che è molto difficile da
conoscere durante la fase di Planning di un progetto, si utilizza generalmente per stabilire l’e↵ort dopo la
fine del progetto.

I Function Point sono una metodologia per valutare la complessità delle funzionalità del prodotto
software. Sono un metodo di stima della dimensione di un progetto (size).
Dopo aver stabilito i requisiti funzionali e non funzionali del sistema (anche in maniera sommaria) a cias-
cun requisito si assegna un numero che caratterizza la complessità di quest’ultimo. Tipicamente, esistono

44
dei particolari parametri che ci permettono di valutare la complessità di un determinato requisito come ad
esempio il numero di input, di output, quanti file deve leggere etc.
Per stimare l’e↵ort di un sistema software tramite i FP, dal momento che rappresentano una misura di stima
soltanto della size di un progetto, bisogna convertirli in una misura di e↵ort e quindi bisogna avere un’idea
della produttività del gruppo (bisogna in qualche modo convertire Function Points in una misura di sitma
come mese-uomo). Per fare ciò, assumendo che dai progetti passati si stima che mediamente ogni 25 Function
Points corrispondono ad un’unità di mese-uomo, si potrebbero convertire i ZZZ Function Points totali con la
seguente formula:
(ZZZ Function Points)
E↵ort = (4.2)
25 Function Points/mese-uomo
Un caso patologico si puo riscontrare quando una funzione ha gli stessi Function Point di un’altra, nonostante
sia molto più complessa. Questo succede quando hanno gli stessi parametri (input, output, files, etc.) ma
difficoltà implementative completamente diverse. Per questo bisogna fare particolare attenzione e in questi
casi modificare a mano il valore dei FP.

La maggior parte delle tecniche di stima usano, in forme di↵erenti, tale formula generale 4.1, come ad
esempio:

• COCOMO, abbreviazione di COnstructive COst MOdel, è un modello matematico utilizzato da chi


progetta software per stimare alcuni parametri fondamentali come il tempo di consegna e i mesi-uomo
necessari per lo sviluppo di un prodotto software.

• Function Point Model, come visto in precedenza.

I modelli algoritmici di stima del costo sono un modo sistematico di valutare l’impegno richiesto per lo
sviluppo di un sistema. Tuttavia, alcuni di questi modelli sono complessi e difficili da usare, in quanto, sono
presenti molti attributi e un considerevole margine di incertezza nella stima dei valori. Questa complessità
fa sı̀ che gli aspetti pratici dell’utilizzo di tali modelli siano limitati ad un piccolo numero di grandi aziende.
Inoltre, questo tipo di modello sono applicabili soltanto nei casi in cui si ha un’esperienza pregressa ed un
elenco di vecchi progetti da cui possiamo ricavare dei dati utili. Nei casi in cui questi modelli non sono
applicabili (ad esempio, quando l’azienda produttrice si a↵accia per la prima volta in un mercato in cui non
ha esperienza), si possono usare altre tecniche utili, come:

• Affidarsi ad un panel di esperti esterno. Il panel di esperti è competente in quello specifico


dominio applicativo, fornendoci una stima dell’e↵ort necessario per il progetto. In molti casi, è utile
contattare più di un’organizzazione per confrontare le varie stime ed arrivare ad un consenso comune.
In questo tipo di tecnica però, non si è a conoscenza del metodo utilizzato dagli esperti per fare la
stima.

• Stima per analogia. Ci si basa su esperienze precedenti confrontando con altri progetti, ma invece
di e↵ettuare un’analisi accurata come i modelli algoritmici, si lavora in maniera più grossolana.

• Legge di Parkinson. La legge di Parkinson è un metodo di stima che a↵erma che il lavoro si espande
come un gas per riempire il tempo disponibile. Il costo è determinato dalle risorse disponibili piuttosto
che da una valutazione obiettiva. Se il software deve essere consegnato in 12 mesi e 5 persone sono

45
disponibili, lo sforzo richiesto è stimato a 60 mesi-uomo. L’utilizzo di questo metodo può causare forti
sovrastime e sottostime. Mai usato dalle aziende serie.

• Pricing to win. Il costo del software è stimato per eguagliare quello che il cliente è disposto a spendere
per il progetto. Lo sforzo stimato dipende solo dal budget del cliente e non dalla funzionalità del
software. In alcuni casi, questo metodo può portare dei vantaggi. Prima di tutto, si è sicuri che il cliente
accetti, dal momento che la disponibilità è la sua. In secondo luogo, una mossa del genere potrebbe
assicurare un posto all’azienda produttrice in un mercato nuovo che potrebbe portare dei vantaggi futuri
(o semplicemente per farsi conoscere dal cliente ed avere altri contatti). Solitamente, questa tecnica si
utilizza quando si è sicuri che un eventuale perdita del contratto non porta danni economici immediati,
ma può portare dei vantaggi economici nel tempo (specialmente grandi aziende come Google, non si
fanno tanti problemi ad andare in perdita su un determinato contratto se si ritiene che strategicamente
rappresenta una mossa per entrare in un mercato futuro importante); oppure un’azienda che è in una
situazione disastrosa e ha bisogno di un’entrata immediata in termini economici.

4.5.2 Valutazione della produttività degli sviluppatori

Un buon project manager deve essere in grado di avere consapevolezza sulla reale situazione del progetto, sia
in termini di avanzamento, ma soprattutto in termini di buona collaborazione, efficienza e produttività dei
membri dei team.
È fondamentale quindi, oltre che stimare l’e↵ort di un progetto, valutare anche la produttività degli svilup-
patori, attraverso le due unità di misura viste per la variabile size in precedenza: le LOC e i Function
Points implementati:

• Le LOC non sono una buona misura di valutazione per sè perchè non tengono conto di alcuni aspetti
e fattori importanti come:

– I test relativi al codice prodotto.


– La qualità ed efficienza del codice prodotto.
– Del fatto che lo sviluppatore abbia riusato del codice esistente, favorendo un paradigma della
programmazione ad oggetti.
– Del particolare linguaggio di programmazione utilizzato.
– Della presenza di commenti.
– La complessità del codice che si sta implementando.

Per questo motivo due programmatori, a parità di linee di codice ma con qualità di prodotto finale
diverso, verrebbero valutati allo stesso modo. Le LOC vanno dunque utilizzate in modo complementare
ad altre metriche di valutazione, come i FP.

• I Function Points rappresentano una metrica di valutazione migliore rispetto alle LOC perchè tengono
conto dell’insieme di requisiti che sono stati realizzati in una certa unità di tempo. Non è sempre
una misura perfetta, dal momento che ci può essere del codice legato a un insieme di requisiti, ma è
comunuque un ottimo strumento da affiancare alle LOC.

46
4.5.3 Planning e Organizing - Work Breakdown Structure

La tecnica chiamata Work Breakdown Structure (WBS) consiste in una destrutturazione del progetto
in una serie di sotto attività che possono essere svolte per completare il progetto stesso. I passi da seguire
per applicare la tecnica sono:

1. Esaminare e determinare le consegne esterne richieste per completare il progetto software.

2. Identificare i passi e task necessari per completare ogni consegna.

3. Sequenziare i task, indicando anche quali di questi possono essere svolti in parallelo.

4. E↵ettuare una stima del carico di lavoro necessario per svolgere i task.

5. Produrre una stima della produttività del personale che ha più probabilità di essere assegnato ad uno
specifico task.

6. Calcolare il tempo necessario a completare un task, dividendo il carico di lavoro stimato per la produt-
tività del team affidato al task stesso ed iterare il processo per ognuno dei task.

7. Per ogni consegna, produrre una timeline contenente ogni task necessaria a completare quella consegna
ed associare la risorsa assegnata al task.

Di seguito è presentato un esempio, del tutto generale, dell’uso di questa tecnica:


Consideriamo una generica consegna appartenente ad un progetto. La prima fase della WBS è produrre una
lista dei task:

• TASK 1: “Do Something”

• TASK 2: “Do Something”

• TASK 3: “Do Something”

• ...

• TASK n: “Do Something”

Valutiamo se è possibile dividere ulteriormente i task trovati in sotto attività più semplici da svolgere. Nel
caso in cui fosse possibile dividere il TASK 3 in 4 sottoattività, queste verranno indicate con la notazione:

• TASK 3.a: “Do Something Simpler”

• TASK 3.b: “Do Something Simpler”

• TASK 3.c: “Do Something Simpler”

• TASK 3.d: “Do Something Simpler”

A questo punto si produce una WBS network of tasks (rete di compiti), che serve a mettere in evidenza
quali sono i task che vanno eseguiti in parallelo e quali in sequenza:

47
Figure 4.5: Network of tasks example

Il prossimo passo da seguire per applicare la WBS è stimare il carico di lavoro necessario al completamento
di ogni task. Ricordiamo che più il task è avanti nella nostra rete e più sono le dipendenze coi vecchi task, più
sarà difficile stimare. Non vale la pena scoraggiarsi dato che la fase di aggiustamento permette di modificare
la rete anche in una fase più inoltrata del progetto. Si procede poi con la stima della competenza o della
produttività delle persone che verranno affidate ai vari task e si calcola, infine, il tempo necessario ad ogni
task per essere completato.

Figure 4.6: work breakdown structure table

La tabella risultante rappresenta la prima forma di programmazione di attività che avremo nel nostro
processo di produzione del software ed, inoltre, va fatta controllare al maggior numero di partecipanti al
progetto per garantirne la validità.
Si faccia attenzione a non assegnare una risorsa ad un’attività quando questa è già impegnata in altro!
L’uso corretto dalla WBS permette dunque di ottenere una programmazione delle attività realistica ed

48
attendibile del processo di produzione del software.

4.5.4 Monitoraggio dello stato del progetto con “Earned Value”

Ricordiamo che monitorare un progetto significa mettere a confronto ciò che è stato pianificato con ciò che è
stato realmente fatto al momento del controllo. Per poter far ciò, sarebbe utile avere degli attributi in grado
di darci consapevolezza sullo stato di avanzamento del progetto, per questo motivo introduciamo il concetto
di earned value (EV).
In particolare, il concetto fondamentale alla base della tecnica di management EV, è quella di tener traccia
dello stato del progetto, ad una precisa data temporale, comparando:
• Quanto è stato realizzato del progetto;

• Quanto si era pianificato di realizzare del progetto se il lavoro fosse andato secondo i piani.
Introduciamo una serie di concetti necessari per questa tipologia di analisi:
• BUDGETED COST OF WORK (BCW) : costo stimato per ogni task;

• BUDGETED COST OF WORK SCHEDULED (BCWS) : somma dei costi stimati per tutti i
tasks di cui era stato pianificato il completamento ad una determinata data;

• BUDGET AT COMPLETION (BAC) : costo stimato per tutto il progetto, o semplicemente la


somma di tutti i BCW;

• BUDGETED COST OF WORK PERFORMED (BCWP) : somma dei costi stimati per tutti
i tasks che sono stati completati, ad una determinata data;

• ACTUAL COST OF WORK PERFORMED (ACWP) : somma di tutti i costi realmente imp-
iegati per il completamento di determinati tasks, ad una determinata data.

• EARNED VALUE (EV) : indica quanto il lavoro complessivo è stato completato, ad una determinata
data. (una sorta di percentuale di completamento).
EV = BCW P/BAC

Figure 4.7: Esempio di applicazine di earned value

49
Come si può notare dalla tabella 13.4, il costo stimato è in UOMO-MESE, la seconda colonna indica il BCW,
e la somma di tutte le righe di tale colonna è il BAC.
Alla data 4/5/6 il BCWS, ovvero quelli che avevamo pianificato di finire è 25 (task 1 +task 2), ma il BCWP,
ovvero la somma dei costi stimati di quelli realmente terminati è 50, contro un ACWP, ovvero somma dei
costi realmente spesi, di 55. L’EV in questo caso è 50/115 = .434 (43%).
Altri indicatori utili, che sfruttano i concetti sopra esposti, sono:
• Cost Variance: pari a BCWP - ACWP, ovvero la di↵erenza tra il costo stimato dei task definiti-
vamente completati ad una certa data, meno il costo e↵ettivo.

• Schedule Variance: pari a BCWP - BCWS , ovvero la di↵erenza tra il costo stimato dei task defini-
tivamente completati ad una certa data, meno il costo dei task che avevamo pianificato di completare
alla data stessa.
Dato che queste di↵erenze possono avere valori negativi, osserviamo che il primo è un indicatore più
orientato al costo, per capire se siamo in linea con i costi stimati o meno (¡0 stiamo spendendo mesi uomo in
più), mentre il secondo serve a capire se siamo in ritardo o meno (¡0 siamo in ritardo).

4.6 Software Pricing


Una parte importante del lavoro di chi gestisce un progetto software per un’azienda è quello di determinarne
il prezzo, che è di↵erente dal costo. Quest’ultimo viene stimato per comprendere quanto costerà il prodotto
all’azienda e per stabilire quale sarà il prezzo da proporre al cliente. Non è detto che esista una relazione
semplice tra i due. Tuttavia, possiamo a↵ermare con certezza che il prezzo non è uguale al costo siccome,
nell’ipotesi che lo fosse, l’azienda produttrice non ne ricaverebbe nulla.
Pertanto, esistono una serie di situazioni che possono rendere la relazione tra costo e prezzo meno lineare;
• Contratti diversi: esiste la possibilità di scambiare una parte del valore economico del progetto
con delle condizioni contrattuali più favorevoli per l’azienda o per il cliente.
Un esempio è chi mantiene la proprietà del software dopo che questo è stato realizzato:

– il proprietario del software è il cliente, cosı̀ facendo, per modificare il prodotto il cliente dovrà
rivolgersi ad un’azienda esterna o a degli sviluppatori interni se li ha a disposizione. In questo
caso, la casa produttrice cercherà di recuperare tutti i costi sostenuti ed avere un certo margine
di profitto;
– il proprietario del software rimane l’azienda, cosı̀ facendo, cederà la licenza d’uso al cliente
ma potrà vendere il prodotto anche ad altre aziende. In questo caso, l’azienda cercherà di non far
pagare tutto il costo al cliente perchè si aspetterà di ricavare un profitto anche da altri clienti per
il medesimo software;
– realizzo il software per immetterlo sul mercato e non per uno specifico cliente (Caso Es-
tremo). In questo caso il prezzo che farò pagare a chi comprerà il prodotto sarà una frazione
piccola del costo speso (Se mi aspetto mille copie vendute,ogni copia costerà un millesimo del
prezzo originario)
– cedo in maniera esclusiva il prodotto a un cliente. In questo modo se il cliente necessita
di una manutenzione, un aggiornamento ecc dovrà rivolgersi all’azienda perchè non dispone del
codice sorgente.

50
• Incertezza nella stima: nel momento in cui l’azienda proporrà l’o↵erta economica al cliente tramite
delle stime eseguite inizialmente, queste sarranno a↵ette da un margine di errore. Per poter controbi-
lanciare questi errori, utilizzo dei margini sul prezzo aggiungendoci il profitto dell’azienda. In questo
modo è possibile ammortizzare dei possibili aumenti di costo senza subire perdite. Pertanto, stabilisco
quanto sia grande il mio margine in base a quanto ritengo sia alta l’incertezza. Chiaramente, il margine
che stipulerà non dobrà essere troppo alto in modo da non perdere il cliente.

• Finanziario: un azienda potrebbe ritrovarsi in una situazione economica favorevole (somma dei
suoi crediti meno somma dei suoi debiti positiva), ma situazione finanziaria sfavorevole, ovvero, i
soldi che l’azienda possiede in questo momento sono minori rispetto a quelli che deve pagare. Questo
accade, per esempio, quando l’azienda ha un credito con un cliente a fine anno, di una somma tale
che riesca a saldare tutti i suoi debiti ma, questi scadano predentemente al pagamento del cliente. Di
conseguenza, siccome l’azienda ha urgenza immediata di liquidità, perchè altrimenti non riuscirebbe a
pagare i suoi debitori e potrebbe fallire, potrebbe capitare che l’azienda riduca il prezzo per un cliente
a patto che paghi subito.

• Opportunità di mercato: l’azienda potrebbe voler entrare in un mercato in cui non è presente
siccome si aspetta di poter realizzare un giro di a↵ari su di esso, ma deve in qualche modo entraci. Per
farlo, deve presentarsi con un progetto già realizzato per quel mercato ma essendo che l’azienda non ha
contratti nel settore avrà difficoltà ad avere altri contatti. Pertanto, quest’ultima potrebbe decidere di
fare un contrattto svantaggioso per se stessa (perdita) ma vantaggioso per il cliente in modo da poter
entrare in quel mercato. Questo vale se il mercato è ritenuto strategico per l’azienda.

• Volatilità dei requisiti: In diversi progetti può capitare che essendo i requisiti non chiari all’inzio si
potrebbero aggiunger dei requisiti durante il progetto che verrano valorizzati economicamente separati
dai requisiti iniziali. Di conseguenza, nela caso l’azienda si aspettasse una situazione di questo tipo
potrebbe stabilire un prezzo minore sui requisiti iniziali per accapararsi il cliente per poi recuperare
sui requisiti aggiuntivi. Un esempio, sono i venditori di stampanti e rasoi che recuperano sui prodotti
di ricambio.

• Costi nascosti: l’azienda deve stimare all’inizio quanti mesi/uomo ci vogliono per ultimare il progetto
in modo da calcolare il costo per poter pagare il personale.
Attenzione!! se viene pagato solo il personale l’azienda risulta in perdita siccome avrà ulteriori spese
interne che dovrà pagare.
Pertanto l’azienda dovrà incorporare una quota anche per queste spese chiamate spese generali che
non sono dierettamente imputabili al progetto. L’azienda calcola questa quota tramite un fattore di
proporzionalità tra il costo del personale e i costi accessori.

4.7 Pricing Strategies


Da quanto detto nei paragrafi precedenti emergono una serie di motivazioni per cui l’azienda proporrà un
prezzo che non è direttamente collegato con il costo. L’azienda può decidere se attuare:

• Under Pricing: vende a un prezzo più basso rispetto a quello di vendita;

• Over Pricing: vende a un prezzo più alto rispetto a quello che dovrebbe chiedere.

51
4.8 Pricing to win
Una particolare strategia è il pricing top win che si attua, nel caso in cui l’azienda voglia a tutti i costi
entrare in un mercato in cui non è presente, oppure, quando l’azienda ha una difficoltà fininziaria e necessita
di un entrata immediata in modo da sopperire i costi. L’azienda, in questo caso, stabilisce il prezzo in base a
quanto si aspetta che il cliente voglia pagare che, in generale, è minore rispetto al costo di produzione del
software. Di conseguenza la priorità per l’azienda in questo momento è esclusivamente l’ingresso nel settore,
se ci riuscirà cercherà di recuperare la perdita. Questa tecnica, applicata in alcune situazioni, può risultare
una buona strategia, tuttavia se un’azienda la utilizza troppo spesso c’è qualche problema.

52
5 Scrum
5.1 Introduzione
Nei primi anni 2000 sono stati proposti dei metodi per lo sviluppo software che proponevano un approccio
in cui si dava meno enfasi sulla pianificazione in anticipo e più enfasi sulla possibilità di avere feeedback
costanti. Nascono quindi i metodi di sviluppo agile del software e tra questi l’approccio allo sviluppo
agile del software Scrum.

5.1.1 Definizione di Scrum

Scrum è un framework all’interno del quale le persone possono a↵rontare diversi e complessi problemi adattivi,
o↵rendo al contempo in modo produttivo e creativo prodotti del massimo valore possibile.

Scrum non è un processo standardizzato in cui si segue metodicamente una serie di passaggi sequenziali
che garantiscono di produrre, nei tempi e nel budget previsti, un prodotto di alta qualità che soddisfa i
clienti. Piuttosto, è un framework per organizzare il lavoro all’interno del quale è possibile impiegare vari
processi e tecniche. Esso chiarisce l’efficacia relativa alla gestione del prodotto e delle tecniche di lavoro in
modo da poter migliorare continuamente il prodotto, il team e l’ambiente di lavoro.

Il framework Scrum è costituito dallo Scrum Team e dai loro ruoli, cerimonie, artefatti e regole associati.
Ogni componente all’interno del framework ha uno scopo specifico ed è essenziale per il successo e l’utilizzo
di Scrum. Le regole di Scrum, infine, legano insieme i ruoli, le cerimonie e gli artefatti, governando le relazioni
e l’interazione tra di essi.

5.1.2 Caratteristiche di Scrum

Il termine deriva dal sostantivo inglese utilizzato in ambito rugby mischia, per enfatizzare il fatto che il team
insieme, spalla a spalla, spinge il prodotto software verso la versione finale.

Le principali features per cui Scrum è diventato popolare come framework per lo sviluppo di prodotti sono
le seguenti:

• Processo agile: mette tutta l’attenzione sul consegnare il massimo valore possibile al cliente nel min-
imo tempo possibile. Ciò è ottenuto rimuovendo tutti gli impedimenti (e.g. ridurre la documentazione
eccessiva, necessaria solo se serve a dare il massimo valore al software).

• Approccio iterativo: Al cliente viene presentata periodicamente una release funzionante del software.
Le iterazioni che consentono release periodiche sono di durata generalmente breve: questo favorisce un
feedback frequente da parte dei clienti.

• Il cliente determina la priorità: È il cliente a scegliere le features del software che hanno più priorità
rispetto alle altre, è poi il team di sviluppo a determinare gli obiettivi in base a queste priorità.

• Software funzionante e reale: Ad ogni release viene rilasciata una versione del software funzio-
nante, in modo da permettere al cliente di poter decidere se andare avanti col progetto o fermarsi.

53
Infatti, gli accordi con i clienti non sono dettati da rigide regole per tutta la durata dello Sprint, ma
possono cambiare tra un’iterazione e l’altra: flessibilità.

• Team auto-organizzati: In forte contrapposizione con i metodi tradizionali. L’organizzazione interna


del team viene infatti decisa in base alle singole abilità dei vari componenti che ne fanno parte.

• Nessuna specifica sulle tecniche ingegneristiche: Scrum non dà indicazioni su tecnologie e
metodologie da adottare, quello è deciso dal team. Infatti Scrum può essere pensato come le fondamenta
e le pareti di un edificio: i valori, i prı̀ncipi e le pratiche di Scrum sono i componenti strutturali chiave
senza i quali si rischia il collasso. Tuttavia, si può personalizzare la ”struttura”, aggiungendo dettagli
finchè non si ha un processo che funziona. Scrum dà una forte enfasi sulla gestione del progetto.

5.1.3 Funzionamento di Scrum

Il funzionamento di Scrum segue un preciso schema, come mostrato in Figura 5.1.

Figure 5.1: Ciclo di Scrum

Il Product Backlog (cfr. Sezione 5.4.1) è la coda dei requisiti da implementare. Prima dell’inizio di
ogni Sprint (cfr. Sezione 5.3.1), viene selezionato un sottoinsieme dei requisiti da implementare contenuti
all’interno del Product Backlog per la successiva iterazione. Durante la fase di Sprint, le persone all’interno
del team lavorano su iterazioni giornaliere, decidendo quale task svolgere ogni giorno. Giornalmente, alla
stessa ora, vengono organizzate delle riunioni dalla breve durata (Daily Scrum Meeting, cfr. Sezione 5.3.3)
allo scopo di aggiornare i vari membri sullo stato di avanzamento e discutere di eventuali problematiche. A
questi stand-up quotidiani partecipano soltanto i membri del team di sviluppo ed eventualmente lo Scrum
Master (cfr. Sezione 5.2.2). Alla fine del ciclo di Sprint, viene rilasciato un incremento del software che ha le
importanti caratteristiche di essere reale, funzionante e conforme alla definizione di ”Fatto” (cfr. Sezione
5.8).

In Figura 5.2 è possibile apprezzare un’ overview sui principali ruoli, attività e artefatti di Scrum. Dalla
figura si può notare che le informazioni da cui vengono costruiti i requisiti del prodotto non provengono

54
soltanto dal cliente che lo ha commissionato, bensı̀ da una vasta tipologia di persone, ognuna ricoprente
un ruolo diverso nella defizionione delle caratteristiche e dei vincoli: Stakeholders, Team, Clienti, Dirgenti,
Utenti, etc.

Figure 5.2: Overview su Scrum

A di↵erenza di una metodologia tradizionale, in un progetto basato sul framework Scrum le attività sono spal-
mate o interfogliate per tutta la durata e↵ettiva del processo, quindi non sono rigidamente sequenziali.
Ci si aspetta che nelle prime fasi, come si nota in Figura 5.3 sia fatto un particolare sforzo maggiore nella
definizione dei requisiti. Dato che quest’ultimi potrebbero variare nel tempo, o esserne aggiunti di nuovi, tale
fase potrebbe essere ripresa nell’iterazioni successive.

55
Figure 5.3: Di↵erenza tra sviluppo sequenziale e sovrapposto

5.1.4 Ruoli

Il team di Scrum è composto principalmente da tre ruoli (cfr. Sezione 5.2):


• Product Owner

• Scrum Master

• Team di Sviluppo
Il Product Owner è una singola persona che ha la responsabilità di definire i requisiti e le loro priorità
all’interno del Product Backlog.
Il Team di Sviluppo è un team di professionisti che si occupa del lavoro di pianificazione e realizzazione
delle attività.
Lo Scrum Master si occupa di supervisionare la corretta applicazione del framework. Dà indicazioni
soltanto su come applicare la metodologia, non fornisce informazioni tecniche od organizzative al resto del
team.

5.1.5 Product Backlog: uno strumento per la pianificazione

La pianificazione del progetto è basata sul Product Backlog, che principalmente contiene i seguenti elementi
previsti per il progetto:
• Funzionalità del prodotto o requisiti funzionali (nuove o da migliorare)

• Miglioramenti tecnologici
Sulla base degli elementi presenti all’interno del Product Backlog, ad ogni Sprint si tengono due incontri:
uno per decidere le caratteristiche del prossimo Sprint, gli elementi da poter estrarre dal Product
Backlog e inserire nello Sprint Backlog (cfr. Sezione 5.4.2);

uno per pianificare il lavoro per per il prossimo Sprint e scegliere quali attività/task vanno fatte per
realizzare i requisiti all’interno dello Sprint Backlog.

56
A un requisito possono essere associate una o più attività.

5.1.6 Il processo Scrum

Il processo di Scrum si divide in tre fasi : Pre-Game Phase, Game Phase e Post-Game Phase.

5.1.6.1 Pre-Game Phase

Durante questa fase ha luogo la pianificazione ad alto livello (dal momento che i requisiti possono cambiare
insieme al piano) che comprende diverse attività svolte dal Product Owner e dal Team di Sviluppo. Il Product
Owner:

stabilisce l’obiettivo del progetto;

definisce i requisiti del prodotto;

crea una lista ordinata di requisiti conosciuta come Product Backlog, ordinata per priorità;

decide il numero di release del prodotto;

stabilisce le date di consegna del prodotto.

Il Team di Sviluppo:

rivede il Product Backlog;

identifica e assegna i deliverable ai diversi Sprint.

Infine, il Team di Sviluppo e il Product Owner esaminano il Product Backlog per elaborare una proget-
tazione ad alto livello.

5.1.6.2 Game Phase

La Game Phase è la fase dove esistono le pratiche fondamentali di Scrum e dove viene svolto il grosso del
lavoro.
Durante questa fase il team di Scrum pianifica il lavoro da includere in ogni Sprint e completa i task per
lo sviluppo dell’iterazione corrente, che possono comprendere anche unit test dei vari moduli e correzioni
o miglioramenti. Si tengono standup giornalieri (Daily Scrum Meetings) per aggiornare tutti sui loro
progressi e su eventuali ostacoli che vanno risolti.

Al termine dello Sprint, viene condotto uno Sprint Review (cfr. Sezione 5.3.4) durante il quale il team
presenta il risultato dello sprint al cliente per la revisione ed eventuali feedback, che saranno poi incorporati
negli sprint successivi sottoforma di requisiti.
Un’ultimo meeting prima di una successiva iterazione è lo Sprint Retrospective che è condotto dallo
Scrum Master per ragionare su come è stato applicato il processo e indentificare possibili miglioramenti al
modus operandi degli Sprint imminenti.

57
5.1.6.3 Post-Game Phase

In questa fase il progetto viene definitivamente chiuso. Le ragioni possono essere diverse:

• Il cliente è soddisfatto e decide di fermare le release.

• Il cliente è insoddisfatto e decide di fermare il progetto.

• Il cliente ha deciso che il progetto non può più aggiungere valore ai suoi scopi e decide di fermare le
iterazioni.

Alcuni progetti software sono in continua evoluzione e non raggiungno mai questo stadio.

5.1.7 Monitoraggio dei progressi

Il monitoraggio dei progressi può avvenire secondo due strumenti: lo Sprint Burndown chart (cfr. Sezione
5.4.3) e la Sprint task board. Entrambi sono ottimi strumenti sia per il monitoraggio, sia per garantire
trasparenza sull’operato ai membri di tutto il team ed aumentare il morale e la motivazione grazie alla visu-
alizzazione concreta del lavoro svolto.

Lo Sprint Burndown chart tiene traccia del numero cumulativo di lavoro rimanente in uno Sprint rispetto
al numero di giorni rimasti nello sprint. È anche un buon indicatore visivo della velocità del team (cfr.
Sezione 5.7.5).

La Sprint task board viene utilizzata per tenere traccia dello stato di avanzamento delle singole attività.
Consente infatti di vedere nell’immediato quali attività devono essere ancora avviate, quali sono in corso e
quali sono state completate.

5.2 Ruoli di Scrum


Lo Scrum Team è composto da un Product Owner, dal Team di Sviluppo e da uno Scrum Master.
Gli Scrum Team sono auto-organizzati e interfunzionali. I team auto-organizzati scelgono il modo
migliore per svolgere il proprio lavoro, piuttosto che essere diretti da altri al di fuori del team. I team
interfunzionali hanno tutte le competenze necessarie per svolgere il lavoro senza dipendere da altri che non
fanno parte del team. Il modello di team in Scrum è infatti progettato per ottimizzare flessibilità, creatività
e produttività.

5.2.1 Product Owner

Il Product Owner è responsabile della massimizzazione del valore del prodotto risultante dal lavoro del Team
di Sviluppo. È l’unico che si occupa della gestione del Product Backlog e di esprimerne chiaramente i vari el-
ementi, le loro priorità (ordine) e le date di rilascio, facendo in modo che tutti i membri del Team di Sviluppo
ne abbiano compreso i significati. Inoltre, è responsabile del valore di mercato del prodotto e di quanto esso
possa fruttare guadagni.

Il Product Owner deve garantire che il Product Backlog sia sempre visibile, trasparente, chiaro a tutti
i membri e mostri su cosa lo Scrum Tream lavorerà successivamente.

58
Ad ogni iterazione gli è possibile rimuovere, modificare o aggiungere nuove features sulla base dei feedback
del cliente o di ciò che succede al mondo esterno e può accettare o respingere i risultati di un’iterazione.

Affinché il prodotto abbia successo, l’intera organizzazione deve rispettare le decisioni del Product Owner,
direttamente visibili nel contenuto e nell’ordine del Product Backlog: nessuno può obbligare il Team di
Sviluppo a lavorare con un diverso insieme di requisiti.

Chi è? Un rappresentante del cliente, una persona interna all’azienda produttrice ma separata dal team di
sviluppo, un cliente, etc.

5.2.2 Scrum Master

Lo Scrum Master è la figura a servizio del team che ha il compito di rimuovere gli impedimenti che lo
rallentano. Egli deve:

• capire se ci sono problemi di natura non tecnica (interpersonale);

• proteggere il team da influenze esterne, ad esempio, dai dirigenti dell’azienda: è il team che decide
gli aspetti tecnici ;

• guidare il team nella corretta applicazione di Scrum, non solo seguendo alla lettera la metodolo-
gia, ma anche seguendone lo spirito.

Egli fornisce servizio anche al Product Owner in diversi modi, come aiutarlo nel trovare tecniche per la
definizione efficace degli obiettivi, nella gestione del Product Backlog e facilitare la collaborazione con gli
stakeholders.

Lo Scrum Master non è un project manager, non ha l’autorità sopra il Teaem di Sviluppo, non è il re-
sponsabile della gestione dei progressi, non rappresenta il Team di Sviluppo negli incontri fra più team.

5.2.3 Team di sviluppo

Il Team di Sviluppo è tipicamente composto dalle 6 alle 9 persone. È cross-funzionale, quindi contiene
tutte le competenze necessarie per lo sviluppo: sviluppatori, tester, designer, etc. Inoltre il team è auto-
organizzato, quindi il lavoro viene gestito dagli stessi membri e non esistono gerarchie tra le persone.
Collabora con i clienti e gli utenti per chiarire e accettare i requisiti, dando un feedback al Product Owner
sulla loro chiarezza.
Il Team dovrebbe rimanere stabile, ciò significa che un cambiamento alla struttura, nel caso fosse necessario,
deve avvenire tra un’iterazione e l’altra il prima possibile.

E il Project Manager? Le varie attività del Project Manager sono suddivise tra i vari membri dello
Scrum Team come mostrato in Figura

59
5.3 Cerimonie di Scrum
Gli eventi della metodologia Scrum sono utili per creare regolarità e minimizzare la necessità di riunioni non
definite. Tutti gli eventi sono di tipo time-box, ovvero hanno una durata massima prefissata.
Una volta che uno Sprint inizia, la sua durata è fissa e non può essere abbreviata o allungata. Invece, gli
eventi restanti di Scrum possono cessare ogni volta che si raggiunge lo scopo dell’evento stesso, evitando
quindi di spendere più tempo del necessario per i processi Scrum.
A parte lo Sprint stesso, ogni evento in Scrum è un’opportunità formale per ispezionare e adattare qualcosa.
La mancata inclusione di uno qualsiasi di questi eventi si traduce in una minore trasparenza ed un minore
controllo della situazione.
Le cerimonie principali sono:

• Sprint Planning Meeting

• Daily Scrum

• Sprint Restrospective

• Sprint Review

Figure 5.4: Scrum Checkpoints

60
5.3.1 Sprint

Il cuore di Scrum è lo Sprint, ovvero una iterazione dello sviluppo del prodotto in questione.
Gli Sprint hanno una durata solita che va dalle 2 ad un massimo di 4 settimane, o in genere una misura
ordine della settimana. Il motivo di tale scelta risiede nel fatto che usare Sprint troppo lunghi o brevi portano
problemi:

• Con uno Sprint troppo lungo passa troppo tempo fino al completamento dello stesso, i requisiti
potrebbero cambiare, o peggio ancora potrebbero esserci state incomprensioni e quindi viene rilasciato
un incremento che non rispetta le volontà del cliente. Inoltre, non aspettare troppo a lungo aiuta ad
aumentare significativamente la fiducia che il cliente ha nei confronti del team e il livello di soddisfazione,
dal momento che il software è funzionante ed è rilasciato con frequenza precisa.

• Con uno Sprint troppo breve il team dovrebbe spendere maggior tempo per organizzarsi e, a causa
dell’overhead introdotto da tutte le cerimonie di Scrum, non avrebbe nemmeno il tempo di consegnare
un incremento soddisfacente. Inoltre, crescerà anche il lavoro del cliente, dato che a cadenze temporali
troppo brevi è costretto a partecipare ai meeting di review e formulare dei feedback.

Il fine ultimo di uno sprint è quindi quello di rilasciare un incremento del prodotto (deliverable) che possa
essere usato e potenzialmente rilasciato ai clienti, favorendo un feedback costante e rapido.
Durante uno Sprint è importante che:

• Non ci siano cambiamenti, come quelli relativi ai requisiti, che possano ostacolare lo Sprint Goal.

• La qualità non diminuisca per riuscire a portare a termine tutti i tasks.

• Ci sia un contatto costante con il Product Owner, affinché nel caso in cui ci siano scoperte che vadano
contro le indicazioni sugli attuali tasks, si possa rinegoziare su di questi.

Durante uno Sprint è concessa un’unica operazione correttiva, ovvero cambiare il numero di cose da fare
nello sprint, a seconda che ci sia una condizione di anticipo o ritardo. E’ infatti vietato:

• Cambiare le risorse allocate (team)

• Cambiare i tempi programmati, anticipandoli o posticipandoli.

È inoltre possibile, solo ed esclusivamente sotto volontà del Product Owner, cancellare uno Sprint in
corso, tutto quello che è stato “fatto” viene revisionato, i restanti tasks vengono reintrodotti nella Product
Backlog. Cancellare uno Sprint è una decisione, oltre che traumatica per il Team, estremamente costosa,
dovendo rifare la pianificazione e tutti gli eventi necessari.

61
5.3.2 Sprint Planning Meeting

In questa cerimonia viene pianificato il lavoro che dovrà essere svolto dal team, ed il team stesso collabora
per capire cosa sia possibile e conveniente fare nel prossimo Sprint.
La durata di questa cerimonia varia in base alla durata dello Sprint, ad esempio per pianificare uno Sprint
mensile sono richieste 8 ore (valore massimo per uno Sprint Planning Meeting).

Essenzialmente si parte e ci si basa sull’insieme dei requisiti, con annesse priorità, presenti nella
Product Backlog, i quali ricordiamo essere definiti dal Product Owner e revisionati dai membri del Team.

Per poter pianificare al meglio ci sono però ulteriori informazioni che devono essere considerate:

• La velocità del team, misurata costantemente dal team stesso, è usata per rendere le prossime piani-
ficazioni più precise (calcolo con Story Points). Nel caso di prima iterazione il team può considerare
stime di progetti passati.

• I vincoli presenti tra i requisiti della Product Backlog, che definiscono quindi quali requisiti dipendono
dall’implementazione di altri (es. requisito di login come amministratore dipende dall’implementazione
del login stesso).

L’output di tale cerimonia è una Sprint Backlog, contente tutti i requisiti da implementare nello Sprint in
pianificazione, e la divisione in attività dei requisiti stessi.

5.3.3 Daily Scrum

Ogni giorno, all’inizio della giornata lavorativa i membri del team si incontrano e discutono sui progressi ed
eventuali problematiche riscontrate.
Questo viene anche chiamato “Daily Standup” dato che il meeting solitamente avviene con tutti i presenti
in piedi. Il motivo di tale scelta è dovuta al fatto che questo deve essere un meeting rapido, dalla durata
massima di 15 minuti. Durante questa cerimonia, le persone a turno prendono la parola e illustrano:

• Cosa ho fatto ieri o dallo scorso Daily Scrum

• Cosa farò oggi o prima del prossimo Daily Scrum.

• Quali sono le difficoltà che ho riscontrato

L’utilità di questo evento è sicuramente quello di prendere consapevolezza sullo stato di avanzamento dei
lavori, rilevando eventuali problemi o impedimenti riscontrati. In particolare, alcuni dei problemi riscontrati
potrebbero già essere stati risolti da altri componenti del team, i quali possono darci suggerimenti e farci di
conseguenza risparmiare tempo.
Tra le cose da non fare durante un Daily Scrum vi è sicuramente l’assumere atteggiamenti “zombie”, ovvero
non tirare fuori nessuna informazione relativa al proprio lavoro ed eventuali problemi riscontrati.

62
5.3.4 Sprint Review

Questa cerimonia si tiene alla fine dello Sprint ed ha come scopo quello di revisionare l’incremento maturato
durante lo Sprint, magari tramite una vera e propria dimostrazione delle funzionalità sviluppate, eventual-
mente confrontandole con quelle presentate nell’incremento precedente.

Lo scopo è quello di ricevere un feedback dal Product Owner, capendo quindi se l’incremento è stato
utile, e soprattutto se è stato realizzato ciò che era stato realmente richiesto. Questo può essere utile an-
che per suscitare nuove idee al Product Owner al fine di aggiungere nuovi requisiti utili nella Product Backlog.

Il risultato dello Sprint Review è quindi una Product Backlog rivista che definisce i probabili requisiti presenti
per lo Sprint successivo.
La durata massima di questa cerimonia è di circa 4 ore al massimo, lo Scrum Master deve far sı̀ che questa
avvenga nel luogo e nel modo idoneo per gli scopi della stessa.

5.3.5 Sprint Retrospective

Questa cerimonia è il momento in cui, anziché discutere su cosa è stato prodotto, si discute su come ha
lavorato il team.
Quindi si ragiona su eventuali modifiche da apportare al modo di lavorare, discutendo su cosa è stato utile e
cosa no, in ottica di migliorare le condizioni di lavoro del prossimo Sprint.

Solitamente viene sfruttato un diagramma a stella marina, utile soprattutto per capire cosa è stato
prodotto durante tale cerimonia. Il diagramma è composto da 5 gruppi di indicazioni:

• More of

• Less of

• Start

• Stop

• Keep Doing

Bisogna fare attenzione al fatto che in questo caso si parla solo di attività relative al modo di lavorare. Ad
esempio, se nello scorso Sprint abbiamo fatto un certo numero di test, che non è bastato a scovare determinati
bug successivamente emersi, nella sezione MORE OF verrà introdotto “Numero di Tests”.

La durata massima di questa cerimonia è di circa 3 ore al massimo, e viene e↵ettuata dopo la Sprint Review
e prima di iniziare un nuovo Sprint.
Lo Scrum Master ha il compito di assicurarsi che il meeting avvenga in maniera costruttiva e produttiva.

63
5.4 Artefatti Scrum
Gli artefatti definiti da Scrum sono progettati per massimizzare la trasparenza delle informazioni chiave
necessarie ad assicurare ai Team Scrum il successo nella realizzazione di un Incremento. In particolare essi
sono:

• Product Backlog

• Sprint Backlog.

• Incremento

Altri artefatti tipici ma che non fanno parte di Scrum sono:

• Task Board

• Burn-down chart.

5.4.1 Product Backlog

Il product Backlog è una lista prioritizzata delle funzionalità del prodotto desiderate. Esso provvede
a una comprensione centralizzata e condivisa di cosa costruire e in che ordine farlo. Il Product Owner è
responsabile del Product Backlog compreso il contenuto, la disponibilità e l’ordinazione. Il Product Backlog
è dinamico; esso costantemente cambia per identificare di cosa il prodotto ha bisogno per essere appropriato,
competitivo, e utile.

Il product Backlog può essere visto come un iceberg, in cima vi sono i requisiti con più alto livello di dettaglio
e servono allo Scrum Team per poter iniziare il lavoro, infatti questi, devono poter coprire almeno due o
tre Sprint iniziali. Spostandoci verso il basso i requisiti possiedono un minor livello di dettaglio, essi infatti
vengono chiamati storie se rappresentano un singolo requisito o epiche se sono un gruppo di storie che non
rispettano il vincolo di atomicità, man mano che il progetto prosegue essi vengono raffinati.

Figure 5.5: Iceberg rappresentate il livello di elaborazione dei Requisiti presenti nella Product Backlog

64
In particolare si nota nella Figura 5.5 la presenza di 4 livelli:

• Waiting for Development: ovvero quei requisiti che, oltre ad essere ben curati, sono testabili, quindi
presentano criteri di accettazione, e sono stimati per l’impegno necessario al loro completamento.
Solitamente in questo stato vi devono essere un numero di requisiti necessario per coprire almeno 2 o
3 Sprint, in modo da poter sempre andare avanti col lavoro in caso di necessità.

• Waiting for Estimation: sono tutti quei requisiti dove non sono ancora definiti i criteri di ac-
cettazione, oppure manca la stima stima dello sforzo necessario al completamento di questi.

• In Process: I Requisiti non sono ancora suddivisi in singole storie, infatti si parla epiche, ovvero
un insieme di storie collegate. Spesso di alcune storie è addirittura definito solo il titolo.

• To Be Defined: sono l’insieme di requisiti che sono definiti ad un livello di dettaglio molto basso, le
funzionalità sono spesso poco chiare e necessitano di un’ ulteriore definizione.

5.4.2 Sprint Backlog

Lo Sprint Backlog è un insieme di elementi prelevati del Product Backlog selezionati per essere implementati
nello lo Sprint corrente e rappresenta una previsione dal team di sviluppo su quali funzionalità saranno nel
prossimo Incremento. Successivamente questi requisiti selezionati vengono inseriti all’interno di una Task
Board in ordine di priorità.
La Task Board è una lavagna divisa in 3 colonne:

• To-Do: ovvero i requisiti che devono essere implementati.

• In Progress: ovvero i requisiti attualmente in fase di completamento.

• Done: ovvero i requisiti completati.

La Task Board aiuta gli sviluppatori a capire come procede l’andamento del lavoro e tutte le informazioni
all’interno di essa venongo discusse all’interno dello Sprint Retrospective.

Sono anche chiamate radiatori di informazioni perchè vengono visualizzate pubblicamente e utilizzate
per trasmettere informazioni sullo stato del progetto.

5.4.3 Burndown Chart

Il Burndown Chart è un artefatto utile al Team per avere percezione dell’avanzamento del lavoro, esso si
basa su una serie di Story Point (cfr. Sezione 5.6) da completare, che dimuiscono man mano che le attività
vengono realizzate. In questo modo ci si può rendere conto se il progetto è in ritardo o in anticipo rispetto
ai tempi e di conseguenza si possono diminuire o aumentare il numero di attività da svolgere.

Un altro importante paramentro è la velocità del Team nel completare lo Sprint. Siccome Scrum si basa
prettamente sulle misure e↵ettutate, piuttosto che su previsioni, inizialmente viene e↵ettuata una stima della
velocità con la quale si spera di completare lo Sprint. Tuttavia, non sempre viene raggiunto tale obiettivo e
alla fine dell’iterazione otteniamo la velocità e↵ettiva del team che verrà usata come parametro di riferimento
nella successiva iterazione.

65
Figure 5.6: Burndown Chart

5.4.4 Doppio istogramma

Oltre che monitorare i progressi di un singolo Sprint, è possibile tenere sotto controllo l’avanzamento dell’intero
progetto. Questo grazie ad un particolare istogramma detto ”doppio istogramma”.

Come si può notare dalla Figura 5.7, lo Scope burn identifica il numero di story points che vengono bruciati
durante ogni iterazione, mentre lo Scope growth indica il tasso di crescita dei requisiti o delle attività da
fare che vengono aggiunte e completate durante i cicli di Sprint.
Tracciando una retta che intercetta entrambe le crescite dei due instogrammi, l’intersezione rappresenta la
possibile data di rilascio del prodotto.

Figure 5.7: Doppio istogramma per visualizzare i progressi di un progetto Scrum

Il grafico può essere fermo e non crescere per due motivazioni:

• il Team non lavora come dovrebbe ed è troppo lento;

• vengono aggiunti troppi requisiti al Product Backlog ad ogni iterazione.

66
In entrambi i casi si dovrebbe intervenire, rispettando sempre i principi della metodologia Scrum.

5.4.5 Incremento

L’incremento è la somma di tutte le attività del Product Backlog completate durante lo Sprint. Alla fine di
ogni iterazione devo ottenere un incremento funzionante e testato.

Bisogna porre particolare attenzione al modo di organizzare il lavoro durante un incremento, il sistema
va pensato come tanti piccoli incrementi funzionanti e testabili. Per fare un esempio, nel caso in cui stiamo
lavorando ad un singolo requisito che richiede circa 3 giorni per il completamento, devo essere in grado di
suddividere quest’ultimo in modo tale che alla fine di ogni giornata sia possibile e↵ettuare una verifica. Se
vengono fuori nuovi problemi, è più facile rilevarli e rimediare se la verifica viene fatta giornaliermente
anzichè alla fine dell’implementazione del requisito.

Figure 5.8: Modo di suddividere le Attività durante un incremento

5.5 Pianificazione Agile


Nononstante Agile si basi molto sul feedback, ciò non significa che non si possa pianificare (sempre in modo
che non sia vincolante). Lo Sprint Planning pianifica il lavoro che deve essere e↵ettuato negli Sprint e quanti
ne devo compiere per completare il progetto. In un primo momento si e↵ettua una stima delle dimensione
dei requisiti, successivamente si divide questo numero per la velcoità del Team ed infine si ottengono i numeri
di Sprint.(non so se serve)

67
5.5.1 Stime della durata e dimensioni

Figure 5.9: Grafico che mostra la sudddivisione dei problemi

Durante il progetto non tutto il lavoro sarà dedicato al puro completamento dei task, per questo bisogna
considerare altri fattori importanti che sono:

• Bug + correzione: ovvero nel momento in cui si verificano dei bug questi vengono inseriti all’interno
del Product Backlog ed è compito del Product Owner decidere quando completarli.

• Debito tecnico: ovvero delle conseguenze a lungo termine dovute a scelte sbagliate nell’incremento
che producono un costo per essere aggiustate (come un debito). Queste possono essere:

– Volute: scelgo la strada più veloce ma meno efficiente;


– Non volute: la parte funziona ma non è ben progettata.

Dal momento che la qualità è al primo posto in accordo con il framework Scrum, raramente si sceglie
deliberatamente di andare in debito tecnico. Se succede, questo deve essere piccolo, quindi soltanto
nel caso in cui il Product Owner decide che quella determinata funzionalità è molto importante.

• Nuove questioni.

• Spikes: gli Spikes sono un’invenzione dell’Extreme Programming (XP), essi sono un tipo speciale di user
story e venogono utilizzati per ottenere le conoscenze necessarie, magari al momento non presenti, per
migliorare la comprensione di un requisito che richiede aspetti tecnici-architetturali particolari, oppure
per aumentare l’affidabilità o meglio ancora trovare soluzioni migliori per un requisito già implementato.

• Riserva non pianificata: ovvero durante la settimana lavorativa riservo del tempo per attività non
pianificate, come l’aiutare un membro del team in difficoltà, in questo modo si migliora anche il rapporto
intrapersonale.

68
5.6 User Story
Le user story sono un modo conveniente per rappresentare molti degli elementi che andranno inseriti nel
Product Backlog, specialmente le funzionalità del sistema. Anche se la user story non è l’unico modo per
rappresentare questi elementi, esse sono prodotte in modo tale da avere una struttura semplice e dunque
sono pensate per essere comprensibili sia al personale tecnico (membri del team) che amministrativo (prod-
uct owner e stakeholder).
Principalmente, le user stories sono descrizioni di funzionalità del sistema dal punto di vista dell’utilizzatore.

Product Scrum Dev


Cliente
Owner Master Team
Chi può scrivere le stories..?
Chi è il proprietario di una story..?
Chi gestisce le stories..?
Chi prioritizza le stories..?
Chi implementa le stories..?

Table 1: La responsabilità di una user story

5.6.1 Vantaggi nell’utilizzo delle user story

• Danno più enfasi alla comunicazione diretta piuttosto che a quella scritta. Le user story non sono state
fatte per scrivere tutti i dettagli sulla scheda, ma questi vanno raccolti mediante le conversazioni.

• Sono comprensibili sia al personale tecnico (developers) che ai clienti, dato che sono scritte in linguaggio
semplice, favorendo la partecipazione del cliente al processo di produzione del software.

• Sono della dimensione giusta per la pianificazione del lavoro. Il team può chiedere al product owner di
spezzare la user story in più parti nel caso in cui questa necessitasse di più di una iterazione per essere
implementata.

• Sono adatte ad un processo di sviluppo iterativo (essendo snelle e veloci da scrivere).

• Spingono il Product Owner a focalizzarsi sulla caratterizzazione dell utilizzatore del sistema e sul perchè
egli vuole che si implementi quella serie di funzionalità. Quindi il sistema non viene visto solo come
una collezione di funzionalità, ma gli si dà anche uno scopo e questo fornisce una visione unificata del
sistema.

5.6.2 Formati di una user story

Il formato che si raccomanda di usare quando si scrive una user story va scelto tra le seguenti alternative:

69
As a <Type of user>
I want <Some functionality>
so that <some benefit>
In order to <achieve some value>
As a <Type of user>
I want <Some functionality>
As a <Type of user>
I can <activity>
so that <business value>

Table 2: I formati di una user story

Ricapitolando, la parte descrittiva è racchiusa in 3 concetti chiave:

• <Type of user>: identifica il tipo di utente per il quale progettiamo la funzionalità. A volte, gli
utenti possono essere di vario tipo, come un utente esperto o un principiante, caso in cui vanno definite
user stories per entrambe le tipologie.
Altre volte, i tipi di utente sono formalmente definiti e distinti nello spazio del problema (e.g. task
del fioraio dove c’è un’amministratore, un utente regolare e un utente occasionale).
Il compito di individuare le varie tipologie di utente è del Product Owner.

• <Some functionality >: identifica la funzionalità che il <Type of user > deve poter fare all’interno
del sistema.

• <Some benefit - Some value >: corrisponde al beneficio che l’utente trae nell’utilizzare la specifica
funzionalità descritta in <Some functionality >. Questo campo aiuta notevolmente il Team di
Sviluppo a capire meglio come implementare la funzionalità.

.
Dove in <Type of user> va inserito l’utente per il quale progettiamo la funzionalità che si scrive in
<Some functionality>. Si può anche specificare (opzionalmente) il perché l’utente vuole implemtare uno
specifico requisito, cosicché la funzionalità venga prodotta nel modo più consono. Questo dettaglio lo si
aggiunge in <some benefit>.

5.6.3 User Story: le tre C

Un modo per dare una chiara visione di quello che e↵ettivamente sono le user story è descriverle in termini
delle tre C: Card, Conversation e Confirmation.

5.6.3.1 Card

Una user story deve essere composta di un numero limitato di frasi che servono a catturare l’essenza del
requisito stesso. Per questo, la user story viene scritta su piccolo foglio di carta o su un post-it dato che non
deve contenere tutte le informazioni del requisito e lo spazio limitato aiuta a garantire questa caratteristica.

70
5.6.3.2 Conversation

I dettagli di un requisito, seguendo la filosofia di Scrum, devono essere esposti conversando con i membri dello
Scrum Team, infatti la user story serve solo a ricordare che prima o poi si dovrà tenere quella conversazione.
La conversazione non deve però essere vista come un evento singolo, ma più come un dialogo che va avanti
nel tempo e dove si raffinano i requisiti raccolti. Anche se la conversazione permette uno scambio maggiore
di informazioni e migliora la collaborazione tra i membri del team, essa non esclude la presenza di una
documentazione di supporto.

5.6.3.3 Confirmation

Una User story contiene anche delle informazioni, sempre brevi e concise, che indicano sotto quali con-
dizioni l’user story può essere considerata realizzata. Queste informazioni prendono il nome di criteri di
accettazione.
I criteri di accettazione sono utilizzati per:

• Definire dei limiti, cioè ci aiutano a capire quando l’applicazione da implementare ha finalmente
raggiunto il comportamento desiderato;

• Raggiungere un consenso, cioè permette di sincronizzare il team di sviluppo e il cliente, facendo si


che il lavoro del team rispecchi quelle che sono le richieste e le aspettative del cliente;

• Essere usati come base per i test, cioè possono essere utilizzati per capire se ciò che era richiesto
dalla user story è stato correttamente implementato.

• Garantire una corretta pianificazione e stima di come le user story dovranno essere divise at-
traverso i vari sprint.

Mentre la parte descrittiva della user story è scritta dal punto di vista dell’utilizzatore, i criteri di accettazione
sono costruiti seguendo il punto di vista del tester.

5.6.4 User Story: i criteri INVEST

INVEST è un acronimo usato per indicare una serie di criteri che possono essere utilizzati per valutare la
semantica di una user story e se questa è completa o se necessita di aggiustamenti. I criteri INVEST sono:
Indipendent, Negotiable, Valuable, Estimable, Small (o Sized appropriately) e Testable.

5.6.4.1 Indipendent

Le user story devono essere indipendenti tra loro o, almeno, bisogna cercare di minimizzare le dipendenze
che possono esistere.

5.6.4.2 Negotiable

Le user story non devono essere viste come un contratto che non può essere più modificato una volta stipulato,
ma devono essere negoziabili, cioè devono essere spunto di conversazione per generare eventualmente nuovi
requisiti utili o raffinare quelli già presenti.

71
5.6.4.3 Valuable

Le user story devono avere un valore per l’utente finale che sceglie e paga un prodotto. Per delle User story
che non hanno un valore per il cliente o l’utente, non ha senso investire del tempo per produrla.

5.6.4.4 Estimable

Una user story deve essere scritta in modo tale che il team riesca a fare una stima della dimensione dei
requisiti e del costo per progettare, costruire e testare la user story.

5.6.4.5 Small

Una user story deve essere dimensionata opportunamente in base alla lunghezza, in termini temporali, di un
singolo sprint.
Questo criterio va però applicato solo quando siamo in procinto di lavorare su quella storia per evitare di
perdere tempo prezioso in qualcosa che non ha alta priorità.

5.6.4.6 Testable

Una user story deve esssere testabile in modo binario: deve essere chiaro come queste possono passare il test
associato o come fallirlo. Questo criterio è rispettato se ad una user story sono associati dei buoni criteri di
accettazione.

5.6.5 Di↵erenza tra task e user story

Una user story tipicamente descrive, ad alto livello, un pezzo di funzionalità dal punto di vista dell’utente.
Man mano che si andrà avanti nello sviluppo del software essa necessiterà dell’impiego di programmatori,
tester o altri. Dall’altra parte un task si pone la domanda: ”Quali sono le attività che bisogna compiere in
modo da consegnare risultati?”. Le user story si concentrano più su COSA fare mentre i task si concentrano
su COME farli. Probabilmente però la miglior distinzione che si potrebbe fare è che le storie contengono
molteplici tipi di lavoro mentre i task sono ristretti a un singolo tipo di lavoro. In definitiva, una user story
può essere scomposta in più task in questo modo:

• Creazione di task significativi, cioè fare in modo che trasmettino chiaramente l’intento;

• Usare la definizione di Fatto come una checklist;

• Creare dei Task dalla giusta dimensione;

• Evitare di delineare esplicitamente un’attività di test dell’unità, se possibile fare in modo che
l’unità di testing sia parte del task e non un task a parte;

• Mantenere i task piccoli, in modo da conoscere il progresso attuale;

5.6.6 Tipologia di elementi del product backlog

Gli elementi che possono trovarsi nel product backlog non sono solo user story, per questo è possibile
usare un sistema di colori che permette di categorizzarli in base a due parametri: Valore e visibilità, come
mostrato in Figura 5.10.
Per elementi:

72
• Visibili e con valore positivo, s’intendono le funzionalità che possono essere aggiunte al sistema,
ovvero le user stories.

• Visibili e con valore negativo, s’intendono difetti visibili come bug di sistema.

• Invisibili e con valore positivo, s’intendono le funzionalità architetturali che possono essere aggiunte
al sistema, ma sono caratteristiche di cui l’utilizzatore potrebbe non percepirne l’utilità, come ad
esempio i requisiti non funzionali del prodotto.

• Invisibili e con valore negativo, si parla in questo caso di technical debt, cioè elementi progettati
senza garantire uno standard di qualità alto.

Figure 5.10: Tipologie di elementi nel Product Backlog

5.7 Agile Estimation


Dato che è difficile dare una stima assoluta del carico di lavoro di un progetto, è possibile optare per un metodo
basato sulla stima relativa, cioè stimare il carico di un lavoro basato su un compito svolto in precedenza. Nei
processi Agile vengono utilizzate alcune tecniche per stimare relativamente il carico di lavoro associato ad un
processo, tra cui: Story point, Value point, BFTB.

5.7.1 Principi dietro la stima Agile

Stimare la dimensione del progetto non risulta essere sempre immediato, infatti, bisogna tenere conto di diversi
fattori come il contesto, la prospettiva dell’utente rispetto all’oggetto ed inoltre bisogna avere una distinzione
tra dimensione del prodotto e sforzo del Team per completare il progetto che non sono assolutamente la
stessa cosa.

5.7.2 Story Points

Gli story point non sono un’unità temporale vera e propria, ma sono da considerarsi come una un’unità
temporale astratta, che ci permettono quidni di stimare il tempo necessario a produrre una user story, ed il

73
cui valore e↵ettivo può variare da persona a persona. Essendo un metodo a stima relativa, dato un insieme
di user story delle quali dobbiamo stimare il loro valore, si sceglie la user story che secondo il team è la più
veloce da realizzare e si dà uno story point pari a 1. Si procede poi con la valutazione della storia che secondo
il team è quella più difficile da realizzare e che quindi occuperà il maggior tempo. Il suo valore non può
essere scelto a caso dato che bisogna mantenere delle proporzioni tra i valori affidati alle successive storie, per
garantire questa caratteristica si possono affidare i valori seguendo la serie di Fibonacci, che garantisce una
proporzione tra valori successivi, evitando valori di stima fuorvianti. Una volta presa la storia più piccola
e scelto il valore di quella più grande relativamente a quella con valore più piccolo, si può procedere con la
valutazione delle altre in proporzione ai valori di queste due.

5.7.2.1 Planning Poker

Il Planning Poker è una delle tecniche possibili per poter raggiungere un accordo tra i membri del team sulla
stima del progetto. Il suo nome deriva dal gioco del poker, infatti, i membri del Team si riuniscono ad un
tavolo e scrivono su dei fogli un numero che rappresenta la loro stima del progetto, quando successivamente
tutti hanno finito di scrivere, le carte vengono girate e se queste sono uguali si è giunto ad un’accordo mentre
al contrario si discute il perchè e si ripete il procedimento fino al raggiungimento di un valore comune a tutti.

5.7.3 Value Point

Mentre gli story point indicano una stima di quanto tempo sarà necessario per produrre una storia, i value
point per indicare quanto la storia sia importante per il cliente. Tecnica utilizzata per stimare i value point
è la stessa utilizzata per gli story point e la scala scelta può essere quella che fa uso della serie di Fibonacci
per l’assegnazione dei valori.

5.7.4 Bang for the Buck

Il termine ”Bang for the Buck” BFTB è un modo di dire americano che sta per ”A↵are”. Nel nos-
tro contesto questo termine viene utilizzato per indicare che si vuol ottenere il maggior valore possibile
dall’implementazione di una user story, nel minor tempo possibile. Praticamente, invece, questo valore è
dato dalla divisione degli story point SP di una user story per il value point VP associato dal cliente.

SP
BF T B =
VP
In questo modo si possono ordinare le user story in maniera decrescente in base al valore di ”Bang for
the Buck”, valore che può essere considerato come la priorità di realizzazione di una user story e dunque
indicherà l’ordine da usare per la loro implementazione.

5.7.5 Velocità

La velocità del team un valore che si calcola alla fine di ogni iterazione ed indica quanti story point il team è
riuscito a realizzare nell’ultimo sprint completato. E’ possibile inoltre valutare la velocità attuale del team
facendo la media delle velocità diviso il numero di sprint completati. Il valore della velocità può essere anche
usato per stimare a quanto tempo corrisponde uno story point.

74
Esempio:
Se nell’ultimo sprint, durato S settimane, sono stati completate storie per un valore di SP story point,
il valore concreto associato ad uno story point TSP (time per Story Point), relativo alla velocità di
quest’ultimo sprint, è dato dal numero di story point diviso la durata dello sprint (da convertire nell’unità di
tempo desiderata):

SP
T SP =
S
Fine Esempio.

5.8 Definizione di ”Fatto”


Anche se, come sappiamo, il risultato di uno sprint dovrebbe essere un miglioramento o un’aggiunta al
prodotto software potenzialmente consegnabile, non significa che ogni elemento che ne incrementi il valore
vada per forza integrato alla versione consegnabile. Per consegnare questa aggiunta, se il fine ultimo è con-
segnare qualcosa, bisogna controllare che l’elemento rispetti gli standard di qualità fissati per il progetto,
che devono essere espliciti. Per questo motivo, è necessario definire quando un elemento è completo o con-
segnabile. La definizione di ”Fatto”, concettualmente, è avere una lista di punti che sono stati portati a
termine con successo.

Chiaramente, aumentando lo standard, aumenta la qualità, aumenta il numero di punti della lista da portare
a termine con successo e conseguentemente aumenta il lavoro da fare. Solitamente, è l’azienda a decidere il
livello di qualità dei suoi progetti. Una possibile rappresentazione dei vari livelli è mostrata in Figura 5.11.

Figure 5.11: Diagramma dei livelli di qualità

75
Se qualche elemento non rispetta il criterio di qualità o la definizione di ”Fatto” definita in precedenza,
l’elemento ritorna nel Product Backlog marcato come Undone.
L’insieme degli elementi Undone può crescere, quindi periodicamente si organizzano degli Sprint di Stabi-
lizzazione in cui si recupera tutto il lavoro non finito, come mostrato in Figura 5.12.

Figure 5.12

Una possibile checklist per controllare che un elemento rispetti la definizione di ”Fatto”, potrebbe essere la
seguente:

• L’implementazione della user story incontra tutti i criteri di accettazione;

• La user story è stata dispiegata nella produzione ma non disabilitata;

• Le unit tests sono scritte,eseguite e passate;

• Ogni criterio accettato ha almeno un test case associato;

• La documentazione tecnica è caricata dal team sulla Confluence;

• La performace è sotto X;

• La user story è stata oggetto di una peer-reviewed;

• Test di integrazione eseguiti e compilati;

• Le configurazioni manuali che bisogna performare dopo l’implementazione della produzione sono con-
trassegnati nelle user story;

• La distribuzione nell’ambiente di produzione della storia ha note di rilascio;

• La documentazione dell’utente finale è disponibile;

/*aggiungere burndown chart per il PROGETTO INTERO (doppio istogramma)*/


/*vedere se aggiugngere di↵erenza tra task e user story*/
/*aggiungere immagine punto di vista tester*/

76
6 Progettazione: Architettura e Metodologia
6.1 Introduzione al Design
Una volta chiari i requisiti del progetto, si può iniziare a trasformare i requisiti in un design specifico. In
particolare ricordiamo che i requisiti (cosa dovrebbe fare il sistema, vincoli sotto cui opera, etc.) specificano
il ”Cosa” deve fare il sistema, mentre ora, tramite il design, andremo a specificare il ”Come”, ovvero:

• Come il sistema è stato diviso in componenti

• Quali funzionalità svolgono questi componenti

• Come i componenti comunicano e si interfacciano tra di loro

Questa è una fase molto delicata, dato che stiamo trasformando un bene intangibile come i requisiti, in un
altro bene intangibile, ovvero il design.

Il Software Design è quindi un’ attività dedita alla strutturazione del software, cioè l’individuazione dei
componenti e delle relazioni tra questi, in particolare, è solitamente utile dividire questa attività in due parti:

• Design Architetturale ad Alto Livello: è una visione di massima sull’organizzazione del sistema.
Sono elencati i componenti, le proprietà di questi ed i collegamenti tra di loro. Questa fase è prettamente
guidata dai requisiti funzionali e non funzionali di partenza.

• Design Architetturale a Livello Dettagliato: i componenti identificati vengono decomposti ad un


livello di dettaglio ancora maggiore. Questa fase è guidata dall’architettura e dai requisiti funzionali del
sistema, in particolare dobbiamo garantire che tutti i requisiti funzionali siano coperti da almeno uno
dei componenti introdotti.

Figure 6.1: Relazione tra i Requisiti, l’Architettura e la Progettazione Dettagliata

77
Com’è possibile notare dalla Figura 6.1, sia la Progettazione Architetturale che quella Dettagliata sono in-
fluenzate dai requisiti. Sarebbe quindi ideale avere una corrispondenza uno-a-uno tra i requisiti funzionali ed
i moduli della Progettazione Dettagliata, in modo da coprire tutte le funzionalità del sistema.

Sempre dalla Figura 6.1 si nota che i requisiti, ed in particolare quelli non funzionali, influenzano
l’architettura. Ad esempio, requisiti come l’affidabilità, presumono che ci sia ridondanza dei componenti
affinchè, in caso di guasti, il sistema sia sempre disponibile.

Sistemi di piccole dimensioni potrebbero non avere un’architettura esplicitata, nonostante è sempre utile
averne una. Infatti nelle metodologie agili ed in generale negli attuali processi di sviluppo software, per i
sistemi di piccole dimensioni, si fa in modo che i programmatori abbiano un ruolo più importante in fase di
progettazione dettagliata.

6.2 Design Architetturale


Un’architettura software di un programma o di un sistema computazionale è una descrizione della sua
struttura in termini:

- dei suoi componenti

- delle interfacce esterne di tali componenti

- delle relazioni tra tali componenti

Le architetture software sono spesso modellate in maniera informale tramite diagrammi a blocchi.

E’ possibile considerare l’architettura di un sistema come una sintesi dello stesso, e dunque anche un in-
dicatore di qualità.
Il processo che porta alla creazione di un’architettura di un sistema è la progettazione architetturale.

Per progettazione architetturale si intende quel processo creativo che ha come fine quello di organiz-
zare i vari componenti di un sistema in virtù dei requisiti funzionali e non funzionali.

Le cose importanti da tenere a mente sulle architetture software sono:

- Ogni sistema ha una struttura, che tu ne sia consapevole o meno.

- Un sitema potrebbere poter essere rappresentato da più strutture di↵erenti, quindi bisogna fare atten-
zione a quale scegliamo, cercando di documentare il tutto con più viste possibili.

- L’architettura a↵ronta soprattutto il problema dell’interfacciamento e della comunicazione tra i vari


componenti.

I vantaggi del documentare e progettare un’architettura software sono:

• Comunicazione con gli Stakeholders: Essendo l’architettura una descrizione ad alto livello del sis-
tema, ovvero senza particolari tecnicismi, questa potrebbe essere usata per discutere con gli stakeholders
in maniera semplificata, visto che non tutti hanno competenze specifiche.

78
• Analizzare il Sistema: Nel momento in cui andiamo a definire un’architettura del sistema in una fase
primordiale del suo sviluppo, stiamo anche facendo un’analisi su di questo, sapendo che le particolari
decisioni architetturali influenzano enormemente il modo in cui verranno implementati i requisiti (in
particolare i requisiti non funzionali).

• Riutilizzo su vasta scala: Sistemi che vengono sviluppati in un particolare dominio, e che magari
hanno requisiti simili, hanno spesso architetture simili. Avere a disposizione un’architettura documen-
tata, che sappiamo essere stata una soluzione vincente in quel dominio, favorisce il riutilizzo della stessa
e velocizza i tempi di sviluppo.

6.3 Viste Architetturali


Ricordiamo che un sistema potrebbe essere composto da più strutture di↵erenti o addirittura da mix di
queste. Al tal motivo, per avere una visione completa e corretta è necessario visionare tutte le possibili
rappresentazioni del sistema da noi considerato.
Per questo introduciamo il concetto di vista architetturale:

Una vista architetturale è una rappresentazione di un’architettura di sistema.

In alcuni casi, come quando consideriamo sistemi di grandi dimensioni, una vista può essere considerata
anche come una prospettiva del sistema, ovvero una rappresentazione di questo ponendo particolare atten-
zione ad un aspetto specifico.
E’ infatti difficile, quasi impossibile, rappresentare tutte le informazioni rilevanti di un’architettura di sistema
in un singolo diagramma, è invece più semplice rappresentarlo solo sotto determinati punti di vista.

Bisogna tenere sempre a mente che una struttura esiste indipendentemente dal fatto che questa
sia rappresentata o meno, il compito della vista è solo quello di rappresentarla.

Ci sono opinioni di↵erenti su quali sono le prospettive di un sistema necessarie da rappresentare, Krutchen
nel suo modello a 4+1 viste di un’architettura software, propone quattro viste fondamentali:

• Logica: rappresentazione della decomposizione del sistema in oggetti, ovvero tutte le classi e le relazioni
tra queste. Solitamente viene utilizzato un diagramma UML delle classi.

• Processo: rappresentazione dei vari processi/thread del sistema (componenti run-time) e come questi
comunicano tra di loro.

• Decomposizione dei Sottosistemi (Sviluppo): rappresentazione dei moduli e dei sotto-sistemi,


con annessa descrizione delle loro comunicazioni in ingresso ed uscita.

• Fisica: rappresentazione del sistema in termini della distribuzione del software sull’hardware, ovvero
capire come è fisicametne distribuito il sitema (es. software distribuito su più computer).

La vista +1 è quella relativa alla raprresentazione del sistema tramite use cases.

79
Figure 6.2: Modello a 4+1 viste di Krutchen

Vi sono comunque altre tipologie di viste, analoghe a quelle di Krutchen, come quelle proposte da Bass,
Clements and Kazman:

- Vista dei Moduli

- Vista Run-Time

- Vista di Allocazione

Un punto fondamentale è che, avere viste di↵erenti aiuta ad interfacciarsi con diversi stakeholders.
Ad esempio, una vista relativa ai moduli presenti ed i file con i quali hanno a che fare può essere utile per gli
sviluppatori, mentre una vista relativa alle classi può essere utile anche per chi ha meno competenze.

6.4 Stili/Patterns Architetturali


Sebbene molti sistemi siano stati sviluppati con di↵erenti architetture, molte di queste condividono caratter-
stiche comuni sotto molti punti di vista.

Gli Ingegneri del Software hanno ”raccolto” queste varie architetture, tracciandone le di↵erenze, i vantaggi,
gli svantaggi ed i domini tipici di applicazione. Tali informazioni costituiscono un punto di partenza per
chi sta progettando una nuova architettura, ed è per questo che introduciamo il concetto di stile/pattern
architetturale come guida alla progettazione.

Uno stile architetturale è una descrizione della particolare organizzazione di un prodotto software, in termini
dei suoi componenti, la quale ha avuto successo in altri sistemi preesitenti.

Ogni stile porta con se vantaggi e svantaggi, da valutare in base alle nostre esigenze. Tra i principali stili più
utilizzati vi sono:

80
• Pipe and Filter 6.4.1

• Client Server 6.4.2

• Model View Controller (MVC) 6.4.3

• Layered 6.4.4

• Database-centric (shared data) 6.4.5

• Three-Tier 6.4.6

6.4.1 Pipe and Filter

Lo Stile Architetturale Pipe and Filter è una soluzione di design ad alto livello che si basa sull’idea di
decomporre il sistema in due parti ”generiche” :

- Pipe: tradotto tubo, sta proprio a metaforizzare il fatto che i dati, sono paragonabili ad un liquido che
fluisce all’interno di un tubo.

- Filter : tradotto filtro, sono appunto dei ”blocchi” dove confluiscono i flussi di informazioni (input), i
quali vengono rielaborati e trasmessi in output (messi in input ad altri blocchi).

Questo stile si basa sull’idea che, l’output di un processo fa da input ai processi successivi, e cosı̀ via.
I processi non hanno bisogno di aspettare che il proprio precedente finisca tutte le operazioni a lui assegnate,
ma possono iniziare a lavorare già nel momento in cui arrivano gli input necessari.
Tutti i blocchi di questo modello potrebbero essere sia progettati per convivere nello stesso sistema, ma anche
per essere distributi in rete.
I principali utilizzi si possono ritrovare negli applicativi di creazione di script Unix e negli applicativi di
elaborazione dei segnali. In generale questa non è una soluzione appetibile nel momento in cui vi deve essere
interazione con utenti, ma più per applicativi di batch file processing.
E’anche possibile usufruire di parallelismi tra i vari flussi.

Figure 6.3: Stile Architetturale Pipe-Filter

Questa rappresentazione ricorda molto quella relativa alla rappresentazione dei requisiti secondo D-F-D
(Data Flow Diagram). Infatti è molto semplice passare da requisiti, raprresentati tramite D-F-D, a una
rappresentazione del sistema mediante questa struttura.

81
6.4.2 Client Server

Questo è uno stile dove si nota una marcata distinzione tra componenti di tipo client e quelli di tipo server,
ovvero rispettivamente chi usufruisce di servizi e chi ne fornisce.
Questo componenti possono, non necessariamente, risiedere in reti di↵erenti o su nodi di↵erenti della stessa
rete. La comunicazione avviene mediante i più classici dei protocolli di comunicazione, oppure tramite chia-
mate remote a procedure (RPC).

Solitamente, sono i vari client ad accedere e richiedere servizi dallo stesso server, ma possono esserci casi
più complessi dove gli stessi server richiedono servizi ad altri server ancora, comportandosi anch’essi da
client.

Figure 6.4: Stile Architetturale Client-Sever, (a) Server unico per servizi, (b) Server fanno anche da client

I vantaggi dell’utilizzo di tale architettura sono:

• Facile distribuzione dei dati.

• Rendere efficace l’uso di sistemi collegati in rete, permettendo anche utilizzo di hardware meno costoso
per i client (carico computazionale sul server).

• Scalabilità nell’aggiungere/rimuovere nodi dalla rete.

Gli svantaggi dell’utilizzo di tale architettura sono:

• Bisogna definire uno standard unico per il modello dei dati.

• Potrebbe esserci una gestione ridondante dei dati, dovendo replicare gli stessi dati su più server (prob-
lemi di coerenza dei dati).

• Se la rete è di grandi dimensioni, ed i nodi sono molti, potrebbe essere difficoltoso trovare gli specifici
server.

82
6.4.3 Model View Controller (MVC)

E’ uno degli stili architetturali più utilizzati per applicativi che necessitano di una GUI, e necessitano di
gestire molteplici modi di visualizzare i dati.
L’idea alla base di questo stile è quella di separare i dati dalla visualizzazione, o meglio bisogna consid-
erare 3 livelli logici separati:

• Il model, che gestisce i dati associati al sistema e le operazioni su di essi

• La view, che definisce come i dati sono rappresentati agli utenti.

• Il controller, che gestisce le interazioni dell’utente con il sistema e le passa ai componenti view e model.

Con gli attuali sistemi moderni, spesso il controller viene integrato nella vista, come nel caso delle internet
web applications.

Figure 6.5: Stile Architetturale MVC integrato

Questo modello è particolarmente utile quando, ci ritroviamo in situazioni in cui utenti di↵erenti hanno la
necessità di visualizzare gli stessi dati, ma in maniera di↵erente.
Inoltre, questo stile favorisce anche lo sviluppo incrementale, ovvero è possibile sostiture componenti dei
tre livelli logici senza particolare difficoltà, se ad esempio stessi lavorando ad una nuova GUI, potrei benissi-
mamente realizzarla e solo successivamente integrarla con il controller e con il model.

I vantaggi dell’utilizzo di tale architettura sono:

• Indipendenza tra i vari livelli.

• Favorisce le modifiche ai singoli livelli, senza dover necessariamente modificare l’intero sistema.

• Favorisce la portabilità, come nel caso in cui volessi mantenere la GUI sviluppata ma vogliamo usare
un modello dei dati di↵erente.

Gli svantaggi dell’utilizzo di tale architettura sono:

• Non è semplice garantire una netta separazione ed indipendenza tra i livelli logici.

• Le performance sono molto influenzate in negativo da questo approccio, dovendo passare per tutti gli
strati durante un’elaborazione.

83
6.4.4 Layered

In questo stile tutti i componenti sono raggruppati in livelli, ed i componenti di un livello possono
comunicare solo con i componenti che fanno parte dei livelli immediatamente sottostanti o soprastanti.

Quando questa architettura viene combinata con una di tipo client-server, magari con livelli che risiedono in
computer di↵erenti, parleremo di struttura suddivisa in tiers piuttosto che in livelli.

Un esempio può essere l’implementazione delle API di Java, le quali al fine di comunicare con il kernel,
eseguono chiamate a funzione passando attraverso le API del sistema operativo e non direttamente tramite
il kernel.

Figure 6.6: Stile Architetturale Layered

Ovviamente, nonostante ci sia un guadagno in termini di sicurezza e rilevamento dei problemi nei singoli
livelli, queste architetture vanno a danneggiare in maniera rilevante le prestazioni del sistema,
dovendo spesso passare forzatamente per più livelli.

6.4.5 Database-Centric (Shared Data)

In questo stile, tutti i dati di un sistema vengono gestiti da un database centrale, il quale è accessibile
a tutti i componenti del sistema.
I componenti non possono interagire tra di loro, ma possono farlo solo e esclusivamente tramite il database
centrale.
Solitamente è infatti possibile distinguere due tipologie di componenti:

- componenti che generano i dati

- componenti che usano i dati

Uno dei grandi vantaggi nell’utilizzo di questo stile è quello di poter definire dei vincoli sui dati in
entrata al database, i quali saranno validi per tutti i componenti che si interfacceranno con il DB, favorendo
quindi una maggiore coerenza e correttezza dei dati.

84
Figure 6.7: Stile Architetturale Shared Data

Spesso questo stile viene combinato con lo stile Client-Server, come è possibile notare dalla Figura 6.7, dove
appunto un Server gestisce i dati presenti e ne regola l’accesso tramite i vari client.

I vantaggi dell’utilizzo di tale architettura sono:

• Indipendenza tra i vari componenti che accedono ai dati.

• Centralizzazione dei dati, potendo quindi mantenere una coerenza e gli eventuali vincoli definiti dall’utente.

• E’ possibile attivare determinati componenti nel momento in cui sono disponibili determinati dati (utile
in applicativi AI).

Gli svantaggi dell’utilizzo di tale architettura sono:

• Bisogna far sı̀ che tutti i componenti che accedano ai dati siano allineati al modello dei dati da tutti
condiviso.

• Essendo tutti i dati centralizzati, in caso di guasto del repository centralizzato potrebbero esserci gravi
conseguenze (single point of failure).

• Nel caso in cui volessimo distribuire i dati contenuti nel database centralizzato su più macchine (magari
quando ci sono grandi moli di dati), vi saranno problemi relativi alla comunicazione affinchè queste
macchine appaiano all’esterno come un unico componente.

6.4.6 Three-Tier

Questo stile è una variazione sia del modello Database-Centric che di quello Client-Server.
Viene infatti aggiunto un ulteriore tier che farà da tramite tra i client ed i server. Questo tier imple-
menterà tutta la logica di business del nostro applicativo, infatti i client non accederanno direttamente
al database centrale, ma lo faranno tramite questo Business Tier.

Il motivo dell’introduzione di questo stile è che, per molti sistemi, la logica di business è difficle da es-
primere solo con i vincoli del database utilizzato, a tal motivo introduciamo un nuovo componente che si
occupa di far rispettare tutti i vincoli e le regole sotto cui il sistema opererà.

85
Questo modello è spesso utilizzato per le applicazioni web-based, dove i client accedono agli applicativi server
tramite i web browser. Questi applicativi server implementano la logica di business e comunicano con il
database.

Figure 6.8: Stile Architetturale Three-Tier

Le tipologie di architetture come quelle viste in Figura 6.8, possono essere anche considerate come una
variazione delle architetture MVC, dove:

- il database è il model.

- gli applicativi server implementano il controller, e generano le view ai clienti tramite il web browser.

Lo stile architetturale three-tier può essere a sua volta esteso, introducendo più tier di controllo.
Un esempio può essere lo standard J2EE (MVC2), dove averne 3 di tier, come in Figura 6.8, ne abbiamo 4.

Figure 6.9: Stile Architetturale J2EE

86
6.4.7 Considerazioni sugli Stili Architetturali

Abbiamo appena esposto una serie di stili che si sono rivelati utili e funzionali in determinati domini di
applicazione, bisogna però considerare che l’utilizzo di un particolare schema, non preclude l’utilizzo anche
di un ulteriore altro schema sempre per lo stesso applicativo, possiamo infatti combinare più stili a seconda
delle nostre esigenze.

6.5 Tattiche archietteturali


Le tattiche architetturali sono progettate per risolvere piccoli problemi molto specifici per i vari stili archi-
ettturali e non influiscono direttamente sulla struttura generale del sistema. Per esempio, per aumentare
l’affidibilità di un sistema, introdurremo nella progettazione, delle specifiche funzionalità o componenti per
la rivelazione di guasti(specialmente per i sistemi distribuiti), in modo da poterci accorgere in tempo del
problema, prima che le cose peggiorino ulteriormente. Per fare ciò possiamo usare due diverse tattiche:

• Heartbeat: ovvero che ogni componente manda periodicamente un messaggio al controllore, se quest’ultimo
non riceve un messaggio entro l’intervallo stabilito produrrà una notifica per segnalare il problema;

• Ping/echo: ovvero che il controllore manda periodicamente un messaggio(ping) ad ogni componente


aspettando una sua risposta, se questa non perviene allora saprà che c’è un errore.

6.6 Progettazione di dettaglio


La progettazione dell’architettura di un sistema, insieme ai suoi requisiti, deve essere perfezionata per pro-
durre una progettazione più dettagliata e soddisfare tutti i requisiti. Una volta scelta in una prima fase
l’architettura ad alto livello dell’intero sistema, vanno progettati nel dettaglio i vari componenti.

Il processo di sviluppo utilizzato determina il livello di dettaglio con cui viene scomposta la fase di pro-
gettazione e il livello di formalità della sua documentazione:

• Un processo di sviluppo a cascata prevede un livello di dettaglio molto elevato e una descrizione formale
dell’intera architettura.

• Un processo di sviluppo agile prevede invece un livello di dettaglio non necessariamente alto e formale,
dal momento che è un processo iterativo e non si ha la completa conoscenza dell’intero sistema.

Inoltre, può dipendere anche da una serie di altri fattori, come ad esempio fattori interni al team di sviluppo:

se chi progetta il sistema non è lo stesso che lo implementa, una scarsa documentazione e un basso
livello di dettaglio possono generare ambiguità o errori di comprensione;

se chi progetta il sistema è lo stesso che lo implementa, è possibile mantenere un livello di dettaglio
non troppo alto e specificare soltanto le parti critiche del sistema.

Dunque, se la progettazione viene eseguita con il massimo livello di dettaglio, l’attività di implementazione
è un mapping quasi uno-ad-uno degli elementi della progettazione al linguaggio di implementazione che si
sceglie; se invece la progettazione non è specificata al suo livello più fine, si lasciano alcune attività di
progettazione dettagliata da svolgere nella successiva fase di implementazione.

87
6.6.1 Decomposizione del sistema

Durante la fase di decomposizione del sistema per poter mettere in atto una progettazione dettagliata, i macro
blocchi identificati durante la prima fase di progettazione vengono ulteriormente decomposti per rispondere
a tutti i requisiti del sistema.
Esistono due principali tipi di decomposizione: decomposizione funzionale e decomposizione Object
Oriented.

6.6.2 Decomposizione funzionale

La decomposizione funzionale è molto utilizzata nella programmazione strutturata (nelle app sviluppate
con un paradigma di programmazione diverso dall’Object Oriented ).
L’idea di base è quella di decomporre una funzione o un modulo in moduli più piccoli, ognuno dei
quali ad un livello granulare tale da essere implementato senza particolari decisioni significative. General-
mente, i moduli padre dei sottomoduli più piccoli sono delle procedure o sistemi che vengono chiamati dal
modulo principale (main). Nella programmazione OO è utile per scomporre metodi particolarmente difficili
da implementare.

Sebbene i sistemi ed i linguaggi OO ricevono oggi la maggior parte dell’attenzione, ci sono ancora molti
sistemi sviluppati con tecniche procedurali, come ad esempio piccole applicazioni web-based, in cui il sistema
è scomposto in moduli funzionali e ogni modulo corrisponde a una o alcune pagine web correlate che svolgono
la funzione richiesta.

Un esempio a cui applicare questo tipo di decomposizione, può essere un sistema per gestire la registrazione
e l’iscrizione a un corso di studi. I requisiti specificano quattro attività da svolgere:

1. modificare ed eliminare gli studenti dal database;

2. modificare ed eliminare i corsi dal database;

3. aggiungere, modificare ed eliminare sezioni per un determinato corso;

4. registrare ed eliminare gli studenti da una sezione.

Facendo una decomposizione funzionale, si dividerebbe il modulo principale in quattro sottomoduli per
trattare studenti, corsi, sezioni e registrazioni. Questi quattro sottomoduli verranno poi ulteriormente scom-
posti fino ad arrivare ad un livello di decomposizione atomico, come in Figura 6.10.

88
Figure 6.10: Decomposizione funzionale

I moduli sono rappresentati dai rettangoli ed esiste un sistema di numerazione tale che ad ogni modulo
viene assegnato un numero in base al proprio livello. I numeri sono univoci, quindi è intuitivo capire il livello
del modulo e chi è il suo parent.

Chiaramente, le decomposizioni funzionali non sono univoche, infatti si può partizionare lo stesso sis-
tema come in Figura 6.11.

Figure 6.11: Decomposizione funzionale alternativa

In questo modo, tutte le operazioni dei quattro sottomoduli sono state raggruppate in un unico modulo
principale Database: questa scelta progettuale può aprire la strada ad un potenziale riuso del modulo.

89
Durante la progettazione, è buona pratica non concentarsi solamente sulla struttura delle decomposizioni, ma
cercare di capire quale sia l’alternativa giusta in quel particolare contesto. Ad esempio, nell’esempio prece-
dente la seconda alternativa (Figura 6.11) sembra migliore perchè favorisce un riuso del codice, tuttavia viene
introdotta una certa quantità di coesione e accoppiamento tra le varie entità, cosa che andrebbe limitata.
Questo problema è a↵rontato nella Sezione 6.7.1.

6.6.3 Progettazione Object Oriented e UML

Molti sistemi software moderni sono sviluppati seguendo tecniche Object Oriented.

Durante la fase dei requisiti vengono prodotti gli use cases che descrivono le principali funzionalità del
sistema e i diagrammi a loro associati (use case diagrams). A partire dagli use cases e gli use case dia-
grams prodotti nella fase precedente, si riconoscono le entità fondamentali che poi si tradurranno in classi,
documentate ognuna con il rispettivo diagramma delle classi.

Il diagramma delle classi prodotto, solitamente è costruito secondo il formato UML: una notazione di pro-
gettazione grafica che serve per documentare l’architettura di un sistema orientato agli oggetti, facendo uso
di classi e relazioni tra classi.
Chiaramente, a seconda del di↵erente processo, l’utilizzo dell’UML per documentare la progettazione può
essere utile o no, dal momento che è un tipo di documentazione formale e dettagliata.

Figure 6.12: Esempio di use case diagram

In Figura 6.12 è possibile osservare un esempio di use case diagram per un sistema di registrazione ad un
corso di laurea. Raffigura i due attori, lo studente e l’ente registratore e i casi o scenari a cui partecipano
gli attori. A questo diagramma è affiancata una descrizione (use cases) per spiegare nel dettaglio tutti i casi
d’uso presenti nel diagramma. In questo esempio:
• Le frecce tratteggiate rappresentano le estensioni delle operazioni o funzionalità.

90
• Gli ellissi rappresentano gli scenari/casi d’uso, ovvero funzionalità del sistema.

• Gli omini rappresentano gli attori del sistema.

Durante la fase dei requisiti, vengono comunemente sviluppati casi d’uso essenziali, che verranno poi per-
fezionati in casi d’uso del sistema durante la fase di progettazione dettagliata.

• I casi d’uso essenziali forniscono meno dettagli e non forniscono dettagli sul sistema: descrivono
principalmente ciò che l’attore dovrebbe fare e ciò che cerca di ottenere.

• I casi d’uso di sistema perfezionano il caso d’uso essenziale, aggiungendo informazioni dettagliate su
come il sistema raggiunge tali obiettivi.

6.6.3.1 Progettazione delle classi

Uno dei problemi più importanti nella progettazione di dettaglio è la progettazione di classi e diagrammi di
classi UML per rappresentare il sistema. Negli use cases e nei use case diagrams si individuano i concetti
fondamentali (oggetti, entità del mondo reale) e in base a questi si costruiscono le classi.

Gli oggetti sono organizzati in classi. Esse sono delle entità astratte che servono per raggruppare oggetti
con struttura simile oltre che come modello di creazione per nuovi oggetti.

Classe = astrazione di oggetti simili.

Agli oggetti sono associati gli attributi, dette anche proprietà dell’oggetto. Ogni oggetto ha i propri at-
tributi, che possono essere diversi dagli attributi degli altri oggetti.
Oltre ai dati, gli oggetti contengono anche i metodi, ovvero moduli di codice eseguibile che determinano il
comportamento dell’oggetto e manipolano i dati contenuti in esso.

Un importante concetto nella progettazione di una classe è l’incapsulamento. In un oggetto sono inclusi
sia i dati che i metodi. Esprimere se entrambi, uno o nessuno dei dati (metodi) è accessibile pubblicamente è
una caratteristica importante. In UML, i metodi e gli attributi pubblicamente accessibili sono contrassegnati
da un segno (+) mentre i metodi e gli attributi privati sono contrassegnati da un segno ( ).
La struttura di un diagramma di classe è costituita da un rettangolo e tre sezioni orizzontali che rappresen-
tano, dall’alto verso il basso:

il nome della classe;

gli attributi della classe;

i metodi della classe.

La visualizzazione degli ultimi due elementi è vincolata dal livello di dettaglio della documentazione (UML).
Un possibile diagramma UML della classe Studente è visualizzabile in Figura 6.13.

91
Figure 6.13: Diagramma UML della classe Studente

6.6.3.2 Relazioni tra classi

Oltre a mostrare i diagrammi delle classi, l’UML mette a disposizione dei progettisti dei costrutti per rapp-
resentare le relazioni tra le vare classi. Le possibili relazioni sono quattro: associazione, aggregazione,
composizione ed ereditarietà.

L’associazione, mostrata tramite delle linee che collegano due classi (A e B), rappresenta il rapporto di
associazione che esiste tra un’oggetto della classe A e un’oggetto della classe B.

Riprendendo l’esempio della classe Studente, una possibile associazione con la classe Scuola potrebbe rapp-
resentare se un determinato studente (oggetto della classe Studente) è associato o meno ad una determinata
scuola (oggetto della classe Scuola) dal metodo IsEnrolled, Figura 6.14.

Figure 6.14: Associazione

Si mostrano anche le cardinalità consentite, nell’esempio uno studente deve essere iscritto ad una sola scuola,
una scuola può avere zero o più studenti.

L’aggregazione è un particolare tipo di associazione, che corrisponde all’associazione ”parte di ”. Questo


tipo di legame sta a significare che la Classe B è aggregata alla Classe A se un oggetto della classe B è parte
dell’oggetto nella Classe A. Visualmente, è rappresentata da un rombo vuoto verso la classe aggregante.

La composizione è una versione più forte dell’aggregazione, che corrisponde all’associazione ”fatto di ”.
Nella versione compositiva l’oggetto subordinato non esiste al di fuori dell’oggetto primario. Vale a
dire, Se la Classe A è composta dalla Classe B, un oggetto della classe B non può esitere al di fuori dell’oggetto
della classe A, che lo contiene. In UML questa associazione è rappresentata da un rombo pieno verso la classe
che compone, come in Figura 6.15.

92
Figure 6.15: Composizione

N.B. Uno studente può essere parte di (aggregazione) una scuola, ma gli studenti esistono al di fuori
delle scuole. Una persona invece è fatta di (composizione) vari organi, i quali non avrebbero senso di esistere
al di fuori della persona.

L’ereditarietà corrisponde all’associazione di subordinazione tra due classi, tale che la Classe B è erede
della Classe A se la Classe B è un tipo specifico della Classe A, ovvero condivide alcune caratteristiche con
la Classe A. Un esempio può essere il rapporto di ereditarietà tra le classi Persona, Studente e Impiegato in
Figura 6.16.
Le relazioni di ereditarietà si suddividono in generalizzazione e specializzazione. Nell’esempio la classe
Studente è specializzazione della classe Persona, mentre la classe Persona è generalizzazione della classe
Studente.

Figure 6.16: Ereditarietà

N.B. Creare sottoclassi non è sempre necessario. L’aggiunta di nuove classi ha luogo soltanto quando si
ha bisogno di incorporare funzionalità o attributi aggiuntivi, ma se il comportamento o i dati che stiamo
cercando di modellare non prevedono questi ultimi, diventa inutile.

6.6.3.3 Diagramma delle transizioni di stato

Un aspetto particolarmente importante della progettazione delle classi è quello di descrivere il comporta-
mento dei diversi oggetti, dal momento che possono essere in stati diversi a seconda dei vari momenti. È

93
quindi importante poter rappresentare sia i diversi stati sia le transizioni. Tutte queste informazioni sono
contenute all’interno del diagramma delle transizioni di stato.

Considerando sempre il dominio degli studenti e i corsi di laurea, il diagramma di stato di uno studente
potrebbe descrivere i diversi stati che l’oggetto della classe studente può attraversare e le condizioni di at-
traversamento (Figura 6.17).

Figure 6.17: Diagramma delle transizioni di stato

La rappresentazione mostrata sopra è detta ad automa a stati finiti.

6.6.3.4 Diagrammi di sequenza: interazioni tra classi

La progettazione delle classi e delle loro relazioni fornisce solo la struttura statica del progetto. Per completare
la progettazione, vanno specificate anche le diverse interazioni che intercorrono tra gli oggetti delle classi.
In UML, queste interazioni sono solitamente illustrate tramite i diagrammi di sequenza o diagrammi di
comunicazione, un esempio in Figura 6.18.

Figure 6.18: Diagramma di sequenza

Si identificano i seguenti costrutti all’interno del diagramma:


• Le frecce rappresentano gli scambi di messaggi tra gli oggetti delle diverse classi (flusso di messaggi ),
partendo dall’alto e scorrendo da sinistra a destra. I messaggi di ritorno sono rappresentati da linee
tratteggiate.

94
• Le linee verticali rappresentano la timeline dell’oggetto a cui appartengono.

• I rettangoli interposti sulle linee verticali rappresentano i momenti in cui gli oggetti delle classi sono
attivi.

6.6.3.5 Mismatch tra modello relazionale e a oggetti

Dal momento che la maggior parte delle applicazioni sono costruite con un database relazionale di supporto,
bisogna trovare un match tra il modello relazionale e quello ad oggetti. I problemi principali riguardano i
seguenti costrutti, che non trovano un match nel corrispettivo modello:

• Incapsulamento del modello ad oggetti

• Ereditarietà e polimorfismo del modello ad oggetti

• Struttura annidata delle classi rispetto alla struttura a tabella del modello relazionale

Figure 6.19: Mismatch tra modello a oggetti e modello relazionale

Le soluzioni a questo problema sono principalmente due:

• Si utilizza un database non relazionale, ma che supporta il paradigma di programmazione ad oggetti:


database ad oggetti.

• Si utilizzano strumenti come Object Relational Mapper, ovvero delle librerie che traducono il
paradigma ad oggetti in relazionale.

6.6.4 User Interface Design

L’interfaccia utente(UI) è la parte del software più visibile all’utente ed è per questo che deve essere fatta
in modo che risponda alle richieste del cliente. La progettazione dell’interfaccia utente non è solo com-
pito dell’ingegnere del software, ma anche di un team di specialisti in tecniche di design dato che bisogna
interfacciarsi con due problematiche principali, legate al design dell’interfaccia utente:

• Il flusso di interazione con il programma: ovvero come si passa da un’interfaccia all’altra;

• Il ”look and feel” del programma.

L’aspetto è meno importante del flusso, siccome possiamo progettare male l’interfaccia utente ma farla
sembrare bella. Abbiamo diversi tipi di interfacce:

95
• Linea di comando: che è la più veloce;

• Menu di testo;

• Grafica(GUI).

6.6.4.1 Flusso di interazioni nell’interfaccia

L’utente di un sistema ha specifici obiettivi da raggiungere nel sistema. Questi obietttivi sono direttamente
collegati con gli use cases e i diagrammi di sequenza progettati per il sistema. Il prototipo della nostra
architettura può essere progettato con due livelli di fedeltà:

• Basso livello: una semplice bozza del prodotto disegnata a mano come si può vedere in Figura 6.20;

• Alto livello: una dettagliata imitazione del prodotto finale come si può vedere in Figura 6.21.

Figure 6.20: Prototipo a bassa fedeltà dello schermo di registrazione

96
Figure 6.21: Prototipo ad alta fedeltà dello schermo di registrazione

Tuttavia spostandoci su un livello più professionale emergono numerosi problemi quali:

• Tipi di utenti;

• Euristica: come la difficoltà dell’essere umano nel gestire più concetti contemporaneamente;

• Linee guida UI;

• Problemi multiculturali: come l’orientamento nel quale si scrive che non è uguale dappertutto;

• Software multipiattaforma;

• Accessibilità;

• Interfacce multimediali.

6.6.5 Trabocchetto comune

Un errore comune è la confusione tra la qualità della progettazione e la qualità della documentazione.
Per esempio, anche se ho una buona descrizione del progetto che comprende tanti UML e altrettanti modelli
dettagliati, ma la progettazione è fatta male, tutto questo è inutile. Alcuni metodi addirittura non prevedono
proprio la documentazione (source code is the documentation).

6.7 ”Good” Design


Un sistema ben progettato deve avere due caratteristiche principali:

1. Coerenza o uniformità, tra tutti gli elementi del sistema. Questo si traduce in componenti del
sistema che hanno:

(a) Un’Interfaccia grafica coerente

97
(b) Una gestione identica degli errori
(c) Una coerenza tra i dati, che magari potrebbero essere replicati tra i componenti in maniera non
uniforme.
(d) Un interfacciamento comune con le altre componenti
(e) Una progettazione fatta allo stesso livello di dettaglio (caratteristica che può non essere garantita
per tutte le componenti se guidati da una buona motivazione).

2. Completezza del design, cioè che tutti i requisiti vengano implementati correttamente.
Se queste due caratteristiche fondamentali sono garantite durante la progettazione, si ha che il sistema
prodotto risulta essere:
• semplice da comprendere

• semplice da modificare

• semplice da riusare

• semplice da testare

• semplice da integrare

• semplice da codificare
Intuitivamente se ne deduce che un sistema complesso non è sintomo di una buona progettazione. A
questo punto è di interesse tentare di misurare la semplicità di un sistema in modo tale da comprendere
la bontà della progettazione. Per fare ciò si utilizzano due caratteristiche: Coesione e Accoppiamento.

6.7.1 Coesione

La coesione di un unità è un attributo che indica in che misura le sue componenti interne sono relazionate tra
loro. Qualunque sia il paradigma di progettazione utilizzato, si ha che un’ unità è definita “altamente coesa”
se tutte le sue componenti interne lavorano insieme per compiere un singolo lavoro. In generale, esistono
sette categorie di coesione e saranno presentate dalla peggiore alla migliore:
1. Coesione Coincidente, il peggior livello di coesione che può essere presente in un sistema; a questo
livello le componenti dell’unità svolgono una moltitudine di compiti che non sono minimamente legate
allo scopo dell’unità stessa. In generale, questo livello di coesione si ottiene quando si e↵ettuano
operazioni di manutenzione del codice (bug fix o cambiamenti dei requisiti) scorrette e frettolose.

2. Coesione Logica, le componenti dell’unità svolgono compiti che, solo apparentemente, sono simili tra
loro. In questo livello di coesione, dunque, gli elementi accorpati in una stessa unità sembrano essere
relazionati tra loro, ma rimangono indipendenti.

Esempio:
Un’unità destinata alla gestione dell’I/O, progettata per e↵ettuare operazioni di scrittura e lettura da diversi
dispositivi. Questo tipo di funzionalità, anche se all’apparenza sono simili, sono distinte tra loro date le
diverse architetture che possono avere i vari dispositivi e dunque di↵erente gestione dell’I/O.

98
3. Coesione Temporale, si mettono nella stessa unità del sistema, funzionalità che temporalmente,
verranno usate nello stesso momento.

Esempio:
Progettare un’unità che comprende tutte le funzioni di inizializzazione del sistema e che le esegue nello stesso
momento all’avvio del programma, anche se queste appartengono ad unità separate.

4. Coesione Procedurale, questo livello di coesione aggrega funzionalità che hanno buona probabilità
di essere usate insieme nella stessa unità.

Esempio:
Progettare un’unità che contiene una funzione che permette di controllare l’esistenza di un file ed una funzione
che permette di leggere da quel file. Un’unità di questo tipo ha un livello di coesione procedurale.

5. Coesione Comunicazionale, si accorpano nella stessa unità elementi legati proceduralmente tra loro
che però e↵ettuano operazioni sugli stessi dati.

6. Coesione Sequenziale, questo livello è caratterizzato dall’aggregazione, nella stessa unità, di elementi
legati comunicazionalmente tra loro e che hanno la caratteristica di avere, in alcuni casi, come input di
un’operazione, l ’output di un’altra.

7. Coesione Funzionale, il grado migliore di coesione che può essere presente in un sistema; l’insieme
degli elementi presenti nel singolo modulo svolgono, dal punto di vista funzionale, un singolo compito
complessivo.

6.7.2 Accoppiamento

L’accoppiamento è un attributo che indica in che misura il grado di interazione ed interdipendenza tra due
componenti software. Assumendo di aver accuratamente progettato un sistema altamente coeso, è possibile
che le sue componenti debbano interagire tra loro. Se l’interazione tra le componenti è complessa, il sistema
risultante non garantirà la semplicità che caratterizza un sistema ben progettato.

Esempio:
Consideriamo un sistema che presenta una classe o un modulo altamente dipendente dalle altre componenti.
Il modulo in questione, data l’elevata dipendenza con gli altri, non sarà facilmente riusabile, modificabile o
manutenibile dato che la sua comprensione dipende dalla comprensione dei moduli a lui connessi. Non di
rado, infine, un errore in un modulo altamente interconnesso, si riversa sulle componenti che dipendono da
questo.

E’ dunque intuibile che un elevato livello di accoppiamento nel sistema non è desiderabile. Cosı̀ come
per la coesione, anche per l’accoppiamento è possibile identificare diversi livelli:

99
1. Content Coupling (Accoppiamento dei contenuti), questo è il peggior livello di accoppiamento
che può essere presente in un sistema, si ottiene quando l’implementazione di un’unità dipende da come
è implementata un’altra a lei connessa. Modifiche ad una componente cosı̀ strettamente accoppiata ad
un’altra, si traducono quasi sicuramente in errori. (Tight coupling)

2. Common Coupling (Accoppiamento su dati comuni), un sistema è a questo livello se le compo-


nenti lavorano sulle stesse variabili globali.

3. Control Coupling (Accoppiamento di controllo), il prossimo livello di accoppiamento si ha quando


un’unità software invia specifiche informazioni di controllo ad un’altra per influenzarne il comporta-
mento, come ad esempio nel caso in cui, cambiando un particolare flag in un componente, si può
modificare il comportamento di un altro ancora.

4. Stamp Coupling, due unità sono accoppiate in questo modo se una invia delle strutture dati o
gruppi di dati, molto spesso non utili, ad un’altra componente, come nel caso in cui volessi stampare la
matricola di un oggetto studente, ma anzichè inviare solo quest’ultima, inviassi l’intero oggetto studente,
introducendo cosı̀ ridondanza dei dati inviati.

5. Data Coupling (Accoppiamento sui dati), praticamente, il miglior livello di accoppiamento rag-
giungibile da un sistema; simile al livello precedente ma, invece di inviare delle strutture dati o gruppi di
dati ad altre unità accoppiate, invia solamente i dati necessari all’operazione per la quale si necessitano.
(Loose coupling). Ricollegandoci all’esempio precedenete, in questo caso invieremo solo la matricola
anzichè l’intero studente.

6. No Coupling, il livello ideale di accoppiamento, ma difficilmente raggiungibile. Si parla di livello


ideale perché in questo caso nessuna componente comunica con le altre.

6.7.3 Accoppiamento e programmazione ad oggetti

Nella programmazione ad oggetti, le unità che devono avere un basso livello di accoppiamento, se non nullo,
sono le classi. Possiamo a questo punto dire che due classi sono accoppiate quando:

• Esiste una relazione di ereditarietà tra due classi. La classe derivata eredita metodi ed attributi pubblici
e protected della classe base. L’accoppiamento è monodirezionale dato che al variare della classe
derivata, non possono scaturire problemi nella classe base.

• Una classe ha un attributo che è un riferimento ad una istanza, o collezione di istanze, di un’altra classe.

• Una Classe A ha un metodo che usa una variabile (o parametro, valore di ritorno) che ha come tipo la
Classe B.

Nel caso in cui nel sistema sia presente una catena di dipendenze, cioè una serie di accoppiamenti tra le varie
classi, è possibile gestirla e spezzarla utilizzando le interfacce. /*Mettere la foto*/

6.7.4 Legge di Demeter

Una guida utile da seguire quando si usa un approccio Object-Oriented durante la progettazione è la cosidetta
Legge di Demeter. Questa guida ha lo scopo di ridurre l’accoppiamento tra le classi presenti di un sistema,

100
migliorando la coesione degli oggetti. Per raggiungere questi obiettivi, la Legge di Demeter limita le azioni
che un oggetto può fare, consentendogli di inviare messaggi (e dunque richiamre metodi ed usarne le risorse)
solo ai seguenti tipi di oggetto:

• L’oggetto stesso

• Gli oggetti che sono in relazione di composizione con l’oggeto chiamante (Variabili di istanza)

• I parametri dei metodi dell’oggetto in studio

• Ogni oggetto creato dai metodi dell’oggetto in questione

• Qualsiasi oggetto restituito come output da un metodo dell’oggeto in studio

• Qualsiasi oggetto in una collezione che però appartiene ad una delle precedenti categorie

Essenzialmente la Legge di Demeter si assicura che gli oggetti inviino messaggi ai soli oggetti direttamente
collegati a loro, parafrasando, un oggetto può ”parlare solo con un suo diretto vicino e non con un estraneo”.

101
7 Testing
7.1 Introduzione
Uno dei maggiori obiettivi nello sviluppo software è la produzione di software di alta qualità.
Al fine di perseguire tale obiettivo, è importante che ci sia una fase di testing del prodotto, tramite una serie
di tecniche utili al fine di rilevare e correggere gli errori tipici dei prodotti software.
In generale si parla di:

• Quality assurance (QA): come attività che ci permette di misurare e ottimizzare la qualità del
prodotto e del processo.

• Quality control (QC): come attività che ci permette di validare e verificare la qualità del prodotto
attraverso il rilevamento di errori che permettono di eliminare le varie imperfezioni del prodotto.

Le principali tecniche di rilevazione di errori nei programmi e nelle documentazioni intermedie sono:
Testing, Ispezione e Revisione, Metodi Formali, Analisi Statica.

Tutti questi metodi hanno la necessità comune di avere una definizione di “qualità”, in particolare pos-
siamo dire che un prodotto è di qualità se:

• Conforme alle specifiche, ovvero copra e rispetti tutte le specifiche del prodotto.

• Serve al suo scopo, ovvero rispetta le volontà del cliente.

Notiamo che queste due definizioni non sono equivalenti sebbene esprimano due concetti simili.
Infatti potrebbe esserci il caso in cui il prodotto sviluppato rispetti tutte le specifiche ma non è utile agli
scopi del cliente, o peggio ancora non rispetti nemmeno le specifiche, che sia per colpa di un fraintendimento
durante la fase di ingegneria dei requisiti, o magari ci sia semplicemente un elevato grado di interpretazione
personale dei requisiti, che ha fatto discostare le richieste dalle volontà del cliente.

Per questo questo oltre a dover verificare che il sistema sia conforme ai requisiti, bisogna soprattuto val-
idarlo, ovvero capire se sono soddisfatte le volontà del cliente.
In corrispondenza di ciò che abbiamo appena esposto, possiamo riconoscere due attività principali:

• Verifica della qualità: azione che ci permette di verificare se un prodotto software è conforme ai suoi
requisiti e le sue specifiche.

• Validazione della qualità: azione che ci permette di verificare se un prodotto software finito copre
tutte le esigenze del cliente.

7.2 Definizioni di Errori, Difetti e Problemi


Solitamente, gli addetti alla qualità di un sistema software usano la seguente terminologia:

• Error (errore): sbaglio e↵ettuato da un essere umano durante la progettazione o la realizzazione del
sistema (esempio attività fatta non seguendo le regole).

102
• Fault (difetto, bug): è la conseguenza di un errore, è presente all’interno del sistema.
Se ad esempio io come programmatore sbaglio a definire la terminazione di un ciclo (errore), ne deriva
un bug, ovvero un ciclo che viene eseguito una volta di troppo.

• Failure (problema): è la manifestazione di un bug, infatti nel momento in cui si manifesta, il sistema
non è in grado di operare in accordo alle specifiche del sistema.
Il difetto, quando vi è un bug, è sempre presente, ma questo potrebbe non manifestarsi in maniera
deterministica. (ad esempio il problema di cattiva gestione dei mutex con i thread potrebbe non
manifestarsi sempre).

Dato un problema, vi possono essere diversi livelli di severità del problema, a seconda delle conseguenze
di questo. Infatti un problema che fa andare in crash un programma, causando la perdita di dati, ha delle
conseguenze diverse rispetto ad un problema che causa solo un errore di ”risultato”.
Oltre alla severità, ogni problema ha una priorità di risoluzione. Quest’ultima dipende spesso dalla
severità, ma anche dalla probabilità che il problema si manifesti, ad esempio un problema che si verifica molto
raramente ha meno priorità rispetto ad uno che si manifesta spesso.

7.3 Definizione di Testing


Tra le attività necessarie al fine di migliorare la qualità di un sistema software, la più importante è sicura-
mente quella relativa al Testing.

Il Testing è un’attività di verification dinamica, dove ci assicuriamo che il programma rispetti tutti i req-
uisiti, e ciò viene verificato in maniera dinamica, ovvero lanciando in esecuzione il programma, scengliendo
opportuni input, al fine di controllare i risultati ed e↵ettuare misure sul corretto funzionamento, verificando
quindi sia i requisiti funzionali che i non funzionali.

Ovviamente durante il testing è possibile e↵ettuare solo un numero finito di controlli, con un numero finito
di input, perciò bisogna scegliere in maniera accurata i test da e↵ettuare, di ciò discuteremo meglio nelle
sezioni /*aggiungi ref*/.
Quindi, per quanto possa essere accurata una fase di testing, questo non dimostrerà mai con certezza che il
programma funziona in maniera corretta.

Le quattro domande principali da porci quando approcciamo al testing sono:

• Who : chi testerà il nostro sistema ?


Solitamente abbiamo tre possibilià che non sono esclusive, in un processo infatti solitamente vengono
tutti coinvolti, e sono:

- Sviluppatori, i quali prima di consegnare, testano le parti del sistema da loro sviluppate.
- Testers, dato che spesso i problemi escono fuori quando si vanno ad aggregare più parti realizzate
da sviluppatori di↵erenti, vi sono figure che si occupano di verificare che tutto funzioni per il meglio
(in alcuni ambiti le ore di testing sono addirittura di gran lunga superiori a quelle di sviluppo).

103
- Utenti, facendo provare il programma agli utilizzatori, possiamo assicurarci che si rispettino le
volontà del cliente.

• What : cosa verrà testato del nostro sistema ?


Vi sono circa quattro livelli di testing del sistema:

- Unit Code, vengono testate singole ”unità” del sistema, come una funzione, una classe etc.
- Functionale Code, in questo caso si vanno a testare insiemi di unità funzionanti del sistema.
- Integration/System, si va a testare il sitema completo, o completo fino a quel momento.
- User Interface, il testing della GUI viene solitamente e↵ettuato con tecnologie e metodologie diversi
da quelle con cui si testano i programmi.

• Why : perchè testare il nostro sistema ?


Le motivazioni per cui si va a testare un sistema sono solitamente quattro:

- Test di Accettazione (Acceptance), sono test fatti dal cliente, al fine di verificare che il software sod-
disfi le sue necessità. Sono di solito importanti, perchè spesso nei contratti, determinati pagamenti
corrispondono al superamento di specifici test di questa tipologia.
- Test di Conformità (Conformance), in questi tests si verifica che il sistema sia conforme a requisiti
speciali, come normative o in generale aspetti giuridici.
- Test di Configurazione (Configuration), bisogna testare che il sistema funzioni anche su macchine
di↵erenti, infatti non è detto che se il sistema funziona sulla macchina di sviluppo, allora funziona
su tutte le macchine.
- Test di Performance, sono test delle prestazioni fornite dal nostro prodotto software, un esempio
può essere lo ”stress test” dove si va appunto a portare al limite un sistema per osservarne il
comportamento.

• How : come testiamo il nostro sistema ?


Sono le metodologie con cui vengono scelti i casi di test del nostro sistema, le principali sono

- Black-Box Testing
- White-Box Testing

7.4 Black Box Testing


Il Black Box Testing (testing funzionale) è una metodologia di testing che consiste nello scegliere i casi
di test paretendo dalla specifica dei requisiti funzionali della particolare unità che si sta sviluppando, con
l’obiettivo di coprire tutto lo spazio di input possibile, senza considerare l’e↵ettivo contenuto del codice
sorgente.

Purtoppo, testare tutti i casi di input è impossibile, dal momento che nella maggior parte delle situ-
azioni i potenziali dati in input sono in numero praticamente infinito.

Serve quindi una strategia per ottenere una buona copertura di tutti i casi possibili con un numero finito
(e non troppo elevato) di casi di test.

104
7.4.1 Classi di equivalenza

Una classe di equivalenza, in termini di Black Box Testing, rappresenta un insieme di input considerati
equivalenti tra loro, appartenenti ad uno specifico range determinato dal tipo di classe di equivalenza di cui
l’insieme fa parte.
La suddivisione in classi di equivalenza è quindi una tecnica di testing Black Box in cui si suddivide lo spazio
di input in diverse classi che sono considerate equivalenti ai fini dell’accertamento e la ricerca di errori.
Quindi, se il programma fallisce per un determinato elemento della classe, ci si aspetta che fallisca per tutti
gli altri elementi della classe (stesso se l’input è corretto e non fallisce).

Come sono determinate? Le classi di equivalenza sono determinate a partire dai requisiti funzionali
e dall’intuizione del tester, senza considerare l’implementazione.

Per e↵ettuare un caso di test con una tecnica Black Box, prima si formano le classi di equivalenza, poi
si sceglie un rappresentante per ogni classe, ovvero, un dato di input che rappresenta al meglio la classe di
equivalenza di cui fa parte (solitamente si sceglie l’input al centro del range della classe).

Esempio:
Se si considera un’applicazione di marketing che analizza i dati delle persone, un fattore molto importante
nel settore del marketing è l’età. Se i requisiti suddividono le persone in quattro gruppi di età (0-12 anni
bambini, 13-19 anni adolescenti, 20-35 anni giovani adulti e >35 anni adulti ) allora è ragionevole dividere
lo spazio di input in quattro classi di equivalenza corrispondenti ai quattro intervalli. In più, si aggiungono
due ulteriori classi di equivalenza considerate input non validi (< 0 e > 120).

La tebella rappresentante le classi di equivalenza è in Figura 7.1.

Figure 7.1: Esempio di classi di equivalenza per un’applicazione di marketing

Più in generale, considerato un progetto di grandi dimensioni, si suppone di avere n requisiti funzionali

105
distinti tra loro. Si suppone inoltre che questi n requisiti funzionali sono tali che:

r1 [ r2 [ ... [ rn = insieme di tutti i requisiti e,

ri [ r j = ;

Allora possiamo creare un caso di test ti per ogni requisito funzionale ri per verificare che ri funzioni corret-
tamente. Allora:

t1 [ t2 [ ... [ tn = insieme di tutti i casi di test che coprono le funzionalità del software

Per ogni requisito funzionale ri potrebbero esistere più casi di test, ma scegliendone soltanto uno dal
potenziale insieme dei casi di test di ri , si forma una classe di equivalenza dei casi di test.

Esempio:
Un altro esempio di suddivisione dello spazio di input in classi di equivalenza, potrebbe essere quello di
considerare una funzione o metodo largest che dati in input due numeri interi restituisce il più grande tra i
due.

Per questo tipo di dati in input si potrebbero definire tre classi di equivalenza, ovvero:

P rimo > Secondo

Secondo > P rimo

P rimo = Secondo

Le classi di equivalenza sono molto utili quando i requisiti sono espressi attraverso una serie di condizioni.
Sono poco utili quando i requisiti richiamano a dei loop, ad esempio, nel caso in cui si debba testare una
funzione che somma gli elementi all’interno di un array o un vettore (sarebbe difficile stabilire delle classi di
equivalenza).

7.4.2 Analisi dei casi limite

L’esperienza ha reso evidente che i casi limite dello spazio di input sono una fonte di errore maggiore rispetto
agli input in condizioni ”normali”. Un caso di test e↵ettauto utilizzando soltanto uno qualsiasi dei valori di
una classe di equivalenza risulterebbe non molto efficace. Per questo motivo, nasce l’analisi dei casi limite.

L’analisi dei casi limite è una particolare tecnica di Black Box Testing che consiste nel dividere lo spazio
di input con le classi di equivalenza, per poi testare i boundaries invece che gli elementi all’interno dei limiti
estremi (inferiore e superiore). Un’analisi completa dei limiti dell’input consiste nell’e↵ettuare casi di test

106
per i valori che si trovano:

• Al limite

• Appena sopra il limite

• Appena sotto il limite

Solitamente, la divisioni in classi di equivalenza forma degli intervalli o range uno concatenato all’altro, quindi
nello sviluppare casi di test tramite l’analisi ai casi limite potrebbero esserci degli overlap tra i vari casi di
test, caso in cui si semplificano i casi di test in comune e si considerano solo una volta.

Esempio:
Seguendo l’esempio dell’applicazione di marketing, si potrebbero individuare i casi di test al limite con-
siderando i vari bounds tra le classi di equivalenza, dopodichè si selezionano i valori al limite, appena sotto
il limite e appena sopra il limite. I valori limite in comune tra le varie classi vengono poi semplificati, come
in Figura 7.2

Figure 7.2: Esempio di analisi ai casi limite di un’applicazione di marketing

7.5 White Box Testing


Il White Box Testing (glass box testing o testing strutturale) è una metodologia di testing dove i casi di
test derivano, oltre che dai requisiti funzionali dell’applicazione, anche dall’analisi approfondita del codice. A
di↵erenza del Black Box Testing, il cui obiettivo è coprire tutti i possibili input all’applicazione, il White Box
Testing ha l o scopo di garantire che ogni sezione del programma sia coperta da almeno un test. Per definire
il concetto di copertura, introduciamo il cosidetto Control Flow Graph (grafo del flusso di controllo). Il
Control Flow Graph di un programma è un grafo in cui:

1. Un nodo rappresenta una sequenza di istruzioni con un solo punto di ingresso ed uno solo di uscita;

2. Un arco rappresenta il passaggio di controllo da un nodo ad un altro;

Un Control Flow Graph può essere costituito da diverse sezioni:

107
• Sequence, una sezione di questo tipo prevede che le istruzioni dei nodi vengano eseguite in sequenza

• If-then-else, il primo nodo consiste nelle istruzioni di controllo della condizione, le istruzioni ap-
partenenti ad uno dei due nodi centrali verranno eseguite in base al risultato della valutazione della
condizione. Il quarto nodo verrà eseguito a priori

• If-then, le istruzioni del nodo centrale vengono eseguite se e solo se la valutazione della condizione da
esito positivo

• Iterative, corrisponde ad un ciclo.

Figure 7.3: Le diverse sezioni che possiamo incontrare in un CFG

Di seguito si presenta un esempio di Control Flow Graph associato ad un programma

Figure 7.4: Si vuol fare notare che si può inglobare più di una istruzione in un singolo nodo, anche se in
figura non è stato fatto.

108
Definito il concetto di Control Flow Graph, possiamo parlare dei livelli di copertura dei casi di test. Il
primo livello è rappresentato dalla Statement Coverage (copertura delle istruzioni ). Si raggiunge questo
livello di copertura se i casi di test che abbiamo scelto attraversano tutti i nodi del Control Flow Graph
associato all’applicazione che si sta testando. Specifichiamo che non deve esistere un test che raggiunge tutti
i nodi, ma basta che un nodo venga attraversato almeno da uno dei test predisposti.
Nel caso in cui esiste un nodo che non può essere attraversato da alcun caso di test, siamo di fronte a Dead
Code o codice morto, che va rimosso.

Figure 7.5: Un caso di test che permette di coprire tutti i nodi

Si parla invece di Branch Coverage (copertura delle diramazioni ) quando i casi di test scelti passano
attraverso tutti gli archi del Control Flow Graph della nostra applicazione. Per definizione, se si coprono
tutti gli archi, si copriranno anche tutti i nodi

Figure 7.6: Un caso di test non permette di coprire tutti gli archi, per questo bisogna introdurre un secondo
caso

Definiamo con Condition Coverage (copertura delle condizioni ) la copertura dei test che prevede che,
per ogni condizione elementare presente nel nostro programma, si abbia un test che copra sia il caso di

109
condizione soddisfatta e sia il caso contrario.

Figure 7.7: esempio di casi di test per una espressione booleana

Si parla invece di Path Coverage (copertura dei percorsi ) quando i test coprono tutti i possibili percorsi
esistenti. Una ragionevole approssimazione di questo tipo di copertura che, data la mole di possibili percorsi
esistenti, risulta molto difficile da garantire, è data dalla copertura dei soli percorsi linearmente indipendenti.
Si definisce percorso linearmente indipendente un percorso che presenta un arco non presente degli altri
percorsi.

Figure 7.8: percorsi possibili per il nostro esempio

Si dimostra che il numero massimo (limite superiore) di percorsi linearmente indipendenti tra loro in un
Control Flow Graph è dato dalla Cyclomatic Complexity (complessità ciclomatica) ad esso associata:

Cyclomatic Complexity = Edges Nodes + 2 (7.1)

Dunque, se si sceglie di e↵ettuare del White Box testing nella nostra applicazione, bisogna scegliere il livello
di copertura da raggiungere. In generale si cerca di raggiungere un livello di copertura che copre un set di
percorsi linearmente indipendenti.

7.6 Unit Testing


Le Unit Testing sono test svolti su una singola unità del programma per verificarne le funzionalità, come
una singola classe o un singolo metodo. Generalmente questi test vengono svolti due volte:

110
• sia dal programmatore, mentre sta lavorando, per avere una maggiore sicurezza di quello che ha
scritto nel codice;

• sia dal tester, dopo che l’intero componente è stato completato.

Vi sono alcune buona pratiche da seguire quando si testa un programma:

• testare piccole porzioni di codice una volta che queste sono state scritte, più piccole sono meglio è, in
questo modo le probabilità di trovare un errore aumentano considerevolmente;

• i test tipicamente devono essere svolti in maniera automatica tramite libreriere apposite come JUnit
per Java. In questo modo è posssibile conservare i test perchè possono servire come test di regressione
ovvero test che ci aiutano a verificare che un problema che è gia stato risolto non possa ripresenttarsi
in una futura modifica del codice (ES. Programmi OpenSource)

Avere un unità di test ci consentirà di e↵ettuare il ”refactor” del programma, permettendoci di modificare
la struttura del programma senza cambiarne il comportamento. Se e↵ettuiamo un refactoring costante
arriveremo ad ottenere un codice scritto bene ed inoltre, la presenza di unità di test di regressione ci
assicurerà che non sono stati introdotti errori mentre si e↵ettuava il refactoring. Un’altro vantaggio di tenere
i test è che aiutano a documentare le assunzioni fatte durante la stesura del programma essi infatti sono una
versione eseguibile dei requisiti sopratutto se pensiamo a dei test black box.

7.6.1 Quando finiscono i test?

In un tempo finito non è possibile dimostrare che il nostro programma sia corretto ed è per questo che
scegliamo un sottoinsieme dei possibili casi. Ci possiamo ritenere soddisfatti del testing quando:

• una volta pianificati i piani di test (black box o white box), la nostra applicazione ha testato tutti i
test cases individuati;

• quando non ci sono problemi che non sono stati corretti.

In molti progetti è sufficiente rispettare queste due condizioni per poter terminare il test, tuttavia, in altri tipi
di progetto, ci occorrono tecniche più particolari per decidere quando finire il testing. Una di queste è il defect
seeding, ovvero inserire appositamente dei bug all’interno del codice per controllare quanto accuratamente
i tester stanno e↵ettuando l’attività di test (questo tecnica si usa quando i tester sono diversi dal program-
matore). Se i tester trovano tutti bug inseriti volutamente e non, allora l’attività di testing risulterà conclusa.

ATTENZIONE!!Una cosa che NON si deve fare è considerare conclusi i test quando è finito il tempo,
questo perchè porterà ad avere dei bug nell’iterazione successiva producendo un costo per risolvere il bug.
In un processo come Scrum si preferisce non rilasciare una particolare funzionalità al cliente piuttosto che
rilasciarla con un insieme di test non soddisfacente.

111
7.7 Inspections and Reviews
Andremo ora ad analizzare alcuni attività di verification diversi dal testing che possono essere importanti in
un processo software per incrementarne la qualità.

• Revisione: una persona che cerca di leggere e comprendere un artefatto del progetto, come un docu-
mento di specifca dei requisiti o di documentazione, per trovare degli errori. La revisione è un’attivittà
che possiamo svolgere sia sul codice sia sul documento. Questa attività è importante perchè alcune
tipologie di errore sono difficili da scovare con il testing. Per esempio gli errori legati alla sincroniz-
zazione dei thread non possono essere scovati dalle tecniche di white box testing e black box testing in
quanto queste funzionano solo con programmi single thread. La soluzione a questo problema è quello
di lasciar controllare il codice ad una persona esterna. Questa attività prende il nome di code review

• Walkthrough: gli autori spiegano ad altre persone la natura del codice facendo in modo che queste
riescano ad inidividuare degli errori che gli autori non hanno scovato.

• Software Inspection: è una versione della code review fatta in maniera più strutturata. Le persone
che fanno la inspection hanno un processo da seguire che è stato formalizzato(riunioni da seguire,
rapporti da consegnare ecc.).

Tutte queste attività NON rappresentano un’alternativa al testing bensı̀ un’aggiunta, esse sono complemen-
tari tra di loro. Tuttavia, vi sono delle di↵erenze:

• Inspections:

– si applica sia al codice che ai documenti;


– siccome ci sono persone che visioneranno il codice, la conoscenza della sua struttura è distribuita
tra più persone. Questo è buono perchè se la conoscenza del codice risiedesse su una sola persona
e questa se ne andasse dall’azienda si riscontrerebbero grossi problemi.
– sapere che ci sarà qualcun’altro che guarderà il codice, porterà il programmatore a scrivere un
codice più leggibile, una documentazione scritta meglio e ad evitare soluzioni sporche.

• Testing:

– costo minore
– si applica solo al codice
– trova gli errori, ma corregerli costa
– cattura i difetti più tardi
– necessario per valutare la qualità

7.8 Formal Methods


Un’altra tecnica che si usa in alcune situazioni è quella dei metodi formali per la verifica del programma.
I metodi formali sono delle tecniche di tipo matematico che servono a dare una dimostrazione che il
software abbia certe proprietà (Es. correttezza) con l’aiuto di opportuni strumenti.

112
Esempio:
Costruiamo un modello di una parte del mio programma come un’automa a stati finiti. Su di esso posso
verificare alcune proprietà, per esempio se uno stato x è o non è raggiungibile da un altro stato y. Se il
requisito è ”dimosta di poter arrivare in un certo stato x” io lo posso dimostrare mediante l’automa a stati
finiti.

La cosa interessante è che quando riesco ad applicare questi metodi formali io ho la certezza matematica
che il software rispetti una determinata proprietà, a di↵erenza del testing che da solo non mi darà mai la
certezza di ciò, nemmeno quando tutti i test sono stati superati. Tuttavia, l’applicabilità dei metodi formali
è limitata siccome, richiede una conoscenza di strumenti matematici appropriati e non si possono applicare
su tutti i programmi e a tutte le loro proprietà.

7.9 Static Analysis


L’analisi statica consiste nell’uso di particolari strumenti per visionare se alcune proprietà del software che
ho prodotto, sulla base dell’esperienza, sono indicatori di probabili problemi, ovvero situazioni che possono
aumentare la probabilità che ci sia un bug nel software. Per esempio, ci sono dei strumenti software che
provano a individuare situazioni di alte coesione o basso accoppiamento che non sono necessariamente dei
difetti però, c’è un elevata probabilità che queste portino a dei bug nel software. Questo può essere uno
strumento addizionale, che mi indica quali possono essere le parti del codice da controllare con più attenzione,
per verificare se ci sono dei difetti.

Esempio:
Se io non inizializzo una variabile nel momento in cui l’ho creata, questo non è necessaramente un errore,
perchè l’importante è che io do una valore alla variabile prima di usarla. Tuttavia, la probabilità che io mi
dimentichi di dare un valore alla variabile è piu alta se la variabile non è inizializzata. Per questo vi sono dei
tool che mi indicano tutte le parti del mio programma dove una variabile non è stata dichiarata, in questo
modo, il sistema mi aiuta ad identificare situazioni potenzialmente pericolose ma non necessariamente.

113
8 Test Driven Development
8.1 Introduzione
Questa metodologia di lavoro orientata al Testing viene spesso adottata per le metodologie Agile.
Negli approcci tradizionali, come quello a cascata, il Testing viene e↵ettuato alla fine dello sviluppo, dando
priorità quindi alla progettazione ed alla realizzazione di un sistema, e solo successivamente ne viene testato
il corretto funzionamento.
Le metodologie Agile hanno quindi un approccio diametralmente opposto al tradizionale, ovvero la scrittura
dei test avviene prima dello sviluppo stesso. Questo approccio è detto Test Driven Development,
ed i tre punti cardine di questo sono:

• Test Early

• Test Often

• Test Automatically

8.2 Test Early


Lo sviluppatore deve scrivere il codice di test una funzione prima di scrivere il codice che implementa la
funzione stessa. Quindi quando uno sviluppatore deve aggiungere qualcosa ad un programma, dopo aver
progettato l’aggiunta, avrà il compito di implementare solo l’interfaccia del metodo, dando priorità allo
scrivere il codice per testare questa nuova funzione aggiunta, anzichè all’implementazione della funzione stessa.

Ovviamente in questo caso si parla di Black Box Testing, dato che il test va scritto prima della stesura
del codice stesso, e possiamo quindi basarci nella scelta dei casi di test solo sui requisiti e non sul codice
stesso.

Quindi lo sviluppatore implementerà la funzione solo dopo averne definito i test.

Oltre a lanciare i test appositamente scritti per la funzione aggiunta, lo sviluppatore dovrà lanciare an-
che tutti gli altri testi inseriti per tutte le altre funzionalità. Solo quando si è sicuri di non aver “rotto”
qualcosa di già esistente e testato, e di aver passato anche tutti i test associati alla funzionalità appena
aggiunta, allora si potrà considerare il lavoro terminato.

Quali sono i vantaggi di questo approccio?


• Se lo sviluppatore prima di scrivere il codice di una funzione, ne scrive i test, allora allo stesso momento
controllerà anche se l’interfaccia scelta per la funzione è idonea. Quindi questo controllo ci
permetterà eventualmente di cambiare l’interfaccia quando è ancora poco costoso, a di↵erenza del caso
in cui bisognerebbe cambiarla quando la funzione è già stata sviluppata.

• Sapendo che lo sviluppatore come prima cosa deve scrivere i casi di test usando la metodologia Black
Box, e sapendo tale metodologia si basa sui requisiti funzionali, allora saremo sicuro che affinché lo
sviluppatore scriva i casi di test questo dovrà per forza aver visionato i requisiti con abbastanza
attenzione, evitando quindi una visione superficiale.

114
Un approccio sbagliato è quello di terminare la fase di test quando il tempo è scaduto, questo
potrebbe gravare sulla qualità del prodotto. Con questo approccio non capiterà l’inserimento di una funzione
non testata adeguatamente, favorendo il rilascio di un prodotto di qualità.

8.3 Test Often


L’idea è che i test non vengono lanciati una volta soltanto e poi cancellati, ma ogni volta che aggiun-
giamo una funzione oltre a testare quest’ultima, testeremo nuovamente l’intero insieme di test del sitema.

Quali sono i vantaggi di questo approccio?

• Si va ad anticipare il momento in cui vengono scovati i bug legati alle interazioni di diversi
parti del programma. Questo deriva dal fatto che, idealmente, sarebbe auspicabile un accoppiamento
quasi nullo, ma ciò rimane appunto ideale, e la maggior parte delle problematiche sono conseguenza
di accoppiamento tra le varie classi. Se lanciamo spesso i test, ci accorgeremo prima dei problemi
rendendone più facile la risoluzione.

• Un altro vantaggio, più dal punto di vista psicologico, è il fatto che con questo approccio gli svilup-
patori avranno più sicurezza nel creare il codice. Se infatti uno sviluppatore dopo aver fatto
una modifica lancia i test e danno riscontro positivo, è sicuro di “non aver fatto danni” quindi potrebbe
anche osare nello sviluppo e nell’ottimizzazione, sapendo che può tranquillamente tornare indietro fin
quando i test funzionavano ancora.

• Minor numero di bug nel software rilasciato.

• Con questo approccio si riesce anche ad evitare il “ritorno dei bug”. Infatti grazie ai test di
regressione, possiamo introdurre test in grado di scovare un bug già incontrato, evitandone cosı̀ un
ritorno nel caso in cui un ulteriore sviluppatore e↵ettui lo stesso errore in maniera non consapevole.
In generale nel test driven development è possibile considerare tutti i test come test di regressione,
dato che vengono comunque tutti rilanciati ogni volta, e sono sicuro che, non solo una certa funzione
è priva di bug, ma rimarrà priva di bug anche nel seguito. (Ovviamente ricordiamo che non esistono
test completi, quindi più che priva di bug, è meglio dire “ragionevolmente priva di bug”).

8.4 Test Automatically


Idealmente i test di un programma devono essere eseguibili senza l’intervento umano. La modalità
manuale va bene quando i test vanno eseguiti una sola volta, ma ciò non va bene quando i test andranno
spesso ripetuti, specialmente ad ogni aggiunta di codice. Quindi bisogna avvalersi di strumenti che non solo
ci permettano di eseguire più volte ulteriori test, ma ci permettano di capire a colpo d’occhio l’esito e la
locazione di eventuali errori.

Quali sono i vantaggi di questo approccio?

• Se il test è automatico, anziché manuale, non mi peserà lanciarli più e più volte, favorendo una
minore quantità di errori.

• I test possono essere anche lanciati automaticamente senza richiesta umana. (es. Nightly Build)

115
• Gli sviluppatori sono incoraggiati nel separare la logica di business dall’interfaccia grafica,
essendo quest’ultima più difficile da testare automaticamente. Se infatti dobbiamo preoccuparci di
testare le cose, allora si favorirà anche una migliore progettazione, come appunto la separazione
della logica applicativa dall’interfaccia utente.

8.5 TDD e Debugging


Cosa succede quando, nonostante il programma superi tutti i test, vi è un bug all’interno del sistema?
Solitamente in un normale processo, ci verrebbe di cercare la causa del bug e risolverla.
Nel TDD è diverso, infatti il problema viene inizialmente interpretato come una mancanza nei test costruiti,
quindi la prima cosa da fare non è cercare la causa del bug, ma costruire un test che fallisca a causa
di questo bug, rendendo quindi riproducibile il problema segnalato.
Quindi bisogna costruire un test che fallisca a causa del bug segnalato, studiando bene le cause ed appro-
fondendo le radici di quest’ultimo. Solo una volta che è stato costruito il test si passerà alla correzione del bug.

Il vantaggio principale è che evitiamo che il bug corretto venga reintrodotto, aggiornando sempre la suite
di test in modo da aumentare la qualità del prodotto.

8.6 Refactoring
Attività relativa alla modifica del codice esistente, senza aggiungere nuovi requisiti o cambiare il comporta-
mento dei requisiti già implementati. In generale questa attività è svolta al fine di migliorare la progettazione
o la struttura del codice.

In un progetto potrebbero accumularsi “Technical Debt”, l’attività con cui ripaghiamo questi ultimi è ap-
punto il refactoring, e ciò è evidente soprattutto nei metodi iterativi.

Infatti se ad esempio in iterazioni successive, ci rendiamo conto che i requisiti finora sviluppati potrebbero
essere migliorati con un migliore approccio, nel caso in cui andassimo avanti non curando queste migliorie
che è possibile e↵ettuare, avremmo come prodotto finale un prodotto non di qualità e disomogeneo tra le
varie parti del sistema.

116
Figure 8.1: Esempio pratico del refactoring

Il vantaggio del refactoring è che appunto aggiustando mano mano il sistema evitiamo di “saldare il debito
tecnico” solo alla fine, dato che ricordiamo che i technical debt sono come un prestito.

Ora la domanda è : cosa ha a che fare il refactoring con il TDD?

L’adagio tradizionale è che quando una cosa funziona non conviene metterci mano, se però abbiamo una
buona batteria di test che ci permettono di capire se il codice funziona o meno, saremo più sicuri e spinti a
e↵ettuare una fase di refactoring, senza appunto paura di fare “danni”.

Ovviamente bisogna fare attenzione al fatto che, dopo un refactoring, potrebbe essere necessario anche
cambiare i test, e queste considerazioni non sono cosı̀ ovvie. Infatti nel momento in cui e↵ettuiamo delle
modifiche solo dal punto di vista funzionale e non dell’interfaccia, con test di tipo Black Box non abbiamo
problemi. Se invece i test sono di tipo White Box, questi probabilmente dovranno cambiare.

In genere vi sono due possibilità

• L’interfaccia cambia dal punto di vista sintattico, è più facile accorgersi delle modifiche da e↵ettuare
poiché ci avviserebbe lo stesso compilatore.

• L’interfaccia cambia dal punto di vista semantico, è più difficile da scovare. Se infatti ad esempio
consideriamo una funzione che restituisce l’ora del giorno, inizialmente potrebbe dover ritornare l’ora
come un intero che rappresenta i secondi passati dalla mezzanotte, se cambiassimo il valore di ritorno
considerano i millisecondi anziché i secondi, la funzione ritornerà sempre un intero, ma in questo caso
i test creati per la prima versione della funzione non saranno più idonei. Quindi potremmo trovarci
nel caso in cui il test fallisca per colpa del test stesso e non della particolare implementazione della
funzione.

117
9 JUnit
La fase di TDD richiede l’impiego di test automatici. Per fare questo esistono diverse librerie. La più
utilizzata è JUnit, essa è considerata uno standard de facto per l’automazione di test in Java. JUnit è nata
come porting del linguaggio Java di una libreria chiamata SUnit, sviluppata inizialmente da Beck per il
linguaggio Smalltalk. Essa gode di alcune proprietà:

• è una libreria che usa esclusivamente funzionalità del linguaggio Java infatti essa puo essere portata su
qualunque piattaforma in cui si abbia una Java Virtual Machine

• è integrata nei principali ambienti di sviluppo (Es. NetBeans, ecc.)

• non ha dipedenze dal sistema operativo o da altri pezzi di software

• libreria open source

9.1 JUnit use


JUnit viene usata per realizzare del codice che testi i metodi di ogni classe del vostro codice Java. In generale
vi sono alcuni convenzioni da rispettare:

• nel momento in cui sottoponiamo ogni classe a un test, andiamo a creare un’ulteriore classe parallela
contenente tutti i codici di test;

• la classe di test avrà lo stesso nome della classe originaria ma con ”test” alla fine.

Le posizioni delle classi di test dipendono da cosa vogliamo testare; per esempio se vogliamo testare solo i
metodi pubblici allora queste possono essere posizionate dovunque vogliamo (Package diversi ecc.). Invece,
se vogliamo testare sia i metodi publici che metodi con visibilità protected o default allora la classe di test
dovrà essere posizionata nello stesso package della classe del metodo. In generale non possono essere testati
i metodi privati di una classe. La classe di test deve :

• essere public, siccome deve essere visibile al di fuori del package che la contiene, in modo da poter
essere richiamata dalla libreria che esegue tutti i test;

• utilizzare alcune classi e metodi di una libreria specifica.

JUnit richiede l’utilizzo delle seguenti librerie:

Figure 9.1: Import delle librerie JUnit

In particolare l’import static della figura 9.1 ci consente di richiamare i metodi statici della classe ”Assert”
senza scrivere il nome della classe prima del nome del metodo.
All’interno della nostra classe di test dobbiamo inserire dei metodi appositi per realizzare i nostri test e per

118
ciascun metodo della classe da testare un ulteriore test, i metodi verranno poi denominati con ”test” all’inizio
seguiti dal nome della funzione che testano.
I test devono essere:

• public: in modo da poter essere invocati dalla libreria ;

• void: siccome questi metodi verranno richiamati dalla libreria rendendo inutile qualsiasi altro tipo di
ritorno;

• senza parametri;

• deve avere l’annotazione @Test.

N.B. La classe di test potrebbe avere dei metodi non solo specifici per i test ma anche indirizzati a
supporto degli altri metodi di test.

9.2 JUnit assertions


Generalmente per testare un caso di test all’interno di un metodo di test devo: creare degli oggetti, richiamare
i metodi degli oggetti passando dei parametri scelti in base al caso di test e controllare se i risultati ottenuti
siano quelli aspettati, ovvero controllo se questi risultati soddisfino certe condizioni. Per e↵ettuare questo
passaggio utilizzo dei metodi static derivati dall’import static in figura 9.1 , denominati assert. Gli ”assert”
verificano se uno o più condizioni sono state rispettate; se ciò avviene, allora il test si considera passato con
successo; se anche solo una delle condizioni non è verificata allora il test si considera fallito.
Vediamo adesso i metodi presenti nella classe statica Assert:

• assertEquals(expected, actual): controlla se due valori sono uguali. Possiamo avere due casi:

– confronto tra due valori di tipo primitivo: allora controllo in base all’operatore ”==” del linguag-
gio;
– confronto tra due valori di tipo oggetto: allora viene usato il metodo ”equals” della classe Object;

• assertArrayEquals(expected, actual): controlla se due array sono uguali;

• assertTrue(cond) e assertFalse(cond): prendono un parametro booleano: assertTrue considera la


condizione verificata se il parametro ha il valore True, assertFalse considera la condizione verificata
quando il parametro è False;

• assertNull(obj) e assertNotNull(obj) controllano un riferimento all’oggetto: assertNull è verificata


se il riferimento è un riferimento nullo, assertNotNull è verificata se il riferimento è un riferimento non
nullo.

119
9.3 Esempio
Supponiamo di realizzare una classe che sappia come sommare due numeri interi, seconda la metodologia TDD
come prima cosa realizziamo la nostra funzione con un’implementazione vuota che ci consenta di compilare
il codice

Figure 9.2: Esempio classe Adder

Ora posso incominciare il testing. Creo una classe nella quale andrò a scrivere questi test e la chiamo
”AdderTest”.

Figure 9.3: Esempio classe AdderTest

Dopodichè devo scrivere dei metodi che realizzino la mia funzione da testare.

Figure 9.4: Esempio testAdd method

A questo punto dentro il corpo della funzione devo inserire il codice per realizzare il mio caso di test.

120
Figure 9.5: Casi di test del metodo add

Nella figura 9.5 sto analizzando due casi di test e con il metodo statico ”assertEquals” sto verificando se
il risultato ottenuto mediante l’utilizzo della funzione ”add” sia quello aspettato.

9.4 JUnit Tests


Avendo il test pronto è buona pratica far partire il test, in questo modo posso verificare se ho fatto un buon
lavoro o meno. Esistono diversi modi per lanciare i test, il tutto dipende dall’ambiente di sviluppo che sto
utilizzando:
• da linea di comando;

• main che lancia tutti i test;

• comando apposito dell’ambiente di sviluppo;


Nel riportare i test la libreria indica, oltre ai test che sono stati passati, due possibili soluzioni di test non
passato:
• failure, ovvero che qualcuna delle condizioni (assert) nel test non è stato verificata ;

• error, ovvero si è verificato un errore (Es. eccezione) che ha preventito l’esecuzione del test ;
Se i test sono stati scritti correttamente questi due casi non dovrebbero capitare.
In alcuni casi nell’eseguire il test vogliamo introdurre una limitazione al tempo di esecuzione di una determi-
nata operazione (Es. rischi di loop), per fare questo introduco un parametro nella notazione @Test.

Figure 9.6: Parametro timeout

121
In altri casi invece voglio realizzare un test che mi verifichi se una determinata eccezione è stata lanciata
dopo un’operazione non consentita (Es. pop di uno stack vuoto).

Figure 9.7: Parametro expected

Il test, in questo caso, si dirà fallito se l’eccezione non è stata lanciata.

9.5 Fixtures
All’interno del test tipicamente devo creare almeno un’oggetto che sarà quello da testare, tuttavia, nel caso
in cui volessimo creare più test all’interno della stessa classe, si verificherebbe una ripetizione nel codice. Per
evitare questo, utilizzo una fixture ovvero, un insieme di oggetti usati per il test che vengono ricreati ad
ogni diverso metodo di test. In questo modo, non uso gli stessi oggetti per ogni metodo di test ma utilizzo
una copia pulita ogni volta che viene lanciato un metodo di test.
Una fixture in JUnit viene implementata tramite l’annotazione @Before, i metodi con questa annotazione
vengono automaticamente richiamati prima dell’esecuzione di ciascuno dei metodi di test. Tipicamente i
metodi con questa annotazione creano gli oggetti della fixture e li mettono nelle variabili di istanza della
classe di test, in questo modo sono sicuro che i metodi di test possano usare questi oggetti. Esiste anche una
notazione duale denominata @After con la quale i metodi vengono eseguiti automaticamnete dopo ogni test
per liberare delle risorse (Es. jdbc in @Before apro la connessione e in @After la chiudo).

122
Esempio:

Figure 9.8: Esempio fixture

9.6 Tips
1. Testa una cosa per volta;

2. I test non dovrebbero contenere molta logica(cicli,if ecc.);

3. Nei test non è opportuno che ci siano blocchi try-catch, se c’è la necessita di catturare delle eccezioni
utilizza il parametro expected in @Test.

123
10 Version Control Systems
I Version Contorl Systems sono degli strumenti fondamentali per lo sviluppo di software, specialmente
per teams distribuiti. Un VCS è un software che gestisce un gruppo (insieme) di files e directories per tutto
il tempo di sviluppo. Utilizzando un VCS è possibile:

• tenere traccia di ogni cambiamento ai files sorgente, quando è avvenuto il cambiamento e chi ha
e↵ettuato il cambiamento;

• ritornare ad una versione precedente o confrontare due verioni di↵erenti dello stesso software;

• unire cambiamenti fatti da sviluppatori di↵erenti.

In sostanza, il suo utilizzo agevola le modifiche al software e si ha la garanzia che qualunque modifica è fatta
in modo sicuro ed è possibile annullarla.

Per anni, il problema principale dello sviluppo del software in team distribuiti è stato la sincronizzazione,
ovvero il meccanismo di modifica dei files o directories dei progetti da parte di piu utenti:

• Nelle versioni primordiali dei VCS, si utilizzavano dei mutex sui files per evitare che più utenti ci
lavorasero simultaneamente e questi venivano poi salvati su dei server/repositories centralizzati.

• In versioni più odierne dei VCS, i file erano salvati su server/repositories distribuiti, in opposizione alla
centralizzazione dei VCS primordiali.

10.1 VCS Distribuito e VCS Centralizzato


In un VCS distribuito ogni sviluppatore possiede sia i files del progetto su cui sta lavorando (attualmente),
sia tutta la storia del progetto software. Uno sviluppatore può quindi modificare in o✏ine e a suo piaci-
mento può condividere le sue modifiche con il resto del team.
Non esiste una autorità centralizzata che controlla tutta la storia e i files del progetto perchè appunto, sono
condivisi tra tutti gli sviluppatori.

La di↵erenza tra i due risiede nel fatto che per quanto riguarda il centralizzato, i sorgenti sono collocati
soltanto nel server centrale e le modifiche vanno fatte necessariamente collegandosi al server e acquisendo
una sorta di mutex su ogni file, come in Figura 10.1.

124
Figure 10.1: VCS Centralizzato

Nei VCS distribuiti invece, i sorgenti sono mantenuti su repositories locali. Quando uno sviluppatore
vuole, può poi caricare le proprie modifiche (mantenute nella copia del progetto nel proprio repository) in un
repository che risiede in un server remoto a cui hanno accesso tutti gli sviluppatori, come in Figura 10.2

Figure 10.2: VCS Centralizzato

10.2 Concetti fondamentali


10.2.1 Grafo

Tutta la storia del progetto è rappresentata tramite un grafo diretto aciclico - DAG.

125
• I nodi, creati dalle operazioni di commit, contengono i file che sono stati cambiati rispetto ai suoi
nodi predecessori. Contengono quindi le informaizoni che consentono di ricostruire lo stato del progetto
in un preciso punto nella timeline del suo sviluppo. Bisogna pensare ad un nodo come se fosse un
checkpoint a cui si può facilmente ritornare se qualcosa va storto.

• Gli archi rappresentano i legami che ci sono tra un checkpoint (nodo) e l’altro. QUando si e↵ettua un
commit viene creato un nodo ed un arco che parte dal nodo corrente e arriva al nodo creato tramite il
commit.

10.2.2 Contenuto dei nodi

È buona norma aggiungere al repository soltanto i file sorgente, ovvero files che non sono ottenuti auto-
maticamente da altri files (non file oggetto o eseguibili ).

Esempio:
Un repository con 5 nodi (A, B, C, D, E) dove A è il padre di B, B è il padre di C, etc.
Ogni nodo rappresenta un’istantanea dei files a un particolare punto nel tempo (quando il nodo è stato creato
da un’operazione di commit).

Il nodo etichettato come master rappresentail nodo principale e indica qual è la versione corrente (detto
anche branch).

Figure 10.3: Esempio di grafo di un repository

All’interno dei vari nodi vengono memorizzati soltanto i files che vengono cambiati rispetto al nodo precedente.
Riprendendo l’esempio precedente, se a partire dal Nodo A modifichiamo File A e File C ed e↵ettuiamo il
commit, all’interno del Nodo B saranno memorizzati soltanto i file modificati, ovvero File A1 e File C1,
mentre il File B non sarà presente perchè rimasto immutato (sarà comunque possibile accedervi tarmite ad
esempio un riferimento).

126
Figure 10.4: Modifica dei file all’interno dei nodi

10.2.3 Comandi Git

Esistono due modi principali per interagire con un VCS come Git: tramite interfaccia classica, ovvero riga
di comando oppure tramite interfaccia grafica attraverso un ambiente di sviluppo.
Per analizzare i vari comandi si supporrà di interagire tramite interfaccia classica.

10.2.3.1 Stato di un file

Un file all’interno di un repository può essere in diversi stati e lo stato può cambiare a seconda del comando
che si va a specificare.

Figure 10.5: Lo stato di un file (e le varie transizioni)

Un file in uno stato di Untracked verrà ignorato dal sistema Git (come ad esempio i files non sorgente).
Rappresenta lo stato di default per ogni file creato.

Quando si vuole tenere traccia di uno specifico file, questo va portato in uno stato di Staged, ovvero, viene in

127
qualche modo preso in considerazione dal sistema Git. Non verrà inserito in un nodo del repository, ma viene
contrassegnato. Il file in uno stato di Staged verrà inserito in un nodo soltanto dopo un’operazione di commit.

Un file (a partire dallo stato di Staged) viene portato in uno stato di Unmodified quando si e↵ettua
un commit. L’etichettta Unmodified infatti si riferisce al fatto che il file non è stato modificato in accordo
all’ultima versione o all’ultimo nodo, ovvero è rimasto identico rispetto al nodo precedente.

Quando si modifica il contenuto di un file localmente, le modifiche fatte vengono salvate nel repository
locale e il file viene automaticamente portato dallo stato di Unmodified ad uno stato di Modified. Non
appena si è finito di modificare il file, si dovrà poi portare in uno stato di Staged per e↵ettuare il commit e
portare il file modificato in un nuovo nodo del repository.

Infine, un file può essere rimosso quando non è più utile ai fini del progetto. Le versioni precedenti (nodi)
continueranno ad avere questo file (nel caso in cui si voglia ritornare indietro). Dopo la rimozione infatti, il
file si porterà in uno stato di Untracked.

10.2.3.2 Comandi principali

I comandi principali sono:

• Git status: mostra lo stato di ogni file nella directory corrente del repository.

• Git add: mette un file in uno stato di Staged.

• Git commit: salva tutti i file nello stato di Staged nel repository e crea un nuovo nodo contenente
questi files, successivamente imposta lo stato di tutti i file del nodo come Unmodified.

• Git log: mostra i commenti, la storia dei vari nodi (ad esempio per favorire la coordinazione tra i
membri del team). Utile se si vuole ritornare ad un nodo precedente utilizzando le informazioni di log
di quel determinato nodo per localizzare la versione a cui si vuole ritornare.

• Git blame: dato un file (sorgente), consente di visualizzare per ogni linea di codice l’autore della
modifica di quella determinata linea. Utile per ricostruire le modifiche fatte e far cadere Mario Amato.

• Git revert: riporta il progetto in uno stato rappresento da un nodo precedente, ripristinando tutti i
files e le directories contenute in quel determinato nodo. Non cancella la storia del progetto successiva
al nodo scelto, ma semplicemente crea un nuovo nodo a partire dal corrente identico a quello a cui si
vuole ritornare, rappresenta quindi un’operazione sucura.

• Git init: per inizializzare un repository, va a creare un repository vuoto all’interno di una cartella
nascosta .git, come in Figura 10.6.

128
Figure 10.6: Inizializzazione di un repository

Dopo la creazione di un repository e l’aggiunta dei file nello stato di Staged, tramite l’opzione -m è possibile
aggiungere un commento al commit.

10.2.4 Id e Tag

Ogni nodo è identificato da un ID, ovvero un valore univoco composto da 40 caratteri. Per riferirsi ad un
nodo o referenziare un nodo, è però possibile utilizzare un prefisso più piccolo di tutti e 40 i caratteri, a
condizione che sia univoco anche esso: il tag.

Il tag è un identificatore simbolico dato ad un nodo al momento della sua creazione, in modo da riferirsi a
quel particolare nodo (particolare versione) in maniera più veloce ed intuitiva. Tipicamente, per ogni release
viene creato un tag con il nome della release.

10.2.5 Branch

Durante lo sviluppo di un software, generalmente non si ha una struttura a lista di nodi, bensı̀ si ha una
struttura a diramazioni, per rappresentare le varie versioni di un progetto software a partire da un determi-
nato nodo. Queste diramaizoni sono dette branches.

Un branch è un puntatore ad un particolare nodo del grafo. I commit e↵ettuati su un branch, gener-
ano nuovi nodi soltanto in corripsondenza di uno di questi puntatori e il puntatore da cui si è partiti si sposta
poi al nodo appena creato dal commit.

Ogni utente, ha sia una copia locale dell’intero grafo, mantenuta in una cartella nascosta (.git), sia una
working directory che sta a rappresentare i contenuti del nodo di testa o HEAD di un particolare branch.

10.2.5.1 Nodo HEAD e riferimenti a catena

Il nodo di testa o HEAD del branch rappresenta l’ultimo nodo del branch corrente (ultima versione). Se n
è il nome del nodo (ID o tag), allora nˆ può essere usato per riferirsi al suo genitore. Ricorsivamente, ci si

129
può riferire al k-esimo ancestor di un nodo n tramite la notazione n⇠k.

Esempio:
HEADˆˆe HEAD⇠2 si riferiscono entrambi al nonno del nodo HEAD del branch corrente.

10.2.5.2 Esempio di utilizzo dei branch

Si supponga di creare un repository attraverso il comando git init. In automatico, Git creerà un nodo A
e un branch dal nome master, di cui l’HEAD sarà proprio il nodo A appena creato, Figura 10.7. La stella
verde rappresenta il branch attivo, dove si risiede.

Figure 10.7: Esempio di utilizzo dei branch (1)

Dopo due commit, tramite i comandi git commit -m ’my nth commit’ all’interno del branch master, si
arriverà ad una situazione in cui ci saranno 3 nodi (A, B, C) e il branch master che punterà all’ultimo nodo
(versione più recente) del branch, ovvero C, Figura 10.8.

Figure 10.8: Esempio di utilizzo dei branch (2)

Si supponga poi di voler cambiare branch, spostandosi da quello master (corrente).

Se il branch su cui ci si vuole spostare è esistente, si utilizza il comando git checkout bug123.
Se invece il branch su cui ci si vuole spostare non esiste ancora, si puo usare il comando git checkout -b
bug123 per la creazione del branch e lo spostamento su di esso.

La creazione di un branch in questo caso è utile perchè dal nodo C è stato identificato un bug, e si è

130
deciso di diramare il progetto in due versioni: una in cui viene risolto il bug (bug123 ), e l’altra in cui si
continuano ad introdurre nuove funzionalità (master ), come in Figura 10.9. Se si utilizza un server remoto,
gli altri sviluppatori continueranno a vedere soltanto il branch principale master senza le modifiche e↵ettuate
su bug123, mentre chi risolve il bug si spostarà su una diramazione laterale.

Figure 10.9: Esempio di utilizzo dei branch (3)

Dopo due operazioni di commit sul branch di risoluzione del bug (bug123 ), si arriverà ad una situazione in
cui sarà presente una diramazione: il master continua a puntare al nodo C (perchè sul branch master non
sono stati e↵ettuati commit), mentre il branch bug123 si sposterà e punterà all’ultimo nodo creato all’interno
del branch, ovvero quello generato dopo il secondo commit (nodo E), Figura 10.10.

Figure 10.10: Esempio di utilizzo dei branch (4)

Dopo aver risolto il bug, si decide di ritornare al branch master (principale), attraverso il solito comando git
checkout master, Figura 10.11.

131
Figure 10.11: Esempio di utilizzo dei branch (5)

Dal momento che il bug è stato risolto, bisogna fondere i due branch in modo da poter unire i cambiamenti
e↵ettuati nel branch di risoluzione del bug insieme a quelli del branch master. Questa operazione viene
e↵ettuata con il comando git merge bug123 all’interno del branch master, unisce il branch corrente con
uno diverso, applicando i cambiamenti fatti. nell’altro branch, Figura 10.12.

Figure 10.12: Esempio di utilizzo dei branch (6)

Dopo aver unito i due branch, si cancella il branch che è stato unito tramite il comando git branch -d
bug123, Figura 10.13.

Figure 10.13: Esempio di utilizzo dei branch (7)

132
10.2.5.3 Esempio di merge tra due branch distinti

Nell’esempio precedente, il nodo master non aveva fatto alcun commit dalla diramazione del branch bug123,
quindi l’operazione di merge è stata semplicemente quella di spostare il puntatore al nodo più recente di
bug123. Che cosa succede invece nel caso in cui il master si trova ad un punto più avanzato rispetto all’altro
branch, ovvero, se si e↵ettuano commit anche sul branch master ?

Dopo aver creato il branch bug456 e aver risolto il bug, si decide di ritornare sul master ed e↵ettuare
altri due commit, generando due nodi D ed E, come in Figura 10.14.

Figure 10.14: Esempio di utilizzo dei branch (8)

I due nodi HEAD appartenenti ai due branch, non fanno parte della stessa sequenza e si vuole e↵ettuare il
merge mantenendo tutte le modifiche fatte sia nei nodi D, E sia nei nodi F, G.

La sequenza di operazioni da fare è la stessa del caso precedente, ma il grafo risultante sarà diverso.
Ci si sposta sul branch master e si lancia il comando git merge bug456.
Questo comando genererà un nuovo nodo H che unisce entrambe le modifiche dei branch, ovvero entrambe
le linee di sviluppo, come in Figura 10.15.

133
Figure 10.15: Esempio di utilizzo dei branch (9)

A questo punto, si cancella il branch bug456 tramite il comando git branch -d bug456, in Figura 10.16.

Figure 10.16: Esempio di utilizzo dei branch (10)

10.2.6 Branching review

In un progetto complesso, le operazioni di merge combinano tutte le modifiche e↵ettuate sui branch che
vengono uniti (ci possono essere dei conflitti nel caso in cui le stesse righe di codice vengono modificate da
entrambi i branch che possono essere risolti analizzandoli uno ad uno).

La creazione dei branch è molto utile nel caso in cui si vuole sviluppare una nuova idea per il prodott
software. Quando si è soddisfatti dell’idea si può semplicemente unire il branch con quello principale oppure
scartarlo nel caso in cui l’idea non sia buona.

Un altro caso in cui la creazione di un branch può essere molto utile è quello in cui si vuole sviluppare
e lanciare una nuova versione. In questo caso, si crea un nuovo branch etichettandolo con il nome della
versione. Su questo branch (rappresentante la versione del software) possono essere a loro volta creati altri

134
branch che rappresentano la risoluzione di bug, l’implememtazone di nuove idee, etc.
Quando non si vuole più fornire supporto o si vuole rendere quella specifica versione non più utilizzata, si
elimina semplicemente il branch (il puntatore) ma non i nodi.

10.3 Condivisione dei commit


Ogni sviluppatore all’interno di un team di sviluppo, ha all’interno dell propria macchina in locale una copia
del repository. Il problema è quello di poter sincronizzare i repository di tutti gli sviluppatori all’interno
del team in modo che essi abbiano la versione più recente e una visione coerente con lo stato del progetto,
compresi i nodi creati in locale da ogni sviluppatore.

Una soluzione sarebbe quella di mandare i files sorgente creati e modificati in locale ad ogni altro svilup-
patore. Questo però è poco efficiente dal momento che si manderebbero troppi messaggi e risulterebbe
confusionsario.

La soluzione più efficiente è quella di utilizzare un remote. Un remote è una copia del repository im-
magazzinata in un server. Alcuni vantaggi derivanti dall’adozione di questo approccio sono:

1. Non è necessario inviare le modifiche ad ogni sviluppatore ma basta aggiornare la copia del repository
sul server per permettere agli altri membri di sincronizzarsi alla nuova versione.

2. Non è un approccio centralizzato: ogni utente ha comunque la propria copia del progetto sulla
propria macchina.

Avendo un progetto sulla propria macchina già avviato è possibile collegarlo ad un remote usando il comando:
> g i t remote add o r i g i n ’ linkRemote ’
dove linkRemote è l’URL del remote con il quale ci si vuole sincronizzare.
Il comando:
> g i t remote −v
mostra tutti i remote con il quale il nostro progetto è collegato:

Figure 10.17: Esempio di remote connessi

Ad ogni remote è associato un nome che, per convenzione, è ‘origin’ se questo è l’unico associato al
progetto. Come si nota dalla figura 10.17, il remote associato appare due volte: questo perché è possibile
indicare un remote da cui sincronizzare le modifiche ed uno da cui si caricano le modifiche e↵ettuate.
Cosa comune è clonare un progetto esistente in un remote sulla propria macchina. Il comando che permette
di farlo è:
> g i t c l o n e ’ linkRemote ’

135
Questo comando imposta la connessione tra il repository locale ed il remote.
Una piattaforma che mette a disposizione dei remote gestiti con GIT gratuitamente è GitHub.

10.3.1 Esempio di gestione dei branch

Immaginiamo di lavorare, in un branch diverso da quello master, alla risoluzione di un bug della versione
corrente del progetto nella repository della nostra macchina.

Figure 10.18: Esempio di utilizzo dei branch con un remote (1)

Aggiungiamo un remote a questo progetto usando il comando


> g i t remote add o r i g i n ’ linkRemote ’

Figure 10.19: Esempio di utilizzo dei branch con un remote (2)

Mentre lavoriamo, qualche altro membro del team aggiunge dei nodi al branch master del repository remoto.
Questi nodi però non diventano immediatamente visibile sulla nostra macchina.

136
Figure 10.20: Esempio di utilizzo dei branch con un remote (3)

Per vederli sulla nostra macchina, ci spostiamo sul branch master e scarichiamo le modifiche dal remote
(origin) usando i seguenti due comandi:
> g i t c h e c k o u t master

Figure 10.21: Esempio di utilizzo dei branch con un remote (4)

> git pull origin

137
Figure 10.22: Esempio di utilizzo dei branch con un remote (5)

L’operazione di pull preleva i nodi che sono stati aggiunti al repository (Fetch) ed eseguendo la merge con
il codice presente nel repository locale, allineando la copia sulla propria macchina con quella sul remote. Il
comando duale è il seguente:
> g i t push o r i g i n
il quale invia i nodi che sono stati aggiunti ad un branch locale, al branch corrispondente sul remote.
Dato che è possibile che il remote abbia subito qualche variazione, è buona pratica eseguire prima la pull,
per aggiornare la copia locale e poi la push, per aggiornare il repository. Nel caso in cui questo non venga
fatto, GIT provvederà ad eseguire una merge delle versioni in remoto e locale. GIT non è in grado di unire
le modifiche in due situazioni:

1. Le modifiche riguardano dei file binari. Non mettere i file .exe, .java, ecc. nel repository!

2. File di testo che sono diversi in parti che si sovrappongono.

Quando si verifica un conflitto di questo tipo, l’operazione di merge fallisce ed il conflitto viene riportato
da GIT. A questo punto è il developer che deve manualmente risolvere i conflitto, eseguire il commit e
riprovare ad eseguire la push. La risoluzione manuale dei problemi è diversa nei gue casi:

1. Nel caso di file di testo, GIT inserisce delle annotazione nei punti in cui si è verificato il conflitto. A
questo punto tocca al developer scegliere la riga di codice da lasciare.

2. In caso di file binari, bisogna scegliere uno dei due file, oppure indicarne un terzo.

Per ridurre i conflitti:

1. Evitare di aggiungere file che non siano file sorgenti, a meno che questi non cambino durante il progetto.

2. Eseguire frequentemente operazioni di pull/merge. Almeno una volta al giorno.

138
10.3.2 Buone pratiche da seguire quando si lavora in team con un remote

1. Mettersi d’accordo sulle politiche di branch.

2. Assicurarsi che tutti i file necessari alla compilazione siano presenti nel commit.

3. MAI E POI MAI ESEGUIRE IL COMMIT DI CODICE CHE NON COMPILA

4. Non eseguire il commit di codice che non passa i test

5. Eseguire spesso pull/push

139
11 Design Patterns
11.1 Che cos’è un pattern
Dal vocabolario:

pattern
noun

1. a repeated decorative design : a neat blue herringbone pattern.

• an arrangement or sequence regularly found in comparable objects or events : the house had been
built on the usual pattern.
• a regular and intelligible form or sequence discernible in certain actions or situations : a compli-
cating factor is the change in working patterns

2. a model or design used as a guide in needlework and other crafts.

• a set of instructions to be followed in making a sewn or knitted item.


• a wooden or metal model from which a mold is made for a casting.
• an example for others to follow.
• a sample of cloth or wallpaper.

Un Design Pattern è una soluzione di progettazione per un problema ricorrente in un deter-


minato contesto.

È un modello usato da qualcuno per riprodurre qualcosa in particolare, quindi un elemento associato al
concetto della ripetizione.

Un Design Pattern rappresenta una soluzione tipica ai problemi più comuni nella progettazione.
Sono come dei progetti pre-costruiti che si possono personalizzare per risolvere un problema di progettazione
ricorrente.
Il Pattern però non è qualcosa di specifico, ma un concetto generale per risolvere un problema particolare. È
possibile perô seguire i dettagli del modello e implementare una soluzione che si adatta alla realtà del proprio
problema.

Il termine fu usato per la prima volta da un architetto di urbanistica, che studiando la struttura di di-
verse città ha notato alcune idee di progettazione che accomunavano alcune di queste. Ha riscontrato che i
vari architetti avevano utilizzato soluzioni simili a problemi simili, applicando una sorta di ripetizione
di uno schema di soluzione ! riuso di idee efficienti nella progettazione.

Un Design Pattern quindi:

• Favorisce il riuso di scelte di progettazione che costituiscono soluzioni a problemi comuni;

140
• Costituiscono un linguaggio comune per comunicare le scelte di progettazione, aumentando la ca-
pacità di comunicare le decisioni e↵ettaute (potendo riassumere le scelte fatte attraverso un nome di
un preciso pattern applicato).
Come? Attraverso il concetto di astrazione. Da concetti semplici, si costruiscono concetti man mano
più complessi, a cui poi si danno dei precisi nomi per riferirvi.
Ad esempio, nel gioco degli scacchi, i concetti semplici/chiave sono quelli che si acquisiscono appena si
comincia a giocare, come ad esempio il concetto di scacchiera come insieme di caselle, la struttura e le
mosse dei singoli pezzi, etc. Andando avanti con l’esperienza, sulla base di questi concetti elementari
si costruiscono concetti piú complicati, si pensa in termini piú complessi: una certa sequenza di mosse
(spostare i pedoni in d4 e c4) é chiamata gambetto di donna, quindi una serie di concetti elementari
corrispondono ad un pattern. Il pattern fornisce quindi un linguaggio piú immediato per racchiud-
ere una serie di azioni o concetti, per descrivere in maniera piú breve ed efficace le soluzioni
applicate.

11.1.1 Di↵erenze tra pattern e algoritmo

Sia i pattern sia gli algoritmi descrivono soluzioni tipiche ad alcuni problemi noti. La di↵erenza risiede nel
fatto che mentre un algoritmo definisce sempre un chiaro insieme di azioni che possono raggiungere qualche
obiettivo, un pattern è una descrizione di più alto livello di una soluzione.

Un’analogia con un algoritmo è una ricetta di cottura: entrambi hanno passi chiari per raggiungere un
obiettivo. D’altra parte, un pattern è più simile a un blueprint (progetto): è possibile vedere quali sono
il risultato finale e le sue caratteristiche, ma l’ordine esatto di attuazione delle azioni per raggiungere quel
risultato spetta a chi implementa la soluzione.

11.1.2 Aspetti essenziali di un pattern

In generale, un pattern ha quattro aspetti fondamentali:

• Un nome, che deriva dall’uso della lingua e deve essere accettato unicamente come strumento di
comunicazione. Descrive il problema, la sua soluzione e le conseguenze in una parola o due. Avere un
nome per i pattern consente di parlarne e comunicare le scelte di progettazione in maniera molto piú
semplice e immediata.

• Un problema, che costituisce il contesto nel quale si può applicare il pattern. Infatti, per usare un
determinato pattern il primo passo è quello di riconoscere il problema. Se si riconosce il problema ma
non si sa la soluzione, il pattern può essere di aiuto.

• Una soluzione, formata da un insieme di decisioni di progettazione. Descrive gli elementi (classi o
oggetti nel caso dell’OOP) che compongono la progettazione, le loro relazioni, responsabilità, etc. La
soluzione non decrive una particolare implementazione o progettazione concreta, perchè il pattern é un
template che puó essere usato in diverse situazioni. Invece, fornisce una descrizione astratta di un
problema di progettazione e di come una disposizione generale di elementi (classi e oggetti nel caso di
OOP) lo risolve.

141
• Delle conseguenze, che sono il risultato dell’applicazione del pattern, ovvero tutti i trade-o↵s e le im-
plicazioni dell’applicazione del pattern. Le conseguenze sono fondamentali per valutare le alternative
di progettazione e per comprendere i costi e i benefici dell’applicazione del pattern. Poiché il riutilizzo
è spesso un fattore di rilievo nella progettazione orientata agli oggetti, le conseguenze di un pattern
includono il suo impatto sulla flessibilità, estensibilità o portabilità di un sistema.

11.2 Classificazione dei Design Patterns


I Design Patterns variano in base alla loro granularità e al loro livello di astrazione e si ha quindi bisogno di
un modo per organizzarli.

I pattern possono essere classificati secondo due criteri: la finalità e la visibilità (vedi Figura 11.1).

Il primo criterio, la finalità, riflette che cosa fa un pattern. I pattern possono avere una finalità creazionale
(o creational), strutturale (o structural) oppure comportamentale (o behavioral):

• I pattern creazionali hanno a che fare con il processo di creazione degli oggetti.

• I pattern strutturali trattano della composizione di classi o oggetti.

• I pattern comportamentali caratterizzano il modo in cui classi e oggetti interagiscono tra di loro.

Il secondo criterio, la visibilità, specifica se il pattern si applica principalmente alle classi o agli oggetti. I
pattern con visibilità di classe riguardano le relazioni tra le classi e le loro sottoclassi. Queste relazioni sono
stabilite attraverso l’ereditarietà, quindi sono fisse a tempo di compilazione. I pattern con visibilità di oggetto
hanno a che fare con le relazioni degli oggetti, che possono essere cambiate a run-time e sono più dinamiche.

Figure 11.1: Classificazione dei Design Patterns

142
12 Creational Patterns
I pattern creazionali astraggono il processo di istanziazione di un oggetto. Fanno in modo che i compo-
nenti che usano degli oggetti siano indipendenti da come questi oggetti sono creati, quindi favoriscono la
creazione di un sistema indipendente da come i suoi oggetti sono creati, composti e rappresentati. Un
pattern creazionale di classe usa l’ereditarietà per variare la classe che viene istanziata, mentre un modello
creazionale di oggetto delega l’istanziazione ad un altro oggetto.

In genere, la creazione di oggetti crea un accoppiamento tra l’oggetto che lo crea e l’oggetto che viene
creato. I pattern creazionali danno quindi flessibilità in cosa viene creato, chi lo crea, come viene creato,
e quando, disaccoppiando oggetti che vengono creati e l’oggetto/il componente che invoca la creazione.
Consentono inoltre di configurare un sistema con oggetti trattati come prodotti che variano ampiamente per
struttura e funzionalità. La configurazione può essere statica (cioè specificata al momento della compilazione)
o dinamica (al momento dell’esecuzione).

Le features chiave dei pattern creazionali sono:

• Information hiding delle classi che vengono instanziate.

• Implementazione nascosta di come gli oggetti sono creati o assemblati (esempio parametri del
costruttore, sarà compito di specifici oggetti o classi farlo).

• Minor accoppiamento, più flessibilità.

12.1 Factory Method


È tra i Creational Pattern più usati, il concetto alla base di questo Pattern è quello di avere un oggetto che
crea altri oggetti, da qui il nome “Factory”, ovvero fabbrica.

12.1.1 Problema

Il problema riguarda appunto la creazione di oggetti, detti anche prodotti. Questa operazione può variare
in base alle determinate condizioni di funzionamento. I prodotti al fine di essere utilizzati, dovranno avere
un’interfaccia nota, mentre per crearli serve conoscere, oltre l’interfaccia, la specifica classe, generando
quindi una dipendenza tra la classe che implementa l’interfaccia di un prodotto e la classe che lo crea.

In prima battuta l’obiettivo principale è quindi quello di ridurre le dipendenze tra l’oggetto che utilizza i
prodotti e le classi degli stessi, ma ci potrebbe essere una piccola variazione, come ad esempio avere come
obiettivo anche il preservarsi la possibilità di cambiare l’implementazione di una interfaccia, senza impattare
troppo sul codice che ho già scritto.

12.1.2 Soluzione

La soluzione consiste nel utilizzare un metodo per la creazione di prodotti.


Normalmente quando vogliamo creare un oggetto, in Java, si utilizza l’operatore new. Con il Factory Method
questo compito verrà delegato ad un metodo, quindi tutti i dettagli su come deve essere creato un prodotto

143
sono incapsulati nel factory method.
Ci sono vari modi di realizzare questa soluzione, come ad esempio:
• Classe Creator astratta/interfaccia, con Factory method che verrà implementato dalle specifiche
classi tramite Override.

• Classe Creator concreta, con Factory Method che avrà una implementazione di default che sarà poi
utilizzata dalle varie classi (solitamente metodo statico).

• Classe Creator astratta, ma che fornisce anche già un’implementazione di default del Factory Method(meno
usata).
A seconda delle specifiche situazioni, si può scegliere una particolare implementazione.
Solitamente il Factory Method è un metodo di istanza dell’oggetto che usa i prodotti, in questo modo,
nel caso in cui dovessimo cambiare una particolare implementazione di uno di questi, possiamo successiva-
mente estendere la classe Creator, e tramite Override andare a ridefinire il Factory Method per poter usufruire
della nuova versione del prodotto.

Un’altra soluzione, comoda quando i prodotti devono essere creati da più classi diverse, è quella di far
sı̀ che il Factory Method sia statico in una specifica classe, facilitando il riutilizzo da più classi e
riducendo l’accoppiamento. Questa soluzione potrebbe però variare in base a determinate circostanze, come
ad esempio uno specifico parametro passato al metodo che stabilisce determinate condizioni, o magari utiliz-
zando dei file di configurazione per specificare le particolari implementazioni delle istanze da restituire.

12.1.3 UML

Figure 12.1: UML del Factory Method

Questo in Figura 12.1 è un esempio di struttura della soluzione, non è l’unica dato che vi sono diverse
variazioni sul tema.

144
In Figura si riconoscono i seguenti ruoli:

• Product, potrebbe essere sia una classe astratta che un’interfaccia.

• ConcreteProduct, è l’implementazione dell’interfaccia del prodotto (Product).

• Creator, è l’oggetto che ha il bisogno di creare dei prodotti, e lo farà richiamando il Factory Method
specifico.

• ConcreteCreator, nelle esempio in Figura, sarà lui a dare una particolare implementazione al Factory
Method, implementando/estendendo la classe Creator.

Nel caso in foto, il Factory Method è un metodo astratto, il quale verrà poi implementato dal ConcreteFactory.

12.1.4 Conseguenze

Le conseguenze dell’adozione di questo pattern sono principalmente:

• Dare un punto di riferimento, anche detto hook, in cui una classe è libera di scegliere se utilizzare
una implementazione di un’interfaccia piuttosto che un’altra senza dover cambiare tutto il
codice, ma cambiando solamente l’implementazione del Factory Method (che ritornerà i tipi
opportuni). Questo è un grande vantaggio, poiché nel caso in cui volessi utilizzare un nuovo prodotto
che modifica il comportamento di uno già esistente, basta semplicemente cambiare il Factory Method,
anziché dover cambiare ogni punto in cui è stato creato un prodotto tramite l’operatore new.

• Poter mantenere sia un comportamento iniziale, che uno derivato nel codice. Quindi cambiare
un comportamento senza perdere quello vecchio, questa cosa utilizzando l’approccio con l’operatore new
non è possibile.

• Possiamo nascondere il collegamento tra due gerarchie parallele di classi, cosa che si può capire
dall’esempio del Factory Method statico utilizzato per JDBC, nel paragrafo successivo.

12.1.5 Esempi

Nel caso di JDBC, ricordiamo che era possibile creare un oggetto di tipo connection, utilizzando la classe
DriverManager, una volta assicurati che i driver siano collegati (Driver.forClass. . . ).

Connection conn = DriverManager.getConnection(. . . )

In questo caso, il metodo statico getConnection della classe DriverManager è un esempio di Factory Method,
e il vantaggio di quest’approccio, in questo particolare esempio, risiede nel fatto che la connessione verrà
creata con lo stesso metodo, indipendentemente dal tipo di Database al quale ci stiamo con-
nettendo. Quindi se anziché utilizzare PostGres utilizzassimo MySql, l’oggetto Connection sarà diverso, ma
il metodo con cui lo creiamo sarà sempre lo stesso. Il metodo infatti capirà dall’url del database in che modo
creare l’oggetto Connection.

Un altro esempio può essere sicuramente l’Iterator, o meglio, come questo viene creato. Infatti sappiamo che
ogni Collection ha un metodo Iterator. Nel caso in cui volessimo creare un iteratore indipendentemente dalla

145
classe, verrebbe difficile la creazione, poiché per primo potremmo non conoscere sempre a priori il tipo
di collezione da iterare, ed inoltre verrebbe scomodo ogni volta creare un iterator con new, e soprattutto
non conveniente nel caso in cui cambiassimo la tipologia di collezione (dobbiamo cambiare l’oggetto
iterator in ogni punto).

12.2 Abstract Factory


12.2.1 Problema

L’Abstract Factory si tratta di una generalizzazione del precedente pattern (Factory Method ). In questo caso,
il problema è quello di dover creare diversi prodotti che implementano un gruppo di diverse interfacce,
senza dipendere dal particolare tipo di implementazione degli oggetti.

In questo contesto, inoltre, esistono diverse famiglie di prodotti, in cui ogni prodotto può essere usato
soltanto con uno appartenente alla stessa famiglia.

Esempio:
Si suppone che i due prodotti siano una sedia e un tavolo. Le due famiglia di prodotti invece sono lo stile
antico (sedia antica e tavolo antico) e lo stile moderno (sedia moderna e tavolo moderno). Il problema è quello
di poter creare sedie e tavoli antichi o sedie e tavoli moderni a seconda delle necessità. Un componente che
richiede quindi una di queste due famiglie di prodotti, dovrebbe essere indipendente dalla loro creazione,
in modo che in futuro possa essere possibile cambiare facilmente famiglia di prodotti utilizzati o aggiungerne
addirittura altre.

Il componente dovrá poter interagire con un delegato che astrae la creazione di una delle famiglie di oggetti.
Il delegato prende informazioni dal componente che richiede la creazione, e decide quale famiglia di prodotti
creare, lasciando il componente ignaro di tutto il processo di creazione.

12.2.2 Soluzione

La soluzione a questo tipo di problema consiste nello spostare la responsabilità della creazione dei vari
oggetti ad un’interfaccia o classe astratta, le cui implementazioni concrete sono oggetti in grado di creare
diverse famiglie di prodotti. Ogni prodotto invece sarà costituito da una classe astratta/interfaccia, le cui
implementazioni concrete sono i diversi stili/famiglie di prodotti (es. classe astratta Tavolo, le cui implemen-
tazioni concrete sono TavoloModerno e TavoloAntico).

La classe astratta/interfaccia AbstractFactory(es.Falegnameria) avrà un metodo di creazione per ogni prodotto


astratto da creare (creaTavolo e creaSedia); un’implementazione di questa classe astratta invece fornisce la
creazione di prodotti concreti che fanno parte di una specifica famiglia (FalegnameriaModerna per sedie e
tavoli moderni e FalegnameriaAntica per sedie e tavoli antichi).

146
A tempo di esecuzione, il client (chi richiede la creazione dei prodotti) otterrà una delle varie istanze concrete
di questa AbstractFactory che poi userà per istanziare e creare prodotti (della stessa famiglia).
L’instanza di una concreta AbstractFactory può essere generata usando un Factory Method.

12.2.3 UML

Figure 12.2: UML dell’Abstract Factory

Guardando l’UML, i principali attori sono:

• AbstractFactory (Falegnameria)

– dichiara un’interfaccia per le operazioni che creano prodotti astratti.

• ConcreteFactory (FalegnameriaModerna, FalegnameriaAntica)

– implementa le operazioni di creazione di oggetti concreti che fanno parte della stessa famiglia.

• AbstractProduct (Tavolo, Sedia)

– dichiara unı́nterfaccia per un determinato prodotto.

• ConcreteProduct (TavoloModerno, TavoloAntico), (SediaModerna, SediaAntica)

– definisce un prodotto che verrà creato dalla corrispondente ConcreteFactory.


– implementa l’interfaccia di AbstractProduct.

• Client

– utilizza soltanto l’interfaccia dichiarata da AbstractFactory e dalle classi AbstractProduct una


volta aavuti gli oggetti dalle varie ConcreteFactory.

147
Questo tipo di pattern è molto utile in situazioni in cui:

• un sistema dovrebbe essere indipendente da come i prodotti sono creati, composti e rappresentati.

• un sistema è caratterizzato da una o più famiglie di prodotti.

• una famiglia di prodotti è progettata per essere usata insieme e si ha il bisogno di ra↵orzare questo
vincolo.

12.2.4 Conseguenze

Il pattern Abstract Factory ha le seguenti conseguenze positive:

1. Isola le classi concrete dei prodotti. Dal momento che la factory incapsula il processso di creazione
dei vari prodotti, isola il client dalla particolare implementazione e creazione dei prodotti concreti. Il
client si limita semplicemente ad utilizzare l’interfaccia astratta dei prodotti, qualunque sia la sua par-
ticolare implementazione concreta. La scelta del prodotto concreto da costruire è infatti nascosta nella
factory.

2. Facilita la sostituzione o l’aggiunta di famiglie di prodotti. Avendo il client a che fare con
una sola instanza di AbstractFactory e dal momento che la ConcreteFactory appare soltanto una volta
(quando viene istanziata e associata alla AbstractFactory tramite polimorfismo), diventa molto sem-
plice sostituire un’intera famiglia di prodotti semplicemente cambiando l’istanza concreta associata alla
AbstractFactory con cui il client interagisce.
Inoltre, è semplice anche aggiungerne nuove, dal momento che basterà creare una nuova factory
(concreta) e nuovi tipi di prodotti (concreti).

Ad esempio, se per il corso dell’applicazione si ha sempre avuto a che fare con una ConcreteFactory che
produceva sedie e tavoli moderni (il client richiamava i metodi creaSedia e creaTavolo di Falegnameri-
aModerna, senza preoccuparsi che ci fosse la FalegnameriaModerna o Antica come istanza concreta),
se per un qualsiasi motivo si decide di cambiare lo stile da moderno ad antico, basterebbe sostituire
l’istanza di Falegnameria con una FalegnameriaAntica (al posto di FalegnameriaModerna) tramite ad
esempio un Factory Method.

3. Assicura che tutti i prodotti che vengono costruiti appartengono alla stessa famiglia.
Quando i prodotti che appartengono ad una stessa famiglia sono progettati per lavorare insieme, è
importante che un’applicazione utilizzi oggetti di una famiglia alla volta. Il pattern rende questo
concetto facile da implementare, avendo a disposizione delle ConcreteFactory che producono prodotti
soltanto di una sola famiglia alla volta.

L’unica conseguenza negativa è rappresentata dal fatto che diventa difficile aggiungere nuovi tipi di
prodotto. Estendere le AbstractFactory per aggiugnere nuovi tipi di prodotto non è facile, perchè l’ Abstract-
Factory fissa il set di oggetti che si possono creare. Se si vuole aggiungere un altro prodotto, bisognerebbe
aggiungere un metodo di creazione nella AbstractFactory e implementare questo metodo in
tutte le altre ConcreteFactory, per supportare la creazione del nuovo prodotto.

148
12.2.5 Esempi

Un esempio concreto di Abstract Factory è la libreria Java AWT che crea per ogni elemento dell’interfaccia
grafica (finestra, menù, pulsanti, etc.) un’oggetto la cui implementazione dipende dal particolare sistema
operativo su cui gira l’applicazione.

Per ogni sistema operativo, si forma una famiglia di prodotti che corrispondono a tutti gli elementi grafici
sotto lo stesso sistema operativo (sotto lo stesso stile).

In questo modo, il client non dipende dalla particolare implementazione dei vari oggetti e non si preoc-
cuperà di conoscere il sistema operativo su cui risiede, tramite un file di configurazione verrà creata una
ConcreteFactory che crea oggetti soltanto di quel particolare sistema operativo.

12.3 Singleton
Singleton è un Creational Design Pattern che ci assicura che una classe venga istanziata una sola volta e
che questa sia accessibile globalmente.

Figure 12.3: Singleton Design Pattern

12.3.1 Problema

Il problema da a↵rontare riguarda l’accesso a delle informazioni che dovrebbero essere condivise tra più client.
Questo problema si presenta quando, per esempio, una classe ha delle informazioni di stato che devono essere
condivise tra diversi client e dunque, per evitare inconsistenze, si vogliono evitare duplicati di quella classe
(Cache).

12.3.2 Soluzione

La soluzione si compone di due parti:

149
1. Per assicurarsi che esista una sola istanza della classe, si deve impedire che il client istanzi un nuovo
oggetto. Per fare questo si rende il costruttore privato.

2. Si aggiunge un factory method statico che crea la singola istanza la prima volta che viene chiamato.
Alle chiamate successive, non viene creata una nuova istanza della classe ma viene restituita quella
precedentemente creata (L’istanza viene immagazzinata come membrostatico).

12.3.3 UML

Figure 12.4: Singleton Design Pattern UML

In UML, la classe Singleton è caratterizzata da:

1. Un metodo instance(), che funge da factory method e che restituisce la singola istanza uniqueIn-
stance;

2. Un attributo uniqueInstance che memorizza il riferimento all’unica istanza;

3. Un metodo singletonOperation(), che lavora sui dati dell’istanza (possono esere presenti più metodi
di questo tipo)

12.3.4 Conseguenze

Le conseguenze che derivano dall’uso di questo pattern sono:

• Accesso controllato alla singola istanza. Dato che la classe Singleton incapsula la sua sola istanza, essa
può controllare come il client può accedervi;

• Non vengono usate variabii globali ;

• Possono essere definite delle sottoclassi di Singleton senza intaccare il client

• Se necessario, è possibile cambiare le restrizioni sulla creazione delle istanze, variandone il numero o in
quale contestoistanziarne di nuove.

150
12.3.5 Esempio

Figure 12.5: Singleton Design Pattern: caso d’uso

In questo esempio abbiamo una classe Cache con una variabile statica privata di tipo Cache chiamata
instance, inizializzata a null. Il factory method statico, in questo caso chiamato getCache(), quando viene
chiamato controlla che non sia presente una istanza di Cache ( cioè che instance != null ) per instanziarla,
altrimenti, restituisce instance. Gli altri metodi lavorano sugli attributi di Cache.

151
13 Structural Patterns
13.1 Introduzione
Come abbiamo visto in precedenza i pattern creazionali o↵rivano modi alternativi per creare degli oggetti
senza ricorrere all’operatore new, per garantire una forma di indipendenza dalla classe che usa gli oggetti
rispetto al modo nel quale questi vengono creati. Nei pattern strutturali invece, le soluzioni che vengono
proposte, riguardano il modo in cui più oggetti sono collegati tra di loro, cosı̀ da realizzare strutture più
complesse. Quasi tutti i pattern di questo tipo hanno come scope Object, tranne uno, l’Adapter Class. Di
seguito viene illustrato il pattern Adapter, che ha due versioni: una con scope Object e una con scope Class.

13.2 Adapter Class


13.2.1 Problema

Il problema che a↵ronta questo pattern in entrambe le versioni che presenta, nasce nel momento in cui si
vuole utilizzare un oggetto di una classe già esistente, insieme ad altre classi o con librerie di altre classi che
sono state create in maniera indipendente da questa (per esempio la sto importando o riutilizzando da un
altro oggetto), in modo da favorire la riusabilità del codice. Il problema si presenta quando si importano delle
librerie sviluppate da terze parti, oppure quando abbiamo due librerie e vogliamo utilizzarle insieme anche se
non sono state progettate per farlo. L’oggetto che voglio utilizzare prende il nome di Adaptee, questo deve
essere passato ad altre classi che però si aspettano un’oggetto che implementi una certa interfaccia Target,
siccome i due pezzi di codice sono stati sviluppati in maniera indipendente l’uno dall’altro, l’oggetto della
classe Adaptee non implementerà l’interfaccia Target. In definitiva, vogliamo collegare la classe Adaptee che
è stata sviluppata o importata, con un altro pezzo di codice che non vuole un oggetto di tipo Adaptee ma
uno che implementi l’interfaccia Target.

13.2.2 Soluzione

La soluzione consiste nel creare una nuova classe che eriditi i contenuti di Adaptee e implementi l’interfaccia
Target. Di conseguenza, quest’ultima avrà tutte le funzionalità della classe Adaptee e potrà essere utilizzata
insieme ad un altro oggetto che implementa l’interfaccia Target. Questo nuovo oggetto prenderà il nome di
Adapter.

152
13.2.3 Struttura

Figure 13.1: Struttura Adapter Class

Nella figura 13.1 viene proposta un esempio di struttura dell’Adapter Class, nella quale è presente un oggetto
Client che necessita di un riferimento a un oggetto che implementa Target. Voglio che l’oggetto Client
utilizzi un’oggetto della classe Adaptee che è stato sviluppato in maniera indipedente da esso, tuttavia non
implementerà l’interfaccia Target. In questa situazione, se passassi l’oggetto Adaptee direttamente al Client
riscontrerei un errore di compilazione (non implementa l’interfaccia Target). Quest’operazione ha senso se
l’oggetto Adaptee è quello di cui il Client ha bisogno ma, sintaticamente non posso essere collegati tra di loro
perchè il Client richiede una specifica interfaccia Target e l’oggetto Adaptee non ha questa interfaccia. Per
risolvere questo problema creo quindi una nuova classe Adapter che estende Adaptee e implementa l’interfaccia
Target. Notiamo che i metodi dell’interfaccia Target saranno simili ai metodi della classe Adaptee ma non
uguali (di↵erenze sintattiche). Questa classe Adapter erediterà tutti i metodi e attributi della classe Adaptee
e implementerà i metodi dell’interfaccia Target che saranno meno complessi da implementare, perchè sto
assumendo che nella classe Adaptee ci saranno già metodi molto simili, ma solo con di↵erenze a livello
sintattico (nome,parametri ecc.). Una volta creata questa classe Adapter, la passerò in tutti i contesti dove
prima usavo Adaptee, perchè la estende, e in aggiunta la potrò passare anche al Client perchè implementa
l’interfaccia Target. In questo modo ho collegato due oggetti creati in maniera indipedente l’uno con l’altro.

13.2.4 Conseguenze

Le conseguenze positive di questo pattern sono:

1. Posso utilizzare l’oggetto della classe Adapter sia con il codice della classe Adaptee sia con il codice del
Client che usa l’interfaccia Target.

2. Nello scenario ideale con un solo Target e un solo Client creando questa nuova classe Adapter non
aumento il numero di oggetti presenti nel mio programma siccome potrò usare la classe Adapter al
posto della classe Adaptee. Questo è ottimo quando ho poca memoria.

Le conseguenze negative sono:

153
1. Nella soluzione stiamo supponendo che Target sia un’interfaccia ma nel caso in cui la classe Client
prendesse in riferimento una classe Target e non un interfaccia, questa soluzione non sarebbe utilizzabile
in un linguaggio come Java, poichè sarebbe necessario che la classe Adapter estenda due classi. Questo
ci incoraggia ad usare delle interfacce che ci aiutano a spezzare le classi di dipendenza.

2. Se le librerie che vogliamo collegare all’oggetto Adaptee diventano più di una (due client vogliono due
Target diversi) possiamo avere due soluzioni:

• Soluzione 1: creo un Adapter che estende Adaptee e implementa sia l’interfaccia UNO che
l’interfaccia DUE. Tuttavia dal punto di vista della riusabilità questo Adapter ha senso solo quando
devo riusare entrambi i Client, nel caso in cui, in un’altro progetto, ho necessità di usarne solo
uno sto creando una dipendenza con l’interfaccia Target con la possibilità di complicare le cose.
• Soluzione 2: creo due Adapter diversi, uno che implementa Target1 e uno che implementa
Target2. D’altro canto questa soluzione comporta delle complicazioni nel momento in cui vado a
creare un oggetto siccome devo decidere quale Adapter usare. Qualora debba riusare la classe in
un progetto in cui necessito di entrambi i Client, mi servirà necessariamente un Adapter che li
implementi entrambi con le rispettive interfacce Target. Notiamo che, l’esigenza di un Adapter
dipende da quale problema sto a↵rontando, in alcuni necessito di un Adapter che implementi una
sola delle interfacce Target per altri invece, occorre che le implementi entrambi. A ragione di ciò
notiamo che gli Adapter diventano 3 quello che implementa solo Target1, quello che implementa
solo Target2 e quello che li implementa tutti e due, cosı̀ facendo non stiamo rispettando l’obiettivo
di evitare le ripetizioni nel programma portando a una soluzione sfavorevole.

3. Se io non ho un’unica classe Adaptee ma una gerarchia di N classi mi serviranno N Adapter per
adattare la gerarchia a una singola interfaccia Target in quanto, non mi basta craere l’Adapter solo per
la classe base della gerarchia per poi uitilizzarlo per le classi derivate.

13.3 Adapter Object


L’Adapter Object non esprime una relazione tra classi ma una relazione tra oggetti

13.3.1 Problema

Usiamo lo stesso nome dell’Adapter Class prorpio perchè risolve lo stesso problema.

13.3.2 Soluzione

La soluzione nella variante con scope Object consiste nel creare la classe Adapter che, in questo caso, non
estenderà la classe Adaptee ma manterrà un riferimento ad essa che verrà usata per richiamarne i metodi.

154
13.3.3 Struttura

Figure 13.2: Esempio struttura Adapter Object

Come si nota dalla figura 13.2 è presente solo una piccola di↵erenza rispetto alla figura 13.1 ovvero, in questo
caso la classe Adaptee non sarà in relazione di ereditarietà con Adapter ma manterrà un riferimento ad essa.
A conseguenza di ciò l’oggetto Adapter non potrà più funzionare in maniera autonoma, ma per farlo dovrà
essere per forza collegato ad Adaptee in modo da poter essere passato al Client. Il Client accetterà l’oggetto
Adapter in quanto implementa Target e richiamerà i suoi metodi(Adapter) successivamente, quest’ultimo
redirigerà le richieste all’oggetto Adaptee facendo tutti gli adattamenti del caso. Fino ad adesso abbiamo
supposto che l’oggetto Adaptee sia logicamente simile all’interfaccia Target ma sintatticamente diversa,
di conseguenza l’oggetto Adapter si preoccuperà di gestire solo la conversione tra le richieste che possono
essere fatte dall’interfaccia Target e le richieste che la classe Adaptee è in grado di gestire.

13.3.4 Conseguenze

Vediamo adesso se con questa implementazione risolviamo alcune conseguenze negative dell’Adapter Class.

• Nell’Adapter Class abbiamo visto che nel caso in cui avessimo due Client con due interfacce Target
diverse questo comportava una moltiplicazione del numero delle classi Adapter dovendo scegliere le
varie combinazioni possibili. Nel caso dell’Adapter Object questo problema non si presenta in quanto
ho la possibiltà di creare due classi Adapter, ognuna che implementa una delle interfacce, che però, si
collegheranno allo stesso oggetto Adaptee. Pertanto, avrò un unico oggetto Adaptee che passerò a tutti
e due i Client usando come intermediari due Adapter diversi. In generale posso avere N Adapter che si
collegano allo stesso oggetto Adaptee.

• Se Target non è un’interfaccia pura ma è una classe astratta non avrò più limitazioni in un linguaggio
come Java, in quanto Adapter erediterà solo da Target.

• Se invece di avere un Adaptee avrò una gerarchia di Adaptee non creerò più un Adapter diverso per
ogni classe della gerarchia ma, basterebbe creare un solo Adapter per coprire tutta la gerarchia di classi
grazie al polimorfismo.

155
• Mentre in precedenza, quando creavo l’oggetto Adaptee dovevo decidere se usarlo con una libreria
Client, adesso non ho più la necessità di sapere se verrà utilizzata con una libreria Client perchè, anche
se lo decido dopo, posso in qualunque momento creare l’oggetto Adapter e collegarlo al mio oggetto
Adaptee, in caso posso anche avere degli oggetti Adapter usa e getta solo per passare l’oggetto Adaptee
al Client. In questo modo la struttura del programma si semplifica e ne aumenta la riusabilità.

• L’unica conseguenza negativa è che l’Adapter e l’Adaptee sono due oggetti separati, che nella maggior
parte dei casi non risulta essere un problema, ma delle volte, se stiamo lavorando con linguaggi dove
bisogna gestire manualmente l’allocazione e la deollacazione della memoria, (come ad esempio C++)
avere due oggetti invece di uno potrebbe crearmi dei problemi.

13.3.5 Esempio

Figure 13.3: Esempio Adapter Object

Supponiamo di star realizzando un programma di disegno nel quale abbiamo:

• un editor che gestisce un insieme di oggetti e che implementa l’interfaccia shape;

• tante classi per realizzare diveresi tipi di shape.

In una fase avanzata del progetto si vuole aggiungere la possibilità di scrivere del testo all’interno del disegno.
Questo risulta complicato in quanto il testo è più difficile da disegnare rispetto a una linea geometrica avendo
parametri come font, dimensione, ecc. . Supponiamo di voler riutilizzare una classe già esistente nella nostra
libreria ovvero la classe TextView essa sarà il nostro Adaptee. Tuttavia, questa classe non è stata realizzata
insieme alla libreria che stiamo usando pertanto, non implementa l’interfaaccia Shape. La soluzione consiste
nel creare un Adapter che ,in questo caso, sarà un Object Adapter denominata TextShape, questa classe
implementa l’interfaccia Shape e mantiene un riferimento ad un oggetto della classe TextView, per ogni
metodo dell’interfaccia Shape l’oggetto TextShape richiamerà il metodo logicamente corrispondente della
classe TextView facendo sı̀ che la classe già esistente venga incorporata nel nostro progetto anche se non era
stata progettata per lavorare insieme alle altre classi. In questo modo stiamo aumentando la riusabilità.

156
13.3.6 Soluzione Variante

Nei paragrafi precedenti abbiamo ipotizzato che la classe Adapter fosse una classe separata dall’Adaptee,
in realtà in alcuni casi sopratutto quando essa non è una classe vecchia che stiamo adattando, ma una classe
nuova su cui possiamo lavorare, è possibile realizzare l’Adapter anche come oggetto di una classe interna
o eventualmente anche di una classe anonima rispetto alla classe Adaptee. Ricordiamo, che un’oggetto di
una classe interna può vedere tutti i metodi e gli attributi della classe esterna, sia pubblici che privati, in
quanto è implicito che le due classi abbiano un collegamento, questo vale anche per le classi anonime. La
soluzione alternativa dell’Adapter Object viene usatemolte volte senza che se ne renda conto, per esempio
quando associamo un’azione a un bottone nell’interfaccia utente.

13.4 Decorator
Decorator, chiamato anche Wrapper, è uno Structural Design Pattern, che permette di aggiungere delle re-
sponsabilità ad un oggetto, detto anche componente, dinamicamente. Questo pattern rappresenta un’alternativa
flessibile all’uso di sottoclassi per l’estensione delle funzionalità di un oggetto. In generale il pattern Decorator
aggiunge responsabilità ad un oggetto che non riguardano il “cosa” deve essere fatto, ma “come” va fatto.

Figure 13.4: Decorator Design Pattern

13.4.1 Problema

Il problema si presenta quando si ha una classe (Component) preesistente e si vogliono aggiungere delle ulte-
riori responsabilità ad alcuni dei suoi oggetti, modificarne il comportamento o rimuovere delle responsabilità,
senza cambiarne l’interfaccia. Il problema non si ferma qui, dato che si vuole e↵ettuare queste modifiche a
tempo di esecuzione.

157
13.4.2 Soluzione

Definiamo una classe Decorator che implementa l’interfaccia del Component e che mantiene il riferi-
mento ad un oggetto Component. All’interno del Decorator ci saranno tutti i metodi da implementare
dell’interfaccia Component e, prima di richiamare il metodo dell’attributo component di cui abbiamo il rifer-
imento, si aggiungono le funzionalità aggiuntive che si desiderano. In alcuni casi il Decoratore può anche
scegliere di saltare la parte in cui richiama il metodo del Component originario.

13.4.3 UML

Figure 13.5: Decorator Design Pattern UML

In generale la struttura di questo pattern in UML consiste in:


1. Un’interfaccia Component, che per esempio ha un metodo execute();
2. Un ConcreteComponent, che sarebbe il nostro componente originario, che implementa i metodi di
Component;
3. Un BaseDecorator, una classe astratta che implementa l’interfaccia Component ma che ha anche tra
i suoi attributi un riferimento ad un Component, il cui valore viene passato generalmente al costruttore;
4. ConcreteDecorator, dato che Base Decorator non fornisce un’implementazione diversa dei metodi di
Component, vengono create delle classi specifiche che estendono Base Decorator e che aggiungono le
nuove funzionalità desiderate, extra().

158
13.4.4 Conseguenze

1. Si possono aggiungere dei comportamenti ai componenti e queste aggiunte sono trasparenti al client

2. È possibile applicare diversi Decoratori in cascata

3. L’insieme di Decoratori di un componente può anche essere deciso a run time (ad esempio basandosi su
un file di configurazione) ma è anche possibile rimuoverne di decoratori durante l’esecuzione e questo
rende questo pattern più flessibile rispetto al solo uso dell’ereditarietà

13.4.5 Esempio

Figure 13.6: Decorator Design Pattern: esempio java.io

Un esempio notevole dell’uso del pattern Decorator lo vediamo nella libreria Java I/O, anche se non viene
applicato al in maniera pienamente conforme allo standard dato che vengono aggiunti altri metodi, oltre
quelli originali. In questo caso lo si usa per aggiungere o modificare delle funzionalità per fare in modo che
si possa leggere/scrivere da una particolare sorgente, ma usando la stessa interfaccia.

159
13.5 Proxy
Dall’inglese il termine Proxy indica il procuratore legale di qualcuno, come un avvocato che rappresenta un
suo assistito in sede legale.

Figure 13.7: Proxy Design Pattern

13.5.1 Problema

Il problema riguarda alcuni casi in cui ci sono degli aspetti che rendono complicato accedere ad un
oggetto, quindi l’ostacolo non è tanto il comportamento dell’oggetto, ma l’arrivare a parlarci.
L’obiettivo è quindi quello di nascondere questa complessità ai client che devono usare questi oggetti,
per questo viene introdotto il Proxy, che farà da intermediario per il nostro oggetto originario.

13.5.2 Soluzione

Supponiamo che l’oggetto originario abbia una specifica interfaccia Subject separata dall’implementazione
(RealSubject). La soluzione consiste nel creare una nuova classe Proxy, che implementerà la stessa
interfaccia Subject, e che si occuperà di gestire tutte le problematiche per accedere al Subject
originario. Quindi il Client parlerà con il Proxy, che ricordiamo avrà la stessa e identica interfacica del Sub-
ject originario, e per lui non sarà un problema dato che non gli interessa il fatto di star parlando con l’oggetto
richiesto o un suo delegato, mentre il Proxy, una volta chiamato dal Client, farà da tramite richiamando i
metodi corrispondenti del Subject originario, una volta risolte, e nascoste al client, tutte le problematiche
nell’accedervi.

13.5.3 Conseguenze

Tra le conseguenze principali, notiamo in particolare un ulteriore livello di indirezione che viene aggiunto
tra il Client e l’oggetto originario, essendoci il Proxy nel mezzo.
Questo ci permetterà di fare una serie di accorgimenti che renderà più semplice l’utilizzo dell’oggetto originario
da parte del Client, potendogli nascondere i dettagli relativi all’accesso che renderebbero più complicato la
vita del client.

160
13.5.4 UML

Figure 13.8: UML del Proxy

Guardando il diagramma delle classi del pattern, notiamo una certa somiglianza tra questo ed il pattern
Decorator, infatti secondo alcuni derivano da uno stesso meta-pattern noto come handle-body.
Data la loro somiglianza, e quindi la possibile confusione tra i due, risulta necessario far notare alcune
di↵erenze tra i due pattern, una su tutte è l’intenzione del pattern. Nel caso del Decorator il goal era
aggiungere qualcosa ai metodi originari, mentre qui non vogliamo aggiungere nulla, ma solamente semplificare
il modo in cui il client arriva a parlare con l’oggetto originario.

13.5.5 Esempi

Sono vari gli esempi di utilizzo del Pattern Proxy, andremo quindi a descriverne alcuni.

• Remote Proxy: in questo caso il motivo per cui è difficile parlare con l’oggetto originario è il fatto
che questo appartiene ad un altro processo, magari su un altro dispositivo ed addirittura in un altro
linguaggio di programmazione. Già in uno solo di questi 3 casi potrebbe essere davvero difficile per il
Client la comunicazione. È per questo che una soluzione che migliorerebbe la vita al Client è l’utilizzo di
un Proxy, che magati si occuperebbe di richiedere la connessione, gestire i protocolli di comunicazione
etc.(come ad esempio i Web Services).

• Virtual Proxy: supponendo che il nostro oggetto originario sia un oggetto molto costoso da creare, in
termini di tempo e risorse, per ottimizzare il funzionamento del programma, non lo creiamo all’inizio,
ma solamente quando serve. Quest’ ottimizzazione andrebbe però a complicare la vita del client, che
dovrà verificare se è già stato creato, se non è cosı̀ deve crearlo etc. Questa ottimizzazione renderebbe
quindi più complicato al Client l’accesso all’oggetto, ed è per questo che possiamo ricorrere all’utilizzo
di un Proxy, che si prenderà carico di tutto l’onere relativo alla gestione di tale ottimizzazione.

• Future Proxy: in alcuni casi, l’oggetto con cui il Client vuole dialogare, è un oggetto che viene
prodotto tramite un’operazione costosa, e non volendo bloccare il programma nell’attesa del prodotto
dell’operazione, facciamo partire questa in un Thread separato, sfruttando quindi un Worker Thread,
i quali sono appunto solitamente sfruttati per lavori lunghi. Quest’ottimizzazione, come la precedente,
introduce complessità, e dunque anche in questo caso è conveniente sfruttare un Proxy, che si prenderà
in carico la gestione del lavoro del Worker Thread. In questo caso il Proxy viene chiamato Future,
poiché sarà disponibile in futuro quando l’oggetto sarà disponibile all’utilizzo. Un esempio nella libreria

161
standard di Java, è la classe per gestire le immagini AWT, che non solo permette di creare un’immagine
caricandola da un file, ma anche caricare l’immagine da un URL, quindi scaricata tramite un protocollo
HTTP. Dato che l’elaborazione di un’immagine non è una cosa rapida e scontata, Java farà partire un
Thread separato per non appesantire l’esecuzione, in particolare Java sfrutterà un Proxy per la creazione
di un’immagine, ed il Client ne è completamente all’oscuro, quindi potrà continuare l’esecuzione, e
l’immagine verrà resa disponibile dal Proxy appena pronto, con quest’ultimo che appunto gestirà tutte
le difficoltà.

• Smart Proxy: in questo caso il problema nell’accesso all’oggetto, è relativo al fatto che questo uti-
lizza una logica particolare per i meccanismi di allocazione e deallocazione, in Java questa cosa non è
necessaria dato che vi è già un Garbage Collector che gestisce il tutto, mentre in C++ no. Ovviamente
questo meccanismo dovrà essere oscurato, quindi il Client non deve esserne consapevole. Un esempio di
funzionamento può essere il Reference Counting, che conta quante occorenze puntano a quell’oggeto, e
nel momento in cui il counter arriva a 0 si va a deallocare l’oggetto. Tutto ciò verrà gestito dal Proxy.
Un’altra tecnica può essere il Copy on Write, dato che le copie sono dispendiose, si potrebbe evitare
di farne una, ma usare esclusivamente il reference, nel momento in cui viene modificato l’oggetto, solo
in quel momento verrà fatta una copia dell’oggetto, in modo che non venga compromesso l’oggetto
iniziale. In C++ ci sono delle classi che permettono di fare Smart Proxy.

• Protection Proxy: a volte la difficoltà di acceso ad un oggetto, sono relative a dei controlli di sicurezza
che devono essere superati affinché si venga abilitati all’utilizzo. In questo caso il livello di complicazione
è dovuto al fatto che il Client deve superare dei controlli di sicurezza, possiamo fare in modo che il
responsabile di questi controlli sia il Proxy. Se infatti volessimo far utilizzare un Subject senza controlli
ad un Client di cui non ci fidiamo, possiamo interporre tra Client e Subject un Proxy, che farà tutti i
controlli di sicurezza richiesti.

Tra i vari esempi, molto disparati tra di loro e esposti in situazioni molto diverse tra di loro, il fattore comune
è il fatto che il Client avrebbe difficoltà a parlare con un oggetto, ed al fine di nascondere queste
difficoltà utilizziamo un Proxy.

13.6 Relazioni tra Structural Patterns


In tutti e tre i casi, notiamo che un particolare oggetto viene posto ”davanti” ad un altro, il quale farà da
tramite, e sembrano tutti apparentemente simili tra di loro.
L’Adapter si distingue subito dagli altri due, dato che l’obiettivo è quello di rendere riutilizzabile un
oggetto la cui interfaccia non è consona al riutilizzo, andando quindi ad adattarne l’interfaccia senza cam-
biare nulla a livello implementativo. Gli altri due invece non vogliono cambiare l’interfaccia dell’oggetto,
tanto è vero che implementano la stessa interfaccia originaria, ma hanno come obiettivo quello di cam-
biare qualcosa nell’implementazione preservandone l’interfaccia. Mentre il Decorator vuole aggiungere qual-
cosa all’implementazione dei metodi, il Proxy invece vuole cambiare il modo in cui il client può accedere
all’oggetto.

162
14 Behavioral Patterns
I pattern comportamentali (Behavioral Design Pattern) sono dei design pattern che identificano il
come due oggetti interagiscono tra loro per eseguire il loro compito. Un aspetto importante di questi pattern
è che fanno largo uso di ereditarietà e polimorfismo per descrivere algoritmi, parti di algoritmi o per descrivere
come il controllo dell’esecuzione passa tra i vari oggetti.

14.1 Command
Command è un pattern comportamentale (Behavioral Design Pattern) che incapsula una richies-
ta/azione in un oggetto. Questa trasformazione permette di: inviare le richieste come argomento di un
metodo, ritardarle, aggiungerle ad una coda di esecuzione di richieste ed annullarle.

Figure 14.1: Command Design Pattern

14.1.1 Problema

Normalmente gli oggetti di una classe rappresentano i dati (o parti di essi) e i metodi rappresentano le
operazioni/azioni di comportamenti su questi dati. A volte però capita che all’interno del programma ci
sia la necessita di implementare delle azioni che non lavorano su dei dati, ma lavorano su altre azioni del
programma. Questo problema si presenta, ad esempio, quando si vuole implementare la funzione che annulla
l’ultima azione , eseguita (Undo) oppure quando si vogliono salvare una sequenza di azioni ed eseguirle in un
secondo momento (Script o Macro).

14.1.2 Soluzione

L’idea è quella di trasformare ciascuna operazione che il nostro programma può compiere o richiesta dell’utente
in un oggetto a se stante. Questo oggetto conterrà al suo interno il codice per eseguire quell’operazione e
tutti gli altri dati necessari. Il vantaggio di questo pattern sta nel fatto che l’oggetto può essere istanziato e
manipolato, aggiungendolo per esempio in una struttura dati che permette di eseguire in cascata varie azioni
reificate come oggetto. Per trasformare le azioni in oggetto, va definita un’interfaccia Command che abbia

163
un metodo per eseguire l’operazione generica (execute). L’interfaccia può anche contenere altri metodi quali
uno per l’annullamento dell’operazione eseguita (undo) o magari uno che salva il comando in una struttura
dati o file (save). Per ogni specifica operazione che il nostro programma deve supportare va creata una
classe concreta che implementa l’interfaccia Command. I parametri necessari all’esecuzione dell’operazione
andranno inviati tramite il costruttore e saranno salvati come attributo dell’oggetto.

14.1.3 UML

Figure 14.2: Command Design Pattern UML

La struttura di questo pattern in UML consiste in:

• Interfaccia Command, che presenta il metodo execute();

• ConcreteCommand (Command1, Command2 ), che implementa uno specifico tipo di operazione e


che dunque ha un’implementazione del metodo execute().

• Client, l’oggetto che richiede l’esecuzione della nostra operazione; potrebbe essere una classe dell’interfaccia
utente ad esempio;

• Receiver, l’oggetto sul quale il comando deve lavorare; ad esempio, nella progettazione di un’applicativo
di videoscrittura, dato che le operazioni interagiranno con un testo e con il documento di cui fa parte,
il receiver sarà proprio il documento. Sarà il client a passare il receiver come parametro del Con-
creteCommand ma sarà quest’ultimo a richiamare i metodi del receiver per eseguire l’operazione;

• Invoker, l’oggetto responsabile di attivare l’esecuzione dei comandi, che non dipende dall’implementazione
concreta ma solo dall’interfaccia Command. Anche se il client prepara l’azione da eseguire (instanzia
il ConcreteCommand con tutti i dati necessari), non la esegue. Il client infatti la manda all’invoker

164
perché quest’ultimo può lavorare con queste azioni, aggiungendole, ad esempio, in una struttura dati
per delle esecuzioni future o per poterla annullare in caso di errore. In questo modo, se ci sono più
client, sarà l’unico invoker a mantenere la totalità dei comandi eseguiti.

14.1.4 Conseguenze

• Il pattern Command disaccoppia l’oggetto che definisce l’operazione (Client) da quello che sa come
eseguirla (invoker);

• i vari ConcreteCommand sono classi standard e dunque possono essere manipolate ed estese a piaci-
mento;

• Si possono comporre dei comandi più complessi a partire dai comandi base;

• Aggiungere nuovi comandi è semplice perché non vanno modificate le classi esistenti

14.1.5 Esempio: gestione di conti correnti bancari

La classe Account ci permette eseguire un versamento o un prelievo su un conto corrente. Vogliamo usare il
pattern Command per eseguire queste operazioni tra vari conti e annullarle in caso di errore. In questo caso
la classe Account rappresenta il receiver.

Figure 14.3: Classe Account: receiver

L’interfaccia Command ha un metodo execute() che esegue l’azione ed un metodo undo() per annullare
l’operazione.

165
Figure 14.4: interfaccia Command

L’invoker del nostro progetto sarà la classe CommandExecutor che avrà il compito di eseguire i comandi e si
salverà il comando eseguito in uno stack per poter anche ammettere l’operazone di annullamento, undoLast()

Figure 14.5: interfaccia Command

Un ConcreteCommand può essere la classe che rappresenta l’operazione di versamento. L’oggetto di


questa classe ha bisogno di sapere qual è il conto (Recevier) e quanti soldi devono essere versati che sono i
due attributi di istanza. Il client gli da queste due informaizoni passandogliele al costruttore. Implementa
i due metodi dell’interfaccia command execute e undo. Execute chiama il metodo deposit del receiver per
fare il versamento di amount euro. Undo chiama il metodo withdraw del receiver per annullare il versamento
depositando amount euro.

166
Figure 14.6: ConcreteCommand: DepositCommand

In modo analogo si può implementare l’altro ConcreteCommand che si occupa di prelevare una quantita
di soldi paria ad amount da un conto

Figure 14.7: ConcreteCommand: WithdrawCommand

Grazie al pattern Command è possibile unire più coomandi per crearne alcuni più complessi, come ad
esempio il comando TransferCommand che permette di trasferire soldi da un conto ad un altro, creato grazie

167
ai due comandi che permettono di ritirare e depositare denaro:

Figure 14.8: ConcreteCommand: TransferCommand

Di seguito si presenta invece un esempio di client che ha un collegamento con l’invoker (CommandExecu-
tor) , al quale passa i comandi necessari ad eseguire determinate operazioni quando richiesto.

Figure 14.9: Client: ExampleClient

14.2 Iterator
Iterator, conosciuto anche come Cursor, è un pattern della famiglia dei pattern comportamentali (Behavioral)
che permette di visitare gli elementi di una collezione senza esporne la rappresentazione sottostante, ovvero

168
la sua implementazione/struttura dati utilizzata, (albero, hash map, lista, etc.) e in modo totalmente in-
dipendente da essa.

Figure 14.10: Iterator Design Pattern

14.2.1 Problema

Le collezioni sono tra i tipi di dato più utilizzati in ambito di programmazione. Esse possono essere di vario
tipo: liste, alberi, hash map, etc. ma indipendentemente da come è strutturata, deve fornire un modo per
accedere ai suoi elementi, anche in modo diverso in base alle necessità.

Il problema è che a seconda della struttura dati utilizzata per memorizzare gli elementi, il modo di ac-
cedere a questi ultimi cambia. Il client che utilizza le strutture dati potrebbe non essere a conoscenza della
particolare implementazione e dal momento che le collezioni o↵rono diversi modi di accedere agli elementi
memorizzati in esse, non si hanno altre opzioni che accoppiare il client alla classe della specifica collezione.

Inoltre, come anticipato in precedenza, potrebbe essere utile avare a disposizione diversi modi per visitare
una collezione e in modo che il client possa scegliere senza dover essere a conoscenza della particolare imple-
mentazione.

Figure 14.11: Visita di un albero in diversi modi

Un primo esempio potrebbe essere la visita degli elementi di un albero. Il client potrebbe utilizzare in un
primo momento una visita depth-first dell’albero. Il giorno successivo invece si potrebbe richiedere una visita

169
di tipo breadth-first e la settimana successiva, si potrebbe aver bisogno di qualcos’altro, come l’accesso ca-
suale agli elementi dell’albero. L’aggiunta di sempre più algoritmi per attraversare l’albero alla collezione
sfuma gradualmente la sua responsabilità primaria, che è l’archiviazione efficiente dei dati.

Un altro esempio potrebbe essere la visita degli elementi di un array. Il modo più efficiente per accedere
agli elementi è tramite indice. Ma se la collezione che utilizza il client cambia e viene sostituita con una lista
concatenata, l’accesso tramite indice non ha più senso.

Le esigenze in sostanza sono due:

• Mantenere l’incapsulamento, non permettendo al client di essere a conoscenza dell’implementazione


della collezione.

• Accedere a tutti gli elementi del contenitore in maniera efficiente e diversa in base alle necessità.

14.2.2 Soluzione

L’idea principale del pattern Iterator è quella di spostare la responsabilità di gestire l’attraversamento della
collezione in un oggetto separato chiamato iteratore.

L’iteratore è un concetto astratto che rappresenta la posizione o un cursore all’interno della collezione.
Un oggetto iteratore, oltre che implementare l’algoritmo di visita della particolare collezione mettendo a dis-
posizione un’interfaccia per accedere e scorrere gli elementi della collezione (getNext() accedere al prossimo
elemento, hasMore() per verificare che la collezione non sia terminata, etc.), incapsula delle informazioni ag-
giuntive relative all’attraversamento degli elementi come la posizione corrente e quanti elementi rimangono
per arrivare alla fine del contenitore. Grazie a ciò, è possibile utilizzare (da parte di un client) diversi iteratori
contemporaneamente sulla stessa collezione. La separazione della meccanica di attraversamento
dall’oggetto che rappresenta il contenitore di elementi, ci permette di definire diverse politiche di
attraversamento senza definire nell’interfaccia della collezione stessa.

170
Figure 14.12: Diverse implementazioni di un iteratore per la visita di un albero

Dal momento che il client non conosce l’implementazione della particolare collezione, l’unica classe che
potrebbe fornire un’implementazione dell’iteratore e restituirla al client che ne ha bisogno è proprio la
collezione stessa. Il client infatti non può crearselo perchè si accoppierebbe con la collezione e perchè
non conosce la struttura dati sottostante, ma potrebbe invece chiedere alla collezione di fornirgli un it-
eratore che attraversa gli elementi nel modo che egli desidera. Ciò viene fatto incovando un metodo della
collezione (un factory method) che si occuperà di creare l’iteratore adatto e restituirlo.

Sarebbe inoltre conveniente se il client potesse cambiare iteratore per la stessa struttura dati senza bisogno
di modificare il suo codice, ovvero, in modo che il client possa interagire con gli iteratori allo stesso modo
a prescindere dai quelli che la collezione potrebbe restituirgli (iterazione polimorfica) o nel caso cambi
totalmente struttura dati sottostante. Per questo motivo, si definisce un Iterator astratto che definisce
un’interfaccia comune per ogni iteratore. Ogni istanza concreta di una qualsiasi collezione, avrà associata ad
essa un istanza concreta di questo Iterator.

171
14.2.3 UML

Figure 14.13: UML dell’Iterator Pattern

La struttura dell’UML è la seguente:

• Iterator è un’interfaccia che dichiara le operazioni necessarie per attraversare una collezione.

• ConcreteIterator implementa un algoritmo specifico per attraversare una collezione concreta (Con-
creteCollection) a cui fa riferimento (la collezione che acquisice tramite costruttore). L’oggetto iteratore
dovrebbe tenere traccia di alcune informazioni relative all’attraversamento, permettendo a diversi iter-
atori di attraversare la stessa collezione indipendentemente l’uno dall’altro.

• IterableCollection rappresenta l’interfaccia di una collezione iterabile, che oltre a mettere a dispo-
sizione i metodi per la manipolazione degli elementi all’interno di essa, fornisce un metodo createIterator
che restituisce un’oggetto della classe Iterator. Il tipo di ritorno deve essere Iterator in modo che le
classi concrete che estenderanno la collezione astratta potranno restituire al cleint vari tipi di iteratore.

• ConcreteCollection rappresenta un’istanza concreta di una collezione astratta. Restituisce le istanze


di un particolare iteratore concreto (ConcreteIterator ) ogni volta che il client ne richiede uno. Il metodo
createIterator() infatti può essere visto come un factory method che in base a ciò che viene passsato
come argomento viene generato un iteratore diverso sulla stessa collezione concreta.

• Il Client infine lavora sia con le collezioni che con gli iteratori attraverso le loro interfacce. In questo
modo il client non è accoppiato a classi concrete, consentendo di utilizzare varie collezioni e iteratori
senza modificarne il codice.

Il pattern Iterator è molto utile nelle situazioni in cui:

172
• bisogna accedere agli elementi di un oggetto aggregato senza conoscerne la sua rappresentazione
interna.

• si ha bisogno di molteplici modi per attraversare/visistare una collezione.

• si deve avere un’interafaccia uniforme per visitare diverse collezioni (supporto alla iterazione polimor-
fica).

14.2.4 Conseguenze

Le conseguende dell’applicazione di questo pattern sono le seguenti:

• Il client della collezione può accedere ai suoi elementi in modo efficiente mantenendo i dettagli
implementativi all’interno della classe concreta della collezione.

• Principio della singola responsabilità: applicando questo pattern è possibile ripulire il codice client e
le collezioni mettendo gli algoritmi di visita in classi separate. In questo modo si ripouliscono anche le
interfacce della collezione, non essendoci più i metodi per la visita.

• Facilità nell’implementazione di nuove collezioni e iteratori a codice già esistente, creando un


nuovo iteratore per una collezione e aggiungendo quest’ultimo al metodo di creazione dell’iteratore
specifico della collezione.

• Possibilità di iterare sulla stessa collezione in parallelo utilizzando diversi iteratori.

• Possibilità di controllare quando mettere in pausa l’iterazione, quando riprenderla, etc.

• Possibilità di variare il modo di iterare di una collezione a piacimento del client (iterazione polimor-
fica), dovendo interagire con un’interfaccia.

• Contro: a volte può essere un’overkill applicare il pattern, se la collezione è semplice.

14.3 Observer
L’Observer è un pattern comportamentale (Behavioral) che permette di definire un meccanismo di is-
crizione ad un oggetto per notificare a diversi oggetti di qualsiasi evento che accade all’oggetto che si sta
osservando, permettendo agli oggetti iscritti di rispondere in base ai cambiamenti.

173
Figure 14.14: Design Pattern Observer

14.3.1 Problema

Un e↵etto collaterale del partizionare un sistema in una collezione di classi cooperatrici è il bisogno di man-
tenere consistenza tra gli oggetti correlati, evitando di accoppiare altamente le classi in relazione tra
loro perché potrebbe ridurre la loro riusabilità.

Il problema, quindi, nasce dal fatto che queste classi (correlate) devono continuare a cooperare tra loro
mantenendo un grado di accoppiamento basso.

Figure 14.15: Visitare il negozio vs. mandare spam

Ad esempio, si immagina di avere due tipi di oggetto, un Cliente e un Negozio. Il Cliente è interessato a
una particolare marca di un prodotto (esempio, un nuovo modello di iPhone) che dovrebbe diventare a breve
disponibile presso il negozio.

174
Il Cliente potrebbe visitare il negozio ogni giorno per controllare la disponibilità del prodotto, ma mentre il
prodotto è ancora in consegna, la maggior parte di queste visite al negozio sarebbero inutili.
D’altra parte, il negozio potrebbe inviare migliaia di e-mail (che sarebbero considerate come spam) a tutti i
clienti ogni volta che un prodotto diventa disponibile. Questo risparmierebbe molti viaggi inutili ai clienti, ma
allo stesso tempo invierebbe e-mail riguardo prodotti a cui alcuni clienti non sono interessati, turbandoli
e ottenendo l’e↵etto opposto.
Si ha quindi un conflitto: il cliente spreca tempo controllando di persona la disponibilità e il negozio spreca
risorse notificando i prodotti ai clienti sbagliati.

Un altro esempio potrebbe essere il pattern architetturale MVC dove le classi nel Model implememntano
la logica di business e in modo separato la View implementa l’interfaccia utente.
Quando alcune informazioni dell’applicazione (nel Model) cambiano, la GUI dovrebbe essere aggiornata in
modo da rispecchiare gli avvenuti cambiamenti. L’obiettivo è quello di fare ciò evitando che le classi del
Model siano accopopiate con le classi dell’interfaccia utente.
Infatti, non è buona pratica creare accoppiamento perché si potrebbe volere che la stessa logica dell’applicaizone
(Model) possa essere utilizzata su più interfacce utente diverse, o per il testing (non si può testare il Model
se è accoppiato con la GUI).

14.3.2 Soluzione

L’oggetto che ha uno stato che è di interesse ad altri oggetti è chiamato subject (o publisher ), mentre tutti
gli altri oggetti che vogliono tenere traccia dei cambiamenti del subject sono chiamati observer (o subscriber ).

Il pattern Observer aggiunge un meccanismo di iscrizione alla classe subject in modo che i vari ob-
servers possono iscriversi o meno ad uno stream di eventi che proviene dal publisher. Per tenere traccia di
tutti gli observer, il subject ha a disposizione una lista/array di riferimenti ai vari observer iscritti al
subject in questione. Inoltre, per permettere ai subscribers di aggiungersi o rimuoversi dalla lista di iscritti,
il subject deve comprendere nella sua interfaccia i corrispondenti metodi.

Figure 14.16: Meccanismo di iscrizione

In questo modo, ogni volta che il subject genera un evento, cambia di stato, etc. il subject notifica tuti
gli iscritti dell’avvenuto cambiamendo, scorrendo tutta la lista di observer iscritti ad esso e richiamando il
metodo di notifica di tutti i subscriber.

175
Figure 14.17: Meccanismo di notifica dei subscribers

Gli oggetti che rappresentano i subscriber possono essere di vario tipo (diverse classi) e l’ideale sarebbe
quello di non accoppiare il subject a tutte queste diverse classi o meglio ancora, il subject potrebbe
ancora non essere a conoscenza delle classi di cui gli oggetti che si iscrivono ad esso sono istanze. Per questo
motivo, è importante che tutti i subscribers/observers implementino la stessa interfaccia, in modo
da apparire tutti uguali agli occhi del subject e in modo che il subject possa comunicare con loro attraverso
questa interfaccia. L’interfaccia dei subscribers, detta Observer, dovrebbe dichiarare un metodo di notifica e
un set di parametri che il subject può usare per passare dei dati contestuali ai subscribers insieme alla notifica.

Se invece si ha a che fare con numerosi publisher e si vuole rendere i subscribers compatibili con tutti
loro, si potrebbe far implementare a tutti i publisher un’interfaccia comune detta Subject. Questa
interfaccia servirebbe infatti a dichiarare i metodi di iscrizione/rimozione e a dichiarare un metodo di notifica
(che scorre la lista degli observer e li notifica uno ad uno) che tutte le classi concrete che la estenderanno
potranno richiamare ogni volta che il loro stato cambia. L’introduzione dell’interfaccia Subject risulta essere
molto vantaggiosa dal momento che si può racchiudere la logica di iscrizione degli observer e il mantenimento
dell’elenco di observer in questa classe astratta/interfaccia, riusando questa logica in tutti i subject concreti.
Inoltre, introducendo Subject si riduce l’accoppiamento tra observers e i vari pusblisher: dal momento che i
metodi di iscrizione e rimozione sono in una sola classe, gli observrer non devono conoscere per forza a priori
i subject concreti.

176
14.3.3 UML

Figure 14.18: UML del pattern Observer

La struttura dell’UML è la seguente:


• Il Subject è un’interfaccia/classe astratta che notifica gli eventi di interesse ad altri iscritti, di cui è
a conoscenza e mette a disposizione di altri oggetti un’interfaccia per aggiungere o rimuovere oggetti
Observer.

• Observer interfaccia/classe astratta che definisce un’interfaccia di notifica per gli oggetti che dovreb-
bero essere notificati dei cambiamenti del Subject. Ha a disposizione un metodo di update() che può
avere diversi parametri che consentono al Subject di passare degli dettagli sull’evento insieme alla no-
tifica.

Quando viene generato un evento, il Subject scorre la sua lista di iscrizioni e richiama il metodo di
notifica dichiarato dall’interfaccia di Observer su ogni Observer concreto che la estenderà.

• Il ConcreteSubject Memorizza lo stato di interesse ai ConcreteObserver e notifica i vari ConcreteOb-


server in caso di cambiamento di stato.

• Il ConcreteObserver mantiene un riferimento all’oggetto ConcreteSubject di suo interesse e mantiene


lo stato che dovrebbe essere consistente con quello del ConcreteSubject che si sta osservando implemen-
tando l’interfaccia Observer (acquisendo il metodo di notifica di avvenuto cambiamento).

Il ConcreteObserver, in risposta a notifiche da parte del ConcreteSubject, utilizza le informazioni con-


testuali passate come argomento dal particolare subject nel metodo di update (può avere parametri,
anche il riferimento al particolare subject in modo che l’observer può prendersi il suo stato tramite
getState()).

177
14.3.4 Conseguenze

Le conseguenze dell’utilizzo di questo pattern sono le seguenti:

1. Accoppiamento minimo tra Subject e Observer. Tutto ciò che il Subject conosce è che ha una
lista di iscritti, tutti che implementano la stessa interfaccia Observer, quindi il subject non conosce le
classi concrete di nessun observer e di conseguenza l’accoppiamento è minimo.

2. Facilità di creazione di nuovi Observer concreti. Grazie al meccanismo delle interfacce che
racchiudono la logica di notifica, è molto semplice aggiungere nuovi observer o subject al codice già
esistente, senza cambiare quasi nulla.

3. Supporto alla comunicazione broadcast. Ogni volta che il subject cambia stato, non ha bisogno di
conoscere i particolari observer, bensı́ manderà la notifica a tutti quelli iscritti nella sua lista, mandando
la notifica in broadcast a tutti gli observer interessati, senza importarsene di quali e quanti observer
sono iscritti. L’unica responsabilità del Subject concreto è quella di notificare.

4. Contro: se si definiscono molti strati di dipendenze (un oggetto può essere sia observer sia subject allo
stesso momento), un’operazione innocua potrebbe scaturire una serie di notifiche a cascata. Quindi, è
importante definire bene i criperi di dipendenza tra i vari oggetti dal momento che è difficile da tenere
traccia di una cascata di notifiche tra molti oggetti.

5. Contro: i vari iscritti (observer) sono notificati in ordine casuale. Si potrebbe rimediare specificando
nel metodo di update un criterio di scorrimento degli observer.

14.3.5 Esempi

Si suppone che il sistema da realizzare è un sistema di notifiche di allerta. I valori di allerta possibili sono tre:
verde (pericolo assente), giallo (potenzialmente pericoloso) e rosso (emergenza). Si ha quindi una classe
AlertCondition che rappresenta il livello di allerta corrente. Si vogliono associare diverse azioni al cambia-
mento di stato di questa condizione di allerta, senza rendere dipendenti il componente di AlertCondition da
queste azioni:

1. scrittura su un file di log dell’avvenuto cambiamento.

2. comparsa di un’icona colorata sulla GUI.

3. quando la notifica è rossa, deve essere attivato un allarme fino a che il livello di allerta non si abbassa.

Per fare ciò si applica il pattern Observer.

La classe AlertCondition, essendo l’oggetto da osservare, deve implementare l’interfaccia Observable (for-
nita da Java, corrisponde alla Subject), come in Figura 14.19.

178
Figure 14.19: Classe AlertCondition

AlertCondition avrà tre i livelli come attributi di classe, un metodo per cambiare la condizione e notificare
gli observer registrati del cambiamento e un metodo getter per lo stato corrente. Nel metodo setter, se
il cambiamento è avvenuto, viene richiamato un metodo setChanged() ereditato da Observable che conferma
il cambiamento e vengono poi notificati gli observer (soltanto se lo stato è cambiato) tramite il metodo no-
tifyObservers().

Per quanto riguarda i vari observers concreti, tutti quanti implementano l’interfaccia Observer, per avere
a disposizione il meccanismo di notifica.

Il primo è LogAlertObserver, che si occupa di scrivere su un file di log i cambiamenti di stato del livello
di allerta. Al metodo di update (ereditato da Observer) viene passato dal subject il riferimento a se stesso
(per acquisire informazioni sullo stato grazie a getCondition()) e viene poi scritto sul file di testo lo stato
corrispondente (Figura 14.20).

179
Figure 14.20: Classe LogFileObserver

Il secondo observer è quello che si occupa di visualizzare su una GUI il colore relativo al livello di allerta
corrente, ovvero GraphicAlertObserver.

Figure 14.21: Classe GraphicAlertObserver (1)

Questo observer concreto, tramite il metodo di update() cambia il colore di background della finestra in base
al livello di allerta dell’oggetto osservabile, come mostrato nelle Figure 14.21 e 14.22.

180
Figure 14.22: Classe GraphicAlertObserver (2)

L’ultimo observer concreto, SoundAlertObserver, si occupa della generazione di una traccia audio di emer-
genza nel caso in cui il livello di allarme diventi rosso.

Figure 14.23: Classe SoundAlertObserver

Tramite un factory method della classe Applet, crea un AudioClip contenente la sirena di allarme. Quando
il subject cambia lo stato in rosso, il metodo di update di questo observer manda in loop la traccia audio,
finchè non è richiamato il metodo di update con uno stato diverso dal rosso, come in Figura 14.23.

Infine, il main (client) interagisce con Subject e Observer e si occupa di iscrivere e rimuovere i vari observer

181
dal subject in questione.

Figure 14.24: Classe Main (1)

Si occupa semplicemente di instanziare gli oggetti necessari, iscrivere gli observer al subject e mandare in
esecuzione un thread che ciclicamente cambia lo stato di allerta.

Figure 14.25: Classe Main (2)

14.4 State
14.4.1 Problema

Il pattern che verrà illustrato prende il nome di State.


Esso, prevederà un’oggetto chiamato Context, che potrà assumere diversi stati durante la sua esistenza. La
particolarità di questo oggetto è data dai suoi metodi, che produrranno un e↵etto diverso a seconda dello
stato in cui si trovano.

182
Un modo intuitivo per realizzare questo pattern richiederebbe una semplice codifica degli stati tramite una
sequenza di numeri, dopodiche, all’interno dei metodi che dovrebbero cambiare stato durante l’esecuzione
posso usare uno switch o un if molto grande nella quale inserisco tutti i possibili stati che può assumere il
metodo e il comportamento che dovrebbe avere. Questa metodologia non è complicata da realizzare, tuttavia
non rappresenta una buona soluzione dal punto di vista della manutenibilità del codice in quanto, rendere
un metodo troppo lungo è sintomo di cattiva progettazione. L’obiettivo che ci ponioamo è quello di ottenere
l’e↵etto detto precedentemente ma senza l’ausilio dell’if a cascata.

Esempio:
Supponiamo di avere un oggetto che rappresenta un file, questo oggetto può assumere diversi stati durante
l’esecuzione del programma

• chiuso;

• aperto operazione lettura;

• aperto operazione scrittura;

• aperto per scrittura e lettura;

Ognuna di queste operazioni deve assumere un comportamento diverso a seconda dello stato del file.

Esempio:
Suuponiamo di avere un programma di disegno (Paint,Photoshop) dove un’utente può selezionare diversi tipi
di strumento (penna,matita,gomma, ecc.) il nostro programma può trovarsi in diversi stati a secondo del
tipo di strumento che è stato selezionato.

Alcune operazioni assumeranno una funzione diversa a seconda dello stato in cui si trovano. Per fare un
esempio l’operazione di trascinare il muose sul disegno dipenderà da che strumento sto usando in quel mo-
mento: se ho selezionato lo strumento matita farà una cosa se ho selezionato lo strumento gomma ne farà
un’altra)

14.4.2 Soluzione

Una soluzione alternativa alla codifica degli stati come dei numeri, può essere quello di trasformare lo stato in
un oggetto (questo è anche un Meta pattern chiamato Reificazione). Questa soluzione consiste nel definire
un’interfaccia o una classe astratta chiamata State in cui i metodi corrispondono alle operazioni da eseguire
in maniera diversa a seconda dello stato. State è un’interfaccia e per ogni possibile stato all’interno di esso
definiamo una classe che la implementerà e realizzerà i metodi dell’interfaccia nella maniera più appropriata
per il suo specifico stato. Dopodichè il Context rappresenterà lo stato corrente attraverso un riferimento a un
oggetto che implementerà l’interfaccia State. In questo modo quando il Context dovrà eseguire una particolare
operazione che dipenderà dallo stato, richiamerà il metodo corrispondente dell’oggetto State. Notiamo che, la
catena di if è stata sostituita da una semplice chiamata del metodo, grazie al polimorfismo. Più in generale,

183
nel caso in cui ci trovassimo davanti a un’enorme catena di if è buona pratica di programmazione domandarsi
se è possibile usaare il polimorfismo.

14.4.3 Struttura

Figure 14.26: Esempio struttura State

La struttura del pattern State mostrata in figura 14.26 suppone che ci sia una sola operazione che varia a
seconda dello stato (ovviamente ci possono essere diverse operazioni). L’oggetto Context manterrà lo stato
corrente con un riferimento a State che sarà collegata a tutte le classi ConcreteState che conterranno
l’implementazione delle operazioni specifiche per ogni stato. Nel momento in cui verrà richiamato il metodo
dell’oggetto Context che dovrà eseguire un’operazione in particolare, questo metodo, tramite il polimorfismo,
inoltrerà la chiamata allo stato corrente.

14.4.4 Esempio

Figure 14.27: Esempio State 1

Vediamo ora nel dettaglio l’esempio del programma di disegno. Definiamo una classe astratta Tool , in questo
caso stiamo utilizzando una classe astratta perchè avrà un implementazione, seppur vuota, dei metodi inziali.
In questo modo anche se in uno degli strumenti che andrò a realizzare dovessi cambiare il comportamento

184
di un metodo, con la classe astratta avrò solo un metodo da ridefinire, al contrario, con l’interfaccia dovrei
ridefinire tutti e tre i metodi.

Figure 14.28: Esempio State 2

Nella figura 14.28 viene mostrato un’esempio di una classe concreta che estende la classe astratta Tool.
In questo caso, il tool che ci consente di disegnare una linea retta.
Supponiamo che l’utente clicchi sul disegno, sposti il mouse lungo lo schermo e poi rilasci il bottone una volta
finito. Sucessivamente il programma disegnerà la linea, in particolare ridefinendo esclusivamente due dei tre
metodi della classe Tool:

• il metodo mouseDown che viene invocato nel momento in cui il pulsante del mouse viene cliccato sul
bottone, salvandosi le coordinate correnti in due variabili di instanza prevX e prevY ;

• il metodo mouseUp che viene chiamato nel momento in cui viene rilasciato il pulsante del mouse
disegnando una linea tra le coordinate salvate precedentemente e le coordinate correnti.

Figure 14.29: Esempio State 3

Nella figura 14.29 viene mostrato il tool per disegnare a mano libera. In questo caso non ci basta ridefinire

185
solo i metodi di pressione e rilascio del mouse ma anche il metodo che rileva il movimento di esso. Nel
momento in cui il pulsante del mouse verrà premuto, il nostro oggetto si salverà le coordinate iniziali del
punto di partenza della linea.
Quando sposteremo il mouse in un’altra posizione tenendo il pulsante premuto, disegneremo una linea tra la
posizone precedente e la posizione corrente.
L’e↵etto che otterrà l’utente tenendo premuto il mouse e trascinandolo sarà quello di disegnare tante piccole
linee.

6539
Figure 14.30: Esempio State 4

Come ultimo esempio vediamo una versione non completa del Context, la classe quindi estenderà Canvas
e avrà un attributo privato chiamato selectedTool che rappresenterà il tool corrente. I metodi dell’interazione
con il mouse verranno gestiti usando MouseListener che richiameranno il metodo appropriato dell’oggetto
selectedTool. Vi è inoltre, un metodo setTool che ha la possibilità di cambiare lo strumento selezionato
dall’oggetto. Moficando il tool cambierà il modo con cui la classe reagisce quando l’utente preme, sposta e
rilascia il mouse.

14.4.5 Conseguenze

Le conseguenze del pattern sono:

• La parte del comportamento che dipende da un particolare stato non è distribuita all’interno di lunghe
catene di if ma è concentrata in una delle classi concrete che rappresentano le implementazioni di State.

• È facile cambiare il comportamento di uno stato ed è facile aggiungere nuovi stati all’interno del nostro
sistema

14.5 Strategy
Questo è uno dei pattern più usati nella programmazione ad oggetti.

186
Figure 14.31: Strategy Design Pattern

14.5.1 Problema

Dato un oggetto Context che svolge determinate operazioni, potrebbe capitare che tra queste ci siano delle
parti che possono essere realizzate in maniere di↵erenti. Il nostro obiettivo è quello di rendere il Context
svincolato dalla scelta della particolare implementazione, rendendolo quindi più flessibile ed aperto,
evitando di fissare dentro di esso una specifica implementazione, e lasciando inoltre la libertà di scelta, anche
in un secondo momento, della specifica implementazione di cui usufruire.

Figure 14.32: Strategy Example

Se ad esempio volessimo realizzare un algoritmo di ordinamento, è risaputo che vi sono varie tecniche, che
di↵eriscono a seconda dei vari criteri di ordinamento scelti, degli elementi che stiamo ordinando (stringhe,
interi etc.). Quindi sapendo che la parte di sorting può essere fatta in modi diversi, vogliamo evitare di
sceglierla a priori e vincolare l’esecuzione ad un particolare algoritmo di ordinamento.

187
14.5.2 Soluzione

La soluzione o↵erta dal Pattern Strategy a questo problema, consiste nel creare un’interfaccia (Strategy) che
conterrà la specifica parte dell’operazione che avrà la possibilità di essere implementata in maniere di↵erenti
(nell’esempio del sorting, il criterio di ordinamento specifico).
Dopodiché l’oggetto che rappresenta il Context, riceverà (con costruttore o semplicemente con un setter) il
riferimento ad un oggetto di tipo Strategy, che sarà quindi l’implementazione della specifica parte di
operazione che era possibile fare in maniere di↵erenti. Ovviamente per ogni diversa implementazione ci sarà
un oggetto ConcreteStrategy che implementerà l’interfaccia Strategy e lo specifico modo di eseguire una oper-
azione. Questo verrà passato al Context che non dovrà preoccuparsi delle implementazioni e dell’esecuzione,
in questo modo il Context diventa indipendente dalle particolari implementazioni, e potremo quindi aggiun-
gere nuovi algoritmi o modificare quelli esistenti senza cambiare il codice del Context, ma semplicemente
passandogli una nuova ConcreteStrategy.

14.5.3 UML

Il Context non sa qual è l’implementazione scelta, ma ne riceve uno che poi utilizzerà, è infatti in una relazione
di tipo has-a con oggetti di tipo Strategy. Il vantaggio è proprio quello che nel momento in cui volessimo uti-
lizzare un’implementazione o una logica di↵erente per una particolare operazione, basterà solamente passare
una nuova ConcreteStrategy creata ad hoc.

Figure 14.33: Strategy UML

14.5.4 Conseguenze

Possiamo realizzare facilmente una famiglia di algoritmi simili tra di loro nella struttura, ma che
di↵eriscono per qualche passaggio dell’algoritmo.
Potrei infatti avere ad esempio, due algoritmi che fanno qualcosa di molto simile, ma che di↵eriscono per
pochi passaggi. Senza il pattern Strategy, dovremmo fare delle ripetizioni di codice per “replicare” parti
comuni. Con il pattern la parte comune verrà implementata una sola volta, e verrà utilizzata da entrambi
gli algoritmi, con le rispettive modifiche.

In particolare, un ulteriore vantaggio risiede nel fatto che l’implementazione può cambiare a run-
time, decidendo di utilizzare un oggetto Strategy piuttosto che un altro a seconda di particolari

188
condizioni.

14.5.5 Esempio

Un esempio, sempre in riferimento a Java, può essere il Comparator, dove implementandone l’interfaccia si
andrà a dare una logica al metodo compare(), per poi essere passato allo specifico algoritmo di ordinamento,
o ad esempio alle strutture ad albero come i TreeMap etc.

In particolare, per comprendere meglio l’utilità del pattern Strategy, potrebbe essere opportuno illustrare
un esempio, come ad esempio le operazioni di accumulazione su di un array, come la somma o la moltipli-
cazione degli elementi:

Figure 14.34: Strategy Example 1

Lo pseudo-codice in Figura 14.34 è un banale esempio di iterazione di un array al fine di combinare i suoi
valori ed e↵ettuare un operazione di accumulazione.

Figure 14.35: Strategy Example 2

L’interfaccia da implementare per le operazioni di accumulazione è la stessa, ed è quella in Figura 14.35.

189
Figure 14.36: Strategy Example 3

Dalla Figura 14.36 è possibile notare che il nostro Context (ovvero Accumulator), ha un oggetto di tipo
Strategy, che verrà utilizzato per richiamare le particolari strategie con cui deve operare (initialValue() e
combine()).

Figure 14.37: Strategy Example 4

Figure 14.38: Strategy Example 5

Quindi all’Accumulator in Figura 14.36 non ci resta che passargli la specifica ConcreteStrategy che si vuole
adottare, come ad esempio nel caso in cui volessimo sommare gli elementi di un array, basta passare quella
in Figura 14.37, oppure nel caso in cui volessimo moltiplicare gli elementi di un array, basta passare quella
in Figura 14.38.

190
Figure 14.39: Strategy Example 6

Un altro esempio, che serve a far capire quanto sia facile a questo punto riutilizzare l’Accumulator, è possibile
notarlo nel momento in Figura 14.39, dove andiamo appunto a definire una nuova Strategia che servirà a fare
la somma di tutti e soli gli elementi positivi dell’array, senza aver bisogno di cambiare nulla all’interno del
Accumaltor (Context), ma semplicemente passandogli questa nuova strategia.

Figure 14.40: Strategy Example 7

Basta appunto semplicemente, come si nota in Figura 14.40, passare all’Accumulator la specifica strategia
che si desidera utilizzare, o magari tramite un metodo di set per cambiamenti anche a runtime.

14.6 Di↵erenza tra State e Strategy


A primo impatto, osservando anche l’UML, sembrerebbe che State e Strategy siano la stessa cosa, ma la
di↵erenza sostanziale risiede nel concetto per cui si applica uno o l’altro, infatti la di↵erenza sta nel
problema al quale si applica, in particolare:

• State, si applica nel momento in cui vogliamo semplificare il modo in cui realizziamo il comportamento
complesso di un oggetto, che appunto cambia a seconda dello stato in cui si trova, e vogliamo evitare
di mettere gli IF a cascata.

• Strategy, si applica nel momento in cui vogliamo rendere flessibile e parametrizzabile un algoritmo,
che è diverso dallo scopo dello State, infatti non vogliamo rendere l’implementazione del Context sem-
plificata, ma fare in modo che sia riusabile.

Sempre derivante dal fatto che i due pattern di↵eriscono nell’obiettivo, un’altra di↵erenza, visibile più dal
punto di vista implementativo (no struttura generale, ma cosa succede con l’oggetto) riguarda il Context:

• Per lo State, il Context non rimarrà sempre legato allo stesso oggetto State durante
l’esecuzione, infatti normalmente, sarà la possibilità che lo stato cambi diverse volte durante l’esecuzione.

191
• Per lo Strategy, invece, solitamente ad un Context viene assegnato una singola tipologia di
oggetto Strategy, che probabilmente rimarrà lo stesso per tutta la vita del Context. Non
è obbligatorio, ma normalmente l’oggetto Context riceve lo Strategy dal costruttore, non ha metodi di
set per l’oggetto Strategy, come ad esempio il Comparator passato al TreeMap, cambiarlo non avrebbe
senso, poiché perderebbe di consistenza l’albero creato secondo il precedente ordinamento.

Un’ultima di↵erenza, riguarda la conoscenza che ha il Context dei possibili stati o strategie:

• Per lo State, il Context ha già un’idea di quali sono i possibili stati dell’oggetto, spesso in
fase di progettazione si andrà infatti a modellare il ciclo di vita dell’oggetto con un automa a stati finiti
(vedere tutti gli stati ed i passaggi di stato). Quindi il Context viene solitamente progettato insieme
agli oggetti State, che dovranno essere noti a priori.

• Per lo Strategy invece, non vale lo stesso, non si sa infatti nel momento in cui andrò ad
utilizzare il Context quale Strategy verrà utilizzata. Ciò non preclude il fatto che le Strategy
possano essere note.

192

Potrebbero piacerti anche