README.adoc 11.5 KB
Newer Older
sunye's avatar
sunye committed
1
2
3
:sectnums:
:toc:

sunye's avatar
sunye committed
4
= Distanciel SCE: Création d'une Interface Fluide en Java
sunye's avatar
sunye committed
5
6
7

Pour cette partie en distanciel du cours de Construction et Évolution de Logiciels, nous allons nous intéresser à la création d'APIs lisibles et faciles à utiliser.

sunye's avatar
sunye committed
8
Plus précisément, nous verrons comment construire une  _Fluent Interface_ (en français, désignation chaînée ou chaînage de méthodes) et comment se servir de l'auto-complétion des environnements de développement pour utiliser ces interfaces.
sunye's avatar
sunye committed
9
10
11
12
13
14
15
16
17
18
19
20
21
22

WARNING: Cette partie du cours n'est *pas optionnelle* !
Pour l'examen, vous devez comprendre les principes présentés ici et être capables de refaire les exercices.

- La _première partie_ de ce distanciel est un introduction aux Interfaces Fluent.
- La _seconde partie_ de ce distanciel est un *travail pratique*, où vous créerez une Interface Fluent en Java.

== Le Pattern Fluent Builder 

En construction de logiciels, le pattern _Fluent Builder_ (en français, monteur fluide)
est particulièrement utile lorsque l'on souhaite construire des objets complexes. 

Par exemple, supposons que l'on souhaite construire un objet complexe en créant une instance de son _builder_ (monteur), 
en appelant les _setters_ (modificateurs) respectifs, et enfin, en appelant la méthode de construction. 
Gerson Sunyé's avatar
Gerson Sunyé committed
23
Une fois la méthode de construction exécutée, on peut récupérerer l'objet de la classe souhaitée.
sunye's avatar
sunye committed
24
25
26
27
28
29
30
31

```java
EmailMessageBuilder builder = new EmailMessageBuilder();
EmailMessage message = builder.buildEmailMessage();
message.setFrom("gerson.sunye@univ-nantes.fr");
message.seTo("licence.informatique@univ-nantes.fr");
message.setSubject("Distanciel");
message.setBody("Au travail !!!");
Gerson Sunyé's avatar
Gerson Sunyé committed
32
message.build();
sunye's avatar
sunye committed
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
```

Ce code semble très simple, mais il y a une difficulté potentielle:
si la construction de l'objet est complexe et qu'il y a trop d'attributs à modifier, 
le développeur peut en oublier certains et créer un objet incomplet.

Dans de nombreuses  applications, il y a souvent une entité centrale, comme une "Commande", un "Produit" ou un "Service",
dont les instances sont créées dans différentes morceaux de code.
Dans ces cas, l'oubli d`initialiser un attribut  peut s'avérer très couteux en termes de déboggage et de maintenance.
Une question se pose: 

[quote]
Comment aider le développeur à se rappeler de tous les attributs, obligatoires et optionnels?


La réponse est de forcer le développeur à initialiser tous les attributs obligatoires *avant* d'appeler la méthode de construction. 
Gerson Sunyé's avatar
Gerson Sunyé committed
49
50
Ainsi, tous les attributs obligatoires seront initialisés et l'objet créé sera complet.
Ce qui soulève une nouvelle question:
sunye's avatar
sunye committed
51

Gerson Sunyé's avatar
Gerson Sunyé committed
52
53
[quote]
Comment forcer le développeur à initialiser correctement un objet?
sunye's avatar
sunye committed
54

Gerson Sunyé's avatar
Gerson Sunyé committed
55
La réponse est simple : en utilisant le patron _Fluent Builder_. 
sunye's avatar
sunye committed
56

Gerson Sunyé's avatar
Gerson Sunyé committed
57
L'idée c'est qu'au lieu de précéder chaque appel de méthode par le nom de l'objet --ce qui conduit à un style verbeux-- le nom de l'objet reste  actif dans toute l'instruction en cours. 
sunye's avatar
sunye committed
58
59
60
61
62
63
64
65
66
67
68
69
70
Le code devient ainsi à la fois plus concis et plus clair.
Par exemple:

```java
EmailMessage message = EmailMessageBuilder.builder()
    .from("gerson.sunye@univ-nantes.fr")
    .to("licence.informatique@univ-nantes.fr")
    .subject("Distanciel")
    .body("Au travail !!!")
    .build();
```


Gerson Sunyé's avatar
Gerson Sunyé committed
71
La réduction de verbosité est semblable à celle de l'expression `with` en Pascal, bien que basée sur des principes différents.
sunye's avatar
sunye committed
72
73
74
75
76

=== Chainage de méthodes

Ce style (ou idiome) de programmation, appelé _chainage de méthodes_ a été introduit par le langage Smalltalk, où par convention les modificateurs retournent l'objet modifié (`this`).

Gerson Sunyé's avatar
Gerson Sunyé committed
77
Dans le cas de Java, la logique est similaire: toutes les méthodes de modification (`from`, `to`, etc.) seront spécifiées dans la `Builder` et retourneront une instance de cette classe.
sunye's avatar
sunye committed
78
79
80
81
82
83
84
85
86
87

Par exemple, la méthode `from()` sera mise en oeuvre comme suit:

```java
public EmailMessageBuilder from(String sender) {
    this.from = sender;
    return this;
}
```

Gerson Sunyé's avatar
Gerson Sunyé committed
88
Quant à la méthode `to()`, sa mise en oeuvre sera semblable à la précédente:
sunye's avatar
sunye committed
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

```java
public EmailMessageBuilder to(String recipient) {
    this.to = recipient;
    return this;
}
```

Si le chaînage de méthodes permet de simplifier le code de création d'objets,
il ne peut pas empêcher l'oubli de certains attributs, ni l'appel de la méthode `build()` avant que tous les attributs soient initialisés.
Une nouvelle question se pose alors:


[quote]
Comment empêcher le développeur d'appeler la méthode de construction avant d'initialiser tous les attributs obligatoires ?

Pour répondre à cette question, nous allons utiliser le chaînage d'interfaces.

=== Le chaînage d'interfaces

La logique du chaînage d'interfaces est de limiter l'accès lors du chaînage de méthodes, en créant une interface pour chaque attribut obligatoire et
en modifiant le type de retour de chaque modificateur pour le faire retourner la prochaine interface.

Compliqué ? Oui, en effet, mais la logique deviendra plus claire en regardant le code des interfaces:

```java
interface EmailFrom {
    EmailTo from(String sender);
}

interface EmailTo {
    EmailSubject to(String recipient);
}

interface EmailSubject {
    EmailBody subject(String subject);
}

interface EmailBody {
    EmailCreator body(String contents);
}

interface EmailCreator {
    EmailMessage build();
}
```

Gerson Sunyé's avatar
Gerson Sunyé committed
136
Notez que dans cette suite d'interfaces, chaque modificateur a comme type de retour l'interface suivante dans la suite.
sunye's avatar
sunye committed
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
Ainsi, pour atteindre l'interface `EmailCreator`, on doit passer par une suite d'interfaces, ce qui oblige le développeur à appeler différentes méthodes avant de pouvoir accéder à la méthode `build()`. 

La question qu'on peut se poser maintenant est : "- Faut-il implémenter toutes ces interfaces par une nouvelle classe ?".
La réponse est simple, non, car en Java, rappelez-vous, une classe peut implémenter plus d'une interface.
C'est à la classe builder de le faire:

```java
public class EmailMessageBuilder
    implements EmailFrom, EmailTo, EmailSubject, EmailBody, EmailBody {

    }

```

Ainsi, le développeur manipule toujours le même objet, mais n'a pas toujours accès aux mêmes méthodes.
Une autre question que l'on peut se poser est : "- Comment faire lorsque certains attributs sont optionnels ?".

En effet, dans notre exemple on peut penser à ajouter ces attributs optionnels, comme le champ "CC" d'un message.
Dans ce cas, on ajoute les modificateurs de ces attributs à la dernière interface, celle qui contient la méthode `build()`:

```java
interface EmailCreator {
    EmailCreator cc(String recipient);
    EmailMessage build();
}
```

En prime, l'auto-complétion de l'IDE guidera les développeurs à travers les interfaces, en proposant par exemple, la méthode `to()`, après la méthode `from()` ou la méthode `build()` après la méthode `body()`.

sunye's avatar
sunye committed
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
== Exercice pratique : Publication de services DNS-SD

Dans cette partie, vous allez appliquer ces notions pour coder une interface fluide.

=== Préparation

. Effectuez une *divergence* du projet _distanciel_ où vous vous trouvez actuellement.
Pour rappel, il suffit d'aller utiliser le bouton correspondant en faut de la page d'accueil du projet.
. Configurez votre nouveau projet Gitlab obtenu via la divergence de la manière suivante :
** Dans _"Paramètres → Général"_, allez dans _"Visibility, project features, permissions"_, et mettez _"Project visibility"_ à _"Private"_.
Ainsi, vous devenez le seul utilisateur autorisé à accéder à votre code source.
** Dans _"Paramètres → Membres"_, ajoutez comme nouveau membre l’utilisateur virtuel appelé *Naobot*, avec le statut _"Reporter"_.
Cet utilisateur virtuel que nous contrôlons nous donne le droit d’accéder à votre travail et nous permettra de récupérer vos projets.
. Clonez avec `git` votre divergence sur votre ordinateur de travail, ce qui vous donnera un répertoire `remote`

=== Le protocole DNS-SD

DNS _Service Discovery_, ou simplement DNS-SD est un protocole de publication et découverte de services à l'intérieur d'un réseau local, et qui utilise des enregistrements DNS de type SRV, PTR et TXT.

Notre objectif ici n'est pas de comprendre le fonctionnement de ce protocole dans les détails, mais seulement de simplifier la publication de services grâce à une interface fluide.

Gerson Sunyé's avatar
Gerson Sunyé committed
187
Plus précisément, nous allons nous intéresser à la création d'instances de la classe `ServiceDescription`, dont la signature du constructeur est donnée ci-dessous:
sunye's avatar
sunye committed
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205

```java
  public ServiceDescription(String name, int port, String transport,
      String application, String subtype, int weight, int priority, int ttl,
      boolean persistent) {

    this.name         = name;
    this.port         = port;
    this.transport    = transport;
    this.application  = application;
    this.subtype      = subtype;
    this.weight       = weight;
    this.priority     = priority;
    this.ttl          = ttl;
    this.persistent   = persistent;
  }
```

Gerson Sunyé's avatar
Gerson Sunyé committed
206
* Comme nous pouvons déduire à partir du constructeur, cette classe possède 8 attributs:
Gerson SUNYE's avatar
Gerson SUNYE committed
207

Gerson SUNYE's avatar
Gerson SUNYE committed
208
209
210
211
212
** Seulement 4 sont obligatoires: `name`, `port`, `transport` et `application`.
** La valeur par défaut des attributs `weight` et `priority` est de 0.
** La valeur par défaut de l'attribut `ttl` est de 3.600.
** La valeur par défaut de l'attribut `persistent` est `false`.
** Il est possible qu'un service n'ait pas de sous-type (attribut `subtype` nul).
sunye's avatar
sunye committed
213
214
215
216

=== Implémentation

* Première étape:  mettre en oeuvre la classe monteur, appelée  `SimpleServiceDescriptionBuilder`:
Gerson Sunyé's avatar
Gerson Sunyé committed
217
** Cette classe doit contenir exactement les mêmes attributs que la classe `ServiceDescription`.
sunye's avatar
sunye committed
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
** Les attributs qui ont une valeur par défaut doivent être initialisés avec cette valeur.

* Deuxième étape: mettre en oeuvre le chaînage d'interfaces:
** Créer une interface par attribut obligatoire, plus une interface de construction: `ServiceNameModifier`,
`ServicePortModifier`, `ServiceTransportModifier`, `ServiceApplicationModifier` et enfin `ServiceDescriptionBuilder`.
** Les 4 premières interfaces doivent spécifier une seule méthode, le modificateur de l'attribut concerné.
Le type de retour de ces modificateurs doit être l'interface suivante.
**  Ainsi, le code de l'interface  `ServiceNameModifier` doit ressembler à:
+
```java
interface ServiceNameModifier {
    ServicePortModifier name(String name);
}
```

** La dernière interface, `ServiceDescriptionBuilder`, doit spécifier la méthode `build()`, dont le type de retour est `ServiceDescription`, ainsi que les modificateurs des attributs optionnels.

* Troisième étape: mettre en oeuvre des interfaces dans la classe `SimpleServiceDescriptionBuilder`:

** Modifier l'en-tête de la classe monteur pour la faire implémenter les 5 interfaces définies ci-dessous.

** Implémenter chacune des  méthodes de modification, sans oublier de retourner l'objet courant.

** Implémenter enfin la méthode `build()` qui doit créer une instance de `ServiceDescription`.

Gerson Sunyé's avatar
Gerson Sunyé committed
243
* Quatrième et dernière étape: mettre en oeuvre un test unitaire qui utilise votre API fluide.
sunye's avatar
sunye committed
244
245
246
247
248
249
250
251
252
253
254
** Pensez à vérifier que l'auto-complétion marche correctement.


=== Rendre le projet

Pour rendre le projet, il vous suffit de vous assurer d'avoir :

- bien ajouté *Naobot* comme membre _Reporter_ de votre projet.
- bien validé et publié (commit & push) tous vos fichiers de projet 

Tant que tout cela est bien fait avant la date limite de rendu, alors tout est bon !
sunye's avatar
sunye committed
255
256
257
258
259
260
261



== Liens utiles

- https://dzone.com/articles/fluent-builder-pattern
- https://dzone.com/articles/making-java-fluent-api-more-flexible
Gerson SUNYE's avatar
Gerson SUNYE committed
262
- https://github.com/guilhermechapiewski/fluent-mail-api