我刚开始使用 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);
}
}
下面是我对实现的看法:
- 基本上,订单可以处于三种状态:“DRAFT”、“VALIDATED”和“INVALID”。 “DRAFT”状态可以包含任何类型的无效数据,“VALIDATED”状态应该只包含有效数据,“INVALID”应该包含无效数据。
- 因此,应该有一个尝试切换订单状态的方法,我们称它为
order.validate(...)
。该方法将执行状态转换所需的验证(DRAFT -> VALIDATED 或 DRAFT -> INVALID),如果成功 - 更改状态并传输 OrderValidated 或 OrderInvalidated 事件。
现在,我正在努力解决的是上述 order.validate(...)
方法的签名。为了验证订单,它需要其他几个聚合,即 BillCycle
和 Client
。我可以看到这些解决方案:
- 将这些聚合直接放入
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();
}
}
}
从我的角度来看,您的三种解决方案的优缺点:
- 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();
}
- order.validate(new OrderValidationData(client.getDiscountInfos(), getListOfPeriods(cycles))
同上,仍然需要为OrderUnitTest和discountOrderProcessUnitTest准备数据。但我认为这个更好,因为订单与 Client 和 BillCycle 没有紧密耦合。
- 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/