为什么DDD不是流行术语,而是拯救项目的工具
每位Java开发者都曾遇到过这样的情况:代码能运行,业务逻辑分散在服务层中,半年后只有作者(而且也很吃力)才能理解项目。熟悉吗?这就是经典的贫血领域模型——实体只是带有getter和setter的POJO,所有逻辑都存在于“臃肿”的服务层中。
这时,领域驱动设计(DDD)登场了——这是一种将业务逻辑而非技术细节置于开发核心的方法。不,这不是又一个“理论性”方法论。Java项目中的DDD提供了具体的模式,能将混乱转变为清晰、可维护的架构。在本文中,我们将通过代码和示例,结合实际来剖析DDD,绝不空谈。
战略性DDD vs 战术性DDD:Java项目该如何选择?
DDD分为两个层次:战略性和战术性。前者帮助将大型系统分解为限界上下文并建立它们之间的通信。后者则为代码提供具体的构建块:实体、值对象、聚合、仓库、领域事件等。
对于Java开发者来说,战术性DDD是入门的最佳选择。它提供了今天就能使用的工具。但如果没有对上下文的战略性理解,你可能会创建一个“通用”模型,最终不适合任何人。因此,让我们从小处着手:通过一个简单但真实的领域——订单管理系统——来剖析关键模式。
Java中DDD的构建块:从实体到聚合
1. 实体——每个实体都有其身份
在DDD中,实体是一个具有唯一标识符和连续生命线的对象。你可以更改其所有字段,但它仍然是同一个实体。在Java中,这不仅仅是POJO——这是一个具有行为的对象。
示例:电商订单。即使订单状态发生变化,它仍然是具有相同ID的同一个订单。
public class Order { private final OrderId id; // 值对象,非原始类型 private OrderStatus status; private List<OrderLineItem> items; private Money totalAmount;
public Order(OrderId id) { this.id = id; this.status = OrderStatus.PENDING; this.items = new ArrayList<>(); this.totalAmount = Money.ZERO; }
public void addItem(Product product, int quantity) { // 业务规则:不能向已确认的订单添加商品 if (this.status != OrderStatus.PENDING) { throw new IllegalStateException("Cannot add items to a non-pending order"); } OrderLineItem item = new OrderLineItem(product, quantity); items.add(item); recalculateTotal(); }
public void confirm() { if (items.isEmpty()) { throw new IllegalStateException("Cannot confirm an empty order"); } this.status = OrderStatus.CONFIRMED; // 此处可触发领域事件 // registerEvent(new OrderConfirmedEvent(this.id)); }
private void recalculateTotal() { this.totalAmount = items.stream() .map(OrderLineItem::getSubtotal) .reduce(Money.ZERO, Money::add); }
// Getter——仅用于读取,没有setter! public OrderId getId() { return id; } public OrderStatus getStatus() { return status; } public List<OrderLineItem> getItems() { return Collections.unmodifiableList(items); } public Money getTotalAmount() { return totalAmount; }}请注意:OrderId不是Long,而是值对象。为什么?因为原始类型不携带语义。OrderId可以包含前缀、创建日期或