-
Notifications
You must be signed in to change notification settings - Fork 2
/
AnalyseConception.qmd
285 lines (232 loc) · 19.9 KB
/
AnalyseConception.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# Analyse et conception de logiciels
Comment développer un logiciel dans un domaine où l'équipe de développement n'a pas forcément une expérience importante?
Comment faire une bonne conception d'un tel logiciel?
Ce manuel vise à décrire une méthodologie de développement de logiciels qui répond à ces questions.
Nous suivrons une approche systématique permettant de modéliser un domaine d'application et de concevoir un logiciel avec des abstractions (des classes) plus intuitives.
Au fond, il s'agit de faire une _analyse_ d'un domaine d'application et de proposer une _conception_ du logiciel à développer dans le domaine visé.
Puisqu'une conception doit être réalisée (codée) pour être validée, nous appliquerons une approche de programmation pilotée par les tests (_test-driven development_ ou _TDD_ en anglais).
Le TDD est un élément essentiel dans le [devops](https://fr.wikipedia.org/wiki/Devops), une approche permettant de livrer et de faire évoluer rapidement les logiciels de manière robuste.
Parallèlement, surtout pour faire face à la complexité, la méthodologie propose des activités en itération ayant une durée relativement courte (1 à 3 semaines), dans lesquelles on réalise un sous-ensemble du système à développer et on le présente au client ou à la cliente pour avoir sa rétroaction.
C'est une approche dite "agile", ayant une base pédagogique neutre, nommée le "processus unifié".
Dans ce chapitre, nous présentons en détail ces dimensions du développement de logiciels.
## Analyse vs Conception
L'**analyse** met l'accent sur une investigation du problème et des besoins plutôt que sur la recherche d'une solution.
La **conception** sous-entend l'élaboration d'une solution conceptuelle répondant aux besoins plutôt que la mise en œuvre de cette solution.
<!-- Pour plus de détails, vous pouvez consulter le chapitre 1\ {{< fa solid book >}}. -->
```{#fig-mddDes .plantuml genurl="true" caption="Diagramme de *classes conceptuelles* décrivant (modélisant) le *problème* d'un jeu de dés (figure 1.3\ {{< fa solid book >}}) avec des statistiques. Ceci est élaboré lors d'une activité d'analyse. "}
@startuml
!include ecriture.pumlinclude
'left to right direction
hide empty members
class Joueur {
nom : String
nbLancers : int
nbLancersGagnés : int
}
class Dé {
face : int
}
class JeuDeDés
JeuDeDés "1" -- "2" Dé : inclut >
Joueur "*" -l- "1" JeuDeDés : joue >
@enduml
```
```{.plantuml genurl="true" #fig-ddcDes caption="Diagramme de *classes logicielles* décrivant une *solution* au problème du jeu de dés. La conception s'inspire du modèle du problème, afin de faciliter sa compréhension."}
@startuml
!include ecriture.pumlinclude
'left to right direction
hide empty methods
class Dé {
+face : int
brasser()
}
class JeuDeDés {
'-joueurs: Map<string, Joueur>;
' -d1 : De;
' -d2 : De;
getJoueurs()
démarrerJeu(nom : string)
jouer(nom : string)
terminerJeu(nom : string)
}
class Joueur {
+nom : string
+nbLancers : number
+nbLancersGagnés : number
}
JeuDeDés --> "d1" Dé
JeuDeDés --> "d2" Dé
JeuDeDés -r- "[nom]" Joueur : " "
@enduml
```
Imaginez que l'on veut construire un logiciel pour un jeu de dés, un jeu qui est joué dans la vraie vie avec deux dés à six faces.
Comment procéder?
On peut spécifier la règle du jeu, dont un des nombreux besoins est de générer un nombre aléatoire entre 1 et 6 (comme un numéro qui apparaît sur l’une des six faces du dé).
On peut aussi modéliser ce besoin (un élément du problème) par une classe conceptuelle `Dé` ayant un attribut `face` dont la valeur est un type `int`.
Les personnes travaillant sur le projet vont facilement comprendre ce modèle, car les gens comprennent les objets qui représentent des aspects de la vraie vie.
Dans l'approche proposée par ce manuel, une modélisation orientée objet est utilisée et pour l'analyse (classes conceptuelles décrivant le problème et les besoins comme à la @fig-mddDes) et pour la conception (classes logicielles proposant une solution dont la représentation est proche de la modélisation du problème comme à la @fig-ddcDes).
## Décalage des représentations {#sec-DecalageRepresentations}
Vous avez sûrement remarqué que le modèle du problème (@fig-mddDes) ressemble beaucoup au modèle de la solution (@fig-ddcDes) pour notre exemple de jeu de dés.
Cependant, il y a des différences, car une solution comporte des détails sur la dynamique du jeu qui sera codée.
Le modèle du problème et le modèle de la solution ne sont donc pas identiques.
::: {.callout-tip appearance="simple"}
Imaginez une autre solution n'ayant qu'une seule classe `Jeu` contenant toute la logique du jeu.
Avez-vous déjà codé une solution simple comme ça?
C'est un bon design au départ, car il est simple.
Mais au fur et à mesure que vous codez la logique du jeu, bien que ça fonctionne parfaitement, la classe `Jeu` grossit et devient difficile à comprendre.
<!-- Par exemple, on voit une méthode `brasser()` dans la classe `Dé` qui montre que ce sera la responsabilité de cette classe de changer sa valeur quand ça sera le bon moment. -->
:::
Une caractéristique souhaitable d'un design est qu'il soit facile à comprendre et à valider par rapport au problème qu'il est censé résoudre.
<!-- Dans un modèle d'analyse, on décompose le problème en concepts.
On comprend plus facilement les éléments dans ce modèle. -->
Plus une solution (conception) ressemble à une description (modèle d'analyse) du problème, plus elle est facile à comprendre et à valider.
La différence entre la représentation d'un problème et la représentation de sa solution s'appelle le *décalage des représentations*.
C'est un terme complexe pour un principe très intuitif.
Méfiez-vous des classes importantes dont le nom retrace difficilement le problème.
Elles vont rendre votre solution plus difficile à comprendre.
Pour des explications de Larman, lisez la section 9.3\ {{< fa solid book >}}.
L'exemple du jeu est trivial, puisque le problème est relativement simple.
Réduire le décalage des représentations est un principe très important, surtout lorsque le problème à résoudre est complexe.
## La complexité et ses sources
Les ingénieures logiciel et ingénieurs logiciel sont constamment dans une bataille avec une adversaire dont le nom est la "complexité".
Mais qu'est-ce que la complexité?
La @fig-complexity est une image de la complexité.
Reconnaissez-vous le domaine d'où vient cette image?
Voici une définition de la complexité.
::: {.callout}
**Complexité**: Caractère de ce qui est complexe, difficile à comprendre, de ce qui contient plusieurs éléments.
:::
![["Complexity"](https://www.flickr.com/photos/lytfyre/6489338411/) [(CC BY-SA 2.0)](https://creativecommons.org/licenses/by-sa/2.0/) par [lytfyre](https://www.flickr.com/people/lytfyre/).](images/flickr_lytfyre_complexity.jpg){width=80% #fig-complexity}
En voici quelques exemples en développement de logiciels:
- Un *problème* peut être complexe, par exemple le domaine des lois fiscales pour lequel des logiciels existent pour aider les gens à faire des déclarations de revenus.
- Un *projet logiciel* peut être complexe, avec plusieurs packages, chacun ayant beaucoup de classes, etc.
- Un cadre d'applications (cadriciel, *framework*) est toujours complexe,
par exemple un *framework* comme Angular ou React pour développer un *front-end* (application frontale), car l'interaction entre l'utilisateur et une application (possiblement répartie dans le nuage) nécessite beaucoup de fonctionnalités supportées par le cadriciel.
- Un *algorithme* peut être complexe, par exemple l'algorithme de [tri de Shell](https://fr.wikipedia.org/wiki/Tri_de_Shell) est plus complexe qu'un simple algorithme de [tri à bulles](https://fr.wikipedia.org/wiki/Tri_%C3%A0_bulles).
Notez que la complexité d'un algorithme peut parfois apporter des gains de performance, par exemple le tri de Shell.
Mais le codage, le débogage et la maintenance d'une implémentation d'un algorithme complexe seront plus coûteux.
- Un _pattern de conception_ peut être complexe, par exemple les patterns Visiteur, Décorateur, Médiateur, etc., de @GoF1994.
Un pattern définit des rôles et parfois du code et des classes supplémentaires à créer.
Le tout doit s'intégrer dans un design existant (qui a son propre niveau de complexité).
- Un *environnement* peut être complexe, par exemple les applications mobiles sont plus complexes à développer et à déboguer que les applications simples sur PC, à cause de l'environnement sans fil, des écrans tactiles de tailles différentes, de l'alimentation limitée, etc.
La @fig-complexitySources présente les sources de complexité ainsi que leurs noms, qu'on va utiliser dans ce manuel.
### Complexité inhérente (provenant du problème)
La complexité inhérente est au sein du problème que résout un logiciel.
Elle est souvent *visible* à l'utilisateur du logiciel.
Elle se compose des parties du logiciel qui sont nécessairement des problèmes difficiles.
N'importe quel logiciel qui tente de résoudre ces problèmes aura une manifestation de cette complexité dans son implémentation.
Exemple: un logiciel qui aide à faire des déclarations de revenus aura une complexité inhérente due à la complexité des lois fiscales qui spécifient comment doit être préparée une déclaration.
### Complexité circonstancielle (provenant des choix de conception) {#sec-ComplexiteCirc}
Les choix que font les ingénieures et ingénieurs dans un projet peuvent amener de la complexité circonstancielle (aussi appelée accidentelle).
En tant qu'ingénieures et ingénieurs, nous avons un devoir de contrôler cette forme de complexité, par exemple en choisissant soigneusement un cadriciel Web ou une architecture logicielle.
La complexité circonstancielle peut aussi être due à des contraintes imposées sur la conception, comme l'utilisation obligatoire d'une vieille base de données ou d'une bibliothèque logicielle héritée, d'un langage de programmation, etc.
La complexité circonstancielle peut être gérée avec des technologies, par exemple les débogueurs, les patterns GoF (un Adaptateur pour les différentes bases de données), etc.
### Complexité environnementale (provenant de l'environnement d'exécution)
Cette forme de complexité comprend des aspects d'une solution qui ne sont pas sous le contrôle des ingénieures et ingénieurs.
Dans un environnement d'exécution, il y a des dimensions comme le ramasse-miettes (*garbage collection*), l'ordonnancement des fils d'exécution (*threads*) sur un serveur, l'utilisation de *conteneurs* (à la Docker), etc. qui peuvent affecter la qualité d'un logiciel.
Les ingénieures et ingénieurs doivent gérer ces formes de complexité, mais il n'y a pas beaucoup de stratégies évidentes face aux technologies qui évoluent à grande vitesse.
```{.plantuml genurl="true" #fig-complexitySources caption="Sources de complexité (inhérente, circonstancielle et environnementale) par rapport au processus de développement (analyse, conception et implémentation)."}
@startuml
scale 0.8
!include normal.pumlinclude
skinparam packageBackgroundColor transparent
skinparam defaultTextAlignment center
cloud "Domaine (du problème)" as P {
cloud "Complexité\ninhérente" as PP #red {
}
}
note right of PP: Exemple :\nLois fiscales (impôts)
rectangle "Spécification d'exigences" as S {
}
cloud "Conception logicielle" as C #lightgreen {
cloud "Complexité\ncirconstancielle" as CC #pink {
}
}
note right of CC: Exemples :\n//Framework// REST,\nutilisation du pattern Visiteur,\nintégration avec un logiciel légataire
rectangle "Solution exécutable" as E {
cloud "Complexité\nenvironnementale" as EE #orange {
}
}
note left of EE: Exemples :\nApplication mobile,\ndéploiement dans conteneur Docker
P --> S
S --> C
C --> E
@enduml
```
## Survol de la méthodologie
La méthodologie d'analyse et de conception proposée dans ce manuel se base sur celle présentée par @craig_uml_2005.
Voici les éléments importants documentés dans ce manuel (voir la @fig-SurvolMethodologie):
- Il y a une spécification explicite des besoins (@sec-besoins) dans le modèle de cas d'utilisation;
- À partir de chaque cas d'utilisation (@sec-cu), il y a une conception de haut niveau (l'interface API du système à développer) documentée sous forme de diagramme de séquence système (DSS) (@sec-dss);
- À partir de chaque DSS, on peut définir un ensemble de contrats d'opération (@sec-contrats), surtout pour les opérations complexes;
- À partir de l'ensemble des besoins, on construit un modèle du domaine (MDD) (@sec-mdd);
- Pour faire une conception intuitive et facile à adapter, on propose un modèle de conception sous forme de plusieurs réalisations de cas d'utilisation (RDCU) (@sec-rdcu) qui sont cohérentes avec le MDD (pour diminuer le décalage des représentations) et avec les contrats;
- Pour implémenter les conceptions, on développe du code à partir des diagrammes dans le modèle de conception, ainsi que du code pour tester tout ça selon le développement piloté par les tests (@sec-tdd);
- Pour gérer la dette technique (@sec-DetteTechnique) on fait du réusinage (@sec-Refactoring) au besoin;
- Le tout se fait de manière évolutive, en itérations courtes selon le _processus unifié_.
![Survol des parties de la méthodologie de @craig_uml_2005 utilisées dans ce manuel.](images/Methodologie-survol_export.svg){#fig-SurvolMethodologie}
## Développement itératif, évolutif et agile
Nous adoptons également un processus moderne de développement avec des itérations, selon une méthodologie "agile".
Dans le chapitre 2\ {{< fa solid book >}}, on définit le processus itératif et adaptatif ainsi que les concepts fondamentaux du "processus unifié", qui est une représentation générique de cette stratégie de développement.
Nous résumons les points importants ainsi:
- Le développement itératif et évolutif implique de programmer et de tester précocement un système partiel dans des cycles répétitifs.
- Un cycle est nommé une itération et dure un temps fixe (par exemple trois semaines) comprenant les activités d'analyse, de conception, de programmation et de test, ainsi qu'une démonstration pour solliciter des rétroactions du client (voir la @fig-IteratifEvolutif).
- La durée d'une itération est limitée dans le temps (*timeboxed* en anglais), de 2 à 6 semaines.
Il n'est pas permis d'ajouter du temps à la durée d'une itération si le projet avance plus lentement que prévu, car cela impliquerait un retard de la rétroaction du client.
Si le respect des délais semble compromis, on supprime plutôt des tâches ou des spécifications et on les reprend éventuellement dans une itération ultérieure.
- Les premières itérations peuvent sembler chaotiques, car elles peuvent être loin de la "bonne voie" ou du résultat convoité.
Avec la rétroaction du client et l'adaptation, le système à développer converge vers une solution appropriée (figure 2.2\ {{< fa solid book >}}).
Cette instabilité peut être particulièrement prononcée dans un contexte d'entreprise en démarrage.
- Dans une itération, la modélisation (par exemple avec l'UML) se fait au début et devrait prendre beaucoup moins de temps (quelques heures) que la programmation, qui n'est pas triviale (voir la @fig-AnalyseConceptionDansLesIterations). Selon le contexte du projet (voir le [Spectre de la conception](#sec-SpectreDeLaConception)), on peut décider de ne pas faire de modélisation. Cependant, en fonction de la complexité du projet à réaliser, cela peut amener des risques, ce que l'on appelle la [dette technique](#sec-DetteTechnique).
```{.plantuml genurl="true" #fig-Modèle_processus_itératif caption="Un processus itératif permet de gérer les complexités, car la planification d'une itération peut viser une partie du système et le système évolue à mesure que les itérations avancent."}
@startuml Iteration
!include normal.pumlinclude
skinparam defaultTextAlignment center
skinparam conditionStyle diamond
start
:Faire planification initial;
while (projet en cours) is (oui)
:Planifier nouvelle itération;
:Plan d'itération (nouveaux objectifs)]
:Élaborer (nouvelles) exigences;
:Actualiser analyse, conception, code et tests;
split
:Nouvelle implémentation]
split again
:Nouveau déploiement]
end split
:Faire démonstration et cueillir rétroaction;
endwhile (non)
stop
@enduml
```
![Processus itératif et incrémental (évolutif).](images/Process_iteratif_et_incremental.svg){#fig-IteratifEvolutif}
\filbreak
Le développement itératif et incrémental amène plusieurs avantages selon @craig_uml_2005:
::: {.callout}
- une diminution d'échecs, une amélioration de la productivité et de la qualité;
- une gestion proactive des risques élevés (risques techniques, exigences, objectifs, convivialité, etc.);
- des progrès immédiatement visibles;
- la rétroaction, l'implication des utilisateurs et l'adaptation précoces;
- la gestion de la complexité (restreinte à une itération);
- la possibilité d'exploiter méthodiquement les leçons tirées d'une itération.
:::
Cependant, il y a des défis associés à ce genre de développement:
- **Instabilité apparente au début**. Dans les itérations initiales, puisqu'on n'a pas beaucoup de temps pour comprendre les exigences, le domaine du client et les contraintes du projet, la compréhension des spécifications et la conception sont loin de la "bonne voie".
La conséquence est que les évaluations et les rétroactions peuvent sembler rudes, et cela peut être déstabilisant pour des personnes qui ne sont pas familières avec le processus.
La bonne nouvelle est que, normalement, cette instabilité diminue au fur et à mesure que le projet avance (voir la figure 2.2\ {{< fa solid book >}}).
- **Modifications des objectifs de l'itération en cours au besoin**. Il arrive souvent que, dans une itération, les choses ne se passent pas comme nous l'avons imaginé.
Par exemple, l'ensemble des récits utilisateur ou des scénarios de cas d'utilisation visés pour l'itération nécessite plus de travail que prévu.
La tendance dans ce cas est de nous donner plus de temps pour terminer ces étapes.
Mais cela voudrait dire que la rétroaction de toute l'itération sera retardée.
Il serait nécessaire de changer la planification de la démonstration avec le client (qui a souvent peu de disponibilités).
Donc, le processus nous impose de toujours respecter le délai des itérations.
Il s'agit d'une [gestion par blocs de temps](https://fr.wikipedia.org/wiki/Timeboxing) (en anglais *timeboxing*).
Que faire alors si, dans une itération, nous n'arriverons pas à tout faire?
La résolution est de demander à l'équipe après la moitié de l'itération si les objectifs d'origine peuvent être atteints.
Si la réponse est non, nous priorisons les objectifs en plaçant les objectifs secondaires dans la catégorie des "choses à faire" (qui seront éventuellement faites à une itération ultérieure).
Voir la @fig-AnalyseConceptionDansLesIterations.
Selon une étude menée par @blincoe2019high sur trois gros projets itératifs d'IBM, jusqu'à 54% des exigences de haut niveau ont été déplacées de cette manière.
Le but ultime de cette stratégie est de pouvoir faire une démonstration à la fin de l'itération, même si elle ne comprend pas toutes les fonctionnalités visées au début, car la rétroaction régulière sur des choses qui fonctionnent est essentielle.
\filbreak
![Pour respecter la méthode du temps limité, on peut modifier les objectifs d'une itération si le travail est trop important [selon @craig_uml_2005, figure 2.4].](images/Modifier_objectifs_mi_iteration.svg){#fig-AnalyseConceptionDansLesIterations}