Exceptions dans DDD

11

J'apprends le DDD et je pense à lancer des exceptions dans certaines situations. Je comprends qu'un objet ne peut pas entrer dans un mauvais état, donc ici les exceptions sont bien, mais dans de nombreux exemples, des exceptions sont également lancées, par exemple si nous essayons d'ajouter un nouvel utilisateur avec un courrier électronique existant dans la base de données.

public function doIt(UserData $userData)
{
    $user = $this->userRepository->byEmail($userData->email());
    if ($user) {
        throw new UserAlreadyExistsException();
    }

    $this->userRepository->add(
        new User(
            $userData->email(),
            $userData->password()
        )
    );
}

Donc, si l'utilisateur avec cet e-mail existe, nous pouvons intercepter une exception dans le service d'application, mais nous ne devons pas contrôler le fonctionnement de l'application à l'aide du bloc try-catch.

Quelle est la meilleure façon de procéder?

yadiv
la source
1
Avoir des exceptions de service de domaine (gestionnaires) est très bien.
Andy

Réponses:

20

Commençons par un bref examen de l'espace des problèmes: l'un des principes fondamentaux de DDD est de placer les règles métier aussi près que possible des endroits où elles doivent être appliquées. Il s'agit d'un concept extrêmement important car il rend votre système plus "cohérent". Déplacer les règles "vers le haut" est généralement le signe d'un modèle anémique; où les objets ne sont que des sacs de données et les règles sont injectées avec ces données à appliquer.

Un modèle anémique peut avoir beaucoup de sens pour les développeurs qui commencent tout juste avec DDD. Vous créez un Usermodèle et un EmailMustBeUnqiueRulequi obtient injecté les informations nécessaires pour valider l'e-mail. Facile. Élégant. Le problème est que ce "type" de pensée est fondamentalement de nature procédurale. Pas DDD. Ce qui finit par se produire, c'est que vous vous retrouvez avec un module avec des dizaines de Rulespaquets soigneusement emballés et encapsulés, mais ils sont complètement dépourvus de contexte au point où ils ne peuvent plus être modifiés car on ne sait pas quand / où ils sont appliqués. Cela a-t-il du sens? Il va peut- être de soi qu'un EmailMustBeUnqiueRuletestament sera appliqué sur la création d'un User, mais qu'en est-il UserIsInGoodStandingRule? Lentement mais sûrement, la granularisation de l'extraction desRulessortir de leur contexte vous laisse avec un système difficile à comprendre (et donc non modifiable). Les règles doivent être encapsulées uniquement lorsque le resserrement / l'exécution réels sont si verbeux que votre modèle commence à perdre le focus.

Passons maintenant à votre question spécifique: le problème avec le Service/ CommandHandlerthrow Exceptionest que votre logique métier commence à fuir ("vers le haut") hors de votre domaine. Pourquoi votre Service/ CommandHandlerbesoin de connaître un e-mail doit-il être unique? La couche de service d'application est généralement utilisée pour la coordination plutôt que pour la mise en œuvre. La raison de ceci peut être illustrée simplement si nous ajoutons une ChangeEmailméthode / commande à votre système. Désormais, les DEUX méthodes / gestionnaires de commandes devraient inclure votre vérification unique. C'est là qu'un développeur peut être tenté "d'extraire" un fichier EmailMustBeUniqueRule. Comme expliqué ci-dessus, nous ne voulons pas emprunter cette voie.

Un resserrement des connaissances supplémentaires peut nous conduire à une réponse plus DDD. L'unicité d'un e-mail est un invariant qui doit être appliqué à travers une collection d' Userobjets. Existe-t-il un concept dans votre domaine qui représente une "collection d' Userobjets"? Je pense que vous pouvez probablement voir où je vais ici.

Pour ce cas particulier (et bien d'autres impliquant l'application d'invariants dans les collections), le meilleur endroit pour implémenter cette logique sera dans votre Repository. Ceci est particulièrement pratique car votre Repository"connaît" également la partie supplémentaire de l'infrastructure nécessaire pour exécuter ce type de validation (le magasin de données). Dans votre cas, je placerais cette vérification dans la addméthode. Cela a du sens, non? Conceptuellement, c'est cette méthode qui ajoute vraiment un Userà votre système. Le magasin de données est un détail d'implémentation.

glissière côté roi
la source
Puis-je vous demander ce que vous entendez par Moving rules "upwards" is generally a sign of an anemic model? Vers le haut signifie pour la couche application? ou au modèle de domaine?
Anyname Donotcare
1
«Vers le haut» signifie vers votre couche d'application (pensez à une architecture en couches). J'ai abordé cela ci-dessus, mais la raison pour laquelle le déplacement des règles vers le haut est le signe d'un modèle anémique est qu'il représente la séparation des données et du comportement d'une manière qui va à l'encontre de l'objectif global d'avoir un modèle de domaine principal. Si la validation d'un e-mail est dans un module distinct de l'envoi d'un e-mail, votre Emailmodèle doit être un sac de données qui est inspecté pour les invariants avant l'envoi. Voir: softwareengineering.stackexchange.com/questions/372338/…
king-side-slide
2
Déplacer la logique du domaine vers le référentiel est incorrect. vous devez appliquer la règle de domaine par racine agrégée dans la couche de domaine.
Arash
1
La validation de @Arash Set est délicate et ne fonctionne souvent pas bien avec DDD. Il vous reste soit à choisir de valider qu'un certain processus fonctionnera avant de tenter ce processus (en ajoutant a User), soit à accepter que ce type spécifique d'invariant ne soit pas soigneusement encapsulé dans votre domaine. Le premier représente une séparation des données et des comportements résultant en un paradigme procédural. C'est loin d'être idéal. La validation doit exister avec les données, pas autour des données. De plus, quel avantage y a-t-il à créer un service de domaine qui ne sert qu'à vérifier cette seule règle? En apparence, ce service ...
king-side-slide
1
@Arash associe votre Repository, Useret cet invariant et n'atteint donc pas la vision puriste que vous préconisez de toute façon. Les implémentations de référentiel font partie du domaine et peuvent donc se voir confier certaines responsabilités.
king-side-slide
0

Vous pouvez imposer une validation dans chaque couche de votre application. Où imposer les règles qui dépendent de votre application.

Par exemple, les entités implémentent des méthodes qui représentent des règles métier tandis que les cas d'utilisation implémentent des règles spécifiques à votre application.

Si vous construisez un service de messagerie électronique comme Gmail, on pourrait affirmer que la règle "les utilisateurs doivent avoir une adresse e-mail unique" est une règle commerciale.

Si vous créez un processus d'enregistrement pour un nouveau système CRM, cette règle est probablement mieux implémentée dans un cas d'utilisation, car cette règle est également susceptible de faire partie de votre cas d'utilisation. En outre, il peut également être plus facile de tester, car vous pouvez facilement bloquer les appels du référentiel dans vos tests de cas d'utilisation.

Pour une sécurité supplémentaire, vous pouvez également appliquer cette règle dans votre référentiel.

alors
la source