Come trattare la convalida dei riferimenti tra aggregati?


11

Faccio fatica a fare riferimento tra gli aggregati. Supponiamo che l'aggregato Carabbia un riferimento all'aggregato Driver. Questo riferimento sarà modellato dall'avere Car.driverId.

Ora il mio problema è quanto dovrei andare per convalidare la creazione di un Caraggregato in CarFactory. Devo fidarmi che il passato si DriverIdriferisca a un esistente Driver o dovrei controllare quell'invariante?

Per il controllo, vedo due possibilità:

  • Potrei cambiare la firma della fabbrica automobilistica per accettare un'entità conducente completa. La fabbrica avrebbe quindi scelto l'id da quell'entità e costruito l'auto con quella. Qui l'invariante viene verificato implicitamente.
  • Potrei avere un riferimento al DriverRepositorynel CarFactorye chiamare esplicitamente driverRepository.exists(driverId).

Ma ora mi chiedo non è troppo un controllo invariante? Potevo immaginare che quegli aggregati potessero vivere in un contesto limitato separato, e ora avrei inquinato il BC dell'auto con dipendenze dal DriverRepository o dall'entità Driver del driver BC.

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

Risposte:


6

Potrei cambiare la firma della fabbrica automobilistica per accettare un'entità conducente completa. La fabbrica avrebbe quindi scelto l'id da quell'entità e costruito l'auto con quella. Qui l'invariante viene verificato implicitamente.

Questo approccio è accattivante poiché si ottiene l'assegno gratuitamente ed è ben allineato con il linguaggio onnipresente. A Carnon è guidato da a driverId, ma da a Driver.

Questo approccio viene infatti utilizzato da Vaughn Vernon nel suo contesto limitato di esempio Identity & Access in cui passa un Useraggregato a un Groupaggregato, ma l' Groupunico vale per un tipo di valore GroupMember. Come puoi vedere, questo gli consente anche di verificare l'abilitazione dell'utente (sappiamo bene che il controllo potrebbe essere obsoleto).

    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' Driveristanza ti apri anche a una modifica accidentale Driverall'interno Car. Passare il valore di riferimento rende più facile ragionare sulle modifiche dal punto di vista di un programmatore, ma allo stesso tempo, DDD riguarda tutto il linguaggio Ubiquitous, quindi forse vale la pena rischiare.

Se riesci davvero a trovare buoni nomi per applicare l' Interface Segregation Principle (ISP), puoi fare affidamento su un'interfaccia che non ha i metodi comportamentali. Forse potresti anche inventare un concetto di oggetto valore che rappresenta un riferimento di driver immutabile e che può essere istanziato solo da un driver esistente (ad es DriverDescriptor driver = driver.descriptor().).

Potevo immaginare che quegli aggregati potessero vivere in un contesto limitato separato, e ora avrei inquinato il BC dell'auto con dipendenze dal DriverRepository o dall'entità Driver del driver BC.

No, non lo faresti davvero. Esiste sempre un livello anticorruzione per assicurarsi che i concetti di un contesto non sfocino in un altro. In realtà è molto più semplice se hai un BC dedicato alle associazioni automobilistiche perché puoi modellare concetti esistenti come Care Driverspecificamente per quel contesto.

Pertanto, potresti avere un DriverLookupServiceresponsabile definito nella BC responsabile della gestione delle associazioni automobilistiche. Questo servizio può chiamare un servizio Web esposto dal contesto Gestione driver che restituisce Driveristanze 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 può anche fare affidamento sulla messaggistica in cui, ad esempio, un UserCreatedmessaggio dal contesto di gestione dei driver verrebbe utilizzato in un contesto remoto che memorizzerebbe una rappresentazione del driver nel proprio database. Il DriverLookupServicepotrebbe quindi utilizzare questa DB e dei dati del conducente sarebbero tenuti aggiornati con ulteriori messaggi (ad esempio DriverLicenceRevoked).

Non posso davvero dirti quale approccio è migliore per il tuo dominio, ma spero che questo ti dia abbastanza spunti per prendere una decisione.


3

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

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

Suggerisco che al posto della convalida, si alloca (che includerebbe la convalida della presenza). In questo modo, non sarà possibile eliminare le eliminazioni mentre si è ancora allocati, proteggendo così dalle condizioni di gara dei dati non aggiornati durante la costruzione, nonché l'altro problema a più lungo termine. (Si noti che l'allocazione convalida e contrassegna sia allocata che 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'auto o dal conducente. Questo tipo di associazione crescerà in complessità solo nel tempo, perché sembra un problema di programmazione (guidatori, automobili, fasce orarie / finestrini, sostituti, ecc ...) Anche se è più simile a un problema di registrazione, si potrebbe desiderare una cronologia registrazioni e registrazioni attuali. Pertanto, può benissimo meritare il suo proprio BC.

È 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 per l'associazione tra auto e conducente. Se fai il primo, puoi consentire (valide) operazioni di cancellazione emesse all'auto o al conducente BC; se si esegue quest'ultima operazione, sarà necessario impedire le eliminazioni dai BC dell'auto e dell'autista e invece inviarle tramite lo schedulatore dell'associazione auto e autista.

Potresti anche dividere alcune delle responsabilità di allocazione tra BC come segue. L'auto e il conducente BC forniscono ciascuno uno schema di "allocazione" che convalida e imposta il booleano assegnato con quel BC; quando è impostata la loro allocazione booleana, il BC impedisce la cancellazione delle entità corrispondenti. (E il sistema è configurato in modo che l'automobile e il conducente BC consentano l'allocazione e la deallocazione solo dall'associazione automobile / conducente che pianifica BC.)

La programmazione di auto e conducente BC mantiene quindi un calendario di conducenti associati all'auto per alcuni periodi / durate, ora e futuro, e notifica agli altri BC di deallocazione solo sull'ultimo utilizzo di un'auto o di un conducente programmato.


Come soluzione più radicale, puoi considerare le auto e i conducenti BC come fabbriche di record storici solo appendici, lasciando la proprietà allo schedulatore dell'associazione auto / conducente. L'auto BC può generare una nuova auto, completa di tutti i dettagli dell'auto, insieme al suo VIN. La proprietà dell'auto è gestita dallo schedulatore dell'associazione auto / conducente. Anche se un'associazione auto / conducente viene eliminata e l'automobile stessa viene distrutta, i record dell'auto esistono ancora nell'auto BC per definizione, e possiamo usare l'auto BC per cercare dati storici; mentre le associazioni automobilistiche / automobilistiche / proprietà (pianificate passate, presenti e potenzialmente future) sono gestite da un altro BC.


2

Supponiamo che l'auto aggregata abbia un riferimento al driver aggregato. Questo riferimento sarà modellato con Car.driverId.

Sì, questo è il modo giusto per accoppiare un aggregato ad un altro.

se parlassi con esperti di dominio, non metterebbero mai in discussione la validità di tali riferimenti

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

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

Quindi qualcosa del genere

class DriverService {
    private final DriverRepository driverRepository;

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

Effettivamente si interroga il dominio su driverId in diversi punti

  • 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

Uno o tutti questi controlli possono ridurre gli errori nell'input dell'utente. Ma funzionano tutti con dati non aggiornati; l'altro aggregato può cambiare immediatamente dopo aver posto la domanda. Quindi c'è sempre qualche pericolo di falsi negativi / positivi.

  • In un rapporto di eccezione, eseguire dopo il completamento del comando

Qui, stai ancora lavorando con dati non aggiornati (gli aggregati potrebbero eseguire comandi mentre stai eseguendo il rapporto, 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 dà un ulteriore livello di difesa contro i rischi.


1
Driver.deletenon dovrebbe esistere. Non ho mai visto un dominio in cui gli aggregati vengono distrutti. Tenendo gli AR intorno a te non puoi mai finire con gli orfani.
plalx,

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 guidatore nel mondo reale. Il motivo per cui questa domanda è importante è perché potrebbe indirizzarti verso la creazione indipendente di automobili e conducenti e quindi la creazione di un meccanismo esterno che assegna un conducente a un'auto. Un'auto può esistere senza un riferimento del conducente ed essere comunque valida.

Se un'auto deve assolutamente avere un conducente nel tuo contesto, allora potresti voler considerare il modello del costruttore. Questo modello sarà responsabile per garantire che le auto siano costruite con conducenti esistenti. Le fabbriche serviranno auto e conducenti validati in modo indipendente, ma il costruttore assicurerà che l'auto abbia il riferimento di cui ha bisogno prima di servire l'auto.


Ho pensato anche alla relazione auto / conducente, ma l'introduzione di un aggregato DriverAssignment non fa altro che spostare il riferimento che deve essere convalidato.
VoiceOfUnreason,

1

Ma ora mi chiedo non è troppo un controllo invariante?

Credo di si. Il recupero di un determinato DriverId dal DB restituisce un set vuoto se non esiste. Pertanto, la verifica del risultato restituito rende superflua la richiesta se esiste (e quindi il recupero).

Quindi il design di classe lo rende anche superfluo

  • Se è richiesto "un'auto parcheggiata può o meno avere un conducente"
  • Se un oggetto Driver richiede un DriverIded è impostato nel costruttore.
  • Se le Caresigenze sono solo il DriverId, avere un Driver.Idgetter. Nessun setter.

Il repository non è il luogo adatto per le regole aziendali

  • A Carimporta se ha un Driver(o almeno il suo ID). A Driverse ne frega se ha a DriverId. Si Repositorypreoccupa 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 / tabella, non le regole aziendali. In questo caso abbiamo una relazione simbiotica fortemente correlata, ma non mescoliamo le due cose.
  • Il fatto che a DriverIdsia una cosa di dominio aziendale viene gestito nelle classi appropriate.

Separazione delle violazioni delle preoccupazioni

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

Costruisci un oggetto dominio. Se non un Driverallora forse un DriverInfo(solo un DriverIde Name, diciamo) oggetto. Il DriverIdè convalidato al momento della costruzione. Deve esistere ed essere il giusto tipo e quant'altro. Quindi è un problema di progettazione della classe client come gestire un driver / driverId inesistente.

Forse a Carva bene senza autista fino a quando non chiami Car.Drive(). Nel qual caso l' Caroggetto ovviamente garantisce il proprio stato. Non posso guidare senza un Driver- beh, non ancora.

Separare una proprietà dalla sua classe è male

Certo, avere un Car.DriverIdse lo 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 Cardevono affrontare tutti i DriverIdproblemi di validità: una violazione del principio di responsabilità unica; e codice ridondante probabilmente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.