Activité Patrons de conception et principes de construction logicielle (première partie)

Situation

Vous venez d’être intégré à une équipe de développement chargée réaliser une bibliothèque pour la création de serveurs HTTP en Java. La bibliothèque est actuellement à l’état de prototype et un travail de refactorisation est nécessaire. Comme elle n’a pas encore été publiée, tout peut encore être changé.

Quelques principes SOLID

L’acronyme SOLID est en fait un acrostiche (la programmation n’exclut pas un peu de poésie) qui énonce cinq principes importants de la construction logicielle orientée objet (object oriented software construction) :

Single responsability principle.
Open/closed principle.
Liskov substitution principle.
Interface segregation principle.
Dependency inversion principle.

Ce travail dirigé a pour but de fournir une introduction à ces principes en montrant comment ils peuvent être mis en œuvre pour guider la conception d’une application.

Objectifs

À la fin de ce travail, vous devez :

  1. Être capable d’énoncer les cinq principes de l’acronyme SOLID.
  2. Être capable d’expliquer le principe de responsabilité unique.
  3. Être capable d’expliquer le principe ouvert/fermé.
  4. Connaître le patron de conception builder et être capable de le mettre en œuvre.

Résultat attendu

  • Un petit document où vous aurez décrit ce que vous avez fait et les difficultés que vous aurez rencontrées.

Ressources

Logiciel :

  • NetBeans
  • Chrome et Firefox

Mise en route

Avant tout, il s’agit de mettre en place votre environnement de développement. Pour cela nous avons besoin de NetBeans et du projet de la bibliothèque disponible sur moodle (httpserver.zip). Télécharger et importer l’archive avec NetBeans. Une fois l’importation terminée (le projet HttpServer devrait être ouvert), créez un nouveau projet d’application Java avec le nom MyWebApp sans package (default package). Dans la fenêtre « Projects », ouvrez le menu contextuel sur le projet MyWebApp et cliquez sur « Set as main project ». Le « main project » est celui qui est exécuté lorsque l’on clique sur le bouton « Run main projet », son nom devrait apparaître en gras.

Pour pouvoir utiliser la bibliothèque HttpServer, il faut l’ajouter dans les bibliothèques (Librairies) du projet MyWebApp. Pour cela, ouvrez le menu contextuel du dossier « Librairies » du projet MyWebApp et cliquez sur « Add project… ». Dans la boîte de dialogue qui devrait apparaître, sélectionnez le projet HttpServer et cliquez sur le bouton « Add Projet JAR Files ».

Copiez maintenant le code de la figure 1 et lancez votre application. Vous devriez voir apparaître un message dans la console. Pour arrêter le serveur, vous pouvez soit forcer l’arrêt du processus avec NetBeans (bouton « stop » de la fenêtre Output), soit presser la touche entrée dans cette même fenêtre Output.

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
// Attention au fait qu'il n'y a pas de package!
// Si vous avez le mot clé package à cet endroit, utilisez l'outil 
// de refactoring pour le supprimer en donnant un nom vide

import ch.epaiict.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.logging.Level;
import java.util.logging.Logger;


public class MyWebApp {

    public static void main(String[] args) {
        try {
            
            final HttpServer server = new HttpServer(new InetSocketAddress(8282));
            server.start();

            System.out.println("Press enter stop the server.");
            System.in.read();
            server.stop();
            
        } catch (IOException ex) {
            Logger.getLogger("MyWebApp").log(Level.SEVERE, ex.getMessage(), ex);
        }
    }
}
Fig. 1 – Code l’application Web.

Pour vérifier que votre environnement de développement est prêt pour la suite, tapez l’URL http://localhost:8282/poetry/shewalksinbeauty.html dans votre navigateur. Vous recevez un message d’erreur 404 de la part du serveur que vous venez de créer (EPAI-ICT http server). L’erreur que vous recevez n’est pas une erreur du point de vue du HTTP. En effet, une réponse avec un code de statut 404 indique simplement que la ressource que vous avez demandée n’a pas pu être trouvée par le serveur. Mais le fait qu’il ait pu envoyer cette réponse indique qu’il fonctionne parfaitement. Note : si le serveur ne fonctionne pas, vous recevez un message d’erreur de votre navigateur indiquant qu’il n’est pas possible d’établir une connexion.

Que se passe-t-il précisément ? Lorsque vous lancez votre application, le serveur se met à attendre les demandes de connexion des clients (par exemple votre navigateur). Lorsqu’une demande arrive, il l’accepte et lit la requête HTTP qui lui est envoyée. Une fois qu’il a lu la requête, le serveur délègue son traitement à un objet appelé gestionnaire HTTP (HttpHandler) dont la figure 2 vous montre l’implémentation.

1
2
3
4
5
6
7
8
9
import java.io.IOException;

public class HttpHandler404 extends HttpHandler {

    @Override
    protected void doGet(HttpRequest request, HttpResponse response) throws IOException {
        response.sendErrorStatus(HttpStatus._404);
    }
}
Fig. 2 – Classe HttpHandler404 (gestionnaire HTTP par défaut)

Pour résumer, la classe HttpServer permet de créer un serveur HTTP qui fonctionne, mais qui ne sert à rien puisqu’il renvoie toujours la même réponse. On doit donc trouver un moyen de modifier son comportement. Mais il est hors de question de modifier son code, puisque tout l’intérêt d’une bibliothèque est de disposer de code testé et compilé qu’on n’a pas à toucher. Nous voilà bien avancés…

La solution de ce dilemme apparent est la mise en œuvre du principe ouvert/fermé qui dit en substance que l’on doit s’appliquer à réaliser des modules qui sont ouverts à l’extension, mais fermés à la modification (sous-entendu, du code source). Comme nous allons le voir, le polymorphisme par sous-typage est un outil puissant pour appliquer ce principe.

Modifier le comportement du HttpServer sans modifier son code

Le type HttpServer à une méthode appelée setDefaultHandler qui permet de donner au serveur un autre gestionnaire par défaut que celui de la figure 2. Il ne nous reste donc qu’à créer une classe de type HttpHandler (qui hérite de la classe HttpHandler), créer une instance de cette classe et la passer à la méthode setDefaultHandler.

Procédons par étape. La première à chose à faire est d’ajouter une classe que nous allons nommer « MyHandler » et surécrire (override) sa méthode doGet (exactement comme la classe HttpHandler404). Utilisez le corps de la méthode pour construire la réponse que vous voulez. Vous pouvez également utiliser le code de la figure 3.

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
import ch.epaiict.httpserver.HttpHandler;
import ch.epaiict.httpserver.HttpRequest;
import ch.epaiict.httpserver.HttpResponse;
import ch.epaiict.httpserver.HttpStatus;
import java.io.IOException;
import java.util.Map;

public class MyHandler extends HttpHandler {
    
    @Override
    protected void doGet(HttpRequest request, HttpResponse response) throws IOException {
        
        // Le statut est 200 (Ok) et nous envoyons une représentation 
        // de la ressource sous forme de HTML.
        response.setStatus(HttpStatus._200);
        response.setContentType("text/html");
        
        // Construction du document HTML. 
        // Affiche un message ainsi que l’URL et les en-têtes de la requête HTTP.
        response.write("<html>");
        response.write("<head>"); 
        response.write("<meta charset=\"utf-8\">");
        response.write("</head>");
        response.write("<body>");
        response.write("<h1>Une ressource dynamique</h1>");
        response.write("<p>");
        response.write("Le code html de cette ressource est produit par la classe MyHandler.");        
        response.write("</p><p>");
        response.write("L’Uri de la ressource est : ");
        response.write(request.getUri().toString());
        response.write("</p><p>");
        response.write("Les en-têtes de la requête sont :");      
        response.write("<ul>");      
        for (Map.Entry<String, String> header : request.getHeaders().entrySet()) {
            response.write(String.format("<li>%s: %s</li>", header.getKey(), header.getValue()));
        }
        response.write("</ul>");      
        response.write("</p></body>");
        response.write("</html>");
        
        response.end();
    }
}
Fig. 3 – Classe MyHandler

Vous devez ensuite indiquer au serveur qu’il doit utiliser une instance de cette classe au lieu de l’instance de la classe HttpHandler404. Modifier la fonction main pour qu’elle corresponde à la figure 4. Naviguer vers http://localhost:8282/poetry/shewalksinbeauty.html et observez le résultat.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public static void main(String[] args) {
        try {
            
            final HttpServer server = new HttpServer(new InetSocketAddress(8282));
            
            // Crée une instance de la classe MyHandler et passe l’objet
            // au serveur pour qu’il l’utilise comme gestionnaire par défaut.
            // Toutes les requêtes HTTP sur le serveur "localhost:8282" seront
            // désormais traitées par ce gestionnaire. 
            HttpHandler handler = new MyHandler();
            server.setDefaultHandler(handler);

            server.start();

            System.out.println("Press enter stop the server.");
            System.in.read();
            server.stop();
            
        } catch (IOException ex) {
            Logger.getLogger("MyWebApp").log(Level.SEVERE, ex.getMessage(), ex);
        }
    }
Fig. 4 – Modification de la fonction main

Le bon fonctionnement de ce système repose sur le fait que la classe HttpServer ne dépend que du type HttpHandler et non pas d’une implémentation particulière de celui-ci. Dès lors, pour peu que le principe de substitution de Liskov soit respecté, HttpServer peut fonctionner avec n’importe quelle instance d’un sous-type de HttpHandler. Pour bien illustrer cela, nous allons modifier une dernière fois notre application.

Dans la bibliothèque, on trouve une classe appelée HttpHandlerStatic qui est une implémentation de HttpHandler qui permet de servir les fichiers d’un répertoire. Avant de commencer, téléchargez l’archive poetry.zip qui se trouve sur moodle et procédez à l’extraction des fichiers dans votre répertoire de travail. Notez le chemin complet du répertoire qui contient «css» et «poetry» puis modifiez le code de la procédure «main» comme indiqué à la figure 4.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public static void main(String[] args) {
        try {
            
            final HttpServer server = new HttpServer(new InetSocketAddress(8282));
            
            // Crée une instance de la classe HttpHandlerStatic et passe l’objet
            // au serveur pour qu’il l’utilise comme gestionnaire par défaut.
            // Toutes les requêtes HTTP sur le serveur "localhost:8282" seront
            // désormais traitées par ce gestionnaire. 
            HttpHandler handler = new HttpHandlerStatic("<chemin du répertoire qui contient vos documents HTML>" );
            server.setDefaultHandler(handler);

            server.start();

            System.out.println("Press enter stop the server.");
            System.in.read();
            server.stop();
            
        } catch (IOException ex) {
            Logger.getLogger("MyWebApp").log(Level.SEVERE, ex.getMessage(), ex);
        }
    }
Fig. 5 – Modification de la fonction main

Une fois cette modification effectuée, lancez l’application et naviguez une fois encore vers l’URL http://localhost:8282/poetry/shewalksinbeauty.html. Vous devriez cette fois voir apparaître un poème de Lord Byron (de la vraie poésie cette fois) qui se trouve être le père de celle que l’on considère comme le premier programmeur de l’histoire : Ada Lovelace.

Faisons le point. Nous avons pu modifier radicalement le comportement du serveur avoir à intervenir dans le code source de la bibliothèque. Le code source est fermé, ce qui permet de le tester et de le compiler, mais la classe est ouverte dans le sens où son comportement peut être modifié. La classe HttpServeur a encore d’autres possibilités d’extension qui vont dans le sens du principe ouvert/fermé, mais celles-ci feront l’objet d’une autre activité.

L’un des grands avantages du principe ouvert/fermé est que le code fermé peut être testé indépendamment des extensions. De cette façon, les développeurs de la bibliothèque se chargent de tester la bibliothèque, et l’utilisateur de la bibliothèque teste uniquement ses propres classes.

La classe HttpRequest

Nous allons maintenant nous intéresser au principe de responsabilité unique. Ce principe nous dit qu’il ne devrait exister qu’une seule raison de modifier une classe. Pour illustrer cela, nous allons nous intéresser à la classe HttpRequest. Avant d’aller plus loin, voyons un peu ce que fait cette classe. Lorsque le serveur a accepté une demande de connexion, le client envoie une requête HTTP sur le canal TCP. Cette requête est une suite de caractères qui forme un texte semblable à celui de la figure 6. La première ligne s’appelle «ligne de requête» et se compose de la méthode (GET), du chemin absolu de la ressource (/poetry/shewalksinbeauty.html), du protocole (HTTP) et de la version du protocole (1.1). Les lignes suivantes représentent chacune un en-tête (header) composé d’un nom terminé par un double point (:) suivi d’une valeur terminée par un retour à la ligne. La ligne vide marque la fin des en-têtes et le début du contenu s’il y en a un.

1
2
3
4
5
6
7
8
9
GET /poetry/shewalksinbeauty.html HTTP/1.1
Host: localhost:8282
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.24 Safari/537.36
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8,de;q=0.6,fr;q=0.4
 
Fig. 6 – Requête HTTP

Le rôle de la classe HttpRequest est de présenter cette requête sous la forme d’un objet avec des getters pour les différentes parties (méthode, chemin, en-têtes, etc.). Pour cela, le constructeur de la classe HttpRequest reçoit un InputStream qui représente le canal TCP dans lequel le client envoie la requête. Les caractères de la requête sont lus l’un après l’autre dans l’ordre où ils arrivent et interprétés pour séparer les différentes informations et initialiser les variables membres de l’objet.

Prenez maintenant quelques minutes pour analyser le code de la classe HttpRequest à la lumière de ce qui vient d’être décrit. Il n’est pas nécessaire de tout comprendre, mais vous devez pouvoir identifier les différentes parties.

Cette classe ne respecte pas le principe de responsabilité unique, car elle peut changer si l’on décide de modifier la manière dont on interprète la requête (le parser) ou si l’on modifie la manière de présenter les différentes parties de la requête. La tâche consiste donc à séparer le parser (le machin qui interprète les caractères), la construction de la représentation de la requête et la représentation de la requête qui sera passée au gestionnaire (HttpHandler).

Pour cela, la clé est le patron de conception builder (ou «monteur» en français). La figure 7 montre le diagramme UML de la mise en œuvre de ce patron dans la bibliothèque.

Mise en oeuvre du builder pattern

Fig. 7 – Mise en œuvre du builder pattern

Dans ce diagramme, on peut observer plusieurs choses. D’abord on s’aperçoit que le parser et la classe HttpRequest sont maintenant indépendants l’un de l’autre. C’était le premier objectif. Ensuite, la classe HttpRequest est devenue extrêmement simple, elle ne contient plus aucune logique (uniquement des getters pour renvoyer les valeurs reçues à travers le constructeur). De plus, la classe HttpRequest n’est plus responsable de la construction d’une instance de HttpUri ; c’est maintenant le builder qui en a la charge. Enfin, on peut remarquer que HttpConnection lui, n’a pas changé (ou très peu). En effet, il est toujours dépendant des trois aspects que revêtait la classe HttpRequest. La figure 8 montre le code de HttpConnection avant et après la modification.

Avant
1
2
3
4
5
6
7
8
9
10
11
12
13
    @Override
    public void run() {
        try {
            HttpRequest request = new HttpRequest(server.getPortNumber(), socket.getInputStream());
            HttpResponse response  = new HttpResponse(socket.getOutputStream());

            HttpHandler handler = server.findHandler(request);
            handler.handleRequest(request, response);
                 
        } catch (Exception ex) {
            Logger.getLogger(HttpServer.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
        } 
    }
Après
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    @Override
    public void run() {
        try {
            HttpRequestBuilderImpl builder = new HttpRequestBuilderImpl(server.getPortNumber());
            HttpRequestParser parser = new HttpRequestParser();
            parser.parse(socket.getInputStream(), builder);

            HttpRequest request = builder.build();
            HttpResponse response  = new HttpResponse(socket.getOutputStream());

            HttpHandler handler = server.findHandler(request);
            handler.handleRequest(request, response);
                 
        } catch (Exception ex) {
            Logger.getLogger(HttpServer.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
        } 
    }
Fig. 8 – Méthod run de HttpConnection, avant et après

La prochaine fois, nous verrons ensemble comment ces changements permettent de faciliter la réalisation de tests unitaires pour le parser. Mais en attendant, c’est à vous de jouer !

Attention! Prêt! Codez!