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 ;-)
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
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. –
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 ;-) –