当没有聚合时,域逻辑应该去哪里? 简短的问题一个长而具体的例子我的想法

问题描述

简短的问题。

从 DDD 的角度(或只是可维护的架构和常识),我应该把没有域对象操作但做一些“智能”数据准备并编排一堆副作用调用的业务逻辑层放在哪里? “智能”是指它具有一些规则和操作顺序限制,而不仅仅是构建 DTO。在每个方法之前调用带有数据准备的方法的幼稚实现看起来很混乱,而且随着逻辑变得更复杂,情况会变得更糟。此外,如果不“模拟整个宇宙”,这种实现是无法测试的。

一个长而具体的例子。

我有一个与 Order API 通信的订单 Web 应用程序。我的 Web 应用程序在订单抛出 API 中有一个更新订单位置的操作。从前端获取数据并保存的实现方法包括三个阶段:

  1. 删除现有职位
  2. 添加新职位
  3. 重新申请折扣

这是 Order API 的一个奇怪的实现,我无法控制它。 代码看起来像这样

public async Task SaveOrderPosition(Positionviewmodel positionviewmodel)
{
    var deleteDto = _dtoBuilder.BuildForDeletion(positionviewmodel);
    var deleteResult = await api.Delete(deleteDto);
    if(deleteResult.IsFailure)
    {
        throw new Exception();
    }
    var addDto = _dtoBuilder.BuildForAdding(positionviewmodel); //generates some new ids and data. the "smart" part
    var addResult = await api.Add(addDto);
    if(addResult.IsFailure)
    {
        throw new Exception();
    }

    var discountsDto = _dtoBuilder.Builddiscounts(addDto,positionviewmodel); //also "smart" part as it has dependency on added data
    var discountsResult = await api.Applydiscounts(addDto);
    if(discountsResult.IsFailure)
    {
        throw new Exception();
    }
}

正如我所看到的,这段代码 a) 无法通过单元测试进行测试,b) dto builder 中存在一些重要的逻辑,c) 随着新规则的出现或 API 的更改,它只会变得更加混乱。 (也许我错了,没关系)

我的想法

所以在这里我没有看到任何用于使代码更可测试和可扩展的域模型对象。

  1. 一个想法 - 构建订单对象并执行类似操作
order.UpdatePosition(position);
await Save(order);

但是创建整个 Order 对象是一个开销很大的操作,并且除了 viewmodel 之外的数据实际上对于 Save 操作来说不是必需的

  1. (愚蠢的)实现位置域模型。
public async Task SaveOrderPosition(Positionviewmodel positionviewmodel)
{
    var position = new Position(positionviewmodel); //actually creates inconsistent objects
    position.Save(); 
    await Save(position.Events);
}

class Position
{
    public List<PEvent> Events {get;}

    public void Save()
    {
        Events.Add(new DeleteEvent());
        // do some "smart" staff
        Events.Add(new AddEvent());
        // do some discounts staff
        Events.Add(new ApplydiscountEvent());
    }
}

好的部分是一些业务逻辑与应用层解耦并且是可测试的。但是位置模型(因为它在构造后具有不相关的状态)或其 Save 方法也没有实际意义,因为位置不会自我保存。在我看来

  1. 创建域服务。看起来像我需要的东西 - 保存不属于任何实体的操作的东西。但也有人说域服务应该在几个聚合上运行,我没有。另外,在这种情况下,这种服务的方法应该返回什么?在 API 调用中使用的一系列事件或类似 DTO 元组的东西?

所以实际上我正在努力将代码从程序方式移动到面向对象和 DDD 方式,因为我的域中没有对象负责所需的操作。 (或者我只是没有看到)

解决方法

实际上,根据 KISS 和 YAGNI 的说法,您应该使代码尽可能简单并且不要过度设计,因此如果您只描述了 3 个用例,我认为目前的方法很好。我相信您可以使用单元测试包装对 DtoBuilder 的调用。

如果您希望它更加结构化,我建议您考虑使用贫血域模型的方法,因为您似乎拥有非常简单明了的业务规则。在这种方法中,您的域实体非常简单(它们类似于 dtos),因为它们不包含任何业务逻辑,而仅包含 getter 和 setter。所有业务逻辑都转到服务方法。在这里,您可以对服务方法的调用进行单元测试。

关于富域模型(包含聚合、实体内部的逻辑和其他内容):

  1. aggregate 不仅仅是一个具有行为的实体,而是控制事务边界和自身内部数据一致性的实体,底层实体和值对象。也就是说,如果您似乎有简单的聚合,那就完全没问题
  2. 您不必拥有多个聚合来将业务逻辑移动到域服务,但是您可以(这取决于您认为最适合您的方式,这不是法律)如果实体内部的逻辑看起来不自然(这里我也更喜欢考虑数据一致性:如果数据仍然一致,我可以将逻辑移至域服务)