• 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

Ajouter de l'abstraction

Patience jeune padawan, il y a encore une chose importante à voir avant d'attaquer Spring. Vous vous rappelez du D de SOLID et de cette histoire d'abstraction ? ... Non ?

L'inversion des dépendances c'est quoi ?

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.

Euh, j'ai rien compris, c'est quoi cette histoire d'abstraction ?

Ce n'est pas très compliqué, vous allez voir. Dans le chapitre précédent, j'ai découpé le flot de contrôle de l'application (l'exécution du programme), afin que la responsabilité de la gestion des instances des Managers ne se trouve plus dans les classes Resources mais soit gérée de manière extérieure. Eh bien, avec l'abstraction, il s'agit de faire de même avec le code et non plus avec l'exécution.

Il s'agit de séparer les « contrats » de leur implémentation.

C'est clair non ? ;)

  Peu importe la forme du plat à gratin, ça ne change ni la recette, ni les tâches réalisées par le cuisinier pour faire la tartiflette.

Un exemple concret... Dans la classe ProjetResource, ce qui est important pour elle, c'est qu'elle puisse demander à un Manager de lui « donner le projet qu'elle veut », peu importe comment le Manager fait et comment il est implémenté.

Dans ce cas :

  • le contrat serait : « un manager de projet doit pouvoir me renvoyer le projet correspondant à l'identifiant que je lui donne »

  • l'implémentation serait :

    1. prendre l'identifiant passé en paramètre,

    2. appeler le DAO en lui donnant ce paramètre

    3. ...

En pratique, pour écrire un contrat, il suffit d'écrire une Interface.

Voici le diagramme de dépendances avant l'abstraction :

Dépendances avant l'abstraction

Voici le diagramme de dépendances qu'il faut mettre en place :

Dépendances après l'abstraction

Grâce à l'abstraction, les modules de haut niveau ne dépendent plus de l'implémentation faites dans les modules de plus bas niveau, mais seulement de l'abstraction. De même, cette abstraction permet de définir quels contrats doivent implémenter les modules de plus bas niveau. Ces derniers dépendent donc également de cette abstraction.

Donc si je reprends le principe d'inversion des dépendances(Dependency inversion principle), je pense que vous comprenez maintenant le premier point :

  • 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.

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

En ce qui concerne le deuxième point, cela dit qu'il ne suffit pas de faire une interface simplement à partir des méthodes qui se trouveraient dans l'implémentation. Ces interfaces se doivent d'être « génériques » et non dirigées par l'implémentation. En d'autres termes, si l'implémentation change, cela ne doit pas remettre en question l'interface.

  Pour cuire la tartiflette, le cuisinier sait qu'il doit jouer sur la température et le temps de cuisson. Cela impose au four de la cuisine de disposer en façade d'un thermostat et d'un minuteur que le cuisinier peut manipuler. Si on change le four, il faut toujours que celui-ci possède un thermostat et un minuteur, peu importe la marque du four et la technologie employée dans le four pour capter la température.

Par exemple, dans notre application, les utilisateurs sont enregistrés dans une table de la base de données. Si demain, il faut plutôt aller les récupérer dans un annuaire LDAP, il ne faut pas que cela change l'interface UtilisateurDao.

Donc il ne doit pas y avoir de paramètres dans les méthodes qui dépendent de l'implémentation (DataSource, groupe dans l'annuaire LDAP...)

Mais en vrai, je fais quoi ?

OK, j'ai à peu près compris, mais concrètement qu'est-ce que ça donne ?

Voilà comment mettre tout cela en place avec le ProjetManager :

  1. ProjetManager est désormais une interface.

  2. L'ancienne classe ProjetManager devient la classe ProjetManagerImpl et implémente l'interface ProjetManager.

  3. La ManagerFactory dépend de l'interface ProjetManager et non pas de l'implémentation ProjetManagerImpl. Il faut ajouter un attribut projetManager de type ProjetManager (l'interface) avec un getter et un setter.

  4. Dans le DependencyInjectionListener il faut désormais injecter l'implémentation (classe ProjetManagerImpl) dans la ManagerFactory.

// ProjetManager est désormais une interface
public interface ProjetManager {
    Projet getProjet(Integer pId);
    // ...
}



// La classe ProjetManager devient ProjetManagerImpl
// et implémente l'interface ProjetManager
public class ProjetManagerImpl implements ProjetManager {

    @Override
    public Projet getProjet(Integer pId) throws NotFoundException {
        // ...
    }

    // ...
}



public class ManagerFactory {
    // ...

    // ProjetManager est désormais une interface.
    // La Factory dépend de cette interface et non pas de l'implémentation

    // 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;
    }
}


// Dans le DependencyInjectionListener, les implémentations sont injectées
// dans la ManagerFactory

public class DependencyInjectionListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent pServletContextEvent) {
        ManagerFactory vManagerFactory = new ManagerFactory();
        // On ajoute l'injection de l'implémentation des Managers dans la ManagerFactory
        vManagerFactory.setProjetManager(new ProjetManagerImpl());
        vManagerFactory.setTicketManager(new TicketManagerImpl());
        //...
        AbstractResource.setManagerFactory(vManagerFactory);
    }

    // ...
}

Vous pouvez appliquer le même principe à la ManagerFactory :

  1. ManagerFactory devient une interface.

  2. L'ancienne classe ManagerFactory devient la classe ManagerFactoryImpl et implémente l'interface ManagerFactory.

  3. La classe AbstractResource est dépendante de l'interface ManagerFactory et pas de la classe ManagerFactoryImpl.

Grâce à l'abstraction, l'inversion de contrôle et l'injection de dépendances, vous pouvez voir que j'ai pu réduire significativement le couplage entre les classes de couches différentes.

Questions, problèmes, angoisses... ?

Mais du coup, avec l'injection de dépendances, à quoi servent les Factories ? Elles sont toujours utiles ?

Il est possible d'aller plus loin en supprimant les Factories et en injectant directement :

  • les Managers dans les classes Resource,

  • les DAO dans les classes Manager...

Cela va apporter :

  • plus de souplesse dans l'injection de dépendances (comme pour le bouchonnage des tests),

  • affiner et clarifier le graphe de dépendances : on a une vision exacte des dépendances nécessaires dans une classe simplement en lisant les imports (la Factory masquait cela).

Mais en contrepartie :

  • Cela demande beaucoup de travail au niveau de l'injection de dépendances et un temps de démarrage plus long (enfin tout est relatif ;) ).

  • Il y a moins de cohésion dans les dépendances. En passant par une Factory, si on modifie une dépendance, ce changement se retrouve « propagé », de fait, à toutes les classes utilisant cette Factory. Ce n'est plus — forcément — le cas sans Factory.

À vous donc, de peser le pour et le contre en fonction de votre projet. Personnellement, je préfère garder les Factories, si je n'ai pas besoin de plus de souplesse dans les tests. Cela me permet de conserver une cohésion d'ensemble mais aussi d'avoir des points d'accès uniques sur mes éléments (Managers, DAO...).

Euh, il va falloir gérer toutes les dépendances dans le DependencyInjectionListener, dans la classe Main du module ticket-batch, dans les tests... ?

Ben oui, pourquoi ?

… Non, je vous fais marcher. La classe DependencyInjectionListener que j'ai créée ici est mon « moteur » d'injection de dépendances pour les besoins didactiques de ce cours. Vous vous rappelez, je vous ai dit que j'allais faire ça « à la main » pour avancer progressivement et pour que vous compreniez bien le mécanisme.

Maintenant, je vais passer à la vitesse supérieure : il existe des frameworks permettant de faire de l'inversion de contrôle et de l'injection de dépendances. C'est justement le cas de Spring IoC...

Conclusion

Petit résumé de ce que nous avons vu, avant de passer à la suite :

  • Les classes doivent avoir une seule raison de changer : ne gérez pas le cycle de vie des classes des couches inférieures dans les classes des couches supérieures. Faites de l'inversion de contrôle (IoC) grâce à des Factories et de l'injection de dépendances (DI).

  • Les classes des couches supérieures ne dépendent pas des classes d'implémentations des couches inférieures, mais d'abstractions.

  • Les classes des couches inférieures implémentent ces abstractions. Elles en dépendent donc.

  • Les abstractions ne doivent pas dépendre des détails : n'orientez pas les abstractions en fonction de l'implémentation prévue.

  • Les abstractions sont réalisées avec des interfaces.

  • Celles-ci permettent d'écrire le contrat d'échange entre les couches supérieures et l'implémentation faites dans les couches inférieures.

Dans les chapitres suivants, nous allons mettre en pratique tout cela en mettant en œuvre une inversion de contrôle avec Spring IoC.

Example of certificate of achievement
Example of certificate of achievement