:sectnums: :toc: = Distanciel SCE: Création d'une Interface Fluide en Java 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. 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. 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. Une fois la méthode de construction exécutée, on peut récupérerer l'objet de la classe souhaitée. ```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 !!!"); message.build(); ``` 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. Ainsi, tous les attributs obligatoires seront initialisés et l'objet créé sera complet. Ce qui soulève une nouvelle question: [quote] Comment forcer le développeur à initialiser correctement un objet? La réponse est simple : en utilisant le patron _Fluent Builder_. 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. 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(); ``` La réduction de verbosité est semblable à celle de l'expression `with` en Pascal, bien que basée sur des principes différents. === 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`). 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. Par exemple, la méthode `from()` sera mise en oeuvre comme suit: ```java public EmailMessageBuilder from(String sender) { this.from = sender; return this; } ``` Quant à la méthode `to()`, sa mise en oeuvre sera semblable à la précédente: ```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(); } ``` Notez que dans cette suite d'interfaces, chaque modificateur a comme type de retour l'interface suivante dans la suite. 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()`. == 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. 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: ```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; } ``` * Comme nous pouvons déduire à partir du constructeur, cette classe possède 8 attributs: ** 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). === Implémentation * Première étape: mettre en oeuvre la classe monteur, appelée `SimpleServiceDescriptionBuilder`: ** Cette classe doit contenir exactement les mêmes attributs que la classe `ServiceDescription`. ** 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`. * Quatrième et dernière étape: mettre en oeuvre un test unitaire qui utilise votre API fluide. ** 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 ! == Liens utiles - https://dzone.com/articles/fluent-builder-pattern - https://dzone.com/articles/making-java-fluent-api-more-flexible - https://github.com/guilhermechapiewski/fluent-mail-api