Domain-Driven Design dans les projets Rust : quand les types deviennent vos alliés
Avez-vous déjà écrit une application Rust dans laquelle la logique métier se noyait dans une mer de structures, de traits et de macros ? Ce sentiment familier, quand, en regardant le code, vous ne pouvez pas dire immédiatement ce que fait telle ou telle fonction dans le contexte du domaine métier ? Si oui — bienvenue au club. Le Domain-Driven Design (DDD) est conçu pour résoudre ce problème, et Rust, avec son système de types, est un terrain idéal pour implémenter les principes du DDD.
Dans cet article, nous ne allons pas paraphraser le livre d'Evans. Au lieu de cela, je vais montrer comment appliquer le Domain-Driven Design dans les projets Rust avec des exemples concrets. Vous apprendrez comment, grâce aux types-wrapper, à l'architecture modulaire et aux structures expressives, rendre le code domaine propre, sûr et compréhensible. C'est parti !
Pourquoi Rust et DDD forment un duo parfait
Le Domain-Driven Design exige une isolation stricte de la logique domaine par rapport à l'infrastructure. Rust fournit des outils puissants pour cela :
- Système de types — permet de modéliser les règles métier au niveau du compilateur. Une erreur ? Le compilateur ne vous laissera pas la commettre.
- Possession et emprunt — garantissent que les agrégats et les entités ne seront pas modifiés accidentellement à un endroit inattendu.
- Système modulaire — sépare clairement la couche domaine, l'infrastructure et l'application.
Mais il y a aussi des pièges. Rust ne pardonne pas les objets « lourds » avec beaucoup d'état mutable. Par conséquent, le DDD en Rust nécessite une approche de modélisation différente de celle, par exemple, en Java ou en C#.
Idée clé : rendez les états invalides irréprésentables
En DDD, nous cherchons à ce que les règles métier soient encodées dans la structure même des données. En Rust, cela se traduit par un mantra : « Si un état est invalide — faites en sorte qu'il soit impossible à créer ».
Au lieu d'écrire :
struct User { name: String, email: String, age: u8,}Nous créons des types qui garantissent déjà la correction dès la phase de compilation :
pub struct Email(String);pub struct Age(u8);pub struct UserName(String);
pub struct User { name: UserName, email: Email, age: Age,}Cela semble n'être que des wrappers ? En réalité, c'est le premier pas vers le DDD en Rust, où les types deviennent une documentation et un gardien des invariants.
Modéliser le domaine : Value Object et Entity
Les concepts centraux du DDD sont Entity (entité) et Value Object (objet-valeur). En Rust, ils sont implémentés différemment.
Value Object en Rust : immutabilité et égalité par valeur
Un Value Object n'a pas d'identité. Deux objets sont égaux si leurs champs sont égaux. En Rust, cela s'intègre parfaitement avec les structures utilisant #[derive(PartialEq)] et des champs immutables.
use std::fmt;
#[derive(Debug, Clone, PartialEq)]pub struct Money { amount: i64, currency: Currency,}
#[derive(Debug, Clone, PartialEq)]pub enum Currency { USD, EUR, RUB,}
impl Money { pub fn new(amount: i64, currency: Currency) -> Self { // Règle métier : l'argent ne peut pas être négatif assert!(amount >= 0, "Amount must be non-negative"); Self { amount, currency } }
pub fn add(&self, other: &Money) -> Result<Self, String> { if self.currency != other.currency { return Err("Cannot add different currencies".into()); } Ok(Self::new(self.amount + other.amount, self.currency.clone())) }}Remarquez : Money est immutable. Toute opération retourne un nouvel objet. Cela correspond à la philosophie du DDD en Rust : pas de setters, seulement des méthodes créant de nouvelles instances.