Qual è il modo "migliore" per affrontare la convalida dal punto di vista di un purista DDD?

4

Recentemente ho posto questa domanda: Validation inside Constructor

Sto cercando di decidere dove mettere la convalida in un'app DDD. Credo che dovrebbe essere fatto a ogni livello.

Ora mi sto concentrando sul modello di dominio. Mi aspettavo la convalida per andare nei metodi Setter come questo: link

Qual è il modo "migliore" per fare la convalida. Sto parlando dalla prospettiva di un purista DDD. Mi rendo conto che il modo "migliore" potrebbe non essere sempre il modo più pratico in ogni situazione.

Inoltre, non sto necessariamente dicendo che DDD è sempre l'approccio migliore per risolvere un problema. Sto solo cercando di imporvi il mio modo di pensare in questa specifica area.

Credo che le opzioni siano:

1) Convalida all'interno dei setter come descritto qui: link

2) Metodo IsValid come descritto qui: link

3) Verifica la validità nel livello dei servizi applicativi come descritto qui: link

4) TryExecute pattern come descritto qui: link

5) Esegui / CanExecute pattern come descritto qui: link

Sto usando NHibernate in modo che i setter sugli oggetti del dominio debbano avere una visibilità di: set protetto.

    
posta w0051977 14.10.2017 - 14:01
fonte

3 risposte

7

È una domanda interessante che sembra emergere in varie forme.

Sono dell'opinione che l'approccio migliore sia quello di consentire il concetto di un oggetto che è uno stato non valido.

Il motivo è che le regole di convalida per un oggetto non sono solitamente impostate in pietra. Possono cambiare nel tempo o essere diversi per le diverse operazioni. Quindi un oggetto che hai creato, popolato e persistito qualche tempo fa, ora può essere considerato non valido.

Se hai i controlli di convalida del setter o del costruttore, allora hai un grosso problema nel fatto che la tua applicazione commetterà un errore quando tenti di recuperare queste entità dal tuo database, o rielaborare i vecchi input ecc.

Inoltre, non penso che le regole aziendali incorporino la semplice convalida sì / no per la maggior parte. Se il tuo dominio sta vendendo torte e non consegni il sud del fiume, ma qualcuno ti offre un milione di sterline per farlo. Quindi fai un'eccezione speciale.

Se stai elaborando milioni di applicazioni e hai regole severe su cosa possono essere i caratteri in un campo, probabilmente hai un processo per correggere campi danneggiati. Non vuoi affatto essere in grado di accettare un campo errato, ma segue semplicemente un percorso diverso attraverso il Dominio.

Quindi, se nel codice sei così rigido che i dati "non validi" non possono mai esistere perché il costruttore genererebbe un'eccezione, sei destinato a essere fragile e fallire per domande come "quante persone hanno compilato il modulo in modo errato ? "

Consenti i dati e fallisci l'operazione. In questo modo puoi modificare i dati o le regole per l'operazione e riutilizzarla.

Esempio:

public class Order
{
    public string Id {get;set;}
    public string Address {get;set;}
    public void Deliver()
    {
        //check address is valid for delivery
        if(String.IsNullOrWhiteSpace(this.Address))
        {
            throw new Exception("Address not supplied");
        }

        //delivery code
    }
}

Quindi qui non siamo in grado o non vogliamo consegnare a indirizzi vuoti. L'ordine dell'oggetto dominio consente di compilare un indirizzo vuoto ma genera un'eccezione se si tenta di consegnare tale ordine.

Un'applicazione, ad esempio un addetto alla coda che elabora gli ordini dai dati json archiviati in una coda, imbattendosi in un ordine "non valido":

{
    "Address"  :""
}

È in grado di creare l'oggetto Ordine, poiché non ci sono controlli di convalida nel costruttore o il settatore per Indirizzo. Tuttavia, quando viene chiamato il metodo Deliver, verrà generata l'eccezione e l'applicazione sarà in grado di eseguire un'azione. ad esempio

public class QueueWorker
{
    public void ProcessOrder(Order o)
    {
        try
        {
            o.Deliver();
        }
        catch(Exception ex)
        {
            Logger.Log("unable to deliver order:${o.Id} error:${ex.Message}");
            MoveOrderToErrorQueue(o);
        }
    }
}

L'applicazione può ancora funzionare con l'ordine invalido, spostandolo nella coda degli errori, accedendo al proprio ID, segnalando errori, ecc. Ma l'operazione di consegna contiene la logica del dominio di come si desidera gestire la consegna agli indirizzi vuoti.

Se la logica del dominio successivamente cambia con ulteriori requisiti:

public class Order
{
    public string Id {get;set;}
    public string Address {get;set;}

    public void Deliver()
    {
        //check address is valid for delivery
        if(String.IsNullOrWhiteSpace(this.Address))
        {
            throw new Exception("Address not supplied");
        }
        if(Address.Contains("UK"))
        {
            throw new Exception("UK orders not allowed!");
        }
        //delivery code

    }
}

Quindi puoi ancora elaborare gli ordini sulla coda che sono stati generati quando gli indirizzi del Regno Unito sono stati autorizzati e ottenere il risultato previsto.

È interessante confrontare la mia risposta con quella di @VoiceOfUnReason.

Ho usato un indirizzo stringa per semplicità e anche perché non esiste una definizione assoluta di cosa sia un indirizzo. Ma se abbiamo una definizione più assoluta, diciamo che il suo deposito ha sempre una valuta. È assurdo parlare di un deposito senza valuta.

In questo caso sì, puoi definire un nuovo tipo di valore che semplicemente non può esistere a meno che tu non abbia specificato la valuta e il carico di potenziali errori nel tuo codice semplicemente non sia possibile.

Ma devi essere sicuro che sia una cosa fondamentale. Altrimenti stai chiedendo guai in seguito!

    
risposta data 14.10.2017 - 14:53
fonte
7

Un buon termine di ricerca da recensire sarebbe "Obsione primitiva"

I am trying to decide where to put the validation in a DDD app.

La solita risposta è mettere la validazione nei costruttori / fabbriche per i tuoi oggetti valore.

Questo è il modo in cui sono arrivato a pensarci: uno degli scopi degli oggetti valore è isolare il comportamento del dominio dalle rappresentazioni dei dati sottostanti.

Un esempio ingenuo; consideriamo un conto bancario

Account {
    int currentBalance;

    void deposit (int amount) {
        int workingBalanace = currentBalance;
        workingBalance += amount;
        currentBalance = workingBalance;
    }
}

Il design di questa entità è strettamente associato all'espressione di equilibrio sottostante come int. Ma quell'accoppiamento è un incidente di implementazione, non un problema di dominio sottostante. Int è non preso dalla lingua ubiquitaria del dominio bancario.

In altre parole, dovremmo essere in grado di cambiare la nostra decisione su come rappresentiamo i dati in memoria senza dover cambiare la modalità di implementazione della nostra logica aziendale.

Lo stesso modello naive, con questo ulteriore livello di riferimento indiretto, potrebbe apparire come

Account {
    Balance currentBalance;

    void deposit (Deposit amount) {
        Balance workingBalance = currentBalance;
        workingBalance = workingBalance.add(amount);
        currentBalance = workingBalance;
    }
}

Quindi Account non si preoccupa affatto delle rappresentazioni sottostanti; è sufficiente passare tipi che supportano le condizioni post corrette.

La convalida necessaria per garantire che il deposito sia conforme viene spinto più vicino al confine, che è ciò che desideri.

Quindi non direi che la convalida avviene ad ogni livello - avviene ai limiti; un confine è dove converte i messaggi dal mondo esterno in concetti di dominio, un altro in cui convertiamo i messaggi dal negozio persistente in concetti di dominio.

Ma una volta dentro il confine, non è necessario ripetere la convalida a meno che non si scelga di non rilevare il fatto che la convalida è stata eseguita.

Are you saying that you should not pass primitive types to domain classes?

Non proprio. Quello che sto dicendo è che gli oggetti valore nel tuo modello di dominio forniscono uno strato di riferimento indiretto tra le tue entità e le rappresentazioni di dati sottostanti.

Nell'esempio sopra, passiamo un Deposit a Account . Da dove proviene Deposit ? È un tipo di valore nel nostro dominio, presumibilmente istanziato prendendo il messaggio (byte) che siamo stati passati e trasformandoli in un concetto di dominio più specifico.

Deposit from(int amount) {
    // ...
}

È probabile che anche la trasformazione inversa esista da qualche parte, in modo da poter utilizzare un'appliance di base (come un database relazionale) per archiviare i dati

int from(Deposit deposit) {
    // ...
}

I am thinking anti corruption layer. Is that what you are talking about?

Un'idea molto simile; il "sistema legacy" in questo caso non è tanto un database ma piuttosto messaggistica .

Ma secondo me, l'idea chiave è il disaccoppiamento del concetto (l'interfaccia dell'oggetto valore manipolato dal modello di dominio) dalla rappresentazione sottostante (dati).

    
risposta data 14.10.2017 - 15:14
fonte
3

La tua domanda sembra fondere diverse nozioni di convalida, quindi proviamo a metterle a parte:

  1. Possiamo avere la convalida come azienda e amp; funzione di dominio: nozione di convalida a livello di dominio, che parla delle interazioni con i clienti, i clienti e i fornitori dell'azienda e le azioni intraprese dall'azienda, ad es. nell'accettare e soddisfare gli ordini, ecc ... A questo livello, stiamo parlando di convalida che verrebbe eseguita manualmente se non fosse per l'automazione.

  2. Possiamo avere la convalida all'interno dei moduli del codice, in modo che il codice controlli le condizioni che sa che non è programmato per gestire e / o non si aspetta. A questo livello stiamo parlando di validazioni dovute alla costruzione interna dell'automazione / dell'architettura software.

Penso che tutti stiano dicendo che dovremmo eseguire la convalida del dominio sulle transizioni dello stato del dominio aziendale piuttosto che sugli attraversamenti del livello del dominio del computer o nei contenuti delle entità.

In altre parole, convalidiamo a fini commerciali; convalidiamo per realizzare le transizioni di stato a livello di dominio aziendale; convalidare per svolgere le funzioni aziendali.

Non facciamo convalida aziendale ai fini dei concetti di implementazione del software di dominio del computer come la costruzione di oggetti, l'accesso al setter o l'attraversamento di layer - non troverai nessuno di questi in DDD stesso dato che DDD non ti dice come scrivere codice o quale codice devi avere.

E in particolare, non permettiamo che le convalide di tipo (2) introducano restrizioni (al funzionamento del software per l'azienda) che non sono altrimenti presenti a livello di dominio aziendale.

    
risposta data 14.10.2017 - 17:47
fonte

Leggi altre domande sui tag