2009-08-07 15 views
2

J'ai un agrégat de domaine, appelez-le "Order" qui contient une liste de lignes de commande. L'Ordre conserve la trace de la somme du montant sur les lignes de commande. Le client dispose d'un solde créditeur en cours de fonctionnement qu'il calcule en additionnant l'historique de ses transactions de base de données. Une fois qu'ils utilisent tout l'argent dans la «piscine», ils ne peuvent plus commander de produits.Obtention de données supplémentaires pour une entité de domaine

Donc, chaque fois qu'une ligne est ajoutée à la commande, je dois vérifier ce qui reste dans le pool et si l'ordre les y pousse. Le montant de la réserve change continuellement parce que d'autres clients connexes l'utilisent continuellement. La question est, en termes de DDD, comment puis-je obtenir cette quantité puisque je ne veux pas polluer mon Couche de Domaine avec des problèmes DataContext (en utilisant L2S ici). Étant donné que je ne peux pas interroger la base de données à partir du domaine, comment puis-je obtenir ces données pour pouvoir valider la règle métier?

Est-ce une instance où les événements de domaine sont utilisés?

Répondre

5

Votre agrégat d'ordre doit être entièrement encapsulé. Il doit donc être en mesure de déterminer s'il est valable d'ajouter un article, c'est-à-dire si le crédit client est dépassé ou non. Il existe plusieurs façons de le faire, mais elles dépendent toutes du référentiel d'ordre qui retourne un agrégat spécifique qui sait comment faire cette chose particulière. Ce sera probablement un agrégat d'ordre différent de celui que vous utiliseriez pour satisfaire des ordres, par exemple.

Vous devez reconnaître, puis capturer dans le code, le fait que vous attendez que la commande remplisse un rôle particulier dans ce cas, c'est-à-dire le rôle d'ajouter des éléments de ligne supplémentaires.Pour ce faire, créez une interface pour ce rôle et un agrégat correspondant disposant du support interne pour le rôle. Ensuite, votre couche de service peut demander à votre référentiel d'ordres un ordre satisfaisant cette interface de rôle explicite et le référentiel dispose donc de suffisamment d'informations sur ce dont vous avez besoin pour construire quelque chose qui puisse satisfaire cette exigence.

Par exemple:

public interface IOrder 
{ 
    IList<LineItem> LineItems { get; } 
    // ... other core order "stuff" 
} 

public interface IAddItemsToOrder: IOrder 
{ 
    void AddItem(LineItem item); 
} 

public interface IOrderRepository 
{ 
    T Get<T>(int orderId) where T: IOrder; 
} 

Maintenant, votre code de service ressemblerait à quelque chose comme:

public class CartService 
{ 
    public void AddItemToOrder(int orderId, LineItem item) 
    { 
    var order = orderRepository.Get<IAddItemsToOrder>(orderId); 
    order.AddItem(item); 
    } 
} 

Ensuite, votre classe Order qui implémente IAddItemsToOrder a besoin d'une entité client afin qu'il puisse vérifier le crédit équilibre. Vous cascadez simplement la même technique en définissant une interface spécifique. Le référentiel de commandes peut faire appel au référentiel client pour renvoyer une entité client qui remplit ce rôle et l'ajouter à l'agrégat de commandes.

Ainsi vous auriez une interface de base ICustomer et ensuite un rôle explicite sous la forme d'une interface ICustomerCreditBalance qui en descend. Le ICustomerCreditBalance sert à la fois d'interface de repère à votre référentiel client pour lui indiquer ce dont vous avez besoin pour le client, afin qu'il puisse créer l'entité client appropriée et qu'il dispose des méthodes et/ou des propriétés pour prendre en charge le rôle spécifique. Quelque chose comme:

public interface ICustomer 
{ 
    string Name { get; } 
    // core customer stuff 
} 

public interface ICustomerCreditBalance: ICustomer 
{ 
    public decimal CreditBalance { get; } 
} 

public interface ICustomerRepository 
{ 
    T Get<T>(int customerId) where T: ICustomer; 
} 

interfaces rôle explicites donnent des référentiels les informations clés dont ils ont besoin pour prendre la bonne décision au sujet de quelles données extraites de la base, et si la chercher avec impatience ou paresseusement.

Notez que j'ai mis la propriété CreditBalance sur l'interface ICustomerCreditBalance dans ce cas. Cependant, il pourrait aussi bien être sur l'interface de base ICustomer et ICustomerCreditBalance devient alors une interface "marqueur" vide pour laisser le dépôt savoir que vous allez interroger le solde créditeur. Il s'agit de faire savoir au référentiel quel rôle vous voulez pour l'entité qu'il renvoie.

La dernière partie qui rassemble tout cela, comme vous l'avez mentionné dans votre question, concerne les événements de domaine. La commande peut déclencher un événement de domaine de défaillance si le solde créditeur du client est dépassé, pour informer la couche de service que la commande est invalide. Par contre, si le client a suffisamment de crédit, il peut mettre à jour le solde de l'objet client ou déclencher un événement de domaine pour informer le reste du système que le solde doit être réduit.

Je n'ai pas ajouté le code d'événement de domaine à la classe CartService car cette réponse est déjà longue! Si vous voulez en savoir plus sur la façon de faire cela, je vous suggère de poster une autre question ciblant ce problème spécifique et je vais développer là-dessus ;-)

+0

Mike, c'est une excellente réponse en termes de conception d'agrégat. Si j'ai l'occasion au prochain sprint, je vais essayer de refactoriser ce que vous avez ici. La façon dont j'ai résolu ce problème est en utilisant des événements de domaine similaires à l'article récent d'Udi Dahan [http://www.udidahan.com/2009/06/14/domain-events-salvation/]. Lorsque l'événement signifiant l'ajout d'une ligne est déclenché, le gestionnaire d'événements vérifie la balance et définit une propriété interne sur l'agrégat. Maintenant, une règle métier vérifie cette propriété et signale le succès ou l'échec au client. – jlembke

+0

Je pense que les événements de domaine devraient être utilisés pour la notification dans une sorte de mode "feu et oublier". La motivation pour les avoir est de permettre la notification/rappel sans qu'aucun des deux côtés ne soit couplé à l'autre. Votre solution avec les événements de domaine est parfaitement viable, mais elle les utilise comme une sorte d'appel de procédure distante découplée. Je pense qu'il est préférable de réserver des événements de domaine pour une notification unilatérale en utilisant une métaphore de messagerie. Cela contribue grandement à l'évolutivité - vous pouvez ensuite les implémenter avec un bus de service, par exemple. –

+1

J'ai oublié de dire: les deux précédents articles d'Udi sur les événements de domaine (http://www.udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/) ** aussi ** répondez à votre question - il utilise des événements de domaine pour signaler un échec de validation ** et ** la technique que je préconise ici pour faire des vérifications de validation externes. Donc, vous obtenez 2 pour le prix de 1 dans les articles d'événements de domaine d'Udi ;-) –

2

Dans un tel scénario, je décharge la responsabilité en utilisant des événements ou des délégués. Peut-être que la façon la plus simple de vous montrer est avec du code.

Votre classe de commande aura un Predicate<T> qui est utilisé pour déterminer si la ligne de crédit du client est assez grande pour gérer la ligne de commande.

public class Order 
{ 
    public Predicate<decimal> CanAddOrderLine; 

    // more Order class stuff here... 

    public void AddOrderLine(OrderLine orderLine) 
    { 
     if (CanAddOrderLine(orderLine.Amount)) 
     { 
      OrderLines.Add(orderLine); 
      Console.WriteLine("Added {0}", orderLine.Amount); 
     } 
     else 
     { 
      Console.WriteLine(
       "Cannot add order. Customer credit line too small."); 
     } 
    } 
} 

Vous aurez probablement une classe CustomerService ou quelque chose comme ça pour retirer la ligne de crédit disponible. Vous définissez le prédicat CanAddOrderLine avant d'ajouter des lignes de commande. Cela permettra de vérifier le crédit du client chaque fois qu'une ligne est ajoutée.

// App code. 
var customerService = new CustomerService(); 
var customer = new Customer(); 
var order = new Order(); 
order.CanAddOrderLine = 
    amount => customerService.GetAvailableCredit(customer) >= amount; 

order.AddOrderLine(new OrderLine { Amount = 5m }); 
customerService.DecrementCredit(5m); 

Sans doute votre scénario réel sera plus compliqué que cela. Vous pouvez également consulter le délégué Func<T>. Un délégué ou un événement peut être utile pour décrémenter le montant du crédit après la mise en ligne de la commande ou pour déclencher certaines fonctionnalités si le client dépasse sa limite de crédit dans la commande.

Bonne chance!

+0

J'aime ça. Ce que je fais maintenant est d'injecter un service dans l'objet de domaine afin qu'il ne sache pas où il obtient le montant. Dans le concept c'est semblable à ce que vous faites ici. Je vais essayer ça et voir comment je l'aime. Merci Kevin! – jlembke

+0

Ce n'est pas grave si cela fonctionne pour vous. Une alternative consiste à conserver votre modèle de domaine dans une couche inférieure à celle des services de domaine. C'est comme une meilleure séparation des préoccupations et des tests. J'aime vraiment utiliser Onion Architecture de Jeffrey Palermo. Tu devrais y jeter un coup d'oeil. http://jeffreypalermo.com/blog/the-onion-architecture-part-1/ –

+0

Les entités de domaine devraient être en mesure d'effectuer ces exigences métier sans nécessiter d'injection spéciale d'interfaces, de prédicats, etc.La clé est que vos référentiels renvoient des objets adaptés au rôle exact dont ils ont besoin à ce moment-là. Cela libère complètement votre code de service et d'application de la nécessité de connaître ** n'importe quoi ** sur la logique du domaine, qui est entièrement encapsulée dans vos entités de domaine. Ainsi, votre code se termine plus clairement dans son intention, sans une charge de bagage désordonné. –

1

En plus du problème d'obtention de la valeur "pool" (où j'interrogerais la valeur en utilisant une méthode sur un OrderRepository), avez-vous considéré les implications de verrouillage pour ce problème?

Si le "pool" change constamment, y a-t-il une chance que quelqu'un se glisse dans la transaction juste après que votre règle soit passée, mais juste avant que vous ne validiez vos changements sur la base de données?

Eric Evans fait référence à ce problème dans le chapitre 6 de son livre ("Aggregates").

+0

Vijay, vous avez 100% raison. C'est le prochain problème que nous sommes en train de perdre sommeil. J'ai raté cette partie du livre. Je vais lire ça maintenant !! – jlembke

Questions connexes