Come trattare la validazione dei riferimenti tra aggregati?

11

Sto lottando un po 'con il riferimento tra gli aggregati. Supponiamo che l'aggregato Car abbia un riferimento all'aggregato Driver . Questo riferimento sarà modellato avendo Car.driverId .

Ora il mio problema è quanto dovrei andare lontano per convalidare la creazione di un aggregato Car in CarFactory . Dovrei fidarmi del fatto che il% passato di co_de si riferisca a un esistente % diDriverId o dovrei controllare quell'invarianza?

Per il controllo, vedo due possibilità:

  • Potrei cambiare la firma della fabbrica di automobili per accettare un'entità pilota completa. La fabbrica quindi sceglierebbe l'id da quell'entità e costruisse l'auto con quella. Qui l'invariante è controllato implicitamente.
  • Potrei avere un riferimento di Driver in DriverRepository e chiamare esplicitamente CarFactory .

Ma ora mi chiedo se non sia troppo controllo invariante? Potrei immaginare che quegli aggregati potrebbero vivere in un contesto separato separato, e ora vorrei inquinare la macchina BC con dipendenze sul DriverRepository o l'entità Driver del driver BC.

Inoltre, se parlassi con esperti di dominio, non metterebbero mai in discussione la validità di tali riferimenti. Ho la sensazione di inquinare il mio modello di dominio con preoccupazioni non correlate. Ma poi di nuovo, a un certo punto l'input dell'utente dovrebbe essere validato.

    
posta Markus Malkusch 27.05.2016 - 12:37
fonte

5 risposte

5

I could change the signature of the car factory to accept a complete driver entity. The factory would then just pick the id from that entity and build the car with that. Here the invariant is checked implicitly.

Questo approccio è allettante dal momento che si ottiene il controllo gratuitamente ed è ben allineato con la lingua onnipresente. A Car non è guidato da driverId , ma da Driver .

Questo approccio è in effetti utilizzato da Vaughn Vernon nel suo Identity & Accedi a contesto limitato del campione in cui passa un aggregato User a un aggregato Group , ma Group mantiene solo un tipo di valore GroupMember . Come puoi vedere, questo gli consente anche di verificare l'abilitazione dell'utente (siamo ben consapevoli che il controllo potrebbe essere inattivo).

    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
        }
    }

Tuttavia, passando l'istanza Driver ti apri anche a una modifica accidentale del Driver in Car . Il passaggio del riferimento al valore rende più facile ragionare sui cambiamenti dal punto di vista del programmatore, ma allo stesso tempo, il DDD è tutto basato sull'Ubiquitous Language, quindi forse vale il rischio.

Se riesci a trovare un buon nome per applicare il principio di segregazione dell'interfaccia (ISP) allora puoi fare affidamento su un'interfaccia che non ha i metodi comportamentali. Potresti anche inventare un concetto di oggetto valore che rappresenta un riferimento al driver immutabile e che può essere istanziato solo da un driver esistente (ad esempio DriverDescriptor driver = driver.descriptor() ).

I could imagine that those aggregates might live in separate bounded context, and now I would pollute the car BC with dependencies on the DriverRepository or the Driver entity of the driver BC.

No, non lo faresti in realtà. C'è sempre un livello di anti-corruzione per assicurarsi che i concetti di un contesto non si riversino in un altro. In realtà è molto più semplice se disponi di un BC dedicato alle associazioni di driver automobilistici perché puoi modellare concetti esistenti come Car e Driver specificamente per quel contesto.

Pertanto, potresti avere una DriverLookupService definita nel responsabile BC per gestire le associazioni di automobilisti. Questo servizio può chiamare un servizio web esposto dal contesto Gestione driver che restituisce Driver istanze che molto probabilmente saranno oggetti valore in questo contesto.

Si noti che i servizi web non sono necessariamente il miglior metodo di integrazione tra BC. Si potrebbe anche fare affidamento sulla messaggistica in cui, ad esempio, un messaggio UserCreated dal contesto di gestione driver verrebbe utilizzato in un contesto remoto che memorizzerebbe una rappresentazione del driver nel proprio DB. Il DriverLookupService potrebbe quindi utilizzare questo DB e i dati del driver verrebbero aggiornati con ulteriori messaggi (ad esempio DriverLicenceRevoked ).

Non posso davvero dirti quale sia l'approccio migliore per il tuo dominio, ma spero che questo ti fornisca informazioni sufficienti per prendere una decisione.

    
risposta data 30.05.2016 - 12:05
fonte
2

Let's assume the aggregate Car has a reference to the aggregate Driver. This reference will be modeled by having Car.driverId.

Sì, è il modo giusto per associare un aggregato a un altro.

if I would talk to domain experts, they would never question the validity of such references

Non è proprio la domanda giusta da porre ai tuoi esperti di dominio. Prova "qual è il costo per il business se il driver non esiste?"

Probabilmente non utilizzerei DriverRepository per controllare l'ID del driver. Invece, vorrei usare un servizio di dominio per farlo. Penso che faccia un lavoro migliore per esprimere l'intento: sotto le coperte, il servizio di dominio controlla ancora il sistema di registrazione.

Quindi qualcosa di simile

class DriverService {
    private final DriverRepository driverRepository;

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

In realtà stai interrogando il dominio sull'ID driver in un numero di punti diversi

  • Dal client, prima di inviare il comando
  • Nell'applicazione, prima di passare il comando al modello
  • All'interno del modello di dominio, durante l'elaborazione dei comandi

Qualsiasi o tutti questi controlli possono ridurre gli errori nell'input dell'utente. Ma stanno tutti lavorando su dati obsoleti; l'altro aggregato può cambiare immediatamente dopo aver posto la domanda. Quindi c'è sempre il pericolo di falsi negativi / positivi.

  • In un rapporto sulle eccezioni, esegui dopo il completamento del comando

Qui stai ancora lavorando con dati obsoleti (gli aggregati potrebbero eseguire comandi mentre stai facendo il report, potresti non essere in grado di vedere le scritture più recenti su tutti gli aggregati). Ma i controlli tra gli aggregati non saranno mai perfetti (Car.create (driver: 7) in esecuzione contemporaneamente a Driver.delete (driver: 7)) Quindi questo ti offre un ulteriore livello di difesa dal rischio.

    
risposta data 27.05.2016 - 15:26
fonte
2

Il modo in cui stai ponendo la domanda (e proponendo due alternative) è come se l'unica preoccupazione fosse che il driver ID è ancora valido al momento della creazione della vettura.

Tuttavia, devi anche essere preoccupato che il driver associato a driverId non venga cancellato prima che l'auto sia cancellata o assegnata a un altro driver (e possibilmente anche che il driver non sia assegnato a un'altra macchina (questo se il dominio limita un driver da associare a una sola auto)).

Suggerisco che invece di convalidare, si assegna (che includerebbe la convalida della presenza). In questo modo, non potrai eliminare le eliminazioni mentre sono ancora allocate, proteggendo così la condizione di competizione dei dati non aggiornati durante la costruzione, così come l'altro problema a lungo termine. (Si noti che l'allocazione è valida e contrassegna i valori allocati e opera in modo atomico.)

A proposito, sono d'accordo con @PriceJones che l'associazione tra l'auto e il conducente è probabilmente una responsabilità separata dall'automobile o dall'autista. Questo tipo di associazione crescerà in complessità nel tempo, perché suona come un problema di programmazione (autisti, macchine, fasce orarie / finestre, sostituti, ecc ...). Anche se è più come un problema di registrazione, si potrebbe volere uno storico registrazioni e registrazioni attuali. Quindi, potrebbe benissimo meritare il proprio BC a titolo definitivo.

È possibile fornire uno schema di allocazione (come un conteggio booleano o di riferimento) all'interno del BC delle entità aggregate assegnate, o all'interno di un BC separato, ad esempio quello responsabile dell'associazione tra auto e amp; conducente. Se si fa il primo, è possibile autorizzare operazioni di cancellazione (valide) rilasciate all'automobile o all'autista BC; se fai il secondo, dovrai prevenire le delezioni dalla macchina e dall'amplificatore; driver BC's e invece li manda attraverso la macchina & scheduler dell'associazione dei driver.

Potresti anche dividere alcune delle responsabilità di allocazione tra BC come segue. La macchina & il driver BC fornisce ciascuno uno schema di "allocazione" che convalida e imposta il booleano assegnato con quello BC; quando viene impostata la loro allocazione booleana, il BC impedisce la cancellazione delle entità corrispondenti. (E il sistema è configurato in modo tale che l'auto e il driver BC consentano solo l'allocazione e la deallocazione dalla schedulazione di associazione auto / driver BC.)

La macchina & la programmazione dei driver BC mantiene quindi un calendario di driver associati all'auto per alcuni periodi / durate di tempo, ora e futuro, e notifica le altre BC di deallocazione solo sull'ultimo utilizzo di un'automobile o di un conducente programmato.

Come soluzione più radicale, puoi trattare la vettura e driver BC come fabbriche di record storico solo append, lasciando la proprietà allo scheduler dell'associazione auto / driver. La macchina BC può generare una nuova auto, completa di tutti i dettagli dell'auto, insieme al suo VIN. La proprietà dell'auto è gestita dallo scheduler dell'associazione auto / conducente. Anche se un'associazione auto / autista viene cancellata e l'auto stessa viene distrutta, i record dell'auto esistono ancora nell'automobile BC per definizione, e possiamo usare l'auto BC per cercare dati storici; mentre le associazioni / proprietà di auto / conducenti (passate, presenti e potenzialmente future programmate) vengono gestite da un'altra BC.

    
risposta data 27.05.2016 - 18:20
fonte
1

But now I'm wondering isn't that too much of invariant checking?

Penso di sì. Il recupero di un dato DriverId dal DB restituisce un set vuoto se non esiste. Quindi, controllando il risultato del reso, si chiede se esiste (e poi si scarica) non necessario.

Then class design makes it unnecessary also

  • Se c'è un requisito "un'auto parcheggiata può avere o meno un autista"
  • Se un oggetto Driver richiede DriverId ed è impostato nel costruttore.
  • Se Car ha solo bisogno del DriverId , ha un% geno Driver.Id . Nessun setter.

Repository is not the place for business rules

  • A Car si preoccupa se ha un Driver (o almeno il suo ID). A Driver si preoccupa se ha un DriverId . Il Repository si preoccupa dell'integrità dei dati e non potrebbe importare di meno delle auto senza conducente.
  • Il DB avrà regole di integrità dei dati. Chiavi non nulle, vincoli non nulli, ecc. Ma l'integrità dei dati riguarda lo schema di dati / tabelle, non le regole aziendali. Abbiamo un rapporto simbiotico strongmente correlato in questo caso, ma non mescolare i due.
  • Il fatto che una DriverId sia un dominio aziendale viene gestito nelle classi appropriate.

Separation of Concerns Violation

... succede quando Repository.DriverIdExists() pone la domanda.

Costruisci un oggetto dominio. Se non è un Driver allora forse un DriverInfo (solo un DriverId e Name , diciamo) oggetto. Il DriverId è convalidato su costruzione. Deve esistere ed essere il tipo giusto, e qualsiasi altra cosa. Quindi è un problema di progettazione della classe client come gestire un driver / driverId inesistente.

Forse un Car va bene senza un driver finché non chiami Car.Drive() . In tal caso l'oggetto Car garantisce ovviamente il proprio stato. Non posso guidare senza Driver - beh, non ancora.

Separating a property from it's class is bad

Certo, hai una Car.DriverId se desideri. Ma dovrebbe assomigliare a questo:

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
    }
}

Non questo:

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

Ora Car deve occuparsi di tutti i problemi di validità di DriverId - una singola violazione del principio di responsabilità; e codice ridondante probabilmente.

    
risposta data 27.05.2016 - 22:19
fonte
1

Potrebbe essere utile chiedere: sei sicuro che le auto siano costruite con i conducenti? Non ho mai sentito parlare di un'auto composta da un autista nel mondo reale. Il motivo per cui questa domanda è importante è perché potrebbe indirizzarti verso la creazione autonoma di autovetture e conducenti e quindi creare un meccanismo esterno che assegni un autista a un'automobile. Un'auto può esistere senza un riferimento del guidatore ed essere comunque un'auto valida.

Se un'automobile deve assolutamente avere un driver nel tuo contesto, allora potresti prendere in considerazione il modello di builder. Questo modello sarà responsabile per garantire che le auto siano costruite con driver esistenti. Le fabbriche serviranno autovetture e autisti validati in modo indipendente, ma il costruttore garantirà che l'auto abbia il riferimento di cui ha bisogno prima di servire la macchina.

    
risposta data 27.05.2016 - 14:58
fonte

Leggi altre domande sui tag