• 15 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/15/20

Dessine-moi une inversion de contrôle (IoC)

Maintenant que l'environnement de développement est mis en place et que l'organisation du projet est posée, entrons dans le vif du sujet. Explorons un peu l'application pour voir comment elle est implémentée... Je vais commencer par le module ticket-webapp.

J'y trouve tout d'abord la classe RestApplication.

package org.example.demo.ticket.webapp.rest;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/")
public class RestApplication extends ResourceConfig {

    public RestApplication() {
        packages("org.example.demo.ticket.webapp.rest");
    }
}

Cette classe est spécifique au framework Jersey pour l'implémetation des webservices REST avec JAX-RS. Elle fournit la configuration et l'initialisation du framework. Je déclare ici quels sont les packages que Jersey doit scanner pour trouver l'implémentation des webservices REST. Je ne vais pas m'attarder dessus, ce n'est pas le sujet de ce cours.

Passons à la suite, qui est beaucoup plus intéressante...

Oula, tout cela n'est pas très SOLID !

Prenez la classe ProjetResource dans le module ticket-webapp :

package org.example.demo.ticket.webapp.rest.resource.projet;
// import ...

/**
 * Ressource REST pour les {@link Projet}.
 */
@Path("/projets")
@Produces(MediaType.APPLICATION_JSON)
public class ProjetResource {

    /**
     * Renvoie le {@link Projet} d'identifiant {@code pId}
     *
     * @param pId identifiant du {@link Projet}
     * @return Le {@link Projet}
     * @throws NotFoundException Si le {@link Projet} n'a pas été trouvé
     */
    @GET
    @Path("{id}")
    public Projet get(@PathParam("id") Integer pId) throws NotFoundException {
        ProjetManager vProjetManager = new ProjetManager();
        Projet vProjet = vProjetManager.getProjet(pId);
        return vProjet;
    }

    //...
}

Vous voyez que celle-ci fait appel à la couche business et en particulier à la classe ProjetManager. Jusque-là, c'est assez logique.

Ce qui pose un problème en revanche, c'est que la classe ProjetResource instancie la classe ProjetManager. Il n'est pas très logique et propre que ce soit les classes de la couche webapp qui soient responsables du cycle de vie des classes de la couche business !

  C'est comme si je demandais à un cuisinier de préparer une tartiflette et que celui-ci devait fabriquer son plat à gratin, cultiver ses patates, faire son reblochon... Je pense que vous serez d'accord avec moi : ce n'est pas le rôle du cuisiner !

Il serait mieux de pouvoir appeler les méthodes d'une instance de ProjetManager, sans se soucier de son instanciation.

En quoi ce serait mieux ?

Car sinon, cela viole — principalement — le S et le D, du célèbre acronyme SOLID, donnant 5 principes de conception en programmation orientée objet afin d'obtenir une application plus fiable et plus robuste :

  • Single responsibility principle (responsabilité unique)

  • Open/closed principle (ouvert/fermé)

  • Liskov substitution principle (substitution de Liskov)

  • Interface segregation principle (ségrégation des interfaces)

  • Dependency inversion principle (inversion des dépendances)

Ces principes sont décrits dans le livre de Robert Cecil Martin : Agile Software Development. Principles, Patterns, and Practices (2003, ISBN-13: 978-0135974445) :

  • Le principe de responsabilité unique (Single responsibility) veut qu'une classe ne doit avoir qu'une seule responsabilité. Selon Robert C. Martin « A class should have only one reason to change ».

  • Avec le principe d'inversion des dépendances (Dependency inversion principle), Robert C. Martin énonce que :

    • Les modules de haut niveau ne doivent pas dépendre des modules de plus bas niveau. Les deux doivent dépendre d'abstractions.

    • Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions.

Je ne vais pas — enfin, pas tout de suite — entrer dans le détail, mais en gros, afin de respecter ces principes, il serait préférable, ici, d'appeler des méthodes sur une instance de ProjetManager, sans avoir à l'instancier dans la classe ProjetResource.

Comment faire ? Eh bien, c'est ici qu'intervient le principe d'inversion de contrôle (inversion of control en anglais) communément abrégé en IoC.

  Avec l'instanciation, le cuisinier ne sait fabriquer qu'une seule forme de plat à gratin. Avec l'inversion de contrôle, le cuisinier peut utiliser un plat à gratin fait par quelqu'un d'autre ! C'est un sacré progrès ! Si, si, je vous assure ! !

Si on veut une tartiflette dans un plat carré au lieu d'un plat ovale, il n'est plus nécessaire de changer de cuisinier ou de former le cuisinier à fabriquer un plat carré au lieu d'ovale. On change simplement de fournisseur en plats à gratin !

Il y a plusieurs moyens de mettre en place cette inversion de contrôle dans notre cas :

  • en utilisant le patron de conception (design pattern)Factory ;

  • en faisant de l'injection de dépendances...

Maintenant que le problème est identifié, voyons comment nous pouvons le résoudre grâce à l'inversion de contrôle (IoC).

Je vous propose d'y aller progressivement.

Utiliser le design pattern Factory

Une première solution envisageable est d'utiliser le design pattern Factory. Grâce à lui, la classe ProjetResource n'a plus à se soucier de l'instanciation de la classe ProjetManager, elle ne fait que demander à la Factory de lui donner une instance de ProjetManager.

  Même si ce n'est pas tout à fait ça, du point de vue du cuisinier, la Factory serait son commis de cuisine. Quand le cuisinier commence sa tartiflette, il demande au commis de lui fournir un plat à gratin, un reblochon...

Je vais créer une classe ManagerFactory à laquelle on pourra demander les instances des managers (ProjetManager, TicketManager...)

package org.example.demo.ticket.business;

public class ManagerFactory {

    public ProjetManager getProjetManager() {
        return new ProjetManager();
    }

    public TicketManager getTicketManager() {
        return new TicketManager();
    }
    //...
}
package org.example.demo.ticket.webapp.rest.resource.projet;
//...
public class ProjetResource {

    private ManagerFactory managerFactory;

    @GET
    @Path("{id}")
    public Projet get(@PathParam("id") Integer pId) throws NotFoundException {
        Projet vProjet = managerFactory.getProjetManager().getProjet(pId);
        return vProjet;
    }

    //...
}

OK, mais il reste encore une interrogation :

Comment obtenir l'instance de ManagerFactory dans la classe ProjetResource ?

  • On instancie la Factory ?

    public class ProjetResource {
        private ManagerFactory managerFactory = new ManagerFactory();
        //...
    }

    Surtout pas !! On retomberait sur le même problème qu'au début !

      Il faudrait que tous les matins (démarrage de l'application) le cuisinier s'occupe d'embaucher un commis de cuisine !

  • On fait de la Factory un singleton ?

    package org.example.demo.ticket.business;
    //...
    public final class ManagerFactory {
    /** Instance unique de la classe (design pattern Singleton) */
    private static final ManagerFactory INSTANCE = new ManagerFactory();
    
    /**
     * Constructeur.
     */
    private ManagerFactory() {
        super();
    }
    
    /**
     * Renvoie l'instance unique de la classe (design pattern Singleton).
     *
     * @return {@link ManagerFactory}
     */
    public static ManagerFactory getInstance() {
        return ManagerFactory.INSTANCE;
    }
    
    
    public ProjetManager getProjetManager() {
        return new ProjetManager();
    }
    
    public TicketManager getTicketManager() {
        return new TicketManager();
    }
    //...
    }

    Et dans la classe ProjetResource on obtient :

    public class ProjetResource {
        private ManagerFactory managerFactory = ManagerFactory.getInstance();
        //...
    }

    C'est mieux, mais on ne peut plus faire d'abstraction sur la Factory (vous vous rappelez, le D de SOLID). C'est-à-dire que la classe ProjetResource dépend de l'implémentation de la Factory, pas seulement d'un contrat du genre "renvoie-moi un ProjetManager, renvoie-moi un TicketManager" (ceci vous paraîtra plus clair dans quelques minutes...).

      Le commis de cuisine est embauché par le restaurant (l'application). Chaque matin, le cuisinier l'appelle pour qu'il le rejoigne dans la cuisine.

Ce qui serait bien, ce serait qu'un élément extérieur à la classe ProjetResource lui donne, lui injecte, l'instance de ManagerFactory, via son constructeur ou un setter par exemple...

Eh bien, c'est là qu'intervient l'injection de dépendances (abrégé DI pour dependency injection en anglais). ;)

  Avec l'injection de dépendances, le commis est envoyé en cuisine tous les matins. Le cuisinier ne s'en soucie plus. Dès qu'il a besoin de lui, il est là !

Procéder à une injection de dépendances

Afin que vous compreniez le principe de l'injection de dépendances et puissiez bien suivre le déroulement des opérations, je vais tout faire « à la main ». Je ne vais pas utiliser la « magie des annotations JEE ».

Je commence par faire un peu de refactoring pour simplifier les choses :

  1. Je crée une classe abstraite AbstractResource de laquelle vont hériter mes classes Resource (ProjetResourceTicketResource...).

  2. Dans cette classe AbstractResource, j'ajoute un attribut static contenant l'instance de ManagerFactory à utiliser par toutes les classes Resource.

  3. J'ajoute un getter pour cet attribut afin qu'il soit accessible dans les sous-classes (visibilité en protected suffisante).

  4. Je crée également un setter pour cet attribut, avec une visibilité public, afin qu'il soit appelable par une classe tierce.

  5. Je supprime les attributs d'instance managerFactory des classes Resource.

public abstract class AbstractResource {

    private static ManagerFactory managerFactory;

    protected static ManagerFactory getManagerFactory() {
        return managerFactory;
    }
    public static void setManagerFactory(ManagerFactory pManagerFactory) {
        managerFactory = pManagerFactory;
    }
}


@Path("/projets")
@Produces(MediaType.APPLICATION_JSON)
public class ProjetResource extends AbstractResource {
    @GET
    @Path("{id}")
    public Projet get(@PathParam("id") Integer pId) throws NotFoundException {
        Projet vProjet = getManagerFactory().getProjetManager().getProjet(pId);
        return vProjet;
    }
}

Pour injecter l'instance de ManagerFactory, afin que toutes les instances de Resource y aient accès, il ne reste plus qu'à faire appel au setter staticAbstractResource.setManagerFactory(ManagerFactory).

Pour cela, je crée une classe DependencyInjectionListener implémentant l'interface ServletContextListener :

package org.example.demo.ticket.webapp.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.example.demo.ticket.business.ManagerFactory;
import org.example.demo.ticket.webapp.rest.resource.AbstractResource;


public class DependencyInjectionListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent pServletContextEvent) {
        // Création de l'instance de ManagerFactory
        ManagerFactory vManagerFactory = new ManagerFactory();

        // Injection de l'instance de ManagerFactory dans la classe AbstractResource
        AbstractResource.setManagerFactory(vManagerFactory);
    }

    @Override
    public void contextDestroyed(ServletContextEvent pServletContextEvent) {
        // NOP
    }
}

Je déclare ce Listener dans le fichier src/main/webapp/WEB-INF/web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <display-name>Ticket</display-name>

    <listener>
        <listener-class>org.example.demo.ticket.webapp.listener.DependencyInjectionListener</listener-class>
    </listener>

    <!-- ... -->
</web-app>

C'est fini. La classe DependencyInjectionListener sera instanciée au démarrage de l'application web par le serveur d'application puis celui-ci appellera sa méthode contextInitialized(ServletContextEvent). Cela aura pour conséquence d'injecter l'instance de ManagerFactory dans la classe AbstractResource.

Et voilà, je viens de mettre en place une injection de dépendances :

  1. Au déploiement de l'application, la classe DependencyInjectionListener est instanciée par le serveur d'application.

  2. Le serveur appelle ensuite la méthode contextInitialized(ServletContextEvent) sur l'instance qu'il vient de créer.

  3. Cette méthode crée l'instance de ManagerFactory et l'injecte dans l'attribut static dédié (managerFactory) de la classe AbstractResource.

Ça y est, les classes Resource disposent enfin d'une instance de ManagerFactory (via la méthode staticgetManagerFactory()) sans avoir à se soucier de comment créer/récuperer cette instance !

Grâce au mécanisme que je viens de mettre en place, il y a bien inversion de contrôle au niveau des classes Resources. Celles-ci ne gèrent plus la totalité du flot de contrôle de la tâche, mais seulement la partie sous leur responsabilité : prendre les paramètres passés en entrée et effectuer le traitement demandé.

Exemple avec ProjetResource.get(Integer) :

  1. à partir de l'identifiant de projet reçu en paramètre,

  2. rechercher le projet

  3. et renvoyer une réponse contenant ce projet.

Vous voyez que maintenant, la classe ProjetResource n'a plus à gérer quoi que ce soit de plus (instanciation du ProjetManager, récupération du singleton ManagerFactory...) que le traitement demandé.

  Le DependencyInjectionListener correspond au responsable du restaurant. C'est lui qui le matin s'occupe d'ouvrir les locaux et d'envoyer le personnel à son poste de travail.

Améliorons la classe ManagerFactory

Revenons un peu sur la classe ManagerFactory.

public class ManagerFactory {

    // ...
    public ProjetManager getProjetManager() {
        return new ProjetManager();
    }
}

Sérieusement, on va instancier un ProjetManager à chaque fois que l'on va appeler la méthode ManagerFactory.getProjetManager()?

On ne pourrait pas en faire un Singleton ou un attribut « interne » ?

Alors je dis, non, non et non !!

D'abord, non, on ne va pas créer une instance à chaque appel. Il faut changer cela. Sinon, une instance va être créée à chaque requête et cela peut vite faire augmenter la mémoire utilisée car les instances inutilisées ne sont détruites que de temps en temps par le garbage-collector. En cas de sollicitation un peu forte du serveur, les performances de l'application vont s'écrouler.

Deuxièmement, non, on ne va pas créer de singleton. Certes, c'est son objectif de limiter le nombre d'instances, mais un vrai singleton est une classe final. Or cela empêche de « bouchonner » (mock en anglais) simplement la classe par héritage et polymorphisme lors des tests. C'est donc une solution à proscrire ici (valable pour les Managers et d'autant plus pour les DAO).

Et enfin, non, on ne va pas faire un attribut « interne » :

public class ManagerFactory {

    // ...
    private final ProjetManager projetManager = new ProjetManager();

    public ProjetManager getProjetManager() {
        return projetManager;
    }
}

Cela va également compliquer le bouchonnage lors des tests (comment influer sur le new ProjetManager() ou modifier l'instance dans cet attribut ?)

La solution qui je préconise : encore une fois l'injection de dépendances !

public class ManagerFactory {
    // ...

    // Ajout d'un attribut projetManager
    private ProjetManager projetManager;

    // On renvoie désormais simplement l'attribut projetManager
    public ProjetManager getProjetManager() {
        return projetManager;
    }

    // Ajout d'un setter pour l'attribut projetManager
    public void setProjetManager(ProjetManager pProjetManager) {
        projetManager = pProjetManager;
    }
}



public class DependencyInjectionListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent pServletContextEvent) {
        ManagerFactory vManagerFactory = new ManagerFactory();
        // On ajoute l'injection des Managers dans la ManagerFactory
        vManagerFactory.setProjetManager(new ProjetManager());
        vManagerFactory.setTicketManager(new TicketManager());
        //...
        AbstractResource.setManagerFactory(vManagerFactory);
    }

    // ...
}

Ça commence à faire pas mal de choses à injecter ! Et en plus, quand je vais écrire les tests, il faudra que j'écrive l'injection aussi ! Il n'y a pas moyen d'alléger tout ça ?

Eh bien si justement, et c'est là que Spring vole à notre secours ! On y arrive... ;)

Example of certificate of achievement
Example of certificate of achievement