validation - 领域驱动设计 - 跨不同聚合的命令的复杂验证

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

我刚开始使用 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(周期))
  • 在单独的服务中进行验证 可以随心所欲地聚合它的方法 想要(基本上类似于上面的 CRUD 示例)。然而,这似乎 远离 DDD,因为方法 order.validate() 将成为虚拟状态 setter,调用这个方法可以带来一个 命令不直观地进入损坏状态(状态=“有效”但 包含无效数据,因为没有人费心调用验证 服务)。

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

提前致谢。

最佳答案

引入委托(delegate)对象来操作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(client, cycles)

很容易用订单测试验证。

#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(cycles))

同上,仍然需要为OrderUnitTest和discountOrderProcessUnitTest准备数据。但我认为这个更好,因为订单与 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));
}

关于validation - 领域驱动设计 - 跨不同聚合的命令的复杂验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26729462/

相关文章:

language-agnostic - 如何构建无锁队列?

.net - 域对象 - "Smart object"vs POCO

domain-driven-design - 领域驱动设计中跨界上下文的实体

php - 如何在 Laravel 中验证日期

php - 如何只使用某些验证集来验证 Cake PHP 中的数据?

parsing - 'parse' 的反义词是什么?

domain-driven-design - 域内的域服务或方法

c# - 带有javascript确认功能的必填字段验证器未验证

model-view-controller - 比较日期 DataAnnotations Validation asp.net mvc

language-agnostic - 为什么存在静态 Create 方法?