Réduire la complexité des systèmes est un défi perpétuel en ingénierie logicielle. Si les patrons de conception et les techniques de refactoring adressent les complexités techniques, ils peinent souvent à résoudre les problèmes fondamentaux de modélisation métier. Le Domain-Driven Design (DDD) déplace le focus vers le domaine métier, offrant une approche architecturale globale. Cependant, le DDD étant une philosophie plutôt qu'un framwork strict, il manque de contraintes inhérentes au niveau du code. Cela conduit fréquemment à la prolifération de modèles de domaine anémiques, un problème exacerbé par les outils ORM traditionnels et les architectures MVC en couches. Pour combler le fossé entre la théorie DDD et l'implémentation pratique, il est crucial d'établir des conventions de codage robustes. La fondation de cette approche repose sur la Primitive de Domaine (DP).
Le concept de Primitive de Domaine
Tout comme Integer et String sont les blocs de construction primitifs d'un langage de programmation, les Primitives de Domaine sont les éléments fondamentaux d'un modèle métier. Une DP est un Value Object explicitement défini, auto-validant et encapsulant des comportements métier spécifiuqes.
Étude de Cas : Inscription de Client et Assignation Régionale
Considérons l'exigence métier suivante : un système enregistre de nouveaux clients et assigne un agent commercial régional en fonction de l'indicatif téléphonique du téléphone fixe du client.
Implémentation Initiale
public class Client {
Long clientId;
String fullName;
String phone;
String address;
Long agentId;
}
public class ClientRegistrationService {
private AgentRepository agentRepo;
private ClientRepository clientRepo;
public Client register(String fullName, String phone, String address) {
if (fullName == null || fullName.isBlank()) {
throw new IllegalArgumentException("Invalid name");
}
if (phone == null || !phone.matches("^0[1-9]{2,3}-?\\d{8}$")) {
throw new IllegalArgumentException("Invalid phone");
}
String areaCode = extractAreaCode(phone);
Agent agent = agentRepo.findByAreaCode(areaCode);
Client client = new Client();
client.fullName = fullName;
client.phone = phone;
client.address = address;
if (agent != null) {
client.agentId = agent.getId();
}
return clientRepo.save(client);
}
private String extractAreaCode(String phone) {
String[] validCodes = {"0571", "021", "010"};
for (int i = 1; i < 5; i++) {
String prefix = phone.substring(0, i);
if (Arrays.asList(validCodes).contains(prefix)) {
return prefix;
}
}
return null;
}
}
Analyse de l'Approche Initiale
- Clarté de l'Interface : Les signatures de méthodes comme
register(String, String, String)perdent les noms des paramètres à l'exécution. Intervertir l'ordre des arguments compile avec succès mais provoque des bugs silencieux. - Validation et Gestion des Erreurs : La logique de validation est dispersée et répétée, violant le principe DRY. Mélanger les exceptions métier et les exceptions de validation complique la gestion des erreurs pour l'appelant.
- Clarté de la Logique Métier : L'extraction de l'indicatif nécessite du "code colle" qui obscurcit l'intention métier. L'utilisation de classes utilitaires statiques fragmente la logique.
- Testabilité : Tester la validation de chaque paramètre à travers plusieurs méthodes entraîne une explosion combinatoire des cas de test.
Refactoring avec les Primitives de Domaine
Nous appliquons le principe : Rendre les Concepts Implicites Explicites. Le numéro de téléphone n'est pas une simple chaîne de caractères ; c'est un concept métier avec des règles et des comportements propres.
public final class LandlinePhone {
private final String number;
public LandlinePhone(String number) {
if (number == null || !number.matches("^0[1-9]{2,3}-?\\d{8}$")) {
throw new DomainValidationException("Invalid landline format");
}
this.number = number;
}
public String getNumber() {
return number;
}
public String extractAreaCode() {
String[] validCodes = {"0571", "021", "010"};
for (int i = 1; i < 5; i++) {
String prefix = number.substring(0, i);
if (Arrays.asList(validCodes).contains(prefix)) {
return prefix;
}
}
throw new DomainValidationException("Area code not recognized");
}
}
Le service est alors simplifié :
public class ClientRegistrationService {
// ... repositories ...
public Client register(FullName fullName, LandlinePhone phone, PostalAddress address) {
Agent agent = agentRepo.findByAreaCode(phone.extractAreaCode());
Client client = new Client();
client.setFullName(fullName);
client.setContactPhone(phone);
client.setLocation(address);
if (agent != null) {
client.assignAgent(agent.getId());
}
return clientRepo.save(client);
}
}
Bénéfices du Refactorign
- Clarté : La signature
register(FullName, LandlinePhone, PostalAddress)est auto-documentée et immunisée contre les erreurs d'ordre de paramètres. - Validation : La validation est repoussée dans le constructeur de la DP. La couche de service ne traite que des objets valides, éliminant les vérifications répétitives.
- Cohésion : L'extraction de l'indicatif devient un comportement de
LandlinePhone, supprimant le code colle et les classes utilitaires. - Testabilité : Les tests de validation sont isolés dans la DP. La couche de service n'a besoin de tester que les flux métier, réduisant le nombre total de tests d'une multiplication à une addition.
Principes Avancés des Primitives de Domaine
Principe 2 : Rendre le Contexte Implicite Explicite
Considérons une méthode de paiement : processPayment(BigDecimal amount, Long recipientId). Cela suppose une devise par défaut. Si le système s'étend à l'international, cette supposition implicite devient un bug critique. En encapsulant le montant et la devise dans une DP MonetaryAmount, le contexte devient explicite.
public final class MonetaryAmount {
private final BigDecimal value;
private final Currency currency;
public MonetaryAmount(BigDecimal value, Currency currency) {
this.value = Objects.requireNonNull(value);
this.currency = Objects.requireNonNull(currency);
}
public BigDecimal getValue() { return value; }
public Currency getCurrency() { return currency; }
}
Principe 3 : Encapsuler le Comportement Multi-Objets
Lorsqu'une opération métier implique plusieurs objets, comme la conversion de devises pour des transactions transfrontalières, la logique a tendance à fuir dans la couche de service. L'encapsulation dans une DP résout ce problème.
public final class ForexRate {
private final BigDecimal multiplier;
private final Currency source;
private final Currency target;
public ForexRate(BigDecimal multiplier, Currency source, Currency target) {
this.multiplier = multiplier;
this.source = source;
this.target = target;
}
public MonetaryAmount convert(MonetaryAmount original) {
if (!original.getCurrency().equals(this.source)) {
throw new IllegalArgumentException("Currency mismatch");
}
BigDecimal convertedValue = original.getValue().multiply(this.multiplier);
return new MonetaryAmount(convertedValue, this.target);
}
}
Cela encapsule la logique de conversion, gardant la couche de service propre et focalisée sur l'orchestration.
Définition et Cas d'Usage
Une Primitive de Domaine est un Value Object strictement défini, auto-validant et comportemental, spécifique à un domaine métier. Elle s'appuie sur le Value Object traditionnel du DDD en exigeant une complétude conceptuelle, une validité dès l'instanciation et des comportements sans effets de bord.
Quand appliquer les Primitives de Domaine :
- Chaînes contraintes :
EmailAddress,PhoneNumber,TrackingNumber. - Nombres contraints :
Percentage(0-100),Quantity(non négatif). - Décimales significatives :
Temperature,MonetaryAmount,ExchangeRate. - Structures complexes : Encapsuler des Maps ou des Listes pour n'exposer que les opérations pertinentes pour le domaine.
Refactoring d'Applications Existantes
Intégrer des Primitives de Domaine dans une base de code existante peut se faire de manière incrémentale :
- Identifier et Extraire : Repérez la logique de validation dispersée et les règles métier sans état liées à un concept spécifique, et déplacez-les dans une nouvelle classe DP.
- Remplacer la Logique Interne : Instanciez le DP au début des méthodes existantes pour remplacer les validations en ligne et les appels d'utilitaires, tout en gardant temporairement la signature de la méthode inchangée.
- Mettre à Jour les Signatures : Modifiez les signatures des méthodes pour accepter le DP à la place des types primitifs.
- Propager les Changements : Mettez à jour les appelants pour qu'ils construisent et transmettent le DP, repoussant ainsi la validation aux frontières du système.