L'isolement du domaine / modèle de persistance est-il généralement aussi délicat?

12

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:

  1. 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.
  2. 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é
  3. Le service indique à l'objet de domaine, qui encapsule la logique métier, d'effectuer certaines tâches qui modifient son état.
  4. Le service indique au référentiel de conserver l'objet de domaine modifié.
  5. Le référentiel doit mapper l'objet de domaine à la représentation correspondante dans le stockage.

Illustration de flux

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 nulldans 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 Useravec 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 Useravec 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?

Double M
la source
1
Bons points et questions, je suis également intéressé par ceux-ci. Remarque - si vous modélisez correctement l'agrégat, cela signifie qu'à un moment donné de son existence, l'instance d'agrégat doit être dans un état valide - c'est le point principal de l'agrégat (et ne pas utiliser d'agrégat comme conteneur de composition). Cela signifie également que pour restaurer la forme agrégée des données de base de données, le référentiel lui-même devrait généralement utiliser un constructeur spécifique et un ensemble d'opérations de mutation, et je ne vois pas comment un ORM pourrait automatiquement savoir comment effectuer ces opérations .
Dusan
2
Ce qui est encore plus décevant, c'est que ces questions comme la vôtre sont posées assez souvent, mais, à ma connaissance - il y a ZÉRO d'exemples de mise en œuvre des agrégats et des référentiels qui sont par le livre
Dusan

Réponses:

5

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:

public class Customer
{
    public string AddressId {get;set;}
    public string Name {get;set;}
}

public class Address
{
    public string Id {get;set;}
    public string HouseNumber {get;set;
}

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

  1. 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.

  2. 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.

Ewan
la source
Très bonne réponse. Vous avez absolument raison, je suis habitué à un style de codage d'enregistrement actif, c'est pourquoi le modèle de référentiel semble étrange à première vue. Cependant, les objets de domaine "lean" ( AddressIdau lieu de Address) ne contredisent-ils pas les principes OO?
Double M du
non, vous avez toujours un objet Address, ce n'est tout simplement pas un enfant de Customer
Ewan
mappage d'objets publicitaires sans suivi des modifications softwareengineering.stackexchange.com/questions/380274/…
Double M
2

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)

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?

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 :)

Robert Bräutigam
la source
Points très intéressants là-dedans, merci d'avoir confirmé mes hypothèses. Vous avez dit qu'il existe de nombreuses autres façons de persister. Pouvez-vous recommander un modèle de conception performant à utiliser avec des bases de données graphiques (pas d'ORM), qui fournirait toujours PI?
Double M
1
Je me demande en fait si vous avez besoin d'isolement (et quel type) en premier lieu. L'isolement par technologie (c.-à-d. Base de données, interface utilisateur, etc.) apporte presque automatiquement la «maladresse» que vous essayez d'éviter, pour l'avantage d'un remplacement un peu plus facile de la technologie de base de données. Le coût est cependant un changement de logique métier plus difficile car il se propage à travers les couches. Ou vous pouvez répartir les fonctions métier, ce qui rendrait la modification des bases de données plus difficile, mais la modification de la logique plus facile. Lequel voulez-vous vraiment?
Robert Bräutigam
1
Vous pouvez obtenir les meilleures performances si vous modélisez simplement le domaine (c'est-à-dire les fonctions métier) et n'abstenez pas la base de données (que ce soit relationnel ou graphique, peu importe). Étant donné que la base de données n'est pas abstraite du cas d'utilisation, le cas d'utilisation peut implémenter les requêtes / mises à jour les plus optimales qu'il souhaite, et n'a pas besoin de passer par un modèle d'objet maladroit pour accomplir ce qu'il veut.
Robert Bräutigam
Eh bien, l'objectif principal est de garder les préoccupations de persistance loin de la logique métier, afin d'avoir un code propre, facile à comprendre, à développer et à tester. La possibilité d'échanger des technologies DB n'est qu'un bonus. Je peux voir qu'il y a évidemment une friction entre l'efficacité et l'ignorance, qui semble être plus forte avec les bases de données graphiques en raison des requêtes puissantes que vous pouvez (mais n'êtes pas autorisé à utiliser).
Double M du
1
En tant que développeur Java Enterprise, je peux vous dire que nous avons essayé de séparer la persistance de la logique au cours des deux dernières décennies. Ça ne marche pas. Premièrement, la séparation n'a jamais été vraiment réalisée. Même aujourd'hui, il y a toutes sortes de choses liées à la base de données dans des objets soi-disant "métier", le principal étant l'ID de la base de données (et beaucoup d'annotations de base de données). Deuxièmement, comme vous l'avez dit, la logique métier est parfois exécutée dans la base de données de toute façon. Troisièmement, c'est la raison pour laquelle nous avons des bases de données spécifiques, pour pouvoir décharger la logique la mieux faite là où se trouvent les données.
Robert Bräutigam