时间:2025-05-09 21:17
人气:
作者:admin
我清楚的知道一点,其实大家如果上网找文章,90%以上的人肯定是想知道具体编程的时候怎么落地,尤其是聚合根。
现在互联网的文章要么是水军写的,要么是宣传广告来的,他们的问题如下:
所以这篇文章诞生了,我承诺:
某服装企业的业务专家+技术团队要实现自己的ERP系统,实现采购和库存管理,经过头脑风暴的出业务规则如下:(这里是为了演示而提出的假设需求,并不保证符合真实业务,实际比这个复杂太多)
根据与业务专家确定业务价值之后,他们决定先开发价值最高的需求,即:带来80%的业务价值的20%需求,于是暂时将不实现的需求标记为删除线。
架构师+技术经理+技术团队根据领域驱动设计对领域模型的需求描述,团队决定先对业务建模,并且遵循如下规则:
架构与技术经理和团队一起协商之后,根据技术成本和价值决定暂时放弃一些规则,暂时放弃的规则被标记为删除线。
其实他们还讨论了很多其他需求,比如如下的需求,但是本文重点是带你感受一种业务建模的手段,这里列出了这么多详细的需求和过程,只是为了模拟一下全部的开发流程。
在定义了上述技术手段之后,开始落地实现,首先定义采购单聚合根(只展示部分核心代码):
@Table(name = "purchase_order")
@Entity
@Getter
@Setter(AccessLevel.PRIVATE)
@FieldNameConstants
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PurchaseOrder extends AbstractDomainEntity {
public final static DomainError ERR_ORDER_NOT_INITIALIZED = SimpleDomainError.of("order_not_initialized", "the Purchase order is not just initialized");
public final static DomainError ERR_ORDER_NOT_SUBMITTED = SimpleDomainError.of("order_not_submitted", "the Purchase order has not been submitted");
public final static DomainError ERR_ORDER_NOT_APPROVED = SimpleDomainError.of("order_not_approved", "the Purchase order has not been approved");
public final static String STATUS_INITIALIZED = "initialized";
public final static String STATUS_SUBMITTED = "submitted";
public final static String STATUS_APPROVED = "approved";
public final static String STATUS_REJECTED = "rejected";
public final static String STATUS_COMPLETED = "completed";
@Id
@Column(name = "order_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
@Column(name = "order_number", unique = true, nullable = false)
private String orderNumber;
@Column(name = "order_date", nullable = false)
private LocalDate orderDate = LocalDate.now();
@Column(name = "supplier_id", nullable = false)
private Long supplierId;
@Column(name = "status", nullable = false)
private String status;
@Column(name = "amount", nullable = false)
private double amount = 0;
@Embedded
private Audition audition = Audition.empty();
@JoinColumn(name = "purchase_order_id", nullable = false)
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<PurchaseOrderItem> items = new ArrayList<>();
@Builder
@SuppressWarnings("unused")
private PurchaseOrder(String orderNumber,
Long supplierId,
Collection<PurchaseOrderItem> items) {
this.setSupplierId(supplierId);
this.setOrderNumber(orderNumber);
this.setStatus(STATUS_INITIALIZED);
this.setOrderDate(LocalDate.now());
this.setItems(new ArrayList<>(items));
}
public boolean isState(String state) {
return StringUtils.equals(state, this.getStatus());
}
public ImmutableValues<PurchaseOrderItem> getItems() {
return ImmutableValues.of(items);
}
private void calculateAmount() {
this.amount = 0D;
for (PurchaseOrderItem item : this.getItems()) {
this.amount += item.getAmount();
}
}
void setItems(List<PurchaseOrderItem> items) {
this.items.clear();
this.items.addAll(items);
this.calculateAmount();
}
void update(PurchaseOrderPatch patch) {
if (Objects.nonNull(patch.getItems())) {
this.setItems(patch.getItems());
}
if (Objects.nonNull(patch.getSupplierId())) {
this.setSupplierId(patch.getSupplierId());
}
}
void submit() {
DomainValidator.must(this.isState(STATUS_INITIALIZED), ERR_ORDER_NOT_INITIALIZED);
this.setStatus(STATUS_SUBMITTED);
}
void approve() {
DomainValidator.must(this.isState(STATUS_SUBMITTED), ERR_ORDER_NOT_SUBMITTED);
this.setStatus(STATUS_APPROVED);
}
void reject() {
DomainValidator.must(this.isState(STATUS_SUBMITTED), ERR_ORDER_NOT_SUBMITTED);
this.setStatus(STATUS_REJECTED);
}
void complete() {
DomainValidator.must(this.isState(STATUS_APPROVED), ERR_ORDER_NOT_APPROVED);
this.setStatus(STATUS_COMPLETED);
}
}
该聚合根满足如下特点:
总结:
采购单的领域服务不是简单的一个manager完成,而是配合了一个Interceptor机制,原因是团队经过调研,发现采购单在业务上线之后可能会随时增加一个新的教研或者一些其他的需求,这些需求可能是临时的,也可能是集团决策层需要试错用的,总是很可能不是核心逻辑,但是由经常变。于是暂时考虑使用Interceptor机制来满足扩展性。
/**
* This is a demo to demonstrate interceptor pattern. * With this pattern, you can design your interceptor for the domain logic to get a high level of extensibility. * Ps: use this pattern only if you are sure you need it. */
public interface PurchaseOrderInterceptor {
default void preCreate(PurchaseOrder order) {
}
default void afterCreate(PurchaseOrder order) {
}
default void preDelete(PurchaseOrder order) {
}
/// ...
}
@Slf4j
@Component
@RequiredArgsConstructor
public class PurchaseOrderManager implements DomainManager {
private final List<PurchaseOrderInterceptor> interceptors;
private final PurchaseOrderRepository purchaseOrderRepository;
public void createOrder(PurchaseOrder order) {
interceptors.forEach(purchaseOrderInterceptor -> purchaseOrderInterceptor.preCreate(order));
purchaseOrderRepository.save(order);
interceptors.forEach(purchaseOrderInterceptor -> purchaseOrderInterceptor.afterCreate(order));
var event = PurchaseOrderCreatedEvent.of(order);
DomainHelper.publishDomainEvent(event);
}
public void updateOrder(PurchaseOrder order, PurchaseOrderPatch patch) {
//...
}
public void deleteOrder(PurchaseOrder order) {
//...
}
public void submit(PurchaseOrder order) {
//...
}
public void approve(PurchaseOrder order) {
//...
}
public void reject(PurchaseOrder order) {
//...
}
public void complete(PurchaseOrder order) {
interceptors.forEach(purchaseOrderInterceptor -> purchaseOrderInterceptor.preComplete(order));
order.complete();
purchaseOrderRepository.save(order);
interceptors.forEach(purchaseOrderInterceptor -> purchaseOrderInterceptor.afterComplete(order));
var event = PurchaseOrderCompletedEvent.of(order);
DomainHelper.publishDomainEvent(event);
}
}
接下来团队为采购单实现了领域的查询服务:
@Slf4j
@Component
@RequiredArgsConstructor
public class PurchaseOrderFinder implements DomainFinder {
private final PurchaseOrderRepository purchaseOrderRepository;
public Optional<PurchaseOrder> get(Long orderId) {
return purchaseOrderRepository.findById(orderId);
}
public PurchaseOrder require(Long orderId) throws NoDomainEntityException {
return purchaseOrderRepository.findById(orderId).orElseThrow(NoDomainEntityException::instance);
}
public Page<PurchaseOrder> query(PurchaseOrderFilter filter, Pageable pageable) {
var spec = Specifications.query(PurchaseOrder.class)
.and(StringUtils.isNotBlank(filter.getStatus()), Specs.statusIs(filter.getStatus()))
.getSpec();
return purchaseOrderRepository.findAll(spec, pageable);
}
@UtilityClass
public class Specs {
Specification<PurchaseOrder> statusIs(@Nullable String status) {
return (root, query, builder) ->
builder.equal(root.get(PurchaseOrder.Fields.status), status);
}
}
}
到此,技术经理提出,现在实现的事采购应用服务,不是采购单,之所以事采购,不是采购单是因为:
应用层允许跨多个领域聚合根
应用层是很轻薄的一层,不会有大片的逻辑
团队发现现实世界中,只有采购部门,没有采购单部门,采购部门掌管着采购单,以及其他采购相关资源。
技术经理和团队成员开始定义应用服务的技术手段:
架构师开始评审应用服务的技术手段:
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class PurchaseService implements ApplicationService {
public static DomainError ERROR_NO_SUCH_MATERIAL = SimpleDomainError.of("no_such_material", "the materials don't exist");
private final IdentityGenerator orderNumberGenerator = new RaindropGenerator();
private final FinanceManager financeManager;
private final InventoryManager inventoryManager;
private final PurchaseOrderManager purchaseOrderManager;
private final MaterialFinder materialFinder;
private final PurchaseOrderFinder purchaseOrderFinder;
public PurchaseOrder createPurchaseOrder(PurchaseOrderCreateCommand command) {
// 1. create the entire order
var order = PurchaseOrder.builder()
.orderNumber(nextOrderNumber())
.items(command.getItems().toList(this::createPurchaseOrderItem))
.supplierId(command.getSupplierId())
.build();
// 2. invoke the order creation logic
purchaseOrderManager.createOrder(order);
return order;
}
public void updatePurchaseOrder(PurchaseOrderModifyCommand command) {
var order = purchaseOrderFinder.require(command.getOrderId());
var patch = PurchaseOrderPatch.builder()
.items(command.getItems().toList(this::createPurchaseOrderItem))
.supplierId(command.getSupplierId())
.build();
purchaseOrderManager.updateOrder(order, patch);
}
public void submitPurchaseOrder(PurchaseOrderSubmitCommand command) {
// ...
}
public void reviewPurchaseOrder(PurchaseOrderReviewCommand command) {
// ...
}
public void deletePurchaseOrder(PurchaseOrderDeleteCommand command) {
// ...
}
public void completePurchaseOrder(PurchaseOrderCompleteCommand command) {
var order = purchaseOrderFinder.require(command.getOrderId());
// 1. complete by the domain logic
purchaseOrderManager.complete(order);
// 2. stock in for each item in the order
var tracings = order.getItems()
.stream()
.map(item -> createTransaction(order, item))
.map(inventoryManager::stockIn)
.toList();
// 2. record the purchase cost
var cost = PurchaseCost.builder()
.purchaseOrderId(order.getOrderId())
.purchaseCost(order.getAmount())
.transportationCost(command.getTransportationCost())
.build();
financeManager.createCost(cost);
}
}
最后附上分包结构,有助于大家对全文的理解。

分许需求的时候(实时/写操作/强一致)(异步/写操作/最终一致)(异步/只读) 这些都是干什么用的?
为什么领域层不分单独的包,比如 /repository /entity ?
领域层的逻辑很复杂,复杂的东西都在这里,难道我实现24种设计模式要创建24个包吗?
领域层的拦截器机制会不会污染核心逻辑?
是不是核心逻辑取决于你的设计,不取决于技术手段,没有那种设计模式能拦得住你瞎胡乱写。
为什么领域服务不命名为DomainService?
领域驱动是教你如何应对复杂软件的,这个过程包括:统一语言,建模,解耦实现。但不是《程序员装逼指南》,是哲学,不是数学。 如果你认真研究过DDD就会发现,拘泥于命名和分包而不是思想战术的话,只会万劫不复,堕走火入魔。
你是不是想告诉我门 DDD 不只适用于互联网?
我想告诉你DDD不适用于互联网。
架构师让我们写文档这段,你想表达什么?
《敏捷开发》书中说过,要写最重要的文档,而不是流水账的海量文档,这种核心设计文档可以用来推导出所有代码,但是海量的流水文档一旦疏于管理,更加百害而无一益。
你还有问题吗?可以写到评论区。
上一篇:DDD重构项目
《数字经济》 - Visional S. XIA - 博客园