Skip to content

战术设计 ​

核心问题:实体、值对象、聚合根如何设计?领域服务和应用服务有什么区别?仓储模式如何实现?

1. 实体 vs 值对象 ​

特性实体(Entity)值对象(Value Object)
唯一标识有(ID)无
可变性可变不可变
相等性基于 ID基于属性值
生命周期有明确生命周期无,可随时替换
示例Order、UserMoney、Address、DateRange
java
// 值对象:不可变,基于值比较
public record Address(
    String province,
    String city,
    String district,
    String street
) {
    // 值对象的相等性由属性值决定(record 自动生成 equals/hashCode)
    
    public Address withCity(String newCity) {
        return new Address(province, newCity, district, street);
    }
}

// 实体:基于 ID 比较
public class User {
    private final Long id;  // 唯一标识
    private String name;
    private Address address;  // 值对象作为属性
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User user)) return false;
        if (id == null || user.id == null) return false;  // 瞬态实体(未分配 ID)不判等
        return Objects.equals(id, user.id);  // 基于 ID 比较
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

基于 ID 判断相等隐含两个前提:实体标识一旦分配就不再改变;尚未分配 ID 的不同实例(ID 均为 null)不应被视为同一实体。若 ID 可能在对象进入 HashSet、HashMap 之后才分配或变化,需改用稳定的业务标识,或明确瞬态实体的相等策略。

2. 聚合设计原则 ​

聚合的核心职责是维护领域不变量:任何一次合法状态变更后,聚合内部的数据必须保持一致。事务与并发控制是保障手段,不是聚合本身的定义。以下原则是常见设计倾向,而非无条件适用的硬规则。

原则说明
聚合边界内强一致性聚合内的修改通常在同一事务中完成,保证不变量成立
聚合间最终一致性常见倾向是聚合间通过领域事件异步同步;若业务要求跨聚合即时一致,需明确事务范围或锁定策略
通过 ID 引用其他聚合不直接持有其他聚合的引用
小聚合聚合只包含维护不变量所必需、必须强一致的元素
java
// 差:大聚合(Order 持有 Product 引用)
class Order {
    private List<Product> products;  // 直接引用 Product 聚合
    // Product 变更会影响 Order,导致聚合过大
}

// 好:小聚合(通过 ID 引用)
class Order {
    private final Long id;
    private final List<OrderLine> lines = new ArrayList<>();
    
    public void addLine(Long productId, String productName, int quantity, Money price) {
        // 通过 productId 引用,不直接持有 Product
        OrderLine line = new OrderLine(productId, productName, quantity, price);
        lines.add(line);
    }
    
    public Money calculateTotal() {
        return lines.stream()
            .map(OrderLine::getSubtotal)
            .reduce(Money::add)
            .orElse(new Money(BigDecimal.ZERO, Currency.CNY));
    }
}

class OrderLine {
    private Long productId;      // ID 引用,非对象引用
    private String productName;  // 冗余数据快照
    private int quantity;
    private Money price;
    
    OrderLine(Long productId, String productName, int quantity, Money price) {
        if (quantity <= 0) throw new IllegalArgumentException("数量必须为正");
        this.productId = productId;
        this.productName = productName;
        this.quantity = quantity;
        this.price = price;
    }
    
    Money getSubtotal() { return price.multiply(quantity); }
}

订单行保存的是下单时确认的商品名称和成交价格快照,后续商品资料或标价变更不应自动改写历史订单;具体规则由交易领域决定。

3. 领域服务 vs 应用服务 ​

特性领域服务应用服务
位置领域层应用层
职责不属于实体的业务逻辑编排领域对象,协调基础设施
依赖只依赖领域对象依赖领域服务和基础设施接口
示例转账服务、定价服务订单创建流程、支付流程
java
// 领域服务:不属于任何实体的业务逻辑
public class TransferService {
    public void transfer(Account from, Account to, Money amount) {
        if (!from.canTransfer(amount)) {
            throw new InsufficientBalanceException();
        }
        from.debit(amount);
        to.credit(amount);
    }
}

// 应用服务:编排领域对象和基础设施
@Service
public class CreateOrderApplicationService {
    private final OrderRepository orderRepo;
    private final ProductService productService;  // 跨上下文获取商品信息
    private final PaymentGateway paymentGateway;
    private final DomainEventPublisher eventPublisher;
    
    @Transactional
    public OrderResult execute(CreateOrderCommand cmd) {
        // 1. 获取商品信息(跨上下文)
        ProductInfo product = productService.getInfo(cmd.productId());
        
        // 2. 创建订单(领域对象)
        Order order = new Order(generateId(), cmd.userId());
        order.addLine(product.id(), product.name(), cmd.quantity(), product.price());
        order.confirm();
        
        // 3. 持久化
        orderRepo.save(order);
        
        // 4. 发布进程内领域事件(调用发生在事务提交前)
        order.getDomainEvents().forEach(eventPublisher::publish);
        
        return new OrderResult(order.getId(), order.getTotalAmount());
    }
}

示例发布的是进程内领域事件,且 publish() 调用发生在事务提交前。若事件需要可靠投递到消息中间件或跨进程消费,不能在业务事务内直接发送,否则事务回滚后消息已发出;这类问题与 Outbox 方案见领域事件章节。

4. 仓储模式 ​

java
// 仓储接口(领域层定义,不依赖具体实现)
public interface OrderRepository {
    void save(Order order);
    Order findById(Long id);
    List<Order> findByUserId(Long userId);
}

// 仓储实现(基础设施层)
@Repository
public class JpaOrderRepository implements OrderRepository {
    private final OrderJpaRepository jpaRepo;
    private final OrderItemJpaRepository itemRepo;
    
    @Override
    @Transactional
    public void save(Order order) {
        // 聚合根负责整个聚合的持久化
        OrderEntity entity = OrderMapper.toEntity(order);
        jpaRepo.save(entity);
        
        // 保存聚合内的子实体
        List<OrderItemEntity> items = order.getLines().stream()
            .map(line -> OrderMapper.toItemEntity(line, order.getId()))
            .toList();
        itemRepo.saveAll(items);
    }
    
    @Override
    public Order findById(Long id) {
        OrderEntity entity = jpaRepo.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
        List<OrderItemEntity> items = itemRepo.findByOrderId(id);
        return OrderMapper.toDomain(entity, items);
    }
}

上面的 save() 是简化示例:saveAll(items) 只新增或更新当前订单行,不会自动删除数据库中已存在但已从聚合移除的订单行;根实体与子实体的同步边界由 ORM 映射和仓储实现决定。仓储负责正确保存、加载并重建完整聚合,避免持久化破坏聚合状态;领域不变量由聚合对象自身维护。

核心原则:战术设计的目的是让代码反映业务领域。实体封装业务规则,值对象保证数据完整性,聚合维护一致性边界,领域服务处理跨实体的业务逻辑。

Released under the MIT License.