Skip to content
第 3 章 架构 ⏱ 13 分钟阅读

第 3 章:战术设计 ​

学习目标 ​

  • 掌握战术设计的核心构件:实体、值对象、聚合、领域服务、仓储
  • 学会区分"放进实体"和"放进领域服务"
  • 理解仓储(Repository)封装持久化的边界
  • 避开贫血模型和上帝对象的陷阱

一、战术设计构件清单 ​

构件特点例子
实体(Entity)有唯一 ID,可变Order、User
值对象(VO)无 ID,不可变Money、Address
聚合(Aggregate)一组实体的强一致边界Order + OrderItem
聚合根(ARoot)聚合的入口,对外只暴露它Order
领域服务(Domain Service)跨实体的业务规则PricingService
仓储(Repository)聚合的持久化抽象OrderRepository
工厂(Factory)复杂对象的创建OrderFactory
领域事件(Event)已发生的事OrderPaidEvent

二、实体:有 ID 的可变对象 ​

java
public class User {
    private final UserId id;          // 唯一标识,终身不变
    private String nickname;          // 可变属性
    private String avatar;
    private UserStatus status;

    public void changeNickname(String nickname) {
        if (nickname == null || nickname.isBlank()) {
            throw new IllegalArgumentException("昵称不能为空");
        }
        if (nickname.length() > 20) {
            throw new IllegalArgumentException("昵称最长 20");
        }
        this.nickname = nickname;
    }
}

⚠️ 坑 1:用 Long id 当 ID,魔法数字到处传。封装值对象 UserId,所有需要 ID 的地方都强类型,避免和 OrderId 混用。

三、值对象:无 ID 的不可变对象 ​

java
// 金额:用类替代 BigDecimal,防止单位混用
public final class Money {
    private final BigDecimal amount;
    private final String currency;

    public Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new CurrencyMismatchException(currency, other.currency);
        }
        return new Money(amount.add(other.amount), currency);
    }

    // 省略 equals/hashCode/getter
}

// 用法
Money price = new Money(new BigDecimal("99.00"), "CNY");
Money total = price.add(new Money(new BigDecimal("8.00"), "CNY"));
// total = 107.00 CNY

⚠️ 坑 2:金额用 BigDecimal 但到处传原始数值,出现过"元/分"混用导致少收 100 倍的线上事故。用 Money 封装,加 toFen()/fromFen() 显式转换。

四、聚合根:数据修改的入口 ​

聚合根 = 聚合的"门卫",外部只能通过聚合根修改内部对象:

java
public class Order {
    private final OrderId id;
    private OrderStatus status;
    private final List<OrderItem> items = new ArrayList<>();
    private final Address shippingAddress;

    // 唯一修改入口
    public void addItem(SkuId skuId, int quantity, Money unitPrice) {
        if (status != OrderStatus.CREATED) {
            throw new OrderImmutableException("订单已提交,不能改");
        }
        // 不暴露 items.add,只能通过这条命令
        items.stream()
            .filter(i -> i.skuId().equals(skuId))
            .findFirst()
            .ifPresentOrElse(
                OrderItem::increase,
                () -> items.add(new OrderItem(skuId, quantity, unitPrice))
            );
    }

    public Money totalAmount() {
        return items.stream()
            .map(OrderItem::subtotal)
            .reduce(Money.zero("CNY"), Money::add);
    }

    public List<OrderItem> items() {
        return Collections.unmodifiableList(items);  // 只读视图
    }
}

⚠️ 坑 3:把 items 暴露成 getItems()(可写),外部 Service 拿到 list 直接 add,聚合边界形同虚设。要么不暴露,要么返回 unmodifiableList。

五、领域服务:不属于任何实体的规则 ​

java
// 跨实体的业务规则 → 领域服务
@Service
public class PricingService {

    public Money calculateOrderPrice(Cart cart, Member member, Coupon coupon) {
        Money subtotal = cart.items().stream()
            .map(CartItem::subtotal)
            .reduce(Money.zero("CNY"), Money::add);

        Money memberDiscount = member.level().discount(subtotal);
        Money afterMember = subtotal.subtract(memberDiscount);

        Money couponDiscount = coupon != null ? coupon.apply(afterMember) : Money.zero("CNY");
        return afterMember.subtract(couponDiscount);
    }
}

⚠️ 坑 4:把领域服务当"工具类"使用,所有计算都丢进去,实体反而干干净净。先尝试放进实体,实体内部无法表达或跨多个对象时,才用领域服务。

六、仓储:屏蔽持久化细节 ​

java
// 领域层只关心"接口"
public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
    List<Order> findByUser(UserId userId, int page, int size);
}

// 基础设施层提供实现
@Repository
public class OrderRepositoryImpl implements OrderRepository {

    @Resource
    private OrderJpaRepository jpaRepository;

    @Override
    public Order findById(OrderId id) {
        return jpaRepository.findById(id.value())
            .map(OrderMapper::toDomain)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }

    @Override
    @Transactional
    public void save(Order order) {
        OrderPO po = OrderMapper.toPo(order);
        jpaRepository.save(po);
    }
}

⚠️ 坑 5:在领域层直接 @Resource 注入 JPA Repository,导致领域层依赖 Spring Data。领域层只定义接口,基础设施层实现,这是 DDD 四层架构的精髓。

七、领域事件:解耦的桥梁 ​

java
// 1. 定义事件
public record OrderPaidEvent(OrderId orderId, Money amount, Instant paidAt) {}

// 2. 聚合内触发
public class Order {
    private final List<DomainEvent> domainEvents = new ArrayList<>();

    public void markPaid() {
        this.status = OrderStatus.PAID;
        domainEvents.add(new OrderPaidEvent(id, totalAmount(), Instant.now()));
    }

    public List<DomainEvent> pullEvents() {
        List<DomainEvent> events = List.copyOf(domainEvents);
        domainEvents.clear();
        return events;
    }
}

// 3. 应用服务发布
@Service
public class OrderApplicationService {
    @Resource private OrderRepository orderRepository;
    @Resource private ApplicationEventPublisher publisher;

    @Transactional
    public void pay(OrderId orderId) {
        Order order = orderRepository.findById(orderId);
        order.markPaid();
        orderRepository.save(order);

        order.pullEvents().forEach(publisher::publishEvent);
    }
}

本章小结 ​

构件判断标准
实体有 ID,生命周期可变
值对象无 ID,不可变,用值相等
聚合强一致边界,聚合根统一入口
领域服务跨实体规则,无状态
仓储抽象持久化,接口在领域层
领域事件已发生的事,异步协调

动手练习 ​

  1. 充血改造:把 User 实体加上 changeNickname() 和 deactivate() 业务方法
  2. 值对象:实现 Money 类的 multiply(BigDecimal factor) 和 subtract(Money)
  3. 聚合根:给 Order 增加 addItem / removeItem / cancel,状态检查统一定义
  4. 领域事件:定义 OrderCreatedEvent 并在 Order 构造函数里发布

下一章:第 4 章:实体与值对象 →

本站基于 VitePress 构建 · 由 StackHub 团队维护