Points de données
Coder pour la conception pilotée par domaine : conseils pour les développeurs focalisés sur les données
Télécharger l'exemple de code
Cette année, le livre novateur sur la conception de logiciels d'Eric Evans, « Domain-Driven Design: Tackling Complexity in the Heart of Software » (Addison-Wesley Professional, 2003, amzn.to/ffL1k), célèbre le 10e anniversaire de sa sortie. Ce livre est riche de l'expérience des très nombreuses années qu'Evans a passé à guider de grandes entreprises en matière d'architecture logicielle. Evans a ensuite passé plusieurs années à réfléchir au moyen d'encapsuler les modélisations qui mènent ces projets à la réussite : les interactions avec le client, l'analyse des problématiques commerciales à résoudre, la mise en place d'équipes et l'architecture des logiciels. Ces modélisations concernent principalement le domaine de l'entreprise. Ensemble, elles représentent la conception pilotée par domaine (DDD - Domain-Driven Design). Grâce à la conception pilotée par domaine, vous modélisez le domaine en question. Les modélisations résultent de l'abstraction de vos connaissances sur le domaine. Aujourd'hui encore, une relecture de l'avant-propos de Martin Fowler et de la préface d'Evans fournit une riche vue d'ensemble de l'essence de la conception pilotée par domaine.
Dans cet article comme dans les deux suivants, je vais partager des conseils qui m'ont aidé à clarifier mes pensées en ce qui concerne la focalisation sur les données et Entity Framework, ainsi qu'à améliorer mon code grâce aux modèles techniques de conception pilotée par domaine.
La conception pilotée par domaine, quel intérêt ?
J'ai découvert la conception pilotée par domaine au travers de la vidéo d'un bref entretien sur InfoQ.com avec Jimmy Nilsson, un concepteur respecté de la communauté .NET (entre autres), qui parlait de LINQ to SQL et d'Entity Framework (bit.ly/11DdZue). À la fin de l'entretien, Nilsson est invité à citer son ouvrage technique préféré. Sa réponse : « Mon livre d'informatique préféré est celui d'Eric Evans, « Domain-Driven Design ». C'est de la poésie. Ce n'est pas seulement le contenu qui est fascinant. Vous pouvez le relire plusieurs fois, comme un recueil de poésie. » De la poésie ! À l'époque, j'étais en train d'écrire mon premier ouvrage technique, « Programming Entity Framework » (O’Reilly Media, 2009), et sa description m'a intriguée. J'ai donc lu une partie du livre d'Evans pour voir ce qu'il en retournait. Le style d'écriture d'Evans est superbe et fluide. Associé à sa conception perspicace et naturaliste du développement de logiciels, cela rend l'ouvrage très plaisant à lire. Mais j'ai également été surprise par le contenu. Non seulement son style était merveilleux, mais ce qu'il décrivait m'a fascinée. Il ne parlait pas seulement d'écrire du code au kilomètre, mais d'établir des relations avec les clients et de réellement comprendre leurs activités commerciales et les problèmes qui leurs sont associés (en relation avec le logiciel en question). C'est quelque chose qui m'a toujours tenu à cœur au cours de mes 25 ans de développement de logiciels. Je voulais en savoir plus.
Pendant quelques années encore, j'ai tourné autour de la conception pilotée par domaine, puis j'ai vraiment commencé à me documenter sur le sujet. J'ai rencontré Evans lors d'une conférence, puis j'ai participé à son atelier de quatre jours en immersion. Je suis loin d'être une experte en matière de conception pilotée par domaine, mais je me suis rendu compte que le modèle de contexte limité pouvait m'être très utile immédiatement dans l'optique d'orienter mon propre processus de création de logiciels vers une structure plus organisée et maniable. Pour en savoir plus sur ce sujet, reportez-vous à mon article du mois de janvier 2013, « Réduire les modèles EF avec des contextes associés DDD » (msdn.microsoft.com/magazine/jj883952).
Depuis, j'ai poursuivi mon exploration du sujet. La conception pilotée par domaine m'intrigue et m'inspire, mais ma focalisation sur les données me freine dans ma compréhension des modèles techniques qui en font un succès. Il semble probable que de nombreux développeurs soient confrontés au même problème. Je vais donc partager certaines des leçons que j'ai apprises grâce à l'aide, à l'intérêt et à la générosité d'Evans et d'un certain nombre d'autres adeptes et enseignants de la conception pilotée par domaine, notamment Paul Rayner, Vaughn Vernon, Greg Young, Cesar de la Torre et Yves Reynhout.
En matière de modélisation de domaine, oublions la persistance
La modélisation du domaine doit se focaliser sur les tâches de l'entreprise. Lorsque je conçois des types, ainsi que leurs propriétés et comportements, la tentation est grande de réfléchir à la façon dont une relation va fonctionner dans la base de données et à la façon dont mon infrastructure de mappage objet-relationnel (ORM) de prédilection, Entity Framework, va traiter les propriétés, les relations et les hiérarchies d'héritage que je conçois. À moins que vous ne conceviez un logiciel pour une entreprise dont l'activité est le stockage et la récupération de données, comme Dropbox, la persistance des données ne joue qu'un rôle secondaire dans votre application. C'est un peu comme faire appel à l'API d'une source de données météorologiques afin d'afficher la température actuelle pour un utilisateur. Ou d'envoyer des données depuis votre application vers un service externe, comme un service d'inscription à onvasortir.com. Bien entendu, vos données sont peut-être plus complexes, mais avec une approche de conception pilotée par domaine des contextes limités, en restant focalisé sur les comportements et en appliquant les préceptes de la conception pilotée par domaine lors de la construction des types, la persistance peut être bien moins complexe que les systèmes que vous concevez aujourd'hui.
Si vous avez bien étudié votre ORM, et que vous avez appris à configurer le mappage des bases de données par l'intermédiaire de l'API Fluent d'Entity Framework, vous devriez parvenir à faire en sorte que la persistance fonctionne de la façon souhaitée. Dans le pire des cas, vous devrez ajuster légèrement vos classes. Dans un cas extrême, comme celui d'une ancienne base de données, vous pouvez même ajouter un modèle de persistance conçu pour le mappage de base de données, puis utiliser un élément tel qu'AutoMapper pour résoudre les choses entre votre modèle de domaine et votre modèle de persistance.
Mais ces considérations ne concernent pas le problème d'entreprise que votre logiciel est censé résoudre. La persistance ne doit donc pas interférer avec la conception du domaine. Il s'agit pour moi d'un véritable défi car lorsque je conçois mes entités, je ne peux m'empêcher de prendre en compte la façon dont Entity Framework déduira leurs mappages de base de données. J'essaie donc de ne pas me laisser perturber.
Setters privés et méthodes publiques
Un autre principe consiste à rendre les setters de propriété privés. Au lieu d'autoriser le code appelant à définir diverses propriétés de façon aléatoire, contrôlez plutôt l'interaction avec les objets DDD et les données qui leur sont associées par l'intermédiaire de méthodes qui modifient les propriétés. Et, non, je ne fais pas référence aux méthodes telles que SetFirstName et SetLastName. Par exemple, au lieu d'instancier un nouveau type Customer, puis de définir chacune de ses propriétés, vous pouvez envisager certaines règles lors de la création du client. Vous pouvez intégrer ces règles au constructeur du type Customer, utiliser une méthode Factory Pattern ou même inclure une méthode Create dans le type Customer. La figure 1 illustre un type Customer défini selon le modèle DDD d'une racine d'agrégat (c'est-à-dire, le « parent » d'un graphique d'objets, également appelé « entité racine » en DDD). Les propriétés Customer sont dotées de setters privés afin que seuls d'autres membres de la classe Customer puissent affecter directement ces propriétés. La classe expose un constructeur pour contrôler la façon dont il est instancié, et masque le constructeur sans paramètre (exigé par Entity Framework) en tant qu'élément interne.
Figure 1 Propriétés et méthodes d'un type agissant comme une racine d'agrégat
public class Customer : Contact
{
public Customer(string firstName,string lastName, string email)
{ ... }
internal Customer(){ ... }
public void CopyBillingAddressToShippingAddress(){ ... }
public void CreateNewShippingAddress(
string street, string city, string zip) { ... }
public void CreateBillingInformation(
string street, string city, string zip,
string creditcardNumber, string bankName){ ... }
public void SetCustomerContactDetails(
string email, string phone, string companyName){ ... }
public string SalesPersonId { get; private set; }
public CustomerStatus Status{get;private set;}
public Address ShippingAddress { get; private set; }
public Address BillingAddress { get;private set; }
public CustomerCreditCard CreditCard { get; private set; }
}
Le type Customer contrôle et protège les autres entités de l'agrégat (des adresses et un type de carte de crédit) en exposant des méthodes spécifiques (telles que CopyBillingAddressToShippingAddress) avec lesquelles ces objets seront créés et manipulés. La racine d'agrégat doit s'assurer que les règles qui définissent chaque entité au sein de l'agrégat sont appliquées en utilisant la logique et le comportement du domaine implémentés dans ces méthodes. Plus important encore, la racine de l'agrégat est chargée de la logique d'invariant et de la cohérence dans l'intégralité de l'agrégat. Je parlerai plus longuement des invariants dans mon prochain article. Pour le moment, je vous recommande la lecture du billet de blog de Jimmy Bogard, « Strengthening Your Domain: Aggregate Construction » (Renforcer votre domaine : construction d'agrégats), sur bit.ly/ewNZ52, qui fournit une excellente explication des invariants dans les agrégats.
En fin de compte, c'est le comportement qui est exposé par Customer plutôt que les propriétés : CopyBillingAddressToShippingAddress, CreateNewShippingAddress, CreateBillingInformation et SetCustomerContactDetails.
Remarquez que le type Contact, dont Customer est dérivé, réside dans une Assembly différente appelée « Common », car il peut être nécessaire à d'autres classes. J'ai besoin de masquer les propriétés de Contact, mais elles ne peuvent être privées, sinon Customer ne pourrait y accéder. Elles sont donc plutôt définies comme Protected :
public class Contact: Identity
{
public string CompanyName { get; protected set; }
public string EmailAddress { get; protected set; }
public string Phone { get; protected set; }
}
Une parenthèse au sujet des Identités : Customer et Contact peuvent ressembler à des objets de valeur DDD car ils n'ont pas de valeur clé. Toutefois, dans ma solution, la valeur clé est fournie par la classe Identity de laquelle Contact est dérivé. Et aucun de ces types n'est immuable, ce qui signifie qu'ils ne peuvent être considérés comme des objets de valeur de toute manière.
Étant donné que Customer hérite de Contact, il aura accès à ces priorités protégées et pourra les définir, comme dans la méthode SetCustomerContactDetails suivante :
public void SetCustomerContactDetails (string email, string phone, string companyName)
{
EmailAddress = email;
Phone = phone;
CompanyName = companyName;
}
Parfois, CRUD suffit.
Il n'est pas nécessaire d'utiliser la conception pilotée par domaine pour créer tout ce que contient votre application. La conception pilotée par domaine vous aide à gérer les comportements complexes. S'il vous suffit d'effectuer de l'édition ou des requêtes brutes et aléatoires, vous n'avez besoin de rien d'autre qu'une simple classe (ou un ensemble de classes), définie comme vous le feriez avec Code First d'Entity Framework (par l'intermédiaire des propriétés et des relations) et combinée avec les méthodes d'insertion, de mise à jour et de suppression (via un référentiel ou simplement DbContext). Par conséquent, pour accomplir quelque chose comme la création d'une commande et de ses postes, la conception pilotée par domaine peut vous aider à négocier des règles et comportements particuliers propres à certaines méthodes commerciales. Par exemple, le client qui place la commande a-t-il une carte de fidélité ? Dans ce cas, vous devez obtenir des détails sur le client afin de déterminer si la réponse est oui et, le cas échéant, appliquer une remise de 10 pour cent à chaque article ajouté à la commande. L'utilisateur a-t-il fourni les informations liées à sa carte de crédit ? Il est alors possible que vous deviez appeler un service de vérification pour vous assurer de la validité de la carte.
La clé de la conception pilotée par domaine est d'inclure la logique du domaine en tant que méthodes dans les classes d'entités du domaine, en tirant parti d'OOP au lieu d'implémenter des « scripts transactionnels » au sein d'objets commerciaux sans état, ce à quoi ressemble une classe de démonstration Code First normale.
Mais vous effectuez parfois des opérations tout à fait classiques, comme la création d'un contact : nom, adresse, référent, et ainsi de suite, avant d'enregistrer. Autrement dit, une opération CRUD (Create, Read, Update, Delete – Créer, Lire, Mettre à jour, Supprimer). Cette opération ne requiert en aucun cas la création d'agrégats, de racines, ni de comportements.
Il est probable que votre application contienne une combinaison de comportements complexes et d'opérations CRUD simples. Prenez le temps de clarifier les comportements et ne perdez pas votre temps, votre énergie, ni votre argent à complexifier outre mesure l'architecture des éléments de votre application qui sont simples. Dans ces cas-là, il est important d'identifier les limites entre divers sous-systèmes et contextes limités. Un contexte limité peut être piloté par les données (CRUD simple), alors qu'un contexte limité critique essentiel au domaine doit être conçu par l'intermédiaire d'approches de conception pilotée par domaine.
Les données partagées peuvent être une calamité dans les systèmes complexes
Un autre problème sur lequel je me suis pris la tête, puis sur lequel j'ai râlé tant et plus pendant qu'on essayait de m'expliquer gentiment, concernait le partage des types et des données entre plusieurs sous-systèmes. Il est devenu évident que je ne pouvais pas avoir le beurre et l'argent du beurre. J'ai donc été obligée de revoir mon hypothèse selon laquelle je devais absolument, à tout prix, partager les types entre plusieurs sous-systèmes et faire en sorte que ces types interagissent avec le même tableau de la même base de données.
J'apprends à partager les données à bon escient et à arrêter de me battre contre des moulins à vent. Certaines choses ne valent pas le coup d'être tentées, comme mapper à partir de contextes différents vers un seul tableau ou même une seule base de données. L'exemple le plus courant est le partage d'un type Contact qui tente de satisfaire les besoins de chacun entre les systèmes. Comment réconcilier et exploiter le contrôle de la source pour un type Contact susceptible d'être nécessaire dans de nombreux systèmes ? Que se passe-t-il si un système doit modifier la définition de ce type Contact ? Dans le cas d'un ORM, comment mapper un Contact utilisé sur plusieurs systèmes vers un seul tableau d'une seule base de données ?
La conception pilotée par domaine vous éloigne du partage de modèles et de données de domaine en vous expliquant que vous n'êtes pas toujours obligé de pointer vers le même tableau d'une seule base de données.
Ma retenue vient des 25 ans que j'ai passé à me focaliser sur les avantages de la réutilisation du code et des données. J'ai donc un peu de mal à accepter l'idée suivante, mais je commence à m'y faire : il n'est pas criminel de dupliquer des données. Bien entendu, toutes les données ne correspondent pas à ce paradigme nouveau (pour moi). Mais comment cela se passe-t-il avec quelque chose de plus léger, le nom d'une personne, par exemple ? Que se passe-t-il si vous dupliquez le prénom et le nom d'une personne dans plusieurs tableaux ou même plusieurs bases de données dédiées à différents sous-systèmes de votre solution logicielle ? À long terme, en abandonnant la complexité du partage de données, vous simplifiez énormément le travail de construction de votre système. Dans tous les cas, vous devez toujours minimiser la duplication des données et des attributs dans différents contextes limités. Vous avez parfois simplement besoin de l'identifiant du client et de son état afin de calculer des remises dans un contexte limité par la tarification. Le prénom et le nom d'un même client peuvent uniquement être nécessaires dans le contexte limité par la gestion de contacts.
Il reste cependant énormément d'informations à partager entre les systèmes. Vous pouvez exploiter ce qui est nommé, dans le contexte de la conception pilotée par domaine, une « couche anticorruption » (qui peut être quelque chose d'aussi simple qu'un service ou une file d'attente de messages) afin de vous assurer que, par exemple, si quelqu'un crée un contact dans un système, soit vous reconnaissez que la personne existe déjà ailleurs, soit vous vous assurez que la personne, ainsi qu'une clé d'identité commune, est créée dans un autre sous-système.
Des pistes de réflexion pour le mois prochain
Au fur et à mesure de mes progrès dans l'apprentissage et la compréhension du côté technique de la conception pilotée par domaine, j'ai lutté pour concilier de vieilles habitudes et des idées nouvelles, et je suis arrivée à de nombreuses épiphanies. Les conseils que j'ai dispensés ici sont ceux qui m'ont réellement éclairée. Parfois, il s'agit simplement d'une question de point de vue, et la façon dont j'ai exprimé ces conseils ici reflète le point de vue qui m'a permis d'y voir plus clair.
Je partagerai certaines de mes épiphanies dans mon prochain article, où j'évoquerai ce terme condescendant que vous avez peut-être déjà entendu : « domaine de modèle anémique », ainsi que son cousin dans la famille de la conception pilotée par domaine, le « modèle de domaine riche ». J'évoquerai également les relations unidirectionnelles et ce à quoi s'attendre lorsque le moment est venu d'ajouter la persistance des données si vous utilisez Entity Framework. Je parlerai également d'autres sujets liés à la conception pilotée par domaine, qui m'ont donné énormément de fil à retordre, en espérant ainsi aplanir vos difficultés d'apprentissage.
D'ici là, pourquoi ne pas examiner de plus près vos propres classes et tenter de voir comment encore mieux tout contrôler, en masquant ces setters de propriétés, et en exposant des méthodes plus descriptives et explicites. N'oubliez pas : les méthodes « SetLastName » ne sont pas admises. C'est de la triche !
Julie Lerman est une Microsoft MVP, mentor et conseillère .NET qui habite dans les collines du Vermont. Elle participe régulièrement à des groupes d'utilisateurs et à des conférences dans le monde entier, où elle partage son savoir-faire dans le domaine de l'accès aux données et d'autres sujets liés à Microsoft .NET. Son blog est accessible à l'adresse thedatafarm.com/blog. Elle est en outre l'auteur de « Programming Entity Framework » (2010), ainsi que d'une édition Code First (2011) et d'une édition DbContext (2012), publiées chez O’Reilly Media. Vous pouvez la suivre sur Twitter à l'adresse twitter.com/julielerman et consulter ses cours sur Pluralsight sur juliel.me/PS-Videos
Merci aux experts techniques suivants d'avoir relu cet article : Cesar de la Torre (Microsoft)