Esiste un modo elegante per verificare le contraddizioni uniche sugli attributi degli oggetti di dominio senza spostare la logica aziendale nel livello di servizio?


10

Sto adattando il design guidato dal dominio da circa 8 anni e anche dopo tutti questi anni, c'è ancora una cosa che mi ha infastidito. Ciò sta verificando la presenza di un record univoco nella memorizzazione dei dati su un oggetto dominio.

Nel settembre 2013 Martin Fowler ha menzionato il principio TellDon'tAsk , che, se possibile, dovrebbe essere applicato a tutti gli oggetti di dominio, che dovrebbe quindi restituire un messaggio, come è andata l'operazione (nella progettazione orientata agli oggetti questo è fatto principalmente attraverso eccezioni, quando l'operazione non è andata a buon fine).

I miei progetti sono generalmente divisi in molte parti, in cui due sono Dominio (contenente regole aziendali e nient'altro, il dominio è completamente ignaro della persistenza) e Servizi. Servizi a conoscenza del livello di repository utilizzato per i dati CRUD.

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

Per poter verificare l'unicità di un record, è necessario eseguire una query sul set di dati corrente, in genere un database, per scoprire se Nameesiste già un altro record con un diciamo .

Considerando che il livello di dominio ignora la persistenza e non ha idea di come recuperare i dati ma solo come eseguire operazioni su di essi, non può davvero toccare i repository stessi.

Il design che ho adattato appare in questo modo:

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 anziché il livello di dominio stesso e che le regole sono distribuite su più sezioni del codice.

Ho citato il principio TellDon'tAsk, perché come puoi vedere chiaramente, il servizio offre un'azione (salva Producto genera un'eccezione), ma all'interno del metodo stai operando sugli oggetti attraverso l'approccio procedurale.

La soluzione ovvia è quella di creare una Domain.ProductCollectionclasse con un Add(Domain.Product)metodo che lancia ProductWithSKUAlreadyExistsException, ma è molto carente in termini di prestazioni, perché sarebbe necessario ottenere tutti i prodotti dall'archiviazione dei dati per scoprire nel codice, se un prodotto ha già lo stesso SKU come prodotto che stai tentando di aggiungere.

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


Perché estrarre un intero elenco di nomi quando puoi semplicemente richiederne uno e basare la logica sul fatto che sia stato trovato o meno? Stai dicendo al livello di persistenza cosa fare e non come farlo finché c'è un accordo con l'interfaccia.
JeffO,

1
Quando si utilizza il termine Servizio, dovremmo cercare di ottenere maggiore chiarezza, ad esempio la differenza tra i servizi di dominio nel livello del dominio e i servizi dell'applicazione nel livello dell'applicazione. Vedi gorodinski.com/blog/2012/04/14/…
Erik Eidt,

Articolo eccellente, @ErikEidt, grazie. Immagino che il mio design non sia sbagliato, quindi, se posso fidarmi del signor Gorodinski, sta praticamente dicendo lo stesso: una soluzione migliore è che un servizio applicativo recuperi le informazioni richieste da un'entità, configurando efficacemente l'ambiente di esecuzione e fornisca all'entità, e ha la stessa obiettività contro l'iniezione di repository nel modello di dominio direttamente come me, principalmente interrompendo l'SRP.
Andy,

Una citazione dal riferimento "Tell, Don't Ask": "Ma personalmente, non uso Tell-Dont-Ask".
radarbob,

Risposte:


7

Considerando che il livello di dominio ignora la persistenza e non ha idea di come recuperare i dati, ma solo come eseguire operazioni su di essi, non può davvero toccare i repository stessi.

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

Mentre è vero che il dominio dovrebbe essere ignaro della persistenza, sa che esiste una "Raccolta di entità di dominio". E che ci sono regole di dominio che riguardano questa raccolta nel suo insieme. L'unicità è una di queste. E poiché l'implementazione della logica attuale dipende fortemente dalla modalità di persistenza specifica, ci deve essere un qualche tipo di astrazione nel dominio che specifica la necessità di questa logica.

Quindi è semplice come creare un'interfaccia in grado di interrogare se il nome esiste già, che viene quindi implementato nell'archivio dati e chiamato da chiunque abbia bisogno di sapere se il nome è univoco.

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


Grazie per l'input. Il repository potrebbe effettivamente rappresentare ciò Domain.ProductCollectionche avevo in mente, considerando che sono responsabili del recupero di oggetti dal livello Dominio?
Andy,

@DavidPacker Non capisco davvero cosa intendi. Perché non dovrebbe essere necessario conservare tutti gli elementi in memoria. L'implementazione del metodo "DoesNameExist" dovrebbe essere (molto probabilmente) una query SQL sul lato dell'archivio dati.
Euforico,

Ciò che è medio è, invece di archiviare i dati in una raccolta in memoria, quando voglio conoscere tutto il Prodotto di cui non ho bisogno per ottenerli Domain.ProductCollectionma chiedo invece al repository, lo stesso chiedendo, se Domain.ProductCollectioncontiene un Prodotto con superato SKU, questa volta, di nuovo, chiedendo invece al repository (questo è in realtà l'esempio), che invece di iterare sui prodotti precaricati interroga il database sottostante. Non intendo conservare tutto il Prodotto in memoria a meno che non sia necessario, farlo sarebbe una totale assurdità.
Andy,

Il che porta a un'altra domanda, il repository dovrebbe quindi sapere se un attributo dovrebbe essere unico? Ho sempre implementato i repository come componenti piuttosto stupidi, salvando ciò che li passi per salvare e cercando di recuperare i dati in base alle condizioni passate e mettere la decisione in servizi.
Andy,

@DavidPacker Bene, la "conoscenza del dominio" è nell'interfaccia. L'interfaccia dice "il dominio ha bisogno di questa funzionalità" e quindi l'archivio dati fornisce quella funzionalità. Se hai IProductRepositoryun'interfaccia con DoesNameExist, è ovvio cosa dovrebbe fare il metodo. Ma assicurarsi che il Prodotto non possa avere lo stesso nome del prodotto esistente dovrebbe essere nel dominio da qualche parte.
Euforico,

4

Devi leggere Greg Young sulla validazione impostata .

Risposta breve: prima di andare troppo lontano nel nido dei ratti, è necessario assicurarsi di comprendere il valore del requisito dal punto di vista aziendale. Quanto costa, davvero, rilevare e mitigare la duplicazione, anziché prevenirla?

Il problema con i requisiti di "unicità" è che, beh, molto spesso c'è una ragione di fondo più profonda per cui le persone li vogliono - Yves Reynhout

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

È 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 perdita di logica dal livello di dominio, puoi ottenere lo stesso tipo di risultato con a DomainService.

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

Naturalmente, in questo modo l'implementazione del DeduplicationService dovrà sapere qualcosa su come cercare gli skus esistenti. Quindi, mentre reinserisce parte del lavoro nel dominio, devi ancora affrontare gli stessi problemi di base (hai bisogno di una risposta per la convalida impostata, problemi con le condizioni di gara).

È possibile eseguire la convalida nel livello di persistenza stesso. I database relazionali sono davvero bravi a impostare la convalida. 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 ottiene una violazione del vincolo che esegue il backup di bolle in caso di problemi. Quindi il codice dell'applicazione ha un bell'aspetto e le condizioni della tua gara vengono eliminate, ma le regole del "dominio" perdono.

Puoi creare un aggregato separato nel tuo dominio che rappresenta l'insieme di skus noti. Mi vengono in mente due varianti qui.

Uno è qualcosa come un ProductCatalog; i prodotti esistono altrove, ma il rapporto tra prodotti e skus è mantenuto da un catalogo che garantisce l'unicità sku. Non che ciò implichi che i prodotti non abbiano skus; gli skus sono assegnati da un ProductCatalog (se hai bisogno che gli skus siano univoci, puoi ottenere ciò avendo un solo aggregato ProductCatalog). Rivedi il linguaggio onnipresente con i tuoi esperti di dominio: se esiste una cosa del genere, questo potrebbe essere l'approccio giusto.

Un'alternativa è qualcosa di più simile a un servizio di prenotazione sku. Il meccanismo di base è lo stesso: un aggregato conosce tutti gli skus, quindi può impedire l'introduzione di duplicati. Ma il meccanismo è leggermente diverso: acquisisci un contratto di locazione 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 competizione in gioco (aggregati diversi, quindi transazioni distinte), ma ha un sapore diverso. Il vero svantaggio qui è che stai proiettando nel modello di dominio un servizio di leasing senza avere una giustificazione nella lingua del dominio.

È possibile riunire tutte le entità prodotti in un unico aggregato, ovvero il catalogo prodotti descritto sopra. Ottieni assolutamente unicità degli skus quando lo fai, ma il costo è una contesa aggiuntiva, modificare qualsiasi prodotto significa davvero modificare l'intero catalogo.

Non mi piace la necessità di estrarre tutti gli SKU dei prodotti dal database per eseguire l'operazione in memoria.

Forse non è necessario. Se provi lo sku con un filtro Bloom , puoi scoprire molti skus unici senza caricare il set.

Se il tuo caso d'uso ti consente di essere arbitrario su quali skus rifiuti, potresti eliminare tutti i falsi positivi (non è un grosso problema se permetti ai clienti di testare gli skus che propongono prima di inviare il comando). Ciò consentirebbe di evitare di caricare il set in memoria.

(Se vuoi essere più accettante, potresti prendere in considerazione il caricamento pigro degli skus in caso di una corrispondenza nel filtro bloom; rischi comunque di caricare tutti gli skus in memoria a volte, ma non dovrebbe essere il caso comune se permetti il codice client per verificare la presenza di errori nel comando prima dell'invio).


Non mi piace la necessità di estrarre tutti gli SKU dei prodotti dal database per eseguire l'operazione in memoria. È la soluzione ovvia che ho suggerito nella domanda iniziale e messo in discussione le sue prestazioni. Inoltre, IMO, basandosi sul vincolo nel proprio DB, per essere responsabile dell'unicità, è un male. Se dovessi passare a un nuovo motore di database e in qualche modo il vincolo univoco si perdesse durante una trasformazione, hai il codice rotto, perché le informazioni del database che erano prima non ci sono più. Comunque grazie per il link, lettura interessante.
Andy,

Forse un filtro Bloom (vedi modifica).
VoiceOfUnreason,
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.