Architecture Hexagonale : Les Use-cases

Sydney ADJOU-MOUMOUNI
L’Architecture hexagonale
4 min readOct 21, 2020

Dans mon dernier article, ici je vous expliquais les principes de la clean architecture et plus particulièrement de l’architecture hexagonale. Cette architecture est encore appelée architecture de ports et d’adaptateurs. Pour résumer, on définit des ports qui sont des interfaces auxquelles on va brancher des dépendances extérieures qui sont les implémentations du contrat d’interface.

Nous avons essayé de découper le projet en couches et atteindre un bon niveau. Malgré ce niveau de découpage, nous pouvons aller encore plus loin et utiliser des use-cases afin de “faire parler” notre code. Le but est de le rédiger dans un langage simple afin qu’un analyste ou même une personne non-technique puisse comprendre.

Par exemple dans notre code, la classe ProductService permet de gérer des produits, mais ce terme est plus proche d’un développeur ou de quelqu’un de technique que de quelqu’un moins technique. Concrètement nous pouvons ajouter un produit, le supprimer, le mettre à jour et c’est ce qu’on appelle des use-cases. Ce sont essentiellement des classes qui nomment les actions qui peuvent être entreprises.

Dans l’architecture hexagonale, nous pouvons définir strictement comment le monde extérieur peut communiquer avec notre application, et cela, par des “commandes” qui vont lancer les use-cases.

Nous allons nous retrouver avec plein de classes, mais j’y vois personnellement un avantage, c’est que le principe SOLID de responsabilité unique est respecté et ce qui va améliorer les tests unitaires.

Pour implémenter cette théorie nous utilisons le pattern Façade. En effet, les avantages de ce pattern sont :

  • de diviser un système en sous-systèmes, la communication entre sous-systèmes étant mise en œuvre de façon abstraite de leur implantation grâce aux façades.
  • rendre une bibliothèque plus facile à utiliser, comprendre et tester
  • réduire les dépendances entre les clients de la bibliothèque et le fonctionnement interne de celle-ci, ainsi on gagne en flexibilité pour les évolutions futures du système

Concrètement, la façade ici va nous permettre de fournir les ensembles de use-cases en tant que librairie unique pour le client comme le montre l’image ci-dessous :

Image Wikipedia du Pattern Façade

Dans mon article précédent, l’organisation de base était plus classique. Explorons-là dans l’image ci-dessous :

Dans la couche Core ou infrastructure on se retrouve avec deux implémentations du même service ProductService à savoir InMemoryProductService et JpaProductService . Dans ce contexte nous utilisons des annotations spring comme @service dans cette couche pour déclarer les services.

Vous pouvez explorer le code ici :

https://github.com/sydneygael/hexagonal-architecture-demo/tree/c394da82e3bb164f96c23a391a1e639f61cf4898

Pour rendre encore plus clean le code, nous devons éviter de mettre des annotations de framework extérieurs au maximum.

Dans la nouvelle version, nous aurons donc une organisation de code qui ressemble à l’image ci-dessous :

Code :

import java.util.List;
import java.util.Optional;

import com.sadjoumoumouni.demo.hexagon.core.user.usecase.AddNewUser;
import com.sadjoumoumouni.demo.hexagon.core.user.usecase.GetUsers;
import com.sadjoumoumouni.demo.hexagon.core.user.usecase.UpdateMoney;
import com.sadjoumoumouni.demo.hexagon.domain.user.entity.HandleMoneyCommand;
import com.sadjoumoumouni.demo.hexagon.domain.user.entity.User;
import com.sadjoumoumouni.demo.hexagon.domain.user.ports.UserPersistencePort;

public class UserFacade implements AddNewUser, GetUsers, UpdateMoney {

private final UserPersistencePort userPersistencePort;

public UserFacade(final UserPersistencePort userPersistencePort) {
this.userPersistencePort = userPersistencePort;
}

@Override
public List<User> getAllUsers() {
return userPersistencePort.getAllUsers();
}

@Override
public Optional<User> getUserById(String userId) {
return getAllUsers().stream().filter(user -> user.getUserId().equals(userId)).findAny();
}

@Override
public User addNewUser(User user) {
return userPersistencePort.addNewUser(user);
}

@Override
public boolean udpateMoney(HandleMoneyCommand command) {
Optional<User> userById = getUserById(command.getUserId());
userById.ifPresent( user -> user.setMoney(command.getMoney()));
userPersistencePort.updateUserInfos(userById.get());
return true;
}
}

En explorant ce code, nous remarquons que cette couche fait le lien entre nos ports et nos adaptateurs sans utiliser des dépendances de Spring dans les imports et nous identifions facilement nos use-cases.

Voyons comment un contrôleur Spring utilise les use-cases :

Diagramme de séquence use-case
package com.sadjoumoumouni.demo.hexagon.springbootapp.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sadjoumoumouni.demo.hexagon.core.product.ProductFacade;
import com.sadjoumoumouni.demo.hexagon.core.product.usecase.BuyProductUseCase;
import com.sadjoumoumouni.demo.hexagon.core.product.usecase.ReadOperationsProduct;
import com.sadjoumoumouni.demo.hexagon.domain.product.entity.BuyProductCommand;
import com.sadjoumoumouni.demo.hexagon.domain.product.entity.Product;
import com.sadjoumoumouni.demo.hexagon.domain.product.exception.CannotBuyProductException;

@RestController
@RequestMapping("/api/product")
public class SpringProductController {

private final ReadOperationsProduct readOperationsProduct;
private final BuyProductUseCase buyProductUseCase;

@Autowired
public SpringProductController(final ProductFacade productFacade) {
this.readOperationsProduct = productFacade;
this.buyProductUseCase = productFacade;
}

@GetMapping(value="/all", produces = MediaType.APPLICATION_JSON_VALUE)
public List<Product> getAllProducts() {
return readOperationsProduct.getAllProducts();
}

@PostMapping(value="/buyProduct", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> buyProduct(@RequestBody BuyProductCommand productCommand) {
try{
buyProductUseCase.buyProduct(productCommand);
return ResponseEntity.ok("buy succes");
}
catch (CannotBuyProductException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
}

Conclusion :

  • Un adaptateur est utilisé lorsque l’on doit respecter une interface bien définie afin d’adapter des dépendances extérieures
  • La façade est utilisée pour simplifier l’utilisation d’un système de plusieurs adaptateurs

Vous pouvez trouver le code ici :

https://github.com/sydneygael/hexagonal-architecture-demo

Sources :

https://medium.com/@wkrzywiec/ports-adapters-architecture-on-example-19cab9e93be7

https://fr.wikipedia.org/wiki/Fa%C3%A7ade_(patron_de_conception)

--

--