Commençons par un petit rappel de ce que nous avons eu l’occasion d’aborder précédemment. Nous avons commencé par identifier nos contextes, nous avons ensuite créé nos entités et parlé très rapidement des ports et des adaptateurs dans une architecture hexagonale.

Aujourd’hui, nous allons nous intéresser de manière un peu plus précise aux services du domaine ainsi qu’aux objets-valeurs.

Un dépôt Github est toujours à votre disposition pour celles ou ceux d’entre vous qui aiment le concret !

Les services du domaine : la clé d’une architecture ouverte !

La dernière fois, nous avions aborder cette histoire de ports et d’adaptateurs où les ports étaient des interfaces et les adaptateurs l’implémentation de ces interfaces.

Dans notre exemple de blog, on avait par exemple le IPasswordHasher qui nous permettait de hasher un mot de passe. Au sein de notre domaine, le but est de s’intéresser uniquement au quoi et non au comment, relayant au second plan la question de l’implémentation.

Cet IPasswordHasher est un exemple de service du domaine. Il représente un besoin du domaine qu’on doit modéliser mais qu’on ne peut rattacher à une entité particulière car ce serait mettre trop de responsabilités sur cette dernière.

Les dépôts de données (IUserRepository par exemple) peuvent être considérés comme des services du domaine spécialisés dans la persistance des données.

Les services du domaine peuvent parfois prendre la forme d’implémentation sans état interne utilisant d’autres interfaces. Prenons un exemple très simple : l’authentification d’un utilisateur.

Dans ce cas précis, nous avons besoin d’exprimer cette fonctionnalité au sein de notre domaine mais aucune entité ne peut recevoir cette responsabilité. En effet, ce n’est pas à l’entité Utilisateur de savoir comment et dans quelles conditions un utilisateur peut s’authentifier, l’entité est seulement responsable de sa propre intégrité.

C’est donc comme ça que voit le jour l’AuthenticationService ! Il possédera une méthode qui prendra en paramètre toutes les interfaces nécessaires à l’authentification d’un utilisateur. La signature sera donc la suivante :

UserDescriptor Authenticate(IUserRepository userRepository, IPasswordHasher hasher, string name, string password_)

Ce service va permettre de récupérer l’utilisateur, de vérifier que le mot de passe correspond au hash stocké et enfin de retourner un objet représentant l’utilisateur si tout s’est bien passé et, le cas échéant, de lever des exceptions pour donner des informations sur la nature de l’erreur.

Certains d’entre vous se demandent peut-être à quoi sert le retour de type UserDescriptor et s’il s’agit d’une entité. Et bien non ! Il s’agit tout simplement d’un objet-valeur représentant l’utilisateur authentifié !

Les objets-valeurs

Mais qu’est ce donc qu’un objet-valeur ? Derrière ce terme se cache l’un des piliers permettant d’écrire du code maintenable et élégant.

Dis simplement, un objet-valeur est une classe immutable (qu’on ne peut modifier une fois instanciée)représentant un tout. Elle ne possède pas d’identité à proprement parlé car ce sont les valeurs de ses attributs qui fournissent cette information, 2 objets-valeurs étant égaux si leurs attributs sont eux-mêmes égaux.

Un objet-valeur va nous servir à regrouper des propriétés intrinsèquement liées et à les garder cohérentes au sein d’un seul et même objet. Le fait de ne pas pouvoir les modifier va nous permettre de garantir une certaine robustesse du code et étant donné que les objet-valeur représente un concept précis, il est extrêmement simple de les tester !

Comme je sens que je vous perds un peu, prenons l’exemple d’un objet valeur simple que vous avez sûrement eu déjà l’occasion de voir ou d’implémenter : une fourchette de dates. Pour le remettre dans notre contexte de blog abordé initialement, imaginons qu’un article peut-être mis en avant sur la page d’accueil pour une période donnée.

Notre classe Article possèdera donc une propriété Featured de type DateRange.

Cet objet-valeur (DateRange) possède donc 2 propriétés : une date de début et une date de fin. Il est évident que la date de fin doit être supérieure à la date de début donc on validera cette condition dans le constructeur, ainsi il ne sera pas possible d’instancier une fourchette invalide.

Une fois qu’une instance est créée, on ne peut la modifier, la fourchette est figée. Deux fourchettes sont égales si les dates de début et de fin coïncident.

Si on souhaite modifier les dates de mise en avant d’un article, alors on appellera la méthode de l’Article : IsFeatured(DateRange range). Comme on ne peut modifier un objet-valeur directement, on le remplace complétement !

Pour en revenir à notre UserDescriptor, il représente un “handle” utilisateur qu’on ne peut modifier, c’est donc un cas parfait d’objet-valeur !

Quand vous modélisez une entité, si plusieurs de ses propriétés sont liées car exprimant un tout, alors n’hésitez plus et construisez un objet-valeur que vous pourrez réutiliser dans une autre entité, tester facilement et qui vous permettra de rendre votre code plus compréhensible.

En clair, ne sous-estimez pas la force des objets-valeurs !

Conclusion

Avec ces nouveaux outils à votre disposition, modéliser un domaine devrait commencer à prendre du sens. Les services du domaine vous permettent de conserver la connaissance métier au sein de votre domaine sans pour autant l’attacher à une entité qui ne serait pas appropriée et les objets-valeurs de simplifier votre code en regroupant ce qui doit l’être tout en le rendant plus gérable et maintenable dans le temps.