Esiste un modo elegante per controllare i vincoli univoci sugli attributi degli oggetti di dominio senza spostare la business logic nel livello di servizio?

8

Ho adattato il design basato sul dominio per circa 8 anni e anche dopo tutti questi anni, c'è ancora una cosa che mi ha infastidito. Questo è il controllo di un record univoco nell'archiviazione dei dati rispetto a un oggetto dominio.

Nel settembre 2013 Martin Fowler ha citato il principio TellDon't Ask , che, se possibile, dovrebbe essere applicato a tutti i domini oggetti, che dovrebbero quindi restituire un messaggio, come è andata l'operazione (nella progettazione orientata agli oggetti questo avviene principalmente attraverso eccezioni, quando l'operazione non ha avuto successo).

I miei progetti sono solitamente divisi in molte parti, in cui due sono Dominio (contenente regole aziendali e nient'altro, il dominio è completamente persistente-ignorante) e Servizi. Servizi che conoscono il livello del repository utilizzato per i dati CRUD.

Poiché l'unicità di un attributo appartenente a un oggetto è una regola dominio / aziendale, dovrebbe essere lunga per il modulo dominio, quindi la regola è esattamente dove dovrebbe essere.

Per essere in grado di verificare l'unicità di un record, è necessario interrogare il set di dati corrente, di solito un database, per scoprire se esiste già un altro record con diciamo Name .

Considerare il livello del dominio è la persistenza ignorante e non ha idea di come recuperare i dati, ma solo come fare operazioni su di essi, non può davvero toccare i repository stessi.

Il design che ho adattato si presenta così:

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();
    }
}

Questo porta ad avere servizi che definiscono le regole di dominio piuttosto che lo stesso livello del dominio e hai le regole sparse su più sezioni del tuo codice.

Ho menzionato il principio TellDon'tAsk, perché come puoi vedere chiaramente, il servizio offre un'azione (salva il Product o genera un'eccezione), ma all'interno del metodo sei operativo sugli oggetti usando l'approccio procedurale .

La soluzione ovvia è creare una classe Domain.ProductCollection con un metodo Add(Domain.Product) che lancia il ProductWithSKUAlreadyExistsException , ma è carente in termini di prestazioni, perché è necessario ottenere tutti i prodotti dall'archiviazione dei dati per trovare nel codice, se un prodotto ha già la stessa SKU del prodotto che si sta tentando di aggiungere.

Come risolvete questo problema specifico? Questo non è davvero un problema in sé, ho avuto il livello di servizio che rappresenta alcune regole di dominio per anni. Il livello di servizio di solito serve anche operazioni di dominio più complesse, mi sto semplicemente chiedendo se ti sei imbattuto in una soluzione migliore, più centralizzata, durante la tua carriera.

    
posta Andy 17.05.2016 - 14:18
fonte

2 risposte

6

Considering domain layer is persistence ignorant and has no idea how to retrieve the data but only how to do operations on them, it cannot really touch the repositories itself.

Non sarei d'accordo con questa parte. Soprattutto l'ultima frase.

Anche se è vero che il dominio dovrebbe essere ignorante per la persistenza, esso sa che esiste "Raccolta di entità di dominio". E che ci sono regole di dominio che riguardano questa collezione nel suo complesso. L'unicità è uno di questi. E poiché l'implementazione della logica effettiva dipende in gran parte dalla modalità di persistenza specifica, nel dominio deve esserci una sorta di astrazione che specifica la necessità di questa logica.

Quindi è semplice come creare un'interfaccia in grado di interrogare se il nome esiste già, che viene poi implementato nel tuo archivio dati e chiamato da chiunque debba sapere se il nome è univoco.

E vorrei sottolineare che i repository sono servizi DOMAIN. Sono astrazioni intorno alla persistenza. È l'implementazione del repository che dovrebbe essere separata dal dominio. Non c'è assolutamente nulla di sbagliato con l'entità di dominio che chiama un servizio di dominio. Non c'è nulla di sbagliato in un'entità che possa utilizzare il repository per recuperare un'altra entità o recuperare alcune informazioni specifiche, che non possono essere facilmente conservate in memoria. Questo è il motivo per cui il repository è un concetto chiave in il libro di Evans .

    
risposta data 17.05.2016 - 14:36
fonte
1

Devi leggere Greg Young su impostare la convalida .

Risposta breve: prima di andare troppo in basso nel nido dei ratti, devi assicurarti di comprendere il valore del requisito dal punto di vista del business. Quanto è costoso, in realtà, rilevare e mitigare la duplicazione, piuttosto che prevenirla?

The problem with “uniqueness” requirements is that, well, very often there’s a deeper underlying reason why people want them -- Yves Reynhout

Risposta più lunga: ho visto un menu di possibilità, ma hanno tutte un compromesso.

È possibile verificare la presenza di duplicati prima di inviare il comando al dominio. Questo può essere fatto nel client o nel servizio (il tuo esempio mostra la tecnica). Se non sei soddisfatto della logica che sfugge al livello del dominio, puoi ottenere lo stesso tipo di risultato con DomainService .

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

Ovviamente, in questo modo l'implementazione di DeduplicationService avrà bisogno di sapere qualcosa su come cercare lo skus esistente. Quindi, mentre spinge parte del lavoro di nuovo nel dominio, ci si trova ancora di fronte agli stessi problemi di base (che richiedono una risposta per la convalida dell'insieme, problemi con le condizioni di gara).

Puoi eseguire la convalida nel tuo livello di persistenza stesso. I database relazionali sono veramente buoni alla validazione del set. Metti un vincolo di unicità sulla colonna sku del tuo prodotto e sei a posto. L'applicazione salva semplicemente il prodotto nel repository e si verifica una violazione del vincolo che si ripresenta se si verifica un problema. Quindi il codice dell'applicazione sembra buono e la tua condizione di gara è stata eliminata, ma le regole del "dominio" sono state eliminate.

Puoi creare un aggregato separato nel tuo dominio che rappresenta l'insieme di skus conosciuti. Posso pensare a due varianti qui.

Uno è qualcosa come un ProductCatalog; i prodotti esistono da qualche altra parte, ma il rapporto tra prodotti e skus è mantenuto da un catalogo che garantisce l'unicità dello sku. Non che ciò implichi che i prodotti non abbiano skus; Gli skus sono assegnati da un ProductCatalog (se hai bisogno di skus per essere unico, ottieni questo solo con un singolo aggregato ProductCatalog). Rivedi la lingua onnipresente con i tuoi esperti di dominio - se esiste una cosa del genere, questo potrebbe essere l'approccio giusto.

Un'alternativa è qualcosa di più come un servizio di prenotazione sku. Il meccanismo di base è lo stesso: un aggregato conosce tutto lo skus, quindi può impedire l'introduzione di duplicati. Ma il meccanismo è leggermente diverso: si acquisisce un leasing su uno sku prima di assegnarlo a un prodotto; quando si crea il prodotto, si passa il contratto di locazione allo sku. C'è ancora una condizione di gara in gioco (diversi aggregati, quindi transazioni distinte), ma ha un sapore diverso. Il vero svantaggio qui è che stai proiettando nel modello di dominio un servizio di leasing senza realmente avere una giustificazione nella lingua del dominio.

Puoi trasferire tutte le entità di prodotto in un singolo aggregato, ovvero il catalogo prodotti sopra descritto. Ottieni assolutamente l'unicità dello skus quando lo fai, ma il costo è una contesa aggiuntiva, la modifica di qualsiasi prodotto significa davvero la modifica dell'intero catalogo.

I don't like the need to pull all product SKUs out of the database to do the operation in-memory.

Forse non è necessario. Se metti alla prova il tuo sku con un filtro Bloom , puoi scoprire molti skus unici senza caricare il set.

Se il tuo caso d'uso ti permette di essere arbitrario su quale skus rifiuti, puoi mettere via tutti i falsi positivi (non un grosso problema se permetti ai clienti di testare lo skus che propongono prima di inviare il comando). Ciò consentirebbe di evitare di caricare il set in memoria.

(Se si desidera essere più accettando, si potrebbe considerare il caricamento pigro dello skus in caso di una corrispondenza nel filtro bloom, si rischia comunque di caricare tutti gli skus in memoria a volte, ma non dovrebbe essere il caso comune se si consente al codice client di controllare il comando per errori prima dell'invio).

    
risposta data 17.05.2016 - 17:00
fonte