0

简短的问题。

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

一个长而具体的例子。

我有一个与 Order API 通信的订单 Web 应用程序。My Web App 有一个在订单抛出 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 方式,因为我的域中没有对象负责所需的操作。(或者我只是没有看到)

4

1 回答 1

1

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

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

至于富域模型(具有聚合、实体内部的逻辑和其他内容):

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