Existe-t-il un moyen élégant de vérifier les contraintes uniques sur les attributs d'objet de domaine sans déplacer la logique métier dans la couche de service?


10

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 Nameexiste 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 Productou 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.ProductCollectionclasse 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.


Pourquoi tirer une liste entière de noms alors que vous pouvez simplement en demander un et baser la logique sur la question de savoir si oui ou non il a été trouvé? Vous dites à la couche de persistance quoi faire et non comment le faire tant qu'il y a un accord avec l'interface.
JeffO

1
Lorsque nous utilisons le terme service, nous devons viser une plus grande clarté, comme la différence entre les services de domaine dans la couche domaine et les services d'application dans la couche application. Voir gorodinski.com/blog/2012/04/14/…
Erik Eidt

Excellent article, @ErikEidt, merci. Je suppose que ma conception n'est pas fausse alors, si je peux faire confiance à M. Gorodinski, il dit à peu près la même chose: une meilleure solution consiste à demander à un service d'application de récupérer les informations requises par une entité, de configurer efficacement l'environnement d'exécution et de fournir à l'entité, et a la même objection contre l'injection de référentiel dans le modèle de domaine directement que moi, principalement en cassant le SRP.
Andy

Une citation de la référence "tell, don't ask": "Mais personnellement, je n'utilise pas tell-don't-ask."
radarbob

Réponses:


7

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.

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 .


Merci de votre contribution. Le référentiel pourrait-il réellement représenter ce Domain.ProductCollectionque j'avais en tête, étant donné qu'il est responsable de la récupération des objets de la couche Domaine?
Andy

@DavidPacker Je ne comprends pas vraiment ce que tu veux dire. Parce qu'il ne devrait pas être nécessaire de garder tous les éléments en mémoire. L'implémentation de la méthode "DoesNameExist" devrait être (très probablement) une requête SQL du côté du magasin de données.
Euphorique

Ce qui signifie, au lieu de stocker les données dans une collection en mémoire, quand je veux connaître tous les produits dont je n'ai pas besoin de les obtenir, Domain.ProductCollectionmais demander au référentiel à la place, tout comme pour demander, si le Domain.ProductCollectioncontient 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.
Andy

Ce qui conduit à une autre question, le référentiel devrait-il alors savoir si un attribut doit être unique? J'ai toujours implémenté des référentiels en tant que composants assez stupides, enregistrant ce que vous leur transmettez pour sauvegarder et essayant de récupérer des données en fonction des conditions passées et de mettre la décision en service.
Andy

@DavidPacker Eh bien, la "connaissance du domaine" se trouve dans l'interface. L'interface indique que "le domaine a besoin de cette fonctionnalité" et le magasin de données fournit alors cette fonctionnalité. Si vous avez une IProductRepositoryinterface avec DoesNameExist, 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.
Euphorique

4

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?

Le problème des exigences d'unicité est que, bien souvent, il y a une raison sous-jacente plus profonde pour laquelle les gens en veulent - Yves Reynhout

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.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

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.

Je n'aime pas la nécessité de retirer toutes les références produit de la base de données pour effectuer l'opération en mémoire.

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


Je n'aime pas la nécessité de retirer toutes les références produit de la base de données pour effectuer l'opération en mémoire. C'est la solution évidente que j'ai suggérée dans la question initiale et remis en question ses performances. En outre, l'OMI, s'appuyant sur la contrainte de votre base de données, pour être responsable de l'unicité, est mauvaise. Si vous deviez basculer vers un nouveau moteur de base de données et que d'une manière ou d'une autre la contrainte unique s'est perdue lors d'une transformation, vous avez cassé du code, car les informations de la base de données qui étaient auparavant n'étaient plus là. En tout cas merci pour le lien, lecture intéressante.
Andy

Peut-être un filtre Bloom (voir modifier).
VoiceOfUnreason
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.