Hexagonal Architecture (Ports and Adapters)
- Type : Architecturale
- Difficulté : 9/10
- Définition succincte : L'Architecture Hexagonale, aussi appelée Ports and Adapters, est une architecture logicielle qui vise à isoler le cœur métier d'une application de ses dépendances techniques externes (comme les bases de données, les services tiers, ou les interfaces utilisateurs). Elle permet de rendre le domaine métier indépendant des détails d'implémentation, en facilitant le test unitaire, l'extensibilité, et l'interopérabilité avec d'autres systèmes.
Objectif de l'Architecture Hexagonale
L'Architecture Hexagonale a pour objectif de rendre une application plus flexible et maintenable en séparant clairement la logique métier (qui doit rester stable et simple) des technologies externes (qui sont sujettes à changement). Cette séparation permet d'échanger facilement les implémentations externes, tout en conservant un cœur métier isolé. Le cœur métier communique avec le monde extérieur via des ports (interfaces), et les adapters fournissent les implémentations spécifiques à ces interfaces.
Structure de l'Architecture Hexagonale
- Domain (Cœur) : Le domaine métier, qui contient toute la logique centrale de l'application. Il est complètement isolé des technologies externes.
- Ports : Interfaces qui définissent comment le domaine interagit avec le monde extérieur. Elles représentent les points d'entrée et de sortie de l'application.
- Adapters : Implémentations des ports. Ils permettent de connecter le domaine aux services externes (comme une base de données ou un service HTTP) ou à des interfaces utilisateurs (comme une API REST).
- Application Services : Services qui orchestrent l'interaction entre le domaine et les adapters externes.
Implémentation avec Laravel
Imaginons que tu souhaites développer un système de gestion de commandes dans une boutique en ligne où :
- Un utilisateur peut passer une commande.
- Les détails de la commande sont sauvegardés en base de données.
- Une notification est envoyée à l'utilisateur après la création de la commande.
Nous allons gérer cette logique via une commande CLI qui interagira avec le domaine métier sans être couplée directement à l'infrastructure (base de données, services externes).
Étape 1 : Créer le domaine
Le domaine contient la logique métier centrale, indépendante des détails techniques.
1.1. Entité Order
L'entité Order représente une commande, indépendante de la base de données ou de tout autre service.
// app/Domain/Order/Order.php
namespace App\Domain\Order;
class Order
{
public $id;
public $userId;
public $total;
public $items;
public function __construct(int $userId, array $items, float $total)
{
$this->userId = $userId;
$this->items = $items;
$this->total = $total;
}
}
1.2. Service OrderService
Le service OrderService contient la logique métier pour créer des commandes. Il utilise des ports pour la persistance des commandes et l'envoi de notifications.
// app/Domain/Order/OrderService.php
namespace App\Domain\Order;
use App\Domain\Order\Ports\OrderRepositoryPort;
use App\Domain\Order\Ports\NotificationPort;
class OrderService
{
protected $orderRepository;
protected $notificationService;
public function __construct(OrderRepositoryPort $orderRepository, NotificationPort $notificationService)
{
$this->orderRepository = $orderRepository;
$this->notificationService = $notificationService;
}
public function createOrder(int $userId, array $items, float $total): Order
{
// Créer une nouvelle commande
$order = new Order($userId, $items, $total);
// Sauvegarder la commande
$this->orderRepository->save($order);
// Envoyer une notification après la création de la commande
$this->notificationService->notifyOrderCreated($order);
return $order;
}
}
Étape 2 : Créer les ports
Les ports sont des interfaces définissant les interactions avec le monde extérieur.
2.1. Port OrderRepositoryPort
Le port pour la persistance des commandes.
// app/Domain/Order/Ports/OrderRepositoryPort.php
namespace App\Domain\Order\Ports;
use App\Domain\Order\Order;
interface OrderRepositoryPort
{
public function save(Order $order): void;
}
2.2. Port NotificationPort
Le port pour l'envoi de notifications.
// app/Domain/Order/Ports/NotificationPort.php
namespace App\Domain\Order\Ports;
use App\Domain\Order\Order;
interface NotificationPort
{
public function notifyOrderCreated(Order $order): void;
}
Étape 3 : Créer les adaptateurs
Les adapters implémentent les ports et interagissent avec les services externes, comme la base de données et les notifications.
3.1. Adaptateur pour la persistance (Eloquent)
Cet adaptateur persiste les commandes en base de données via Eloquent.
// app/Adapters/OrderRepositoryAdapter.php
namespace App\Adapters;
use App\Domain\Order\Order;
use App\Domain\Order\Ports\OrderRepositoryPort;
use App\Models\Order as EloquentOrder;
class OrderRepositoryAdapter implements OrderRepositoryPort
{
public function save(Order $order): void
{
$eloquentOrder = new EloquentOrder();
$eloquentOrder->user_id = $order->userId;
$eloquentOrder->total = $order->total;
$eloquentOrder->items = json_encode($order->items);
$eloquentOrder->save();
$order->id = $eloquentOrder->id; // Met à jour l'ID de l'entité Order du domaine
}
}
3.2. Adaptateur pour les notifications (Email)
Cet adaptateur envoie des notifications par email via le service de mailing de Laravel.
// app/Adapters/NotificationAdapter.php
namespace App\Adapters;
use App\Domain\Order\Order;
use App\Domain\Order\Ports\NotificationPort;
use Illuminate\Support\Facades\Mail;
class NotificationAdapter implements NotificationPort
{
public function notifyOrderCreated(Order $order): void
{
Mail::raw("Your order has been created!", function ($message) use ($order) {
$message->to('user@example.com')->subject('Order Created');
});
}
}
Étape 4 : Enregistrement des dépendances
Nous devons lier les ports avec leurs adapters respectifs dans le service provider pour que Laravel sache injecter les bonnes implémentations.
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Domain\Order\Ports\OrderRepositoryPort;
use App\Domain\Order\Ports\NotificationPort;
use App\Adapters\OrderRepositoryAdapter;
use App\Adapters\NotificationAdapter;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Lier les ports aux adaptateurs concrets
$this->app->bind(OrderRepositoryPort::class, OrderRepositoryAdapter::class);
$this->app->bind(NotificationPort::class, NotificationAdapter::class);
}
public function boot()
{
//
}
}
Étape 5 : Utilisation avec une commande CLI
Cette commande permettra de créer une commande directement en ligne de commande.
// app/Console/Commands/CreateOrderCommand.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Domain\Order\OrderService;
class CreateOrderCommand extends Command
{
protected $signature = 'order:create {userId} {total} {--items=*}';
protected $description = 'Créer une nouvelle commande pour un utilisateur';
protected $orderService;
public function __construct(OrderService $orderService)
{
parent::__construct();
$this->orderService = $orderService;
}
public function handle()
{
$userId = $this->argument('userId');
$total = $this->argument('total');
$items = $this->option('items');
// Créer la commande
$order = $this->orderService->createOrder($userId, $items, $total);
$this->info("Commande créée avec succès, ID: {$order->id}");
}
}
Étape 6 : Exécution de la commande
Tu peux maintenant utiliser la commande artisan pour créer une commande :
php artisan order:create 1 199.99 --items="item1" --items="item2"
Cette commande va :
- Créer une nouvelle commande pour l'utilisateur avec l'ID 1.
- Sauvegarder cette commande en base de données.
- Envoyer un email de notification à l'utilisateur.
Avantages de l'Architecture Hexagonale
- Indépendance du domaine : Le domaine métier reste indépendant des détails d'implémentation, facilitant ainsi les tests unitaires et les modifications des couches externes.
- Facilité d'évolutivité : Il est facile d'ajouter de nouveaux adapters pour interagir avec d'autres systèmes externes sans modifier le domaine.
- Modularité : La séparation entre les ports et adapters améliore la modularité de l'application et favorise une meilleure gestion des dépendances.
Inconvénients de l'Architecture Hexagonale
- Complexité initiale : La mise en place de cette architecture demande un effort supplémentaire en raison de la séparation stricte des couches.
- Surcharge potentielle : Pour des projets de petite taille ou simples, l'architecture peut sembler excessive et introduire une complexité inutile.
Conclusion
L'Architecture Hexagonale (Ports and Adapters) est une solution architecturale puissante pour créer des applications extensibles, maintenables et faciles à tester. Elle est particulièrement adaptée aux systèmes complexes nécessitant une interaction avec des technologies externes ou plusieurs couches d'intégration. En Laravel, elle peut être appliquée pour organiser proprement les services métier, les adapters (comme les bases de données ou les API), et les clients (comme les interfaces utilisateur ou les systèmes tiers).