Comment traiter la validation des références entre agrégats?

11

Je me bats un peu avec le référencement entre agrégats. Supposons que l'agrégat Carait une référence à l'agrégat Driver. Cette référence sera modélisée en ayant Car.driverId.

Maintenant, mon problème est de savoir jusqu'où dois-je aller pour valider la création d'un Caragrégat dans CarFactory. Dois-je croire que le passé DriverIdfait référence à un existant Driver ou dois-je vérifier cet invariant?

Pour vérifier, je vois deux possibilités:

  • Je pourrais changer la signature de l'usine automobile pour accepter une entité de conducteur complète. L'usine choisirait alors simplement l'identifiant de cette entité et construirait la voiture avec cela. Ici, l'invariant est vérifié implicitement.
  • Je pourrais avoir une référence du DriverRepositorydans l' CarFactoryappel explicite driverRepository.exists(driverId).

Mais maintenant je me demande ce n'est pas trop de vérification invariante? Je pourrais imaginer que ces agrégats pourraient vivre dans un contexte borné séparé, et maintenant je polluerais la voiture BC avec des dépendances sur le DriverRepository ou l'entité Driver du driver BC.

De plus, si je parlais à des experts du domaine, ils ne remettraient jamais en question la validité de telles références. Je sens que je pollue mon modèle de domaine avec des préoccupations indépendantes. Mais là encore, à un moment donné, l'entrée utilisateur doit être validée.

Markus Malkusch
la source

Réponses:

6

Je pourrais changer la signature de l'usine automobile pour accepter une entité de conducteur complète. L'usine choisirait alors simplement l'identifiant de cette entité et construirait la voiture avec cela. Ici, l'invariant est vérifié implicitement.

Cette approche est attrayante car vous obtenez le chèque gratuitement et il est bien aligné avec le langage omniprésent. A Carn'est pas entraîné par a driverId, mais par a Driver.

Cette approche est en fait utilisée par Vaughn Vernon dans son contexte borné échantillon Identity & Access où il passe un Useragrégat à un Groupagrégat, mais le Groupseul tient sur un type de valeur GroupMember. Comme vous pouvez le voir, cela lui permet également de vérifier l'activation de l'utilisateur (nous sommes bien conscients que la vérification peut être périmée).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

Cependant, en passant l' Driverinstance, vous vous ouvrez également à une modification accidentelle de l' Driverintérieur Car. Passer une référence de valeur permet de raisonner plus facilement sur les changements du point de vue d'un programmeur, mais en même temps, DDD est tout au sujet du langage ubiquitaire, donc ça vaut peut-être le risque.

Si vous pouvez réellement trouver de bons noms pour appliquer le principe de ségrégation d'interface (ISP), vous pouvez compter sur une interface qui n'a pas les méthodes comportementales. Vous pourriez peut-être également proposer un concept d'objet de valeur qui représente une référence de pilote immuable et qui ne peut être instancié qu'à partir d'un pilote existant (par exemple DriverDescriptor driver = driver.descriptor()).

Je pourrais imaginer que ces agrégats pourraient vivre dans un contexte borné séparé, et maintenant je polluerais la voiture BC avec des dépendances sur DriverRepository ou l'entité Driver du driver BC.

Non, tu ne le ferais pas vraiment. Il y a toujours une couche anti-corruption pour s'assurer que les concepts d'un contexte ne saignent pas dans un autre. C'est en fait beaucoup plus facile si vous avez un BC dédié aux associations de conducteurs de voitures car vous pouvez modéliser des concepts existants tels que Caret Driverspécifiquement pour ce contexte.

Par conséquent, il se peut que vous ayez une personne DriverLookupServicedéfinie dans la Colombie-Britannique responsable de gérer les associations de conducteurs de voiture. Ce service peut appeler un service Web exposé par le contexte de gestion des pilotes qui renvoie des Driverinstances qui seront très probablement des objets de valeur dans ce contexte.

Notez que les services Web ne sont pas nécessairement la meilleure méthode d'intégration entre les BC. Vous pouvez également compter sur la messagerie où, par exemple, un UserCreatedmessage provenant du contexte de gestion des pilotes serait consommé dans un contexte distant qui stockerait une représentation du pilote dans sa propre base de données. Le DriverLookupServicepourrait alors utiliser cette base de données et les données du conducteur seraient tenues à jour avec d'autres messages (par exemple DriverLicenceRevoked).

Je ne peux pas vraiment vous dire quelle approche est la meilleure pour votre domaine, mais j'espère que cela vous donnera suffisamment d'informations pour prendre une décision.

plalx
la source
3

La façon dont vous posez la question (et proposez deux alternatives), c'est comme si la seule préoccupation était que le driverId soit toujours valide au moment de la création de la voiture.

Cependant, vous devez également vous inquiéter du fait que le pilote associé à driverId ne soit pas supprimé avant que la voiture soit supprimée ou donnée à un autre pilote (et peut-être aussi que le pilote ne soit pas affecté à une autre voiture (ceci si le domaine restreint un pilote à seulement être associé à une voiture)).

Je suggère qu'au lieu de la validation, vous allouez (ce qui inclurait la validation de la présence). Vous interdirez alors les suppressions pendant qu'elles sont encore allouées, vous protégeant ainsi de la condition de concurrence des données périmées pendant la construction, ainsi que de l'autre problème à plus long terme. (Notez que l'allocation valide et marque à la fois l'allocation et fonctionne atomiquement.)

Btw, je suis d'accord avec @PriceJones que l'association entre la voiture et le conducteur est probablement une responsabilité distincte de la voiture ou du conducteur. Ce type d'association ne fera que croître en complexité au fil du temps, car cela ressemble à un problème de programmation (pilotes, voitures, créneaux horaires / fenêtres, substituts, etc.). Même s'il s'agit plutôt d'un problème d'enregistrement, on peut vouloir l'historique les enregistrements ainsi que les enregistrements en cours. Ainsi, il pourrait très bien mériter sa propre Colombie-Britannique.

Vous pouvez fournir un schéma d'allocation (tel qu'un nombre booléen ou de référence) au sein de la BC des entités agrégées allouées, ou au sein d'une BC distincte, disons, celle responsable de l'association entre voiture et conducteur. Si vous faites le premier, vous pouvez autoriser les opérations de suppression (valides) émises pour la voiture ou le conducteur BC; si vous faites ce dernier, vous devrez empêcher les suppressions des BC voiture et conducteur et les envoyer à la place via le planificateur d'association voiture et conducteur.

Vous pouvez également répartir certaines des responsabilités d'allocation entre les BC comme suit. La voiture et le conducteur BC fournissent chacun un schéma "d'allocation" qui valide et définit le booléen alloué avec ce BC; lorsque leur attribution booléenne est définie, le BC empêche la suppression des entités correspondantes. (Et le système est configuré de sorte que la voiture et le conducteur BC autorisent uniquement l'allocation et la désallocation à partir de la programmation de l'association voiture / conducteur BC.)

Le BC de planification des voitures et des chauffeurs tient ensuite à jour un calendrier des conducteurs associés à la voiture pour certaines périodes / durées, actuelles et futures, et notifie les autres BC de la désallocation uniquement lors de la dernière utilisation d'une voiture ou d'un conducteur programmé.


En tant que solution plus radicale, vous pouvez traiter les BC de voiture et de chauffeur comme des usines d'enregistrement historique uniquement en annexe, laissant la propriété au planificateur d'association de voiture et de conducteur. La voiture BC peut générer une nouvelle voiture, avec tous les détails de la voiture, ainsi que son VIN. La propriété de la voiture est gérée par le planificateur d'association voiture / conducteur. Même si une association voiture / conducteur est supprimée et que la voiture elle-même est détruite, les enregistrements de la voiture existent toujours dans la voiture BC par définition, et nous pouvons utiliser la voiture BC pour rechercher des données historiques; tandis que les associations / propriétaires de voitures / conducteurs (passées, présentes et potentiellement futures prévues) sont gérées par un autre pays bénéficiaire.

Erik Eidt
la source
2

Supposons que la voiture agrégée ait une référence au pilote agrégé. Cette référence sera modélisée en ayant Car.driverId.

Oui, c'est la bonne façon de coupler un agrégat à un autre.

si je parlais à des experts du domaine, ils ne remettraient jamais en question la validité de ces références

Pas tout à fait la bonne question à poser à vos experts de domaine. Essayez "quel est le coût pour l'entreprise si le conducteur n'existe pas?"

Je n'utiliserais probablement pas DriverRepository pour vérifier le driverId. Au lieu de cela, j'utiliserais un service de domaine pour le faire. Je pense qu'il exprime mieux l'intention - sous les couvertures, le service de domaine vérifie toujours le système d'enregistrement.

Donc quelque chose comme

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

Vous interrogez réellement le domaine sur le driverId à un certain nombre de points différents

  • Du client, avant d'envoyer la commande
  • Dans l'application, avant de passer la commande au modèle
  • Dans le modèle de domaine, lors du traitement des commandes

Une ou toutes ces vérifications peuvent réduire les erreurs de saisie utilisateur. Mais ils travaillent tous à partir de données périmées; l'autre agrégat peut changer immédiatement après que nous posons la question. Il y a donc toujours un certain danger de faux négatifs / positifs.

  • Dans un rapport d'exception, exécutez une fois la commande terminée

Ici, vous travaillez toujours avec des données périmées (les agrégats peuvent exécuter des commandes pendant que vous exécutez le rapport, vous ne pourrez peut-être pas voir les écritures les plus récentes sur tous les agrégats). Mais les vérifications entre les agrégats ne seront jamais parfaites (Car.create (driver: 7) fonctionnant en même temps que Driver.delete (driver: 7)) Cela vous donne donc une couche supplémentaire de défense contre les risques.

VoiceOfUnreason
la source
1
Driver.deletene devrait pas exister. Je n'ai jamais vraiment vu un domaine où les agrégats sont détruits. En gardant les RA autour de vous, vous ne pouvez jamais vous retrouver avec des orphelins.
plalx
1

Il pourrait être utile de demander: Êtes-vous sûr que les voitures sont construites avec des chauffeurs? Je n'ai jamais entendu parler d'une voiture composée d'un conducteur dans le monde réel. La raison pour laquelle cette question est importante est qu'elle peut vous orienter vers la création indépendante de voitures et de conducteurs, puis la création d'un mécanisme externe qui attribue un conducteur à une voiture. Une voiture peut exister sans référence de conducteur et être toujours une voiture valide.

Si une voiture doit absolument avoir un conducteur dans votre contexte, vous voudrez peut-être considérer le modèle de constructeur. Ce modèle sera chargé de s'assurer que les voitures sont construites avec des pilotes existants. Les usines serviront des voitures et des conducteurs validés indépendamment, mais le constructeur s'assurera que la voiture a la référence dont elle a besoin avant de servir la voiture.

Price Jones
la source
J'ai aussi pensé à la relation voiture / conducteur - mais l'introduction d'un agrégat DriverAssignment ne fait que déplacer la référence à valider.
VoiceOfUnreason
1

Mais maintenant je me demande ce n'est pas trop de vérification invariante?

Je le pense. La récupération d'un DriverId donné à partir de la base de données renvoie un ensemble vide s'il n'existe pas. Donc, vérifier le résultat du retour rend inutile de demander s'il existe (et de le récupérer).

Ensuite, la conception de classe le rend également inutile

  • S'il y a une exigence "une voiture garée peut ou non avoir un chauffeur"
  • Si un objet Driver nécessite un DriverIdet est défini dans le constructeur.
  • Si vous n'avez Carbesoin que de DriverId, Driver.Idprenez un getter. Pas de passeur.

Le référentiel n'est pas le lieu des règles métier

  • A se Carsoucie s'il a un Driver(ou son ID au moins). A se Driversoucie s'il en a DriverId. Les Repositorysoucis de l'intégrité des données et se moquent des voitures sans conducteur.
  • La base de données aura des règles d'intégrité des données. Clés non nulles, contraintes non nulles, etc. Mais l'intégrité des données concerne le schéma de données / tables, pas les règles métier. Nous avons une relation symbiotique fortement corrélée dans ce cas, mais ne mélangez pas les deux.
  • Le fait que a DriverIdsoit un domaine métier est traité dans les classes appropriées.

Séparation des préoccupations Violation

... arrive quand Repository.DriverIdExists()pose la question.

Créez un objet de domaine. Sinon, Driveralors peut-être un objet DriverInfo(juste un DriverIdet Name, disons). Le DriverIdest validé à la construction. Il doit exister et être du bon type, et quoi que ce soit d'autre. Ensuite, c'est un problème de conception de classe client comment traiter un pilote / driverId inexistant.

Peut-être Carque ça va sans chauffeur jusqu'à ce que vous appeliez Car.Drive(). Dans ce cas, l' Carobjet assure bien sûr son propre état. On ne peut pas conduire sans Driver- enfin pas tout à fait.

Séparer une propriété de sa classe est mauvais

Bien sûr, ayez un Car.DriverIdsi vous le souhaitez. Mais cela devrait ressembler à ceci:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

Pas ça:

public class Car {
    public int DriverId {get; protected set;}
}

Maintenant, le Cardoit traiter de toutes les DriverIdquestions de validité - une violation du principe de responsabilité unique; et le code redondant probablement.

radarbob
la source