Je plonge dans les concepts de la conception pilotée par domaine (DDD) et j'ai trouvé certains principes étranges, en particulier en ce qui concerne l'isolement du domaine et le modèle de persistance. Voici ma compréhension de base:
- Un service sur la couche application (fournissant un ensemble de fonctionnalités) demande des objets de domaine à un référentiel dont il a besoin pour exécuter sa fonction.
- L'implémentation concrète de ce référentiel récupère les données du stockage pour lequel il a été implémenté
- Le service indique à l'objet de domaine, qui encapsule la logique métier, d'effectuer certaines tâches qui modifient son état.
- Le service indique au référentiel de conserver l'objet de domaine modifié.
- Le référentiel doit mapper l'objet de domaine à la représentation correspondante dans le stockage.
Maintenant, compte tenu des hypothèses ci-dessus, les éléments suivants semblent maladroits:
Annonce 2:
Le modèle de domaine semble charger l'intégralité de l'objet de domaine (y compris tous les champs et références), même s'ils ne sont pas nécessaires pour la fonction qui l'a demandé. Le chargement complet peut même ne pas être possible si d'autres objets de domaine sont référencés, sauf si vous chargez également ces objets de domaine et tous les objets qu'ils référencent tour à tour, etc. Le chargement paresseux vient à l'esprit, ce qui signifie cependant que vous commencez à interroger vos objets de domaine, ce qui devrait être la responsabilité du référentiel en premier lieu.
Compte tenu de ce problème, la façon «correcte» de charger les objets de domaine semble avoir une fonction de chargement dédiée pour chaque cas d'utilisation. Ces fonctions dédiées ne chargeraient alors que les données requises par le cas d'utilisation pour lequel elles ont été conçues. Voici où la maladresse entre en jeu: Premièrement, je devrais maintenir une quantité considérable de fonctions de chargement pour chaque implémentation du référentiel, et les objets de domaine se retrouveraient dans des états incomplets transportés null
dans leurs domaines. Ce dernier ne devrait techniquement pas être un problème car si une valeur n'était pas chargée, elle ne devrait de toute façon pas être requise par la fonctionnalité qui la demandait. C'est toujours maladroit et un danger potentiel.
Annonce 3:
Comment un objet de domaine vérifierait-il les contraintes d'unicité lors de la construction s'il n'a aucune notion du référentiel? Par exemple, si je voulais créer un nouveau User
avec un numéro de sécurité sociale unique (qui est donné), le conflit le plus précoce se produirait en demandant au référentiel de sauvegarder l'objet, uniquement si une contrainte d'unicité était définie sur la base de données. Sinon, je pourrais chercher un User
avec la sécurité sociale donnée et signaler une erreur si elle existe, avant d'en créer une nouvelle. Mais alors les vérifications de contraintes vivraient dans le service et non dans l'objet de domaine auquel elles appartiennent. Je viens de réaliser que les objets de domaine sont très bien autorisés à utiliser des référentiels (injectés) pour la validation.
Annonce 5:
Je perçois le mappage des objets de domaine à un backend de stockage comme un processus exigeant en termes de travail par rapport au fait que les objets de domaine modifient directement les données sous-jacentes. Il s'agit bien sûr d'une condition préalable essentielle pour dissocier l'implémentation concrète du stockage du code de domaine. Cependant, cela coûte-t-il vraiment si cher?
Vous avez apparemment la possibilité d'utiliser des outils ORM pour effectuer le mappage pour vous. Cependant, cela nécessiterait souvent que vous conceviez le modèle de domaine en fonction des restrictions de l'ORM, ou même introduisiez une dépendance du domaine à la couche d'infrastructure (en utilisant des annotations ORM dans les objets de domaine, par exemple). J'ai également lu que les ORM introduisent une surcharge de calcul considérable.
Dans le cas des bases de données NoSQL, pour lesquelles il n'existe pratiquement aucun concept de type ORM, comment pouvez-vous suivre les propriétés modifiées dans les modèles de domaine save()
?
Modifier : pour qu'un référentiel accède à l'état de l'objet domaine (c'est-à-dire la valeur de chaque champ), l'objet domaine doit révéler son état interne qui rompt l'encapsulation.
En général:
- Où irait la logique transactionnelle? Ceci est certainement spécifique à la persistance. Certaines infrastructures de stockage peuvent même ne pas prendre en charge les transactions (comme les référentiels fictifs en mémoire).
- Pour les opérations en bloc qui modifient plusieurs objets, devrais-je charger, modifier et stocker chaque objet individuellement afin de passer par la logique de validation encapsulée de l'objet? Cela s'oppose à l'exécution d'une seule requête directement sur la base de données.
J'apprécierais quelques éclaircissements sur ce sujet. Mes hypothèses sont-elles correctes? Sinon, quelle est la bonne façon de résoudre ces problèmes?
Réponses:
Votre compréhension de base est correcte et l'architecture que vous dessinez est bonne et fonctionne bien.
En lisant entre les lignes, vous semblez provenir d'un style de programmation d'enregistrement actif plus centré sur la base de données? Pour arriver à une implémentation fonctionnelle, je dirais que vous devez
1: Les objets de domaine n'ont pas à inclure le graphique d'objet entier. Par exemple, je pourrais avoir:
L'adresse et le client ne doivent faire partie du même agrégat que si vous avez une logique telle que «le nom du client ne peut commencer que par la même lettre que le nom de la maison». Vous avez raison d'éviter le chargement paresseux et les versions «allégées» des objets.
2: Les contraintes d'unicité sont généralement du ressort du référentiel et non de l'objet de domaine. N'injectez pas de référentiels dans les objets de domaine, il s'agit d'un retour à l'enregistrement actif, il s'agit simplement d'une erreur lorsque le service tente de sauvegarder.
La règle métier n'est pas "Deux instances d'Utilisateur avec le même SocialSecurityNumber ne peuvent jamais exister en même temps"
C'est qu'ils ne peuvent pas exister sur le même référentiel.
3: Il n'est pas difficile d'écrire des référentiels plutôt que des méthodes de mise à jour des propriétés individuelles. En fait, vous constaterez que vous avez à peu près le même code dans les deux cas. C'est juste dans quelle classe vous l'avez mis.
Les ORM de nos jours sont faciles et n'ont pas de contraintes supplémentaires sur votre code. Cela dit, personnellement, je préfère simplement lancer manuellement le SQL. Ce n'est pas si difficile, vous ne rencontrez jamais de problèmes avec les fonctionnalités ORM et vous pouvez optimiser si nécessaire.
Il n'est vraiment pas nécessaire de garder une trace des propriétés modifiées lors de l'enregistrement. Gardez vos objets de domaine petits et écrasez simplement l'ancienne version.
Questions générales
La logique de transaction va dans le référentiel. Mais vous ne devriez pas en avoir grand-chose. Bien sûr, vous en avez besoin si vous avez des tables enfants dans lesquelles vous placez les objets enfants de l'agrégat, mais qui seront entièrement encapsulées dans la méthode du référentiel SaveMyObject.
Mises à jour en masse. Oui, vous devez modifier individuellement chaque objet, puis ajouter simplement une méthode SaveMyObjects (List objects) à votre référentiel, pour effectuer la mise à jour en masse.
Vous souhaitez que l'objet de domaine ou le service de domaine contienne la logique. Pas la base de données. Cela signifie que vous ne pouvez pas simplement "mettre à jour le nom de l'ensemble client = x où y", car pour tout ce que vous connaissez, l'objet Client, ou CustomerUpdateService fait 20 autres choses étranges lorsque vous changez le nom.
la source
AddressId
au lieu deAddress
) ne contredisent-ils pas les principes OO?Réponse courte: Votre compréhension est correcte et les questions que vous vous posez indiquent des problèmes valables pour lesquels les solutions ne sont pas simples ni universellement acceptées.
Point 2 .: (chargement de graphiques d'objets complets)
Je ne suis pas le premier à souligner que les ORM ne sont pas toujours une bonne solution. Le principal problème étant que les ORM ne savent rien du cas d'utilisation réel, ils n'ont donc aucune idée de ce qu'il faut charger ou comment optimiser. C'est un problème.
Comme vous l'avez dit, la solution évidente est d'avoir des méthodes de persistance pour chaque cas d'utilisation. Mais si vous utilisez toujours un ORM pour cela, l'ORM vous forcera à tout emballer dans des objets de données. Ce qui, en plus de ne pas être vraiment orienté objet, n'est pas non plus la meilleure conception pour certains cas d'utilisation.
Que faire si je souhaite simplement mettre à jour en masse certains enregistrements? Pourquoi aurais-je besoin d'une représentation d'objet pour tous les enregistrements? Etc.
Donc, la solution à cela est simplement de ne pas utiliser un ORM pour les cas d'utilisation pour lesquels il ne convient pas. Mettre en œuvre un cas d'utilisation "naturellement" tel qu'il est, qui ne nécessite parfois pas une "abstraction" supplémentaire des données elles-mêmes (objets de données) ni une abstraction sur les "tables" (référentiels).
Le fait d'avoir des objets de données à moitié remplis ou de remplacer les références d'objet par des "id" est au mieux une solution de contournement, pas une bonne conception, comme vous l'avez souligné.
Point 3 .: (vérification des contraintes)
Si la persistance n'est pas abstraite, chaque cas d'utilisation peut évidemment vérifier facilement la contrainte qu'il souhaite. L'exigence que les objets ne connaissent pas le «référentiel» est complètement artificielle et n'est pas un problème de technologie.
Point 5: (ORM)
Non, non. Il existe de nombreuses autres façons de persister. Le problème est que l'ORM est toujours considéré comme "la" solution à utiliser (pour les bases de données relationnelles au moins). Essayer de suggérer de ne pas l'utiliser pour certains cas d'utilisation dans un projet est futile et dépendre de l'ORM lui-même parfois même impossible, car les caches et l'exécution tardive sont parfois utilisés par ces outils.
Question générale 1: (transactions)
Je ne pense pas qu'il existe une seule solution. Si votre conception est orientée objet, il y aura une méthode "top" pour chaque cas d'utilisation. La transaction devrait être là.
Toute autre restriction est complètement artificielle.
Question générale 2: (opérations en vrac)
Avec un ORM, vous êtes (pour la plupart des ORM que je connais) obligé de passer par des objets individuels. Ceci est complètement inutile et ne serait probablement pas votre conception si votre main n'était pas liée par l'ORM.
L'obligation de séparer la "logique" de SQL vient des ORM. Ils doivent le dire, car ils ne peuvent pas l'appuyer. Ce n'est pas intrinsèquement «mauvais».
Sommaire
Je suppose que mon argument est que les ORM ne sont pas toujours le meilleur outil pour un projet, et même s'il l'est, il est très peu probable qu'il soit le meilleur pour tous les cas d'utilisation d'un projet.
De même, l'abstraction dataobject-repository de DDD n'est pas toujours la meilleure non plus. J'irais même jusqu'à dire que c'est rarement le design optimal.
Cela ne nous laisse pas de solution unique, nous devons donc réfléchir individuellement à des solutions pour chaque cas d'utilisation, ce qui n'est pas une bonne nouvelle et rend évidemment notre travail plus difficile :)
la source