Activité Patrons de conception et principes de construction logicielle (deuxième partie)

Situation

Vous venez d’être intégré à une équipe de développement chargée de 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

Pour rappel, SOLID est un acronyme mnémotechnique pour se remémorer cinq principes que l’on devrait s’efforcer de suivre dans le contexte de la construction logicielle. Ces principes sont :

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

Durant l’activité précédente, nous avons vu comment le principe de responsabilité unique a guidé le choix du builder pattern pour séparer le parsing de la requête HTTP et la construction de sa représentation orientée objet. Puis nous avons illustré le principe ouvert/fermé ainsi que le principe de substitution de Liskov en utilisant des sous-types de la classe HttpHandler pour modifier de manière radicale le comportement d’une instance de HttpServer sans avoir à modifier son code source. Dans ce travail, nous allons revenir sur ce dernier point pour illustrer les deux derniers principes : le principe d’inversion de dépendance et le principe de ségrégation des interfaces.

Objectifs

À la fin de ce travail, vous devez :

  1. Être capable d’expliquer le principe d’inversion des dépendances.
  2. Être capable d’expliquer le principe de ségrégation des interfaces.

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

Des dépendances abstraites uniquement

HttpServer

Pour modifier le comportement du serveur sans avoir à modifier son code source, il aurait également été possible de créer une sous-classe de HttpServer. Toutefois, cette solution va à l’encontre du principe d’inversion des dépendances qui nous dit qu’une classe de haut niveau (notre application) ne devrait pas dépendre d’une implémentation particulière d’une classe de bas niveau (HttpServer), mais uniquement de son type (son interface). Or, l’héritage implique nécessairement une dépendance vers l’implémentation de la classe de base.

Toutefois, même sans utiliser l’héritage, le code de notre application ne respecte pas ce principe. En effet, l’utilisation de l’opérateur « new » comme nous l’avons fait pour créer une instance de HttpServer en est une violation puisque qu’elle implique que cette classe soit une implémentation particulière du type HttpServer (on ne peut pas instancier une classe abstraite pure ou une interface).

Pour corriger cette situation, nous allons procéder en deux étapes. La première consiste à séparer le type HttpServer et son implémentation en réalisant d’une part une interface HttpServer et d’autre part une classe HttpServerImpl. La figure 1 montre le diagramme de classe correspondant.

Séparation de l’interface et de l’implémentation

Fig. 1 – Séparation de l’interface (type) et de l’implémentation de HttpServer.

Renommez la classe HttpServer en HttpServerImpl à l’aide de l’outil de refactorisation (refactoring) de NetBeans puis créez une nouvelle interface nommée HttpServer.

Pour la deuxième étape, nous devons trouver un moyen de créer une instance d’une classe sans dépendre de son implémentation. Comme c’est un problème courant, il existe plusieurs patrons de conception pour le résoudre. Parmi les plus connues, on peut citer :

Dans les deux cas, il est nécessaire d’avoir déjà un objet qui implémente la méthode fabrique ou qui permettent d’obtenir une référence sur la fabrique abstraire. Dans notre cas, l’instance de HttpServer est le premier objet que l’on crée, c’est pourquoi nous allons nous tourner vers une autre option : le patron de conception Static Factory Method proposé par Joshua Bloch. L’idée est d’implémenter la méthode fabrique sous la forme d’une méthode statique dans le type que l’on veut instancier. Comme il est possible d’implémenter des méthodes statiques dans des classes abstraites (ce qui, depuis Java 8, inclut les interfaces) nous pouvons implémenter cette méthode dans l’interface HttpServer.

Ajoutez une méthode statique createServer à l’interface HttpServer conformément au diagramme de classe de la figure 2.

Séparation de l’interface et de l’implémentation

Fig. 2 – HttpServer avec sa méthode fabrique statique

On peut maintenant modifier le code de notre application comme le montre la figure 3. Nous pouvons voir que la dépendance vers HttpServerImpl a disparu et l’application ne dépend désormais que de l’interface HttpServer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyWebApp {

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

            HttpHandler handler = new HttpHandlerStatic("/Users/frossardj/Dropbox/Projects/m226/www" );
            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. 3 – L’application n’est plus dépendente de HttpServerImpl

Dans cette partie, nous nous sommes concentrés sur le respect du principe d’inversion des dépendances. Mais la solution que nous avons apportée résout également, en partie du moins, la violation d’un autre principe, celui de ségrégation des interfaces. En effet, avant la modification, l’interface du serveur comprenait des méthodes telles que run ou findHandler qui n’ont aucune utilité pour un client de la bibliothèque. Or, le principe de ségrégation des interfaces dit qu’un client ne devrait pas dépendre d’une interface dont il n’a pas l’utilité. En créant le type HttpServer, nous avons pu éliminer ces deux méthodes qui appartiennent maintenant uniquement au type HttpServerImpl. On pourrait aller plus loin en se disant qu’à l’intérieur de la bibliothèque, les clients de HttpServerImpl n’ont pas besoin de toutes ces méthodes et qu’en outre, il serait également bon qu’ils dépendent eux aussi d’un type abstrait, mais nous nous arrêtons là pour cette classe.

HttpHandler

La classe HttpHandler viole également le principe d’inversion des dépendances et le fait d’une manière plus grave. En effet, si vous analyser le code de la classe HttpRequest, vous vous apercevez qu’il ne s’agit que d’une implémentation possible d’un handler et, de plus, une implémentation plutôt limitée puisqu’elle ne prend en charge que les méthodes GET et POST du protocole HTTP. En pratique, cette classe ne limite pas ce que l’on peut faire avec, puisque la méthode handleRequest est virtuelle. Cependant, si l’on surécrit cette méthode alors soit les méthodes doGet et doPost deviennent inutile, ce qui viole le principe de ségrégation des interfaces, soit on doit savoir précisément ce que fait cette méthode dans la classe de base et l’on viole alors l’encapsulation.

Pour corriger cette situation, nous allons à nouveau procéder en deux étapes. La première consiste à renommer la classe HttpHandler en HttpHandlerSimpleBase (sans utiliser l’outil de refactoring car on ne pas changer le nom du type HttpHandler). On indique ainsi qu’il s’agit d’une classe de base pour réaliser le traitement de requêtes simples (uniquement GET et POST). Comme le rôle de cette classe est maintenant défini de manière plus précise, on peut rendre la méthode handleRequest non virtuelle (final). De cette manière, les sous-classes ne pourront pas surécrire cette méthode (uniquement les méthodes doGet et doPost). On peut remarquer qu’il s’agit là de la mise en oeuvre d’un patron de conception appelé template method.

La deuxième, étape consiste à créer une nouvelle interface nommée HttpHandler avec la seule méthode handleRequest conformément à la figure 4.

Séparation de l’interface et de l’implémentation

Fig. 4 – HttpHandler

Effectuez les modifications nécessaires dans la bibliothèque et dans votre application. Attention : la classe HttpHandlerSimpleBase doit ête utilisée uniquement comme classe de base (après le mot clé extends) jamais comme type.