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, alors 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 e-mail 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 pour cela?
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 un 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 modèle User
et un EmailMustBeUnqiueRule
qui reçoivent 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 Rules
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 il n'est pas clair quand/où ils sont appliquées. Cela a-t-il du sens? Il peut aller de soi qu'un EmailMustBeUnqiueRule
sera appliqué lors de la création d'un User
, mais qu'en est-il de UserIsInGoodStandingRule
?. Lentement mais sûrement, la granularisation de l'extraction des Rules
hors 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 lié au fait que le Service
/CommandHandler
jette le Exception
est que votre logique métier commence à fuir ("vers le haut") de votre domaine. Pourquoi votre Service
/CommandHandler
doit-il savoir qu'un e-mail doit être unique? La couche de service d'application est généralement utilisée pour la coordination plutôt que pour l'implémentation. La raison de ceci peut être illustrée simplement si nous ajoutons une méthode/commande ChangeEmail
à votre système. Maintenant, 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 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'objets User
. Existe-t-il un concept dans votre domaine qui représente une "collection de User
objets"? 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 le morceau d'infrastructure supplémentaire nécessaire pour exécuter ce type de validation (le magasin de données). Dans votre cas, je placerais cette vérification dans la méthode add
. 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.
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.