从域模型向下传播到应用层 [英] Communicate from Domain Model back down to Application Layer

查看:86
本文介绍了从域模型向下传播到应用层的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个域模型产品,其中包含价格列表。

 公共类产品
{
私人列表< int> _价格; ///请注意,这是我实际代码中的值对象

public void AddPrice(int price)
{

var currentPrice = _prices.LastOrDefault();

if(price< currentPrice)
_prices.add(price)
}

}

价格变动时,我希望发生很多事情。使用贫血域模型非常容易,因为我可以在服务中保留这一点:

  if(price< currentPrice)
_prices.Add(price)

然后添加一堆我想做的事情:

  if(price< currentPrice)
{
product.Prices.Add(price);
_emailService.Email();
_discordBot.Broadcast();
_productUpdater.UpdateRatings();
// etc等
}

如何在不使我的域依赖于服务?还是应该将这些内容传递给我的域?


不确定采用最佳方法(或说实话的任何方法),我已阅读过有关域事件的信息,但我认为这些要比我目前的知识略高一点经验水平,我对材料不太了解

解决方案

我可以想到不同的选择,具体取决于您的具体情况需求-或多或少都适合,也可以为不同的用例选择不同的方法并将其混合在您的解决方案中。


为了说明这一点,我想根据一个我简单地调用 AddPriceToProduct(AddProductPriceCommandpriceCommand)的产品应用程序的操作。它代表用例,其中添加了产品的新价格。 AddProductPriceCommand 是一个简单的DTO,其中包含执行用例所需的所有数据。




选项(A) 注入将域逻辑执行到域对象的方法中时需要调用的相应服务(例如,电子邮件服务)(此处为 AddPrice )。


如果始终选择此方法,请始终传递接口(在您的域层中定义),而不是实际实现(应在基础架构层中定义)。另外,如果您的域操作中发生了某些事情 之后,我不会选择这种方法。

  public void AddPriceToProduct(AddProductPriceCommandpriceCommand)
{
var product = _productRepository.findById(pricingCommand.productId);
product.AddPrice(pricingCommand.price,_emailService);
_productRepository.Update(product);
}

相应的 AddPrice 方法可能如下所示:

  public void AddPrice(int price,IEmailService emailService)
{
var currentPrice = _prices.LastOrDefault();

if(price< currentPrice)
{
_prices.add(price);
//使用所需参数调用电子邮件服务
emailService.Email(this,price);
}
}




选项(B) :在调用相应的聚合(或域服务)方法后,让应用程序服务(协调用例)调用相应的服务。需要针对应用程序用例执行。


如果应该始终在执行特定域模型操作之后执行此操作,则这可能是一种简单有效的方法。我的意思是,在您的集合(或域服务)上调用该方法后,如果您使用的是 AddPrice 方法,那么如果其他服务(例如,

  public void AddPriceToProduct(AddProductPriceCommand PriceCommand)
{
var product = _productRepository.findById (pricingCommand.productId);
product.AddPrice(pricingCommand.price);
_productRepository.Update(product);
//始终发送电子邮件作为常规工作流程的一部分
_emailService.Email(product,PricelineCommand.price);
}

在这种情况下,我们假设正常的工作流程将始终包括此附加步骤。我在这里看不到实用的问题,只需在应用程序服务方法中调用相应的服务即可。




选项(C):类似于选项(B),但有条件逻辑要在调用 AddPrice 后执行。在这种情况下,可以将此逻辑包装到单独的域服务中,该服务将根据 Product 的当前状态或结果(如果存在)来处理条件部分任何操作-域操作( AddPrice )。


我们首先简单地通过包括一些域知识来更改应用程序服务方法:

  public void AddPriceToProduct(AddProductPriceCommandPriceCommand)
{
var product = _productRepository.findById(pricingCommand.productId);
product.AddPrice(pricingCommand.price);
_productRepository.Update(product);

if(product.HasNewPrice())
{
_emailService.Email(product,PricelineCommand.price;
}
if(product.PriceTargetAchieved( ))
{
_productUpdater.UpdateRatings(product,PricelineCommand.price);
}
}

现在这种方法还有一些改进的空间。由于要执行的逻辑与产品的AddPrice()方法绑定,因此可能很容易错过需要调用其他逻辑(调用电子邮件服务)的情况。或在某些情况下的更新程序服务。)当然,您可以将所有服务注入Product实体的 AddPrice()方法中,但在这种情况下,我们希望研究将逻辑提取到 域服务


首先,让我们看看应用程序服务方法的新版本:

  public void AddPriceToProduct(AddProductPriceCommand价格命令)
{
var product = _productRepository.findById(pricingCommand.productId);
_productPricingService.AddPrice(product,PricelineCommand.price);
_productRepository.Update(product);
}

现在,让我们看一下域服务的相应域服务方法,例如 ProductPricingService

  public void AddPrice(产品产品,价格)
{
if( product.HasNewPrice())
{
_emailService.Email(product,PriceCommand.price;
}
if(product.PriceTargetAchieved())
{
_productUpdater.UpdateRatings(product,PricelineCommand.price);
}
}

现在,逻辑用于处理产品价格更新的操作在域层进行处理。此外,域逻辑 更易于单元测试 ,因为 依赖项较少 (例如,此处无需考虑存储库),并且只需使用较少的测试倍数(模拟)即可。


当然是在域模型中,结合最高程度的业务逻辑封装仍然不是最高程度的业务逻辑封装 ,但它至少要紧密得多。


实现以上提到的组合域事件将起作用,但是当然这些也可能需要更多的实现工作。让我们在下一个选项中进行查看。




选项(D):从您的域实体引发域事件并实施相应的处理程序,可能是域服务,甚至可能是基础结构服务。


域事件发布者(您的域实体或域服务)与订阅者(例如电子邮件服务,产品更新程序等)之间的连接。 )。


在这种情况下,我建议不要立即分派引发的事件,而应在一切正常后(例如,未引发任何异常,状态得以持久等)收集它们。


让我们再次使用相应的 Product 实体的 AddPrice()方法。域事件。

 公共无效AddPrice(int price,IEmailService emailService)
{
var currentPrice = _prices.LastOrDefault() ;

if(price< currentPrice)
{
_prices.add(price);
RaiseEvent(
new ProductPriceUpdatedEvent(
this.Id,
price
));
}
}

ProductPriceUpdateEvent 是一个简单的类,代表过去发生的业务事件以及该事件的订阅者所需的信息。在您的情况下,订阅者将是电子邮件服务,产品更新服务等。


RaiseEvent()方法视为添加创建的事件对象是产品实体集合的对象,以便收集从一个应用程序或域服务调用的一个或多个业务操作期间发生的所有事件。此事件收集功能也可以是实体基类的一部分,但这是实现细节。


重要的是,在 AddPrice()方法具有在执行之后,应用程序层将确保所有收集到的事件都将分派给相应的订户。


域模型完全独立于基础结构服务依赖项以及事件


在分发前进行确认 方法。 com / posts / domain-events-simple-reliable-solution / rel = nofollow noreferrer>博客由弗拉基米尔·霍里科夫(Vladimir Khorikov)阐述了这一想法,并且也是基于您的技术堆栈。


注意:与其他解决方案相比,对 Product 域实体的逻辑进行单元测试现在非常简单,因为您没有任何依赖关系,并且模拟不应该完全必要。而且,测试相应的域事件是否已在正确的操作上进行测试也很容易,因为您只需在调用 Product 实体上的业务方法后,从该实体中查询收集到的事件即可。 b $ b



返回您的问题


如何实施


要实现此目的,您可以查看选项(B),(C)和(D )


还是我应该将其传递给我的域?


这可能是一种有效的方法-请参见选项(A)-但请注意,如果要根据域模型的可维护性和可测试性注入多个依赖关系,会使情况变得更加复杂


当我在这些不同的选项之间进行选择时,我总是试图找出所执行动作的哪些部分确实属于该cor响应业务操作以及哪些部分或多或少是不相关的,并且不是真正需要使业务交易有效的部分。


例如,如果某项操作需要由服务执行需要执行,否则整个操作根本就不会发生(就一致性而言),那么选项(A)(将服务注入域模型方法)可能是一个很好的选择。否则,我将尝试将后续步骤与域模型逻辑脱钩,在这种情况下,应考虑其他选项。


I have a domain model Product with a list of Prices.

  public class Product
  {    
    private List<int> _prices; //Note that this is a value object in my actual code

       public void AddPrice(int price)
       {

         var currentPrice = _prices.LastOrDefault();
      
          if(price < currentPrice)
            _prices.add(price)
       }

    }

When a price changes I want a bunch of things to happen. Using an anemic domain model this is quite easy because I can just keep this bit in my service:

 if(price < currentPrice)
        _prices.Add(price)

And then tack on a bunch of stuff I want to do:

     if(price < currentPrice)
        {
            product.Prices.Add(price);
            _emailService.Email();
            _discordBot.Broadcast();
            _productUpdater.UpdateRatings();
           //etc etc
        }

How can I implement this without making my domain reliant on the services? Or should I be passing those to my domain?

Unsure on best approach (or any approach to be honest), I have read about Domain Events but I think those are a bit above my current experience level and I didn't understand the material very well

解决方案

I can think of different options which are - depending on your concrete requirements - more or less suited and it is also OK to choose different approaches for different use cases and mix them in your solution.

To illustrate this I want to look into different options based on an operation of a product application which I simply call AddPriceToProduct(AddProductPriceCommand pricingCommand). It represents the use case where a new price for a product is added. The AddProductPriceCommand is a simple DTO which holds all required data to perform the use case.


Option (A): Inject the corresponding service (for instance, an email service) you need to call when executing your domain logic into your domain object's methods (here AddPrice).

If you choose this approach always pass in an interface (which is defined in your domain layer) rather than the actual implementation (which should be defined in the infrastructure layer). Also, I would not choose this approach if several things should happen after something has happened in your domain operation.

public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
    var product = _productRepository.findById(pricingCommand.productId);
    product.AddPrice(pricingCommand.price, _emailService);
    _productRepository.Update(product);
}

And the corresponding AddPrice method might look like this:

public void AddPrice(int price, IEmailService emailService)
{
    var currentPrice = _prices.LastOrDefault();
  
    if(price < currentPrice)
    {
        _prices.add(price);
        // call email service with whatever parameters required
        emailService.Email(this, price);  
    }
}


Option (B): Let the application service (which orchestrates the use cases) call the corresponding service(s) after you called the corresponding aggregate (or domain service) method which needs to be executed for the application use case.

This can be a simple and valid approach if this should always happen after a specific domain model operation has been executed. By that I mean, after calling the method on your aggregate (or domain service), in your case the AddPrice method, there is no conditional logic if the other services (e.g. email) should be called or not.

public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
    var product = _productRepository.findById(pricingCommand.productId);
    product.AddPrice(pricingCommand.price);
    _productRepository.Update(product);
    // always send an email as part of the usual workflow
    _emailService.Email(product, pricingCommand.price);
}

In this case we assume that the normal workflow will always include this additional step. I do not see a problem with being pragmatic here and just call the corresponding service in the application service method.


Option (C): Similar to Option (B) but there is conditional logic to be executed after AddPrice has been called. In this case this logic can be wrapped into a separate domain service which would take care of the conditional part based on the current state of the Product or the result - if there is any - of the domain operation (AddPrice).

Let's first simply change the application service method by including some domain knowledge:

public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
    var product = _productRepository.findById(pricingCommand.productId);
    product.AddPrice(pricingCommand.price);
    _productRepository.Update(product);

    if (product.HasNewPrice())
    {
        _emailService.Email(product, pricingCommand.price;
    }
    if (product.PriceTargetAchieved())
    {
        _productUpdater.UpdateRatings(product, pricingCommand.price);
    }
}

Now this approach has some space for improvements. As the logic to performed is bound to the AddPrice() method of the product it might be easy missed that the additional logic needs to be called (calling the email service or the updater service under certain circumstances). Of course you could inject all services into the AddPrice() method of the Product entity but in this case we want to look into the option of extracting the logic into a domain service.

At first let's look at a new version of the application service method:

public void AddPriceToProduct(AddProductPriceCommand pricingCommand)
{
    var product = _productRepository.findById(pricingCommand.productId);
    _productPricingService.AddPrice(product, pricingCommand.price);
    _productRepository.Update(product);
}

And now let's look at the corresponding domain service method of a domain service called, e.g. ProductPricingService:

public void AddPrice(Product product, int price)
{
    if (product.HasNewPrice())
    {
        _emailService.Email(product, pricingCommand.price;
    }
    if (product.PriceTargetAchieved())
    {
        _productUpdater.UpdateRatings(product, pricingCommand.price);
    }
}

Now the logic for handling price updates to a product are handled at the domain layer. In addtion, the domain logic is easier to unit test as there are fewer dependencies (e.g. the repository is not of concern here) and with that fewer test doubles (mocking) need to be used.

It is of course still not the highest degree of business logic encapsulation in combination with the lowest degree of dependencies inside the domain model, but it comes at least a little closer.

To achieve the above mentioned combination domain events will be at service, but of course these could also come with a higher amount of implementation efforts. Let's look at this in the next option.


Option (D): Raise domain events from your domain entities and implement the corresponding handlers which could be domain services or even infrastructure services.

The connection between domain event publishers (your domain entities or domain services) and the subscribers (e.g. email service, product updater, etc.).

In this case I recommend to not immediately dispatch raised events but rather collecting them and only after everything has worked out fine (i.e. no exceptions have been thrown, state has been persisted, etc.) dispatch them to be handled.

Let's look at the AddPrice() method of the Product entity again by using a corresponding domain event.

public void AddPrice(int price, IEmailService emailService)
{
    var currentPrice = _prices.LastOrDefault();
  
    if(price < currentPrice)
    {
        _prices.add(price);
        RaiseEvent(
            new ProductPriceUpdatedEvent(
                this.Id,
                price
            ));
    }
}

The ProductPriceUpdateEvent is a simple class which represents the business event that has happened in the past along with the information required by subscribers to this event. In your case the subscribers would be the email service, the product update service, etc.

Consider the RaiseEvent() method as a simple method which adds the created event object to a collection of the product entity in order to collect all events happending during one or more business operations that are called from an application or domain service. This event collecting functionality could also be part of an entity base class but that is an implementation detail.

The important thing is that after the AddPrice() method has been executed the application layer will make sure that all collected events will be dispatched to the corresponding subscribers.

With that the domain model is completely independent of the infrastructure service dependencies as well as from the event dispatching code.

The "Committing before dispatching" approach described in this blog post by Vladimir Khorikov illustrates this idea and is also based on your technology stack.

Note: Unit testing the logic of your Product domain entity is now very simple as opposed to the other solutions as you don't have any dependencies and mocking should not be necessary at all. And testing if the corresponding domain events have been called at the right operations is also easy as you simply have to query the collected events from the Product entity after calling a business method on it.


To get back to your questions:

How can I implement this without making my domain reliant on the services?

To achieve this you can look into options (B), (C) and (D)

Or should I be passing those to my domain?

This can be a valid approach - see option (A) - but be aware that it will make things more complicated if there are several dependencies to be injected in terms of maintainability and testability of your domain model classes.

When I choose between these different options I always try to find out what parts of the performed actions do really belong to that corresponding business operation and what parts are more or less unrelated and are not really required to make the business transaction a valid one.

For instance, if some operation that needs to be performed by a service is required to happen or otherwise the whole operation should not happen at all (in terms of consistency) then option (A) - injecting a service into a domain model method - might be a good fit. Otherwise I would try to decouple any subsequent steps from the domain model logic in which case the other options should be considered.

这篇关于从域模型向下传播到应用层的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆