Dove dovremmo mettere la convalida per il modello di dominio


38

Sto ancora cercando le migliori pratiche per la convalida del modello di dominio. È utile mettere la convalida nel costruttore del modello di dominio? il mio esempio di validazione del modello di dominio come segue:

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

Grazie per tutti i tuoi suggerimenti.

Risposte:


47

C'è un interessante articolo di Martin Fowler sull'argomento che evidenzia un aspetto che la maggior parte delle persone (incluso me) tende a trascurare:

Ma una cosa che penso costantemente inciampa le persone è quando pensano che la validità degli oggetti in un modo indipendente dal contesto come un metodo isValid implichi.

Penso che sia molto più utile pensare alla convalida come qualcosa che è legato a un contesto, in genere un'azione che si desidera fare. Questo ordine è valido per essere completato, questo cliente è valido per il check-in in hotel? Quindi, anziché avere metodi come isValid, utilizzare metodi come isValidForCheckIn.

Da ciò consegue che il costruttore non dovrebbe eseguire la convalida, tranne forse alcuni controlli di integrità molto basilari condivisi da tutti i contesti.

Ancora dall'articolo:

In About Face Alan Cooper ha sostenuto che non dovremmo lasciare che le nostre idee di stati validi impediscano a un utente di inserire (e salvare) informazioni incomplete. Mi è stato ricordato da questo alcuni giorni fa quando ho letto una bozza di un libro su cui Jimmy Nilsson sta lavorando. Ha affermato un principio secondo cui dovresti sempre essere in grado di salvare un oggetto, anche se contiene degli errori. Anche se non sono convinto che questa dovrebbe essere una regola assoluta, penso che le persone tendano a prevenire il risparmio più di quanto dovrebbero. Pensare al contesto per la convalida può aiutare a prevenirlo.


Per fortuna qualcuno l'ha detto. I moduli che contengono il 90% dei dati ma che non salvano nulla sono ingiusti per gli utenti, che spesso costituiscono l'altro 10% solo per non perdere i dati, quindi tutto ciò che è stato validato è costringere il sistema a perdere traccia del 10% è stato inventato. Problemi analoghi possono verificarsi sul back-end: ad esempio un'importazione di dati. Ho scoperto che di solito è meglio provare a lavorare correttamente con dati non validi piuttosto che cercare di impedire che accada mai.
psr

2
@psr Hai anche bisogno di una logica di back-end se i tuoi dati non sono persistenti? Puoi lasciare tutta la manipolazione sul lato client se i tuoi dati non hanno alcun significato sul tuo modello di business. Sarebbe anche uno spreco di risorse per inviare messaggi avanti e indietro (client - server) se i dati sono privi di significato. Quindi torniamo all'idea di "non consentire mai agli oggetti del dominio di entrare in uno stato non valido!" .
Geo C.

2
Mi chiedo perché così tanti voti per una risposta così ambigua. Quando si utilizza DDD, a volte ci sono alcune regole che controllano semplicemente se alcuni dati sono INT o sono compresi in un intervallo. Ad esempio quando permetti all'utente della tua app di scegliere alcuni vincoli sui suoi prodotti (quante volte qualcuno può visualizzare l'anteprima del mio prodotto e in quale intervallo di giorni di un mese). Qui entrambi i vincoli dovrebbero essere int e uno di essi dovrebbe essere compreso tra 0 e 31. Sembra una convalida del formato dei dati che in un ambiente non DDD si adatterebbe a un servizio o controller. Ma in DDD sono dalla parte del mantenimento della validazione nel dominio (90% di esso).
Geo C.

2
Applicare gli strati superiori per sapere troppo sul dominio per mantenerlo in uno stato valido ha un cattivo odore di cattiva progettazione. Il dominio dovrebbe essere quello che garantisce che lo stato sia valido. Muoversi troppo sulle spalle degli strati superiori può rendere anemico il tuo dominio e potresti slittare alcuni vincoli imporatanti che potrebbero danneggiare il tuo business. Quello che mi rendo conto ora, una corretta generalizzazione sarebbe quella di mantenere la tua validazione il più vicino possibile alla persistenza, o il più vicino al tuo codice di manipolazione dei dati (quando viene manipolato per raggiungere uno stato finale).
Geo C.

PS Non mischio l'autorizzazione (è permesso fare qualcosa), l'autenticazione (il messaggio proviene dalla posizione corretta o è stato inviato dal client giusto, entrambi identificati dalla chiave API / token / nome utente o qualsiasi altra cosa) con la convalida del formato o regole commerciali. Quando dico 90% intendo quelle regole commerciali che la maggior parte di esse include anche la convalida del formato. La convalida del formato Ofcourse può essere nei livelli superiori, ma la maggior parte di essi sarà nel dominio (anche il formato dell'indirizzo e-mail che verrà convalidato nell'oggetto valore EmailAddress).
Geo C.

6

Nonostante il fatto che questa domanda sia un po 'stantia, vorrei aggiungere qualcosa di utile:

Vorrei essere d'accordo con @MichaelBorgwardt ed estenderlo facendo apparire la testabilità. In "Lavorare efficacemente con il codice legacy", Michael Feathers parla molto degli ostacoli ai test e uno di questi ostacoli è "difficile da costruire" oggetti. La costruzione di un oggetto non valido dovrebbe essere possibile e, come suggerisce Fowler, i controlli di validità dipendenti dal contesto dovrebbero essere in grado di identificare tali condizioni. Se non riesci a capire come costruire un oggetto in un cablaggio di prova, avrai difficoltà a testare la tua classe.

Per quanto riguarda la validità, mi piace pensare ai sistemi di controllo. I sistemi di controllo funzionano analizzando costantemente lo stato di un'uscita e applicando azioni correttive quando l'uscita si discosta dal set point, questo è chiamato controllo ad anello chiuso. Il controllo a circuito chiuso si aspetta intrinsecamente deviazioni e agisce per correggerle ed è così che funziona il mondo reale, motivo per cui tutti i sistemi di controllo reali utilizzano in genere controller a circuito chiuso.

Penso che l'utilizzo della convalida dipendente dal contesto e la facilità di costruzione degli oggetti renderà il tuo sistema più semplice da utilizzare in futuro.


1
Molte volte gli oggetti sembrano difficili da costruire. Ad esempio, in questo caso, è possibile ignorare il costruttore pubblico creando una classe Wrapper che eredita dalla classe in fase di test e consente di creare un'istanza dell'oggetto base in uno stato non valido. È qui che entra in gioco l'uso dei modificatori di accesso corretti su classi e costruttori e può davvero essere dannoso per i test se usato in modo improprio. Inoltre, evitare classi e metodi "sigillati", salvo ove opportuno, contribuirà notevolmente a rendere più semplice il test di un codice.
P. Roe

4

Come sono sicuro che già sai ...

Nella programmazione orientata agli oggetti, un costruttore (a volte abbreviato in ctor) in una classe è un tipo speciale di subroutine chiamato alla creazione di un oggetto. Prepara il nuovo oggetto per l'uso, spesso accettando i parametri che il costruttore utilizza per impostare le variabili membro richieste quando l'oggetto viene creato per la prima volta. Si chiama costruttore perché costruisce i valori dei membri di dati della classe.

La verifica della validità dei dati passati come parametri c'tor è sicuramente valida nel costruttore, altrimenti è possibile consentire la costruzione di un oggetto non valido.

Tuttavia (e questa è solo la mia opinione, non riesco a trovare buoni documenti su di esso a questo punto) - se la convalida dei dati richiede operazioni complesse (come le operazioni asincrone - forse la convalida basata su server se si sviluppa un'app desktop), allora è meglio inserire una funzione di inizializzazione o convalida esplicita di qualche tipo e i membri impostati su valori predefiniti (come null) nel c'tor.


Inoltre, proprio come una nota a margine come l'hai inclusa nell'esempio di codice ...

A meno che tu non stia eseguendo un'ulteriore convalida (o altra funzionalità) in AddOrderLine, molto probabilmente esporrei List<LineItem>come proprietà piuttosto che Orderagire come una facciata .


Perché esporre il contenitore? Cosa importa agli strati superiori quale sia il contenitore? È perfettamente ragionevole avere un AddLineItemmetodo. In effetti, per DDD, questo è preferito. Se List<LineItem>viene modificato in un oggetto di raccolta personalizzato, la proprietà esposta e tutto ciò che dipendeva da una List<LineItem>proprietà sono soggetti a modifiche, errori ed eccezioni.
Estratto del

4

La convalida deve essere eseguita il prima possibile.

La convalida in qualsiasi contesto, sia il modello di dominio o qualsiasi altro modo di scrivere software, dovrebbe servire allo scopo di COSA vuoi validare ea quale livello sei al momento.

Sulla base della tua domanda, immagino che la risposta sarebbe quella di dividere la convalida.

  1. La convalida della proprietà verifica se il valore per quella proprietà è corretto, ad es. Quando si prevede un intervallo compreso tra 1 e 10.

  2. La convalida dell'oggetto garantisce che tutte le proprietà sull'oggetto siano valide insieme. es. BeginDate è prima di EndDate. Supponiamo di leggere un valore dall'archivio dati e sia BeginDate che EndDate sono inizializzati su DateTime.Min per impostazione predefinita. Quando si imposta BeginDate, non vi è alcun motivo per imporre la regola "deve essere prima di EndDate", poiché questo non si applica ANCORA. Questa regola dovrebbe essere controllata DOPO che tutte le proprietà sono state impostate. Questo può essere chiamato a livello di radice aggregata

  3. La convalida dovrebbe anche essere preformata sull'entità aggregata (o radice aggregata). Un oggetto Order può contenere dati validi e quindi OrderLines. Ma poi una regola commerciale afferma che nessun ordine può superare $ 1.000. Come fareste applicare questa regola in alcuni casi ciò è consentito. non puoi semplicemente aggiungere una proprietà "non convalidare l'importo" poiché ciò comporterebbe un abuso (prima o poi, forse anche tu, solo per togliere questa "cattiva richiesta").

  4. poi c'è la validazione a livello di presentazione. Stai davvero per inviare l'oggetto sulla rete, sapendo che fallirà? O risparmierai all'utente questo peso e lo informerai non appena inserirà un valore non valido. ad esempio, la maggior parte delle volte il tuo ambiente DEV sarà più lento della produzione. Ti piacerebbe aspettare 30 secondi prima di essere informato di "hai dimenticato DI NUOVO questo campo durante ancora UN'ALTRA prova", specialmente quando c'è un bug di produzione da correggere con il tuo capo che ti respira al collo?

  5. La convalida a livello di persistenza dovrebbe essere il più vicino possibile alla convalida del valore della proprietà. Ciò consentirà di evitare eccezioni con la lettura di errori "null" o "valore non valido" quando si utilizzano mapper di qualsiasi tipo o semplici lettori di dati vecchi. L'uso delle procedure memorizzate risolve questo problema, ma richiede di scrivere la stessa logica di valutazione ANCORA e di eseguirlo DI NUOVO. E le procedure memorizzate sono il dominio di amministrazione DB, quindi non tentare di fare anche il SUO lavoro (o peggio di disturbarlo con questo "prelibatezza che non viene pagato".

quindi per dirlo con alcune parole famose "dipende", ma almeno adesso sai PERCHÉ dipende.

Vorrei poter mettere tutto questo in un unico posto, ma sfortunatamente non è possibile farlo. Ciò porterebbe una dipendenza da un "oggetto God" contenente TUTTA la validazione per TUTTI i layer. Non vuoi percorrere quel sentiero oscuro.

Per questo motivo, lancio eccezioni di convalida solo a livello di proprietà. Tutti gli altri livelli che uso ValidationResult con un metodo IsValid per raccogliere tutte le "regole infrante" e passarle all'utente in una singola AggregateException.

Durante la propagazione dello stack di chiamate, le raccolgo nuovamente in AggregateExceptions fino a quando non raggiungo il livello di presentazione. Il livello di servizio può generare questa eccezione direttamente al client in caso di WCF come FaultException.

Questo mi permette di prendere l'eccezione e di dividerla per mostrare singoli errori in ciascun controllo di input o appiattirla e mostrarla in un unico elenco. La scelta è tua.

questo è il motivo per cui ho anche menzionato la convalida della presentazione, per cortocircuitare il più possibile.

Nel caso ti stia chiedendo perché ho anche la convalida a livello di aggregazione (o livello di servizio se vuoi), è perché non ho una sfera di cristallo che mi dice chi utilizzerà i miei servizi in futuro. Avrai abbastanza problemi a trovare i tuoi errori per impedire ad altri di commettere errori tuoi :) inserendo dati non validi. Amministri l'applicazione A, ma l'applicazione B fornisce alcuni dati utilizzando il tuo servizio. Indovina chi chiedono per primo quando c'è un bug? L'amministratore dell'applicazione B informerà felicemente l'utente "non c'è nessun errore da parte mia, ho solo inserito i dati".

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.