DDD - Come fare convalida informativa (in particolare sulla creazione di entità)?

1

Al momento, per le entità di dominio, tutti gli errori di convalida generano eccezioni. Non mi piace perché non mi consente di compilare tutti gli errori di validazione prima di uscire da un metodo. Mi piacerebbe perseguire qualcosa di simile a ciò che è descritto qui: link ... in particolare, l'idea di restituire IReadOnlyList da tutti i metodi di entità. Il problema che sto incontrando riguarda la creazione di entità ... come posso utilizzare questa tecnica? Non vedo altro modo che generare eccezioni dal costruttore quando le regole di convalida non vengono soddisfatte con i valori dei parametri indicati.

    
posta user1560457 28.09.2018 - 15:13
fonte

2 risposte

2

La creazione di oggetti è una preoccupazione tecnica che non ha nulla a che fare con l'applicazione degli invarianti di business. Direi che la validazione aziendale zero deve assolutamente avvenire nei costruttori. La validazione è meglio lasciarla durante i processi entro i quali devono essere applicati gli invarianti (dove vengono usati i dati). Seguire questa regola aiuta a mantenere il sistema molto più dichiarativo e, quindi, più facile da capire. Ricorda che DDD riguarda le regole di modellazione relative al comportamento , non alle regole di modellazione dei dati.

Ciò significa che, dato che un Customer deve avere più di 25 anni per register , l'applicazione di tale invariante si verifica nel metodo register , non nel costruttore Customer . Ciò consente la possibilità di un'altra regola che richiede un Customer di avere meno di 25 anni per qualche altro processo.

L'unica eccezione a quanto sopra è quando i valori passati all'oggetto sono, loro stessi, ciò che l'oggetto intende astrarre (è l'identità), ad es. %codice%. In questo caso, la stringa passata in un costruttore di EmailAddress deve essere conforme a una determinata specifica per poter essere considerata un indirizzo valido. Ciò significa che l'eccezione di cui sopra si applica solo a EmailAddress .

Ecco un link a una risposta che ho postato a una domanda simile che potrebbe interessarti: Costruttori argomento zero e entità sempre valide

Tutto ciò che è stato detto, non vuoi andare sulla strada della compilazione di tutti gli errori di validazione prima di uscire. Non solo rende i tuoi metodi molto più complicati, funziona solo per i processi più semplici in cui tutte le convalide possono verificarsi prima che venga eseguita qualsiasi logica reale. Sembra una buona idea quando gli unici invarianti sono l'esistenza di ValueObjects e address di stringhe, ma cade a pezzi per operazioni più complicate. Inoltre, molti errori di dominio non sono pensati per essere visualizzati direttamente dai client (quindi cosa hai intenzione di fare con loro? Parse a dayOfWeek in DomainErrorList ? Overkill!).

Lascia una semplice convalida dal tuo dominio e alzala "verso l'alto" nella tua vista. Questo è esattamente il motivo per cui ClientErrorList esiste in primo luogo. Ciò che intendo è, se lo scopo di raccogliere i messaggi di errore per alcuni input è di esporli poi al tuo cliente, quindi mantenere tutta la convalida nella vista stessa (e farlo nel modo in cui MS lo fa nella loro ViewModels - & gt ; proceduralmente). Potrei sentirmi ridondante, ma è anche disaccoppiato.

    
risposta data 28.09.2018 - 22:12
fonte
1

Questa è una domanda che mi sono posto e che sto facendo ricerche da un po 'di tempo.
Sotto ci sono un paio di modi in cui potresti dire al chiamante dei tuoi costruttori / metodi cosa è andato storto durante il processo che hanno richiesto, in ordine di quanto sono idiomatici (ma in ordine inverso rispetto a quanto li considero efficaci):

  1. Genera un'eccezione composita (dominio)
    L'essenza di questo approccio è che continuerai ad usare le eccezioni, è solo che raccoglierai ogni eccezione di validazione prima di lanciarla.

    public Person(int age, string fullName)
    {
        var validationExceptions = new List<string>();
    
        if (age < 18)
            validationExceptions.Add("Must be at least 18!");
    
        if (string.IsNullOrWhiteSpace(fullName))
            validationExceptions.Add("Name field must not be empty!");
    
        if (validationExceptions.Any())
        {
            throw new CompositeExpectedException(validationExceptions);
        }
    
        this.age = age;
        this.fullName = fullName;
    }
    
  2. Metodi di fabbrica
    I costruttori sono fondamentalmente una serie di metodi glorificati con la firma di
    {privato / interno / pubblico} {ClassName} {ClassName} (/ * parametri * /)
    quindi l'unica cosa che puoi fare con loro è completare la costruzione con successo o lanciare un'eccezione - non c'è modo di aggirarla.
    Fortunatamente, ci sono altri modi per restituire informazioni da un metodo:

    /// Prevent instantiation of this class through any other means besides the 
    /// TryCreate method below.
    private Person(int age, string fullName)
    {
        this.age = age;
        this.fullName = fullName;
    }
    
    public static IEnumerable<string> TryCreate(int age, string fullName, out Person person)
    {
        var validationExceptions = new List<string>();
    
        if (age < 18)
            validationExceptions.Add("Must be at least 18!");
    
        if (string.IsNullOrWhiteSpace(fullName))
            validationExceptions.Add("Name field must not be empty!");
    
        if (validationExceptions.Any())
            person = null;
        else
            person = new Person(age, fullName);
    
        return validationExceptions;
    }
    

    Una variazione di questo approccio sarebbe quella di scambiare il tipo di ritorno del metodo e il parametro out.

  3. (public nested o internal) factories Una variante della soluzione di cui sopra - trasforma il metodo di fabbrica in un oggetto fabbrica a tutti gli effetti. Ciò è particolarmente utile se hai bisogno di alcune dipendenze per capire se i parametri obbediscono a qualche regola aziendale (ad esempio, se il nome utente fornito esiste nel tuo livello di persistenza).

    public class Person
    {
        private Person(int age, string fullName)
        {
            ...
        }
    
        public class Factory
        {
            public Factory(ISomeDependency someDependency)
            {
                ...
            }
    
            public static IEnumerable<string> TryCreate(int age, string fullName, out Person person)
            {
                ...
            }
        }
    }
    

    Se non sei un fan delle classi nidificate, puoi contrassegnare il costruttore Person come interno e spostare la classe Factory sul proprio file e mantenerlo pubblico. Qualsiasi classe al di fuori dell'assemblea della Persona dovrà ricorrere alla classe Factory se vuole un'istanza Persona.
    In entrambi i casi, qualsiasi utente della classe Person sarà in grado di creare un'istanza di una classe Person attraverso la classe Factory e verrà informato di qualsiasi regola di convalida che potrebbe essere violata.
    Ecco alcune ulteriori informazioni su sostituzione delle eccezioni con le notifiche se desideri seguire questo percorso.

  4. In entrambi i
    Finora, abbiamo utilizzato le eccezioni e i parametri out per restituire 2 tipi di dati completamente diversi: l'istanza Person e l'elenco delle eccezioni di convalida che sono state rilevate.
    Invece di questi, perché non inserire sia l'istanza Person che le eccezioni di convalida all'interno della stessa classe? O sembra un buon candidato per questo - un concetto preso in prestito dal mondo funzionale, ma che si inserisce perfettamente nel mondo OOP di C #.

    public class Person
    {
        private Person(int age, string fullName)
        {
            ...
        }
    
        public static IEither<Person, IReadOnlyList<string>> TryCreate(int age, string fullName)
        {
            var validationExceptions = new List<string>();
    
            if (age < 18)
                validationExceptions.Add("Must be at least 18!");
    
            if (string.IsNullOrWhiteSpace(fullName))
                validationExceptions.Add("Name field must not be empty!");
    
            if (validationExceptions.Any())
                return validationExceptions;
            else
                return new Person(age, fullName);
        }
    }
    

    Personalmente preferisco questo approccio al massimo perché costringe il chiamante del metodo TryCreate a gestire esplicitamente entrambe le situazioni - tutti i dati erano buoni e la Persona è stata istanziata con successo OPPURE qualcosa non funzionava e veniva restituito un elenco di messaggi di convalida. Che IEither interface assomiglia a:

    /// <summary>
    /// Defines the contract for data types that will act as an exclusive or (XOR) discriminated unions.
    /// </summary>
    /// <typeparam name="T1"> Option 1. </typeparam>
    /// <typeparam name="T2"> Option 2. </typeparam>
    public interface IEither<out T1, out T2>
    {
            /// <summary>/// Does pattern matching based on the real underlying type.
            /// </summary>
            /// <param name="func1"> The func to be executed in case the type is option 1. </param>
            /// <param name="func2"> The func to be executed in case the type is option 2. </param>
            /// <typeparam name="TResult"> The type of result. </typeparam>
            /// <returns> The <see cref="TResult"/>. </returns>
            TResult Match<TResult>(Func<T1, TResult> func1, Func<T2, TResult> func2);
    }
    

    Come puoi vedere, questo metodo richiede che il chiamante fornisca 2 funzioni: una che trasformerà il risultato nel tipo desiderato se è effettivamente un T1 e l'altra funzione che farà lo stesso se il tipo di ritorno effettivo è T2. Uso di seguito:

        Person.TryCreate(12, "Peter").Match(
            person => this.Ok() as IHttpActionResult,
            validationErrors => this.BadRequest(string.Join(Environment.NewLine, validationErrors)));
    

    Ora, indipendentemente da ciò che accade all'interno di TryCreate, il chiamante saprà per certo che non verrà generata alcuna eccezione a sorpresa e che avranno gestito ogni scenario che questo metodo può produrre.

    Parola di avvertimento : sebbene consideri questo ultimo approccio il modo più efficace e completo di gestire le possibili eccezioni attese (siano esse di convalida o di altro tipo) (è anche una soluzione al dibattito contro le eccezioni verificate e non classificate, ma questo è un argomento completamente diverso), suggerisco caldamente che prima di introdurre questo tipo di codice nelle applicazioni di produzione, si applichi questo schema nelle proprie applicazioni giocattolo personali e si comprenda perfettamente come funziona!

    Questo sarebbe. Questi sono tutti gli approcci che conosco attraverso cui puoi dire al chiamante non solo che qualcosa è andato storto durante la loro chiamata, ma anche cosa ha fatto!

risposta data 28.12.2018 - 13:01
fonte

Leggi altre domande sui tag