:: Enseignements :: ESIPE :: E4INFO :: 2019-2020 :: Java Avancé ::
[LOGO]

Interface fonctionnelle, la puissance du déclaratif.


Le but de cet exercice est de voir qu'une interface ne sert pas uniquement à abstraire des classes représentant des données mais aussi à abstraire la façon dont un code doit s'exécuter.
Dans notre cas, nous allons abstraire une suite de if...else en cascade.

Exercice 1 - Docteur, je me sens mal ?

Comment savoir si un site/service Web n'a pas crashé ? Simplement en écrivant un petit programme de supervision qui va aller faire une requête HTTP pour savoir si tout va bien.
On se propose d'écrire ce programme qui prend une URI et vérifie la santé d'un service Web.

Pour le programme, l'URI peut venir de différents endroits, elle peut être spécifiée sur la ligne de commande, sous forme d'une propriété "uri" à l'intérieur d'un fichier de properties, sous forme d'une variable d’environnement, etc...
Le code suivant correspond, à peu près, à ce que l'on pourrait écrire trouver cette URI.
     URI uri;
     if (args.length != 0) {
       uri = URI.create(args[0]);
     } else {
       var path = Path.of("healthcheck.txt");
       if (Files.exists(path) {
         uri = URI.create(extractValueFromProperties(path));
       } else {
         var uriValue = System.getenv("HEALTH_CHECK_URI");
         if (uriValue != null) {
           uri = URI.create(uriValue);
         } else {
           uriValue = System.getProperty("healthCheckURI");
           if (uriValue != null) {
             uri = URI.create(uriValue);
           } 
         }
       }
     }
    

Le code précédent n'est pas faux en soi (enfin un peu quand même... voir plus loin), mais plus on enchaîne les if ... else, plus il devient incompréhensible. En effet, au lieu d'expliquer de façon déclarative quel est le résultat que l'on veut obtenir (un peu comme un cahier des charges), on explique comment il faut faire pour arriver à ce résultat.
On voudrait plutôt écrire un code équivalent qui serait le suivant.
   var finder = fromArguments(args)
            .or(fromPropertyFile(Path.of("healthcheck.txt"), "uri"))
            .or(fromMapGetLike("HEALTH_CHECK_URI", System::getenv))
            .or(fromMapGetLike("healthCheckURI", System::getProperty));
   var uri = finder.find().orElseThrow();
    

Pour cela, nous avons besoin de créer une interface URIFinder qui correspond au type de retour des méthodes from*. Les méthodes from* seront des méthodes factories qui créent et renvoient un objet de ce type et la méthode or une méthode d'instance.
Note: dans l'exemple les méthodes from* sont importées grâce à un import static pour éviter la répétition du nom de la classe qui les contient.

Les tests JUnit 5 de cet exercice sont HealthCheckTest.java.

  1. Avant de définir l'interface URIFinder, on va déjà écrire dans la classe HealthCheck la méthode healthCheck qui prend une URI en paramètre et renvoie vrai si il est possible d'effectuer une requête HTTP GET sur cette URI.
    Pour cela, nous allons utiliser les classes du package java.net.http,
    • HttpClient correspond à un objet capable de faire des requêtes HTTP.
      HttpClient.newBuilder() permet d'obtenir un builder qui permet de créer cet objet.
      La méthode d'instance send(request, bodyHandler) permet d'effectuer la requête HTTP (HttpRequest) et renvoie une réponse HTTP (HttpResponse).
    • L'objet HttpRequest correspond à la requête HTTP à effectuer.
      HttpRequest.newBuilder() permet d'obtenir un builder capable de créer la requête HTTP.
    • Comme le contenu (body) d'une réponse HTTP peut être très gros, on essaie d'éviter de le charger en mémoire. C'est pour cela que la méthode send prend comme second paramètre un BodyHandler qui est une fonction qui prend les informations d’en-tête de la réponse HTTP (ResponseInfo) et renvoie comment le contenu de la réponse HTTP doit être interprété (BodySubscriber).
      La classe BodyHandlers contient des BodyHandler prédéfinis.

    Écrire la méthode healthCheck sachant que dans notre cas, le contenu de la réponse HTTP ne nous intéresse pas, seul un code de réponse de 200 est suffisant.
    Note : la doc de la classe HttpClient vous donne un exemple d'utilisation.
  2. Nous voulons maintenant définir l'interface URIFinder sachant qu'elle doit permettre de modéliser l'équivalent d'effectuer un if...else.
    Une façon d'abstraire un if...else est de considérer qu'il s'agit d'un triplet de fonctions : une fonction de condition ainsi que deux fonctions de "calcul", l'une pour le cas à où la condition est valide et l'autre sinon. Le problème de cette modélisation est qu'il va falloir imbriquer les fonctions les une dans les autres (comme on imbrique les ifs).
    L'autre façon de modéliser un if...else consiste à dire que c'est équivalent à une seule fonction condition qui peut renvoyer soit un résultat (le résultat du calcul de la fonction si la condition est valide), soit pas de résultat si la condition n'est pas valide.
    Quelle est la classe en Java qui correspond à un résultat ou pas et qui va donc servir de valeur de retour pour notre fonction condition ?
    Définir l'interface fonctionnelle URIFinder qui possède une méthode find pour que le code d'utilisation d'un URIFinder ci dessous fonctionne
           URIFinder finder = ...
           URI uri = finder.find().orElseThrow();
         
  3. On cherche maintenant à écrire la méthode fromArguments qui prend en paramètre un tableau de String (celui fourni par le main) et renvoie un URIFinder qui considère le premier argument, et si il existe, permet de le transformer en URI (en utilisant URI.create).
    Où doit-on placer la méthode fromArguments et quels sont les modificateurs (public etc) de celle-ci ?
    Écrire la méthode fromArguments.
  4. On cherche maintenant à écrire une méthode fromURI qui prend une chaîne de caractères et la transforme en URI.
    Que se passe-t-il si la chaîne de caractères n'est pas une URI valide ?
    Comment faire si l'on souhaite ignorer les chaînes de caractères qui ne sont pas des URI valides ?
    Écrire le code de la méthode fromURI et réviser le code de fromArguments pour que lui aussi ignore les chaînes de caractères qui ne sont pas des URI valides.
  5. On souhaite maintenant écrire une méthode or qui permet de combiner deux URIFinder de telle façon que si le premier URIFinder ne trouve pas d'URI, alors le second essaie de trouver son URI.
    Où doit-on placer la méthode or et quels sont les modificateurs de celle-ci ?
    Écrire la méthode or.
  6. On souhaite écrire la méthode fromMapGetLike qui prend comme premier paramètre un nom de clé/propriété et comme second paramètre une fonction ayant la même signature et la même sémantique que map::get (avec des String comme type de clé et de valeur dans la Map). Cette méthode devra renvoyer un URIFinder permettant de chercher la valeur de la clé dans la Map.
    Voici un exemple d'utilisation.
            var map = Map.of("1", "http://www.google.fr", "2", "http://www.u-pem.fr");
            var uriFinder = URIFinder.fromMapGetLike("1", map::get);
          

    Écrire le code de la méthode fromMapGetLike et vérifier aussi que le code fromMapGetLike("HEALTH_CHECK_URI", System::getenv) est valide.
    Rappel: map.get renvoie null si il n'y a pas de valeur associée à une clé donnée.
  7. En fait, la méthode fromMapGetLike pourrait avoir des types de paramètre (une signature) acceptant plus de cas valides. Par exemple, le code suivant dans lequel les clés sont des Integer devrait aussi fonctionner
            var map = Map.of(1, "http://www.google.fr", 2, "http://www.u-pem.fr");
            var uriFinder = URIFinder.fromMapGetLike(1, map::get);
          
  8. Enfin, on souhaite ajouter une méthode fromPropertyFile qui prend en paramètre le chemin d'un fichier et le nom d'une clé et renvoie un URIFinder qui renvoie l'URI associé à la clé dans le fichier de properties si l'association existe et que la valeur associée est bien une URI valide.
    Écrire la méthode fromPropertyFile.
    Note : il existe une méthode Properties.load().
    Note 2 : vous vous rappelez surement qu'une bonne façon de lire un fichier de caractères est d'utiliser un BufferedReader...