J'adapte la conception basée sur le domaine depuis environ 8 ans maintenant et même après toutes ces années, il y a encore une chose qui me dérange. Cela vérifie un enregistrement unique dans le stockage de données par rapport à un objet de domaine.
En septembre 2013, Martin Fowler a mentionné le principe TellDon'tAsk , qui, si possible, devrait être appliqué à tous les objets de domaine, qui devrait ensuite renvoyer un message, comment l'opération s'est déroulée (dans la conception orientée objet, cela se fait principalement par le biais d'exceptions, lorsque l'opération a échoué).
Mes projets sont généralement divisés en plusieurs parties, dont deux sont le domaine (contenant des règles métier et rien d'autre, le domaine est complètement ignorant de la persistance) et les services. Services connaissant la couche de référentiel utilisée pour les données CRUD.
Étant donné que l'unicité d'un attribut appartenant à un objet est une règle de domaine / métier, le module de domaine doit être long, de sorte que la règle est exactement là où elle est censée se trouver.
Afin de pouvoir vérifier l'unicité d'un enregistrement, vous devez interroger l'ensemble de données actuel, généralement une base de données, pour savoir si un autre enregistrement avec un disons Name
existe déjà.
Considérant que la couche de domaine est ignorante de la persistance et n'a aucune idée de comment récupérer les données mais seulement comment effectuer des opérations sur elles, elle ne peut pas vraiment toucher les référentiels eux-mêmes.
Le design que j'ai ensuite adapté ressemble à ceci:
class ProductRepository
{
// throws Repository.RecordNotFoundException
public Product GetBySKU(string sku);
}
class ProductCrudService
{
private ProductRepository pr;
public ProductCrudService(ProductRepository repository)
{
pr = repository;
}
public void SaveProduct(Domain.Product product)
{
try {
pr.GetBySKU(product.SKU);
throw Service.ProductWithSKUAlreadyExistsException("msg");
} catch (Repository.RecordNotFoundException e) {
// suppress/log exception
}
pr.MarkFresh(product);
pr.ProcessChanges();
}
}
Cela conduit à avoir des services définissant les règles de domaine plutôt que la couche de domaine elle-même et vous avez les règles dispersées sur plusieurs sections de votre code.
J'ai mentionné le principe TellDon'tAsk, car comme vous pouvez le voir clairement, le service propose une action (il enregistre Product
ou lève une exception), mais à l'intérieur de la méthode, vous opérez sur des objets en utilisant une approche procédurale.
La solution évidente est de créer une Domain.ProductCollection
classe avec une Add(Domain.Product)
méthode lançant le ProductWithSKUAlreadyExistsException
, mais cela manque beaucoup de performances, car vous auriez besoin d'obtenir tous les produits du stockage de données afin de savoir dans le code, si un produit a déjà le même SKU comme le produit que vous essayez d'ajouter.
Comment résolvez-vous ce problème spécifique? Ce n'est pas vraiment un problème en soi, la couche de service représente certaines règles de domaine depuis des années. La couche de service sert généralement également des opérations de domaine plus complexes, je me demande simplement si vous êtes tombé sur une solution meilleure et plus centralisée au cours de votre carrière.
Réponses:
Je ne serais pas d'accord avec cette partie. Surtout la dernière phrase.
S'il est vrai que le domaine doit être ignorant de la persistance, il sait qu'il existe une "collection d'entités de domaine". Et qu'il existe des règles de domaine qui concernent cette collection dans son ensemble. L'unicité étant l'un d'entre eux. Et parce que l'implémentation de la logique réelle dépend fortement du mode de persistance spécifique, il doit y avoir une sorte d'abstraction dans le domaine qui spécifie le besoin de cette logique.
C'est donc aussi simple que de créer une interface qui peut interroger si le nom existe déjà, qui est ensuite implémentée dans votre magasin de données et appelée par quiconque a besoin de savoir si le nom est unique.
Et je voudrais souligner que les référentiels sont des services DOMAIN. Ce sont des abstractions autour de la persistance. C'est l'implémentation du référentiel qui doit être distinct du domaine. Il n'y a absolument rien de mal à ce que l'entité de domaine appelle un service de domaine. Il n'y a rien de mal à ce qu'une entité puisse utiliser le référentiel pour récupérer une autre entité ou récupérer des informations spécifiques, qui ne peuvent pas être facilement conservées en mémoire. C'est la raison pour laquelle le référentiel est un concept clé dans le livre d'Evans .
la source
Domain.ProductCollection
que j'avais en tête, étant donné qu'il est responsable de la récupération des objets de la couche Domaine?Domain.ProductCollection
mais demander au référentiel à la place, tout comme pour demander, si leDomain.ProductCollection
contient un produit avec le passé SKU, cette fois encore, en demandant à la place le référentiel (c'est en fait l'exemple), qui au lieu d'itérer sur les produits préchargés interroge la base de données sous-jacente. Je ne veux pas stocker tous les produits en mémoire, sauf si je le dois, ce serait un non-sens complet.IProductRepository
interface avecDoesNameExist
, il est évident que la méthode doit faire. Mais s'assurer que le produit ne peut pas avoir le même nom que le produit existant doit être quelque part dans le domaine.Vous devez lire Greg Young sur la validation du plateau .
Réponse courte: avant d'aller trop loin dans le nid de rats, vous devez vous assurer que vous comprenez la valeur de l'exigence du point de vue commercial. Combien coûte vraiment la détection et l'atténuation de la duplication, plutôt que de la prévenir?
Réponse plus longue: j'ai vu un menu de possibilités, mais elles ont toutes des compromis.
Vous pouvez vérifier les doublons avant d'envoyer la commande au domaine. Cela peut être fait dans le client ou dans le service (votre exemple montre la technique). Si vous n'êtes pas satisfait de la fuite logique de la couche de domaine, vous pouvez obtenir le même type de résultat avec a
DomainService
.Bien sûr, de cette manière, l'implémentation du DeduplicationService devra savoir quelque chose sur la façon de rechercher les skus existants. Donc, même si cela repousse une partie du travail dans le domaine, vous êtes toujours confronté aux mêmes problèmes de base (besoin d'une réponse pour la validation de l'ensemble, problèmes de conditions de concurrence).
Vous pouvez effectuer la validation dans votre couche de persistance elle-même. Les bases de données relationnelles sont vraiment bonnes pour la validation des ensembles. Mettez une contrainte d'unicité sur la colonne sku de votre produit, et vous êtes prêt à partir. L'application enregistre simplement le produit dans le référentiel et vous obtenez une violation de contrainte remontant en cas de problème. Ainsi, le code d'application semble bon, et votre condition de concurrence est éliminée, mais vous avez des règles de "domaine" qui fuient.
Vous pouvez créer un agrégat distinct dans votre domaine qui représente l'ensemble des skus connus. Je peux penser à deux variantes ici.
L'un est quelque chose comme un ProductCatalog; les produits existent ailleurs, mais la relation entre les produits et les skus est maintenue par un catalogue qui garantit l'unicité des sku. Cela ne signifie pas que les produits n'ont pas de skus; les skus sont attribués par un ProductCatalog (si vous avez besoin que les skus soient uniques, vous pouvez y parvenir en n'ayant qu'un seul agrégat ProductCatalog). Passez en revue le langage omniprésent avec vos experts en domaine - si une telle chose existe, cela pourrait bien être la bonne approche.
Une alternative est quelque chose de plus comme un service de réservation de sku. Le mécanisme de base est le même: un agrégat connaît tous les skus, il peut donc empêcher l'introduction de doublons. Mais le mécanisme est légèrement différent: vous acquérez un bail sur un sku avant de l'attribuer à un produit; lors de la création du produit, vous passez le bail au sku. Il y a toujours une condition de concurrence en jeu (différents agrégats, donc des transactions distinctes), mais il a une saveur différente. Le véritable inconvénient ici est que vous projetez dans le modèle de domaine un service de location sans vraiment avoir de justification dans la langue du domaine.
Vous pouvez extraire toutes les entités de produits en un seul agrégat, c'est-à-dire le catalogue de produits décrit ci-dessus. Vous obtenez absolument l'unicité des skus lorsque vous faites cela, mais le coût est un conflit supplémentaire, la modification d'un produit signifie vraiment la modification de l'ensemble du catalogue.
Peut-être que vous n'en avez pas besoin. Si vous testez votre sku avec un filtre Bloom , vous pouvez découvrir de nombreux skus uniques sans charger le jeu du tout.
Si votre cas d'utilisation vous permet d'être arbitraire sur les skus que vous rejetez, vous pouvez éliminer tous les faux positifs (ce n'est pas grave si vous autorisez les clients à tester les skus qu'ils proposent avant de soumettre la commande). Cela vous permettrait d'éviter de charger l'ensemble en mémoire.
(Si vous vouliez être plus tolérant, vous pourriez envisager de charger les skus paresseux en cas de correspondance dans le filtre de bloom; vous risquez toujours de charger tous les skus en mémoire parfois, mais ce ne devrait pas être le cas commun si vous autorisez le code client pour vérifier la commande des erreurs avant l'envoi).
la source