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
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
Enregistrer un commentaire