实体方法调用上的DDD注入服务


11

问题的简短格式

在DDD和OOP的最佳实践中,是否可以在实体方法调用上注入服务?

长格式示例

假设我们在DDD中有一个经典的Order-LineItems案例,其中有一个称为Order的域实体,它也充当聚合根,并且该实体不仅由其Value Objects组成,而且还包含Line Item的集合实体。

假设我们希望在应用程序中使用流利的语法,以便我们可以做类似的事情(请注意第2行中的语法,在此称为getLineItems方法):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

我们不想将任何LineItemRepository注入OrderEntity,因为这违反了我能想到的几个原则。但是,语法的流畅性是我们真正想要的,因为它易于阅读和维护以及测试。

考虑下面的代码,指出该方法getLineItemsOrderEntity

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

这是在实体中实现流利语法而不违反DDD和OOP核心原则的公认方法吗?在我看来,这很好,因为我们仅公开服务层,而不公开基础结构层(嵌套在服务中)

Answers:


9

这是完全正常到实体呼叫传递域服务。假设我们需要使用一些复杂的算法(例如,取决于客户类型)来计算发票金额。可能是这样的:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

但是,另一种方法是通过域事件来分离位于域服务中的业务逻辑。请记住,这种方法仅意味着不同的应用程序服务,但具有相同的数据库事务范围。

第三种方法是我所支持的方法:如果我发现自己使用域服务,则可能意味着我错过了一些域概念,因为我的概念主要是用名词而非动词来建模的。因此,理想情况下,我根本不需要域服务,并且我所有业务逻辑的很大一部分都驻留在装饰器中


6

我很震惊地在这里阅读一些答案。

将域服务传递到DDD中的实体方法中以委派一些业务计算是完全有效的。例如,假设您的聚合根(实体)需要通过http访问外部资源,以执行一些业务逻辑并引发事件。如果您不通过实体的业务方法注入服务,您将如何做?您将在实体内部实例化http客户端吗?这听起来像一个可怕的主意。

这是不正确的是通过其构造函数将服务注入聚合中。但是通过一种商业方法,这是可以的并且完全正常。


1
为什么您给出的案例不属于域服务的责任?
e_i_pi '18

1
它是一个域服务,但以业务方法注入。应用程序层只是一个协调器,
diegosasw

我没有DDD经验,但是不应从应用程序服务调用域服务,并且在域服务验证之后继续通过该应用程序服务调用实体方法吗?我在我的项目中遇到了同样的问题,因为域服务通过存储库运行数据库调用...我不知道这是否可以。
Muflix

域服务应该协调,如果您稍后从应用程序中调用它,则意味着您以某种方式处理响应,然后对其进行了处理。也许听起来像是业务逻辑。如果是这样,则它属于“域”层,而应用程序以后只需解决依赖性并将其注入到聚合中。域服务可能已经注入了一个存储库,该存储库的实现命中数据库应该属于基础结构层(仅是实现,而不是接口/合同)。如果它描述您的通用语言,则它属于domain。
diegosasw

5

在DDD和OOP的最佳实践中,是否可以在实体方法调用上注入服务?

不,您不应在域层内注入任何东西(这包括实体,值对象,工厂和域服务)。该层应与任何框架,第三方库或技术无关,并且不应进行任何IO调用。

$order->getLineItems($orderService)

这是错误的,因为集合本身不需要其他任何东西即可返回订单项。在整个骨料应的方法调用之前已经加载。如果您认为应该延迟加载,则有两种可能性:

  1. 您的集合边界是错误的,它们太大。

  2. 在此用例中,仅将“聚合”用于读取。最好的解决方案是将写入模型与读取模型分开(即使用CQRS)。在这种更清洁的体系结构中,您不允许查询聚合,而只能查询读取模型。


如果我需要数据库调用进行验证,则必须在应用程序服务中调用它,并将结果传递给域服务或直接传递到聚合根目录,而不是将存储库注入域服务?
Muflix '19

1
@Muflix是的,没错
Constantin Galbenu

3

DDD战术模式中的关键思想:应用程序通过作用于聚合根来访问应用程序中的所有数据。这意味着在域模型之外可访问的唯一实体是聚合根。

订单聚合根永远不会产生对其lineitem集合的引用,该引用将允许您修改该集合,也不会产生对任何允许您对其进行修改的订单项的引用的集合。如果要更改订单总数,则适用好莱坞原则:“告诉,不要问”。

从集合内返回是可以的,因为值本质上是不可变的。您无法通过更改其副本来更改其数据。

使用域服务作为参数来协助聚合提供正确的值是一件非常合理的事情。

通常,您不会使用域服务来访问聚合内的数据,因为聚合应该已经可以访问它。

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

因此,如果我们尝试访问此订单的订单项值集合,则拼写很奇怪。更自然的拼写是

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

当然,这前提是订单项已经加载。

通常的模式是,聚合的负载将包括特定用例所需的所有状态。换句话说,您可能有几种不同的方式来加载同一聚合。您的存储库方法适合目的

这种方法不是您在原始Evans中所能找到的,他在Evans中假设聚合将具有与其关联的单个数据模型。它更自然地落在CQRS中。


谢谢你 现在,我已经阅读了大约一半的“红皮书”,并且第一次尝试在基础结构层中正确应用好莱坞原则。重新阅读所有这些答案,它们都是很有意义的,但是我认为您对于lineItems()首次检索聚合根的范围和预加载有一些非常重要的观点。
e_i_pi

3

一般来说,属于聚合的价值对象本身没有存储库。填充它们是聚合根的责任。在您的情况下,填充Order实体和OrderLine值对象是OrderRepository的责任。

关于OrderRepository的基础结构实现,在ORM的情况下,它是一对多关系,您可以选择紧急加载或延迟加载OrderLine。

我不确定您的服务的确切含义。它与“应用程序服务”非常接近。如果是这种情况,将服务注入到根/实体/值对象聚合通常不是一个好主意。应用程序服务应该是聚合根/实体/值对象和域服务的客户端。关于您的服务的另一件事是,在Application Service中公开值对象也不是一个好主意。应通过聚合根访问它们。


2

答案是:绝对不能,避免在实体方法中传递服务。

解决方案很简单:只需让Order存储库返回带有所有LineItems的Order。在您的情况下,汇总为Order + LineItems,因此,如果存储库未返回完整的汇总,则说明它没有完成其工作。

更广泛的原则是:将功能位(例如域逻辑)与非功能位(例如持久性)分开。

还有一件事:如果可以,请避免这样做:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

改为这样做

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

在面向对象的设计中,我们尝试避免在对象数据中四处走动。我们宁愿要求对象做我们想要做的事情。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.