2016-01-16 8 views
5

Recentemente ho iniziato a leggere sul modello di dominio ricco invece di modelli anemici. Tutti i progetti su cui ho lavorato prima, abbiamo seguito il modello di servizio. Nel mio nuovo nuovo progetto sto cercando di implementare un modello di dominio ricco. Uno dei problemi che sto incontrando è cercare di decidere dove il comportamento va (in quale classe). Considerare questo esempio:Implementazione modello di dominio Rich

public class Order 
{ 

    int OrderID; 
    string OrderName; 

    List<Items> OrderItems; 
} 

public class Item 
{ 
    int OrderID; 
    int ItemID; 
    string ItemName; 

} 

Quindi in questo esempio, ho il metodo AddItem nella classe Item. Prima di aggiungere un articolo a un ordine, devo assicurarmi che venga passato un ID ordine valido. Quindi eseguo la convalida nel metodo AddItem. Sono sulla buona strada con questo? O ho bisogno di creare la convalida nella classe Ordine che indica se l'ID ordine è valido?

risposta

2

L'ordine non ha il metodo AddItem? Un articolo viene aggiunto all'ordine, non viceversa.

public class Order 
{ 

    int OrderID; 
    string OrderName; 
    List<Items> OrderItems; 
    bool AddItem(Item item) 
    { 
    //add item to the list 
    } 
} 

In tal caso, l'ordine è valido, perché è stato creato. Ovviamente, l'Ordine non sa che l'Articolo è valido, quindi persiste un potenziale problema di validazione. Quindi la validazione potrebbe essere aggiunta nel metodo AddItem.

public class Order 
{ 

    int OrderID; 
    string OrderName; 
    List<Items> OrderItems; 
    public bool AddItem(Item item) 
    { 
    //if valid 
    if(IsValid(item)) 
    { 
     //add item to the list 
    } 

    } 

    public bool IsValid(Item item) 
    { 
    //validate 
    } 

} 

Tutto questo è in linea con il concetto di programmazione orientata agli oggetti originali di mantenere i dati e i suoi comportamenti insieme in una classe. Tuttavia, come viene eseguita la convalida? Deve fare una chiamata al database? Controlla i livelli di inventario o altre cose al di fuori del confine della classe? Se è così, molto presto la classe dell'Ordine è gonfia di codice extra non correlato all'ordine, ma per verificare la validità dell'articolo, chiamare risorse esterne, ecc. Questo non è esattamente OOPy e sicuramente non SOLIDO.

Alla fine, dipende. Le esigenze dei comportamenti sono contenute all'interno della classe? Quanto sono complessi i comportamenti? Possono essere usati altrove? Sono necessari solo in una parte limitata del ciclo di vita dell'oggetto? Possono essere testati? In alcuni casi ha più senso estrapolare i comportamenti in classi più mirate.

Quindi, costruisci le classi più ricche, falli lavorare e scrivi i test appropriati. Poi guarda come appaiono e odorano e decidono se soddisfano i tuoi obiettivi, possono essere estesi e mantenuti o se devono essere sottoposti a refactoring.

+1

Una buona risposta, ma non è il cuore di Rich Data Model che le entità si convalidano? quindi perché dovresti convalidare l'oggetto nella classe 'Order'? Anche così, perché è il compito di "Ordine" per determinare se un "Articolo" è valido? Non dovrebbe _that_ essere su 'Item'? –

+1

Questo è il punto cruciale della domanda. Dove vanno i comportamenti? Un oggetto può validarsi da solo? La convalida necessaria cambia nel contesto di un ordine? Ad esempio, un articolo di sconto può essere applicato solo quando alcuni altri articoli sono nell'ordine. Lo sconto stesso è valido, ma potrebbe non essere in alcuni ordini. – dbugger

+0

Grazie per la risposta !! Sono d'accordo con la maggior parte di quello che hai detto tranne la convalida di un oggetto. Penso che dovrebbe essere fatto nella classe dell'oggetto. La classe ordine può chiamare quel metodo nella classe articolo prima che possa essere aggiunto. Da un lato, sto iniziando a sentire che il modello di servizio è più adatto per le applicazioni web .. solo un pensiero :) – user3587180

0

Concordo con la prima parte della soluzione di dbugger, ma non con la parte in cui avviene la convalida.

Si potrebbe chiedere: "Perché non il codice di dbugger? È più semplice e ha meno metodi da implementare!" Beh, la ragione è che il codice risultante sarebbe un po 'confuso. Immagina solo che qualcuno possa usare l'implementazione di dbuggers. Si potrebbe scrivere codice come questo:

[...] 
Order myOrder = ...; 
Item myItem = ...; 
[...] 
bool isValid = myOrder.IsValid(myItem); 
[...] 

Qualcuno che non conosce i dettagli di implementazione di metodo "IsValid" di dbugger non sarebbe semplicemente capire che cosa si suppone questo codice per fare. Peggio ancora, lui o lei potrebbe anche intuire che questo sarebbe un confronto tra un ordine e un oggetto. Questo perché questo metodo ha una debole coesione e viola il principio di responsabilità unica dell'OOP. Entrambe le classi dovrebbero essere responsabili solo della loro convalida. Se la convalida include anche la validazione di una classe di riferimento (come elemento in ordine), poi la voce si potrebbe chiedere se è valido per un ordine specifico:

public class Item 
{ 
    public int ItemID { get; set; } 
    public string ItemName { get; set; } 

    public bool IsValidForOrder(Order order) 
    { 
    // order-item validation code 
    } 

} 

Se si desidera utilizzare questo metodo, è potrebbe voler fare attenzione a non chiamare un metodo che attiva una convalida dell'oggetto dal metodo di convalida dell'oggetto. Il risultato sarebbe un loop infinito.

[Update]

Ora Trailmax dichiarato che acessing un DB dall'interno la validazione del codice del dominio di applicazione sarebbe problematico e che usa una classe speciale ItemOrderValidator a fare la convalida.

Sono assolutamente d'accordo. Secondo me non si dovrebbe mai accedere al DB dall'interno del modello di dominio dell'applicazione. So che ci sono alcuni pattern come Active Record, che promuovono un simile comportamento, ma trovo il codice risultante sempre un po 'impuro.

Quindi la domanda principale è: come integrare una dipendenza esterna nel modello di dominio ricco.

Dal mio punto di vista ci sono solo due soluzioni valide per questo.

1) No. Basta farlo procedurale. Scrivi un servizio che vive in cima a un modello anemico. (Credo che sia la soluzione di Trailmax)

o

2) Inserisci il (ex) informazioni esterne e la logica nel vostro modello di dominio. Il risultato sarà un modello di dominio ricco.

Proprio come Yoda ha detto: fare o no. Non c'è una prova.

Ma la domanda iniziale era come progettare un modello di dominio ricco invece di un modello di dominio anemico. Non come progettare un modello di dominio anemico invece di un modello di dominio ricco.

Le classi risultanti sarebbero simile a questa:

public class Item 
{ 
    public int ItemID { get; set; } 
    public int StockAmount { get; set; } 
    public string ItemName { get; set; } 

    public void Validate(bool validateStocks) 
    { 
     if (validateStocks && this.StockAmount <= 0) throw new Exception ("Out of stock"); 
     // additional item validation code 
    } 

} 

public class Order 
{  
    public int OrderID { get; set; } 
    public string OrderName { get; set; } 
    public List<Items> OrderItems { get; set; } 

    public void Validate(bool validateStocks) 
    { 
    if(!this.OrderItems.Any()) throw new Exception("Empty order."); 
    this.OrderItems.ForEach(item => item.Validate(validateStocks));   
    } 

} 

Prima di chiedere: sarà ancora bisogno di un metodo di servizio (procedurale) per caricare i dati (ordine con gli oggetti) dal DB e innescare la validazione (dell'oggetto ordine caricato). Ma la differenza con un modello di dominio anemico è che questo servizio NON contiene la logica di convalida stessa. La logica di dominio si trova all'interno del modello di dominio, non all'interno del servizio/gestore/validatore o di qualsiasi altro nome che si chiami le classi di servizio. L'utilizzo di un modello di dominio ricco indica che i servizi orchestrano solo dipendenze esterne diverse, ma non includono la logica del dominio.

Quindi, cosa succede se si desidera aggiornare i dati del dominio in un punto specifico all'interno della logica del dominio, ad es. subito dopo viene chiamato il metodo "IsValidForOrder"?

Beh, sarebbe un problema.

Se si dispone di una tale domanda orientata alle transazioni, si consiglia di non utilizzare un modello di dominio ricco.

[Aggiornamento: controlli d'identità DB-correlati rimosso - controlli di persistenza dovrebbe essere in un servizio] [Aggiornamento: Aggiunto controlli voce condizionale azionari, il codice di pulitura]

+0

Il problema inizia quando all'interno di 'IsValidForOrder' vorremmo controllare i livelli di stock per gli articoli - devono andare in un database o in un'altra API. E in questo caso il codice diventa disordinato: 'Item' dovrà prendere una dipendenza da un servizio DB e non va bene. Le classi possono validarsi fino a un certo punto. Se la validazione dipende da fattori esterni, preferisco avere una speciale classe 'ItemOrderValidator' che esegue la convalida generale. – trailmax

+1

Non si deve accedere al livello DB dal modello di dominio. Il modello di dominio dovrebbe gestire solo problemi di dominio, non problemi relativi al database. Basta includere le informazioni nel dominio. O utilizzare un approccio basato sul modello di servizio. – Frank

+0

Sono d'accordo con te risposta !!! Perché preferisci lanciare eccezioni invece di restituire i codici di errore ?? In questo costruttore ho capito, ma aggiungere il metodo item potrebbe restituire i codici di errore che descrivono quali sono gli errori ????? – user3587180

2

Prima di tutto, ogni elemento è responsabile di essa la propria stato (informazioni). In una buona progettazione OOP l'oggetto non può mai essere impostato in uno stato non valido. Dovresti almeno cercare di impedirlo.

Per fare ciò non è possibile avere pubblici setter se uno o più campi sono obbligatori in combinazione.

Nel tuo esempio un Item non è valido se manca la orderId o itemId. Senza queste informazioni l'ordine non può essere completato.

Così si dovrebbe attuare tale classe come questa:

public class Item 
{ 
    public Item(int orderId, int itemId) 
    { 
     if (orderId <= 0) throw new ArgumentException("Order is required"); 
     if (itemId <= 0) throw new ArgumentException("ItemId is required"); 

     OrderId = orderId; 
     ItemId = itemId; 
    } 

    public int OrderID { get; private set; } 
    public int ItemID { get; private set; } 
    public string ItemName { get; set; } 
} 

vedere quello che ho fatto lì? Mi sono assicurato che l'oggetto fosse in uno stato valido fin dall'inizio forzando e convalidando le informazioni direttamente nel costruttore.

Il ItemName è solo un bonus, non è necessario per poter elaborare un ordine.

Se i setter di proprietà sono pubblici, è facile dimenticare di specificare entrambi i campi richiesti, ottenendo uno o più bug in seguito quando tali informazioni vengono elaborate. Costringendolo a includere e convalidando le informazioni si catturano i bug molto prima.

Ordinare

L'oggetto ordine deve assicurare che si tratta di tutta la struttura è valido. Quindi è necessario avere control sulle informazioni che esso contiene, che includono anche gli articoli dell'ordine.

se avete qualcosa di simile:

public class Order 
{ 
    int OrderID; 
    string OrderName; 
    List<Items> OrderItems; 
} 

Si sono fondamentalmente dicendo: ho articoli di ordine, ma non mi interessa quanti o ciò che essi contengono. Questo è un invito ai bug più avanti nel processo di sviluppo.

Anche se si dice qualcosa di simile:

public class Order 
{ 
    int OrderID; 
    string OrderName; 
    List<Items> OrderItems; 

    public void AddItem(item); 
    public void ValidateItem(item); 
} 

si sta comunicando qualcosa di simile: Si prega di essere bello, convalidare la voce e poi inserirlo attraverso il metodo Add. Tuttavia, se hai un ordine con id 1, qualcuno potrebbe ancora fare order.AddItem(new Item{OrderId = 2, ItemId=1}) o order.Items.Add(new Item{OrderId = 2, ItemId=1}), rendendo così l'ordine contenente informazioni non valide.

imho a ValidateItem metodo non appartiene a Order ma in Item in quanto è sua responsabilità essere in uno stato valido.

Un design migliore sarebbe:

public class Order 
{ 
    private List<Item> _items = new List<Item>(); 

    public Order(int orderId) 
    { 
     if (orderId <= 0) throw new ArgumentException("OrderId must be specified"); 
     OrderId = orderId; 
    } 

    public int OrderId { get; private set; } 
    public string OrderName { get; set; } 
    public IReadOnlyList<Items> OrderItems { get { return _items; } } 

    public void Add(Item item) 
    { 
     if (item == null) throw new ArgumentNullException("item"); 

     //make sure that the item is for us 
     if (item.OrderId != OrderId) throw new InvalidOperationException("Item belongs to another order"); 

     _items.Add(item); 
    } 
} 

controllo Ora avete ottenuto oltre l'intero ordine, se devono essere apportate modifiche alla lista elemento, questo deve essere fatto direttamente nell'oggetto ordine.

Tuttavia, un articolo può ancora essere modificato senza che l'ordine lo conosca. Qualcuno potrebbe ad esempio a order.Items.First(x=>x.Id=3).ApplyDiscount(10.0); che sarebbe fatale se l'ordine avesse un campo memorizzato nella cache Total.

Tuttavia, un buon design non lo fa sempre correttamente al 100%, ma un compromesso tra codice con cui possiamo lavorare e codice che fa tutto correttamente secondo principi e schemi.

+1

Ottima risposta. Sto appena iniziando a cogliere ddd e ritengo che il modello di servizio sia più adatto. Considerate questo esempio di un metodo AddItem nel livello di servizio. A prescindere da cosa passi, il flusso vorrebbe questo: controlla se l'articolo è valido (convalida di base), controlla se l'ID ordine è valido, qualsiasi altra convalida personalizzata che desideri eseguire e infine aggiungi a databse. In questo modo ogni elemento che viene aggiunto dovrà passare attraverso questa convalida e i dati saranno sempre validi dal momento che passa attraverso tutti quei controlli prima di aggiungere al database ... – user3587180

0

Per modellare una transazione composito, usare due classi: una transazione (Ordine) e un (OrderLineItem) classe LineItem. Ogni LineItem viene quindi associato a un particolare Prodotto.

Quando si tratta di un comportamento adottare la seguente regola:

"Un'azione su un oggetto nel mondo reale, diventa un servizio (metodo) di tale oggetto in un approccio Object Oriented".

0

Se si utilizza il metodo AddItem del modello Rich Domain nell'ordine. Ma i principi SOLIDI non vogliono che tu convalidi e altre cose all'interno di questo metodo.

Immaginate di avere il metodo AddItem() nell'ordine che convalida l'articolo e ricalcola la somma totale dell'ordine comprese le tasse. La prossima modifica è che la convalida dipende dal paese, dalla lingua selezionata e dalla valuta selezionata. Il tuo prossimo cambiamento è che le tasse dipendono anche dal paese. I prossimi requisiti possono essere il controllo della traduzione, sconti ecc. Il tuo codice diventerà molto complesso e difficile da mantenere. Quindi io cosa è meglio avere un tale cosa dentro AddItem:

public void AddItem(IOrderContext orderItemContext) { 
    var orderItem = _orderItemBuilder.BuildItem(_orderContext, orderItemContext); 
    _orderItems.Add(orderItem); 
} 

ora è possibile testare creazione di oggetti e voce aggiungendo alla fine a parte. metodo IOrderItemBuilder.Build() può essere come questo per un certo paese:

public IOrderItem BuildItem(IOrderContext orderContext, IOrderItemContext orderItemContext) { 
    var orderItem = Build(orderItemContext); 
    _orderItemVerifier.Verify(orderItem, orderContext); 
    totalTax = _orderTaxCalculator.Calculate(orderItem, orderContext); 
    ... 
    return orderItem; 
} 

modo da poter testare e utilizzare separatamente codice per diverse responsabilità e del paese. È facile prendere in giro ogni componente e modificarlo in fase di esecuzione in base alla scelta dell'utente.