【问题标题】:Domain Driven Design - complex validation of commands across different aggregates领域驱动设计 - 跨不同聚合的复杂命令验证
【发布时间】:2014-12-31 00:11:04
【问题描述】:

我刚开始使用 DDD,目前正试图掌握用它做不同事情的方法。我正在尝试使用带有 CQRS 的异步事件(还没有事件源)来设计它。目前我坚持验证命令。我读过这个问题:Validation in a Domain Driven Design,但是,似乎没有一个答案涵盖跨不同聚合根的复杂验证。


假设我有这些聚合根:

  • Client - 包含已启用服务的列表,每个服务都可以有一个价值对象的折扣列表及其有效性。
  • DiscountOrder - 为给定客户的某些服务启用更多折扣的订单,包含带有折扣配置的订单项目。
  • BillCycle - 每个产生账单的时期都由自己的账单周期描述。

这是用例:

可以提交折扣订单。折扣订单中的每个新折扣期不应与任何 BillCycles 重叠。一项服务不能同时激活两种相同类型的折扣。

基本上,在 CRUD 样式中使用 Hibernate,这看起来类似于(java 代码,但问题与语言无关):

public class DiscountProcessor {
    ...
    @Transactional
    public void processOrder(long orderId) {
        DiscOrder order = orderDao.get(orderId);
        BillCycle[] cycles = billCycleDao.getAll();

        for (OrderItem item : order.getItems()) {
            //Validate billcycle overlapping
            for (BillCycle cycle : cycles) {            
                if (periodsOverlap(cycle.getPeriod(), item.getPeriod())) {
                    throw new PeriodsOverlapWithBillCycle(...);
                }
            }
            //Validate discount overlapping
            for (Discount d : item.getForService().getDiscounts()) {
                if (d.getType() == item.getType() && periodsOverlap(d.getPeriod(), item.getPeriod())) {
                    throw new PeriodsOverlapWithOtherItems(...);
                }
            }
            //Maybe some other validations in future or stuff
            ...
        }

        createDiscountsForOrder(order);
    }
}

下面是我对实施的想法:

  1. 基本上,订单可以处于三种状态:“DRAFT”、“VALIDATED”和“INVALID”。 “DRAFT”状态可以包含任何类型的无效数据,“VALIDATED”状态应该只包含有效数据,“INVALID”应该包含无效数据。
  2. 因此,应该有一个尝试切换订单状态的方法,我们称之为order.validate(...)。该方法将执行状态转换所需的验证(DRAFT -> VALIDATED 或 DRAFT -> INVALID),如果成功 - 更改状态并传输 OrderValidated 或 OrderInvalidated 事件。

现在,我正在努力解决的是上述order.validate(...) 方法的签名。要验证订单,它需要几个其他聚合,即BillCycleClient。我可以看到这些解决方案:

  • 将这些聚合直接放入validate 方法中,例如 order.validateWith(client, cycles)order.validate(new OrderValidationData(client, cycles))。不过,这似乎有点 骇人听闻。
  • 从客户端和循环中提取所需信息 进入某种中间验证数据对象。就像是 order.validate(new OrderValidationData(client.getDiscountInfos(), getListOfPeriods(cycles))
  • 在单独的服务中进行验证 可以用任何聚合来做任何事情的方法 想要(基本上类似于上面的 CRUD 示例)。然而,这似乎 远离 DDD,因为方法 order.validate() 将成为虚拟状态 setter,调用此方法将可以带来一个 命令不直观地进入损坏状态(状态=“有效”但 包含无效数据,因为没有人打扰调用验证 服务)。

正确的做法是什么,难道我的整个思考过程都是错误的?

提前致谢。

【问题讨论】:

  • 当多个聚合在一个进程中起作用时,通常可以使用域服务来封装这个逻辑。但是,由于validate 状态更改行为似乎在Order 中找到了它的自然归宿,我可能会选择像您想要的那样将执行验证所需的依赖项传递给order.validate,或者传递IOrderValidatingSpecificationorder.validate 并让订单双重发送给它。

标签: validation language-agnostic domain-driven-design cqrs


【解决方案1】:

这 3 种可能的状态是否反映了您的领域,还是只是推断?我之所以问,是因为您的示例代码似乎并没有改变 Order 状态,而是在它无效时抛出异常。

如果订单在提交后短时间保持 DRAFT 是可以接受的,您可以让 DiscountOrder 发出 DiscountOrderSubmitted 域事件。处理程序捕获事件并(委托给域服务)检查提交是否合法。然后它会发出一个ChangeOrderState 命令来使订单为已验证或无效。

您甚至可以假设默认情况下更改是合法的,并让processOrder() 直接将其带到 VALIDATED,直到验证服务提供的后续 INVALID 反订单证明并非如此。

这与您的第三个解决方案或 Hippoom 的解决方案没有太大区别,只是该过程的每个步骤都使用自己的域事件明确表示。我猜你目前的聚合设计注定会有一个第三方编排器(听起来像非 DDD 和事务脚本式)来控制这个过程,因为 DiscountOrder 聚合没有本机访问权限到所有信息来判断给定的转换是否有效。

【讨论】:

  • 示例代码只是一个示例,任务不是将其迁移到 DDD,而是使 UseCase 正确使用 DDD。该示例取自真实的商业案例,其中确实具有这些状态。在我的情况下,必须尽量减少 DRAFT 和 VALID 之间的等待时间,因为最终用户正在同步等待这种情况发生。至于由于聚合根错误而需要编排器,您能否描述一个更好的聚合根选择以更适合 DDD 覆盖此场景?整个项目本身只是一个草稿,让我了解 DDD,所以我可以做任何更改。
  • 我并不是在暗示存在更好的设计。您可以在 Order 聚合中包含账单周期和服务折扣,但我想这在概念上没有任何意义,除非可以在创建订单时将它们复制并保持不变(因为同步很难和邪恶)。一些不变量本质上跨越了系统的整个部分,您无能为力。我认为您提出的解决方案是有效的,除了我可能不会将其命名为 Validate,而是更接近领域语言(例如,您提到了提交)。
  • 另一种方法是使DraftOrderOrder 成为两个独立的实体,并将验证作为Order 构造函数的一部分包含在内,或者如果构造函数变得过于复杂,则创建OrderFactory臃肿。
  • 是的,我也考虑过将 DraftOrder 和 Order 分开。这可能是另一种看起来更好的解决方案。我要试一试,看看效果如何。
【解决方案2】:

引入一个委托对象来操作 Order、Client、BillCycle 怎么样?

class OrderingService {
    @Injected private ClientRepository clientRepository;

    @Injected private BillingRepository billRepository;

    Specification<Order> validSpec() {
        return new ValidOrderSpec(clientRepository, billRepository);
    }

}

class ValidOrderSpec implements Specification<Order> {
    @Override public boolean isSatisfied(Order order) {
        Client client = clientRepository.findBy(order.getClientId());
        BillCycle[] billCycles = billRepository.findAll();
        // validate here
    }
}

class Order {
    void validate(ValidOrderSpecification<Order> spec) {
        if (spec.isSatisfiedBy(this) {
            validated();
        } else {
            invalidated();
        }
    }
}

从我的角度来看,您的三种解决方案的优缺点:

  1. order.validateWith(客户端,周期)

用顺序测试验证很容易。

#file: OrderUnitTest
@Test public void should_change_to_valid_when_xxxx() {
    Client client = new ClientFixture()...build()
    BillCycle[] cycles = new BillCycleFixture()...build()
    Order order = new OrderFixture()...build();

    subject.validateWith(client, cycles);

    assertThat(order.getStatus(), is(VALID));
}

到目前为止一切顺利,但 DiscountOrderProcess 似乎有一些重复的测试代码。

#file: DiscountProcessor
@Test public void should_change_to_valid_when_xxxx() {
    Client client = new ClientFixture()...build()
    BillCycle[] cycles = new BillCycleFixture()...build()
    Order order = new OrderFixture()...build()
    DiscountProcessor subject = ...

    given(clientRepository).findBy(client.getId()).thenReturn(client);
    given(cycleRepository).findAll().thenReturn(cycles);        
    given(orderRepository).findBy(order.getId()).thenReturn(order);

    subject.processOrder(order.getId());

    assertThat(order.getStatus(), is(VALID));
}

#or in mock style
@Test public void should_change_to_valid_when_xxxx() {
    Client client = mock(Client.class)
    BillCycle[] cycles = array(mock(BillCycle.class))
    Order order = mock(Order.class)
    DiscountProcessor subject = ...

    given(clientRepository).findBy(client.getId()).thenReturn(client);
    given(cycleRepository).findAll().thenReturn(cycles);        
    given(orderRepository).findBy(order.getId()).thenReturn(order);

    given(client).....
    given(cycle1)....

    subject.processOrder(order.getId());

    verify(order).validated();
}
  1. order.validate(new OrderValidationData(client.getDiscountInfos(), getListOfPeriods(周期))

同上,你仍然需要为 OrderUnitTest 和 discountOrderProcessUnitTest 准备数据。但我认为这个更好,因为 order 与 Client 和 BillCycle 没有紧密耦合。

  1. order.validate()

如果您在域层中保留验证,则与我的想法类似。有时这不是任何实体的责任,请考虑域服务或规范对象。

#file: OrderUnitTest
@Test public void should_change_to_valid_when_xxxx() {
    Client client = new ClientFixture()...build()
    BillCycle[] cycles = new BillCycleFixture()...build()
    Order order = new OrderFixture()...build();
    Specification<Order> spec = new ValidOrderSpec(clientRepository, cycleRepository);        

    given(clientRepository).findBy(client.getId()).thenReturn(client);
    given(cycleRepository).findAll().thenReturn(cycles);  


    subject.validate(spec);

    assertThat(order.getStatus(), is(VALID));
}

#file: DiscountProcessor
@Test public void should_change_to_valid_when_xxxx() {
    Order order = new OrderFixture()...build()
    Specification<Order> spec = mock(ValidOrderSpec.class);
    DiscountProcessor subject = ...

    given(orderingService).validSpec().thenReturn(spec);
    given(spec).isSatisfiedBy(order).thenReturn(true);        
    given(orderRepository).findBy(order.getId()).thenReturn(order);

    subject.processOrder(order.getId());

    assertThat(order.getStatus(), is(VALID));
}

【讨论】:

  • 嗯,所以基本上,它与我的 (1) 解决方案相同,但不是委托一组聚合实体,而是委托一组具有更好命名选择的聚合存储库。我也喜欢所有的验证都耦合在一个“规范”类中。我相信这是尽其所能,唯一奇怪的是,订单需要规范来改变状态。我的意思是,总会有一种方法来验证这种订单,所有订单都应该遵循相同的规范,但它们仍然需要一个规范实例。
猜你喜欢
  • 1970-01-01
  • 2010-11-01
  • 2013-09-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-27
相关资源
最近更新 更多