Accéder au contenu principal

Comprendre l'Inversion de Contrôle : Un Guide Pratique de Guice sur l'Injection des Dépendances dans le Développement de Logiciels

L'un des principes fondamentaux dans le développement de logiciels (ou d'applications) est la mise en oeuvre des principes SOLID. Le D ici indique: Dependency Inversion ou Dependency Injection ou encore Inversion Of Control (IoC). Autrement dit: Inversion de Contrôle ou Injection des Dépendances. L'injection des dépendances à pour principal objectif le découplage des modules entre eux. Cela implique donc de créer et développer les différents composants à base de classes abstraites ou d'interfaces, comme le permet Java. Ce faisant, l'application ou le logiciel devra utiliser une librairie ou un framework qui permette d'injecter les fameuses dépendances en question. Nous verrons dans la suite du tutoriel comment une librairie (ou framework léger) comme Guice (Google Guice) nous aidera dans cet objectif.

Note: Spring Framework est l'un des framework les plus connus qui permettent de mettre en oeuvre l'Inversion de Contrôle. 


1. Qu'est ce que l'Injection des Dépendances

L'injection des dépendances consiste à développer une application ou un service en suivant deux étapes principales. La première consiste à ne pas créer de couplage fort entre les différents modules d'une application. Et la seconde consistera à trouver les implémentations adéquates nécessaires au bon fonctionnement de l'application ou du service.

1.1 Couplage faible

Dans le language Java, nous allons développer nos différents composants en utilisant des classes abstraites ou des interfaces, sans forcément indiquer les implémentations dans un premier temps. Ce faisant les composants n'utiliseront que des interfaces pour communiquer entre eux, minimisant ainsi très fortement le couplage en eux. Voyons cela dans l'exemple ci dessous:

@Slf4j
@RequiredArgsConstructor
public class CreateCompanyComponent implements ICreateCompanyComponent {
    private final ICreateCompanyOutputPort createCompanyOutputPort;
    
    @Override
    public Company createCompany(ICreateCompanyInputPort createCompanyInputPort) 
    throws ValidateCompanyException, CreateCompanyException {
        try {
            Company company = createCompanyInputPort.getCompany();
            return createCompanyOutputPort.saveCompany(company);
        } catch (ValidationException e) {
            log.warn("Company validation failed due to: {}", e.getMessage());
            throw new ValidateCompanyException(e.getMessage(), e);
        } catch (Exception e) {
            throw new CreateCompanyException(e.getMessage(), e);
        }
    }
}

Comme nous pouvons le constater, nous avons utilisé une interface ici: ICreateCompanyOutputPort, qui expose la méthode saveCompany. En aucun moment notre composant CreateCompanyComponent n'est en mesure de savoir exactement l'implémentation sous-jacente à l'interface ICreateCompanyOutputPort. Notre composant a donc un faible couplage avec l'implémentation qui se chargera de réaliser effectivement la sauvegarde de l'objet Company.

1.2 Injection des dépendances

L'étape suivante consiste à implémenter notre interface ou nos classes abstraites afin que notre application puisse fonctionner. Il s'agit purement et simplement de l'implémentation de l'interface et rien d'autre.

Ensuite, il va nous falloir, utiliser une librairie ou framework léger qui se chargera d'instancier l'implémentation de l'interface à chaque fois que cela est nécessaire: on parle alors de l'injection de cette implémentation, autrement dit: injection de dépendance.

2. Guice

Guice est un framework léger développé par google qui permet de mettre en oeuvre la JSR 330 (Dependency Injection). Cette librairie est moins gourmande que Spring et rapide à mettre en oeuvre. Comme approche principale, elle privilégie les conventions de développement  au dessus de la configuration (convention over configuration).

3. Utilisation de Guice dans un projet java

Dépendance Gradle

implementation 'com.google.inject:guice:7.0.0'

Dépendance Maven

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>7.0.0</version>
</dependency>

3.1 Définition du module de binding

L'étape suivante est de définir un module Guice qui se chargera d'associer (binding) les interfaces et leur implémentation de manière simple et transparente afin que Guice puisse faire l'injection des dépendances. En effet, dans le module ci dessous, nous indiquons à Guice quelles implémentations instancier pour les interfaces qui nous intéresse. Si l'implémentation que nous souhaitons configurer ici utilise un autre constructeur que le constructeur par défaut, alors nous devons faire le binding avec toConstructor(...). Dans le cas contraire, il faudra utiliser to(...) lors que c'est le constructeur par défaut qui doit être invoqué pour l'instanciation.

@Slf4j
public class CreateCompanyModule extends AbstractModule {
    @Override
    protected void configure() {
        try {
            bind(ICreateCompanyComponent.class).toConstructor(
            	CreateCompanyComponent.class.getConstructor(ICreateCompanyOutputPort.class));
            bind(ICreateCompanyOutputPort.class).to(CreateCompanyAdapter.class);
        } catch (NoSuchMethodException e) {
            log.warn("Unable to locate suitable constructor for CreateCompanyComponent class", e);
            throw new ConfigurationException(e);
        }
    }
}

3.2 Utiliser l'injecteur de Guice pour instancier nos objets

La dernière étape consiste ensuite à utiliser l'injecteur de guice en lui indiquant notre module ou nos modules de configuration. De ce fait, l'injecteur s'aura instancier tous nos objects avec toutes leur dépendances. Autrement dit, c'est à l'aide de Guice que nous assemblons les différents éléments fonctionnels de notre application. C'est avec Guice que le "cablage" des différents composants va se mettre en oeuvre.

@Slf4j
public class CompanyDataStoreApplication {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new CreateCompanyModule());
        ICreateCompanyComponent createCompanyComponent = 
        	injector.getInstance(ICreateCompanyComponent.class);
        ICreateCompanyInputPort createCompanyInputPort = 
        	injector.getInstance(ICreateCompanyInputPort.class);
        try {
            Company company = createCompanyComponent.createCompany(createCompanyInputPort);
            log.info(company.toString());
        } catch (Exception e) {
            System.exit(-1);
        }
    }
}

4. Les annotations de Guice

4.1 @Inject

L'annotation @Inject est utilisé pour indiquer à Guice que nous souhaitons qu'il inject tout seule une valeur dans un champ ou dans paramètres d'une méthode ou les paramètres d'un constructeur. Dans l'exemple ci dessous, Guice instanciera le composant en injectant les paramètres du constructeur, pouvu que dans notre Module Guice, nous ayons créé les bons bindings

@Slf4j
public class CreateCompanyComponent implements ICreateCompanyComponent {
    private final ICreateCompanyOutputPort createCompanyOutputPort;
    
    @Inject
    public CreateCompanyComponent(ICreateCompanyOutputPort createCompanyOutputPort) {
        this.createCompanyOutputPort = createCompanyOutputPort;
    }

    @Override
    public Company createCompany(ICreateCompanyInputPort createCompanyInputPort) throws 
    ValidateCompanyException, CreateCompanyException {
        try {
            Company company = createCompanyInputPort.getCompany();
            return createCompanyOutputPort.saveCompany(company);
        } catch (ValidationException e) {
            log.warn("Company validation failed due to: {}", e.getMessage());
            throw new ValidateCompanyException(e.getMessage(), e);
        } catch (CreateCompanyException e) {
            throw e;
        } catch (Exception e) {
            log.warn("Company save failed due to: {}", e.getMessage());
            throw new CreateCompanyException(e.getMessage(), e);
        }
    }
}
@Slf4j
public class CreateCompanyModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(ICreateCompanyComponent.class).to(CreateCompanyComponent.class);
        bind(ICreateCompanyOutputPort.class).to(CreateCompanyAdapter.class);
    }
}

4.2 @Provides

Cette annotation doit être utilisé au dessus d'une méthode définie dans un module Guice. Et lorsqu'une méthode est annotée avec @Provides, Guice pourra se servir de cette méthode pour créer l'instance d'un objet.

@Slf4j
public class CreateCompanyModule extends AbstractModule {
    @Override
    protected void configure() {
        try {
            bind(ICreateCompanyComponent.class).toConstructor(
            CreateCompanyComponent.class.getConstructor(ICreateCompanyOutputPort.class));
        } catch (NoSuchMethodException e) {
            log.warn("Unable to locate suitable constructor for CreateCompanyComponent class", e);
            throw new ConfigurationException(e);
        }
    }

    @Provides
    public ICreateCompanyInputPort provideCreateCompanyInputPort(){
        ConsoleInputAdapter consoleInputAdapter = new ConsoleInputAdapter(System.in, System.out);
        return consoleInputAdapter;
    }
}

4.3 @Named

Cette annotation est utilisée pour marquer/identifier une interface. En effet, imaginons que pour une meme interface vous disposiez de différentes implémentations. Et selon le besoin, vous souhaitez utiliser une implémentation spécifique. dans ces conditions, il est nécessaire d'utiliser l'annotation @Named, afin de distinguer les implémentations à utiliser, comme le montre l'exemple ci dessous.

@Slf4j
public class CreateCompanyComponent implements ICreateCompanyComponent {
    
    private final ICreateCompanyOutputPort createCompanyOutputPort;

    @Inject
    public CreateCompanyComponent(@Named("Straight") ICreateCompanyOutputPort 
    createCompanyOutputPort) {
        this.createCompanyOutputPort = createCompanyOutputPort;
    }

    @Override
    public Company createCompany(ICreateCompanyInputPort createCompanyInputPort) 
    throws ValidateCompanyException, CreateCompanyException {
        try {
            Company company = createCompanyInputPort.getCompany();
            return createCompanyOutputPort.saveCompany(company);
        } catch (ValidationException e) {
            log.warn("Company validation failed due to: {}", e.getMessage());
            throw new ValidateCompanyException(e.getMessage(), e);
        } catch (CreateCompanyException e) {
            throw e;
        } catch (Exception e) {
            log.warn("Company save failed due to: {}", e.getMessage());
            throw new CreateCompanyException(e.getMessage(), e);
        }
    }
}
@Slf4j
public class CreateCompanyModule extends AbstractModule {
    @Override
    protected void configure() {
        try {
            bind(ICreateCompanyComponent.class).toConstructor(
            CreateCompanyComponent.class.getConstructor(ICreateCompanyOutputPort.class));
            bind(ICreateCompanyOutputPort.class).
                    annotatedWith(Names.named("Straight")).toInstance(new CreateCompanyAdapter());
        } catch (NoSuchMethodException e) {
            log.warn("Unable to locate suitable constructor for CreateCompanyComponent class", e);
            throw new ConfigurationException(e);
        }
    }

    @Provides
    public ICreateCompanyInputPort provideCreateCompanyInputPort(){
        ConsoleInputAdapter consoleInputAdapter = new ConsoleInputAdapter(System.in, System.out);
        return consoleInputAdapter;
    }
}

Pour aller plus loin, vous pouvez accéder à la documentation de Guice ici

Le code source de ce tutoriel est présent sur github, à cette adresse: company-data-store

Ne pas hésiter à commenter ou à poser une question ou à demander de l'aide autours de java et des technologies connexes. Nous nous ferons un plaisir de vous répondre.

Commentaires

Posts les plus consultés de ce blog

Comment valider les données utilisateur dans votre application Java : Guide pratique avec Jakarta Bean Validation

Lorsque vous développez une application qui interagit avec un utilisateur, en lui demandant de saisir des données, il est primordial de vérifier et valider ces données avant tout traitement. Et ceci pour plusieurs raisons: vous aurez la possibilité d'éviter des traitements inutiles si les données reçues ne sont pas conformes aux données attendues et aussi cela permet de protéger votre application en cas d'attaque orchestrée par des hackers. Dans le langage java, il existe une spécification qui vous permet de valider tout type de données transmis dans un bean: Java Bean Validation (ou plus récemment: Jakarta Bean Validation). Dans ce tutoriel, au travers d'une petite application de gestion d'une bibliothèque de quartier, nous allons voir ensemble comment utiliser Jakarta Bean Validation. 1. Pourquoi Jakarta Bean Validation L'objectif de Jakarta Bean Validation est de standardiser la validation des champs au travers de specification (JSR 380). Aussi cette standardisat...

Découvrez comment SpringBoot facilite la parallélisation des tâches

Parfois il nous arrive d'avoir besoin de lancer des taches en parallèles afin d'accélérer le traitement d'une action. Ces taches s'exécuteront de manière indépendante dans des threads différents. Ces threads seront gérés par votre application principale. Ce processus de parallélisation (multithreading) est largement faciliter par springboot. Nous allons donc voir dans ce tutoriel, la parallélisation des taches à l'aide de springboot. 1.  Pourquoi paralléliser les taches avec springboot (multithreading) Comme nous l'avons déjà vu dans l'un de nos tutoriels , springboot vous simplifiera la mis en place et le prototypage de votre application en vous permettant de gérer aisément les taches parallèles, les pools de threads etc... Vous aurez la possibilité de déclarer via une méthode l'asynchornisme d'une tache, et soit attendre ou non la fin de toutes les taches parallèles. La méthode principale d'exécution de l'application sera l'orchestrateu...

Tout ce que vous devez savoir sur l'utilisation des collections en Java

Une collection est une structure de données qui permet à tout développeur java de stocker des données en mémoire et facilement accessible. Les collections peuvent dans certains cas être vues comme des tableaux de données évolués car ces dernières proposent d'innombrables méthodes d'accès afin de faciliter l'accès et la manipulation des données. 1. Qu'est qu'une collection Dans le langage Java, une collection est un objet qui implémente l'interface de base java.util.Collection. Une collection n'est pas une Map. Une collection est un objet dont la structure sou-jacente est un tableau à accès indexé. Il existe plusieurs catégories de collection. 2. Les List Il s'agit du type de collection le plus utilisé. Aucune contrainte sur les objets, aucune contrainte sur la taille de la liste. De plus les objets contenues dans une telle collection sont accessibles directement par un index. L'interface java.util.List est implémentée par plusieurs classes telles que...