DDD领域驱动设计落地实践与六边形架构

引言

先讲一个我亲身经历过的项目事故。

三年前我接手一个"电商中台"的重构项目。这个系统最初只有商品管理和订单创建两个功能,两年后膨胀到 47 个 Service 类、12 万行代码。最要命的是,某次大促前,运营提了一个需求:"VIP 用户在特定品类下单时,可以叠加使用定向优惠券,但优惠券不能和满减活动同时生效。"

听起来是个简单规则,但我们的开发同学改了整整两周,上线后出现了三处资损漏洞。原因很简单——这段业务规则被拆散在 OrderService、CouponService、PromotionService、UserService 四个类里,没有任何一个地方能完整表达"VIP 用户 + 特定品类 + 定向券 + 满减"这个业务事实。

这就是典型的贫血模型(Anemic Domain Model)灾难:对象只有 getter/setter,所有业务逻辑散落在 Service 层,业务规则无处安放,改一处漏三处。

DDD(Domain-Driven Design)和六边形架构(Hexagonal Architecture)正是为了解决这类问题而生的。但市面上大量文章只讲概念不讲落地,看完还是不知道怎么下手。这篇文章,我会用源码级分析 + 可运行代码,把 DDD 落地这件事讲透。


一、核心概念:用餐厅类比理解 DDD 与六边形架构

1.1 餐厅类比

把软件系统想象成一家餐厅:

软件概念 餐厅类比 说明
领域(Domain) 餐厅的核心业务 做菜、点单、结账
实体(Entity) 一道具体的菜 有唯一标识(订单号),状态会变(生的→熟的)
值对象(Value Object) 一份"微辣"口味要求 不可变,只描述特征,没有身份
聚合(Aggregate) 一张订单 订单 + 菜品 + 备注作为一个整体,有事务边界
聚合根(Aggregate Root) 订单本身 外界只能通过订单访问菜品,不能绕过订单直接改菜品
领域服务(Domain Service) 大厨 跨多个食材的复杂烹饪逻辑
仓储(Repository) 仓库管理员 负责取出/存放食材,隐藏存储细节
六边形架构 餐厅的就餐区 + 后厨 + 采购口 就餐区(API)、后厨(领域)、采购口(DB)通过标准接口对接

关键点:餐厅的"菜谱"(业务规则)必须写在厨房里,而不是写在服务员的对讲机里(Service 层)。这就是 DDD 的核心主张——业务逻辑回归领域对象。

1.2 六边形架构的核心价值

六边形架构(也叫端口-适配器架构)的本质是依赖倒置:

  • 端口(Port):领域层定义的接口,表达"我需要什么能力"
  • 适配器(Adapter):基础设施层实现端口,比如 MySQL、Redis、RPC
graph TD subgraph 外部世界 A[HTTP 客户端] B[gRPC 调用方] C[消息队列] end subgraph 适配器层 Adapter D[REST Controller] E[gRPC Adapter] F[MQ Consumer] G[OrderRepositoryImpl] H[PaymentGatewayImpl] end subgraph 应用层 Application I[OrderApplicationService] end subgraph 领域层 Domain J[Order 聚合根] K[Coupon 值对象] L[OrderRepository 端口] M[PaymentGateway 端口] end subgraph 基础设施 N[(MySQL)] O[(Redis)] P[第三方支付] end A --> D B --> E C --> F D --> I E --> I F --> I I --> J I --> K I --> L I --> M G -.实现.-> L H -.实现.-> M G --> N H --> P I --> O style J fill:#ffcccc style K fill:#ffcccc style L fill:#ffcccc style M fill:#ffcccc

注意看箭头方向:领域层是绝对核心,它不依赖任何外层;所有依赖都通过端口(接口)倒置。这就是六边形架构的灵魂——领域层零依赖。


二、源码级深度分析:为什么贫血模型会失控?

2.1 贫血模型的典型代码

来看一段真实的订单创建代码(简化版):

// ❌ 贫血模型:Order 只是个数据袋子
public class Order {
    private Long id;
    private Long userId;
    private BigDecimal amount;
    private Integer status;
    // 只有 getter/setter...
}

// ❌ 业务逻辑全在 Service,规则散落
@Service
public class OrderService {
    public void createOrder(Long userId, BigDecimal amount) {
        // 校验用户
        User user = userService.get(userId);
        if (user == null) throw new BizException("用户不存在");
        
        // 校验金额
        if (amount.compareTo(BigDecimal.ZERO) <= 0) 
            throw new BizException("金额非法");
        
        // 计算折扣(散落在 Service 里的规则)
        if (user.isVip() && amount.compareTo(new BigDecimal("1000")) > 0) {
            amount = amount.multiply(new BigDecimal("0.9"));
        }
        
        Order order = new Order();
        order.setUserId(userId);
        order.setAmount(amount);
        order.setStatus(0);
        orderMapper.insert(order);
    }
}

这段代码的问题:

  1. 规则无法复用:另一个入口(比如 MQ 消费)想创建订单,必须再写一遍校验
  2. 并发不安全:order.setStatus(0) 后如果有人改了 status,没有任何保护
  3. 无法表达不变量:Order 对象可以处于任意非法状态(amount 为负、status 为 999)
  4. 测试困难:想测试"金额非法"的场景,必须启动 Spring 容器

2.2 用 DDD 重构后的代码

// ✅ 充血模型:Order 自己守护业务不变量
public class Order {
    private final OrderId id;           // 值对象,不可变
    private final UserId userId;
    private Money amount;               // 值对象,自带校验
    private OrderStatus status;         // 枚举,状态机
    private final List<OrderItem> items;

    // 私有构造 + 工厂方法,禁止非法创建
    private Order(OrderId id, UserId userId, List<OrderItem> items) {
        if (items == null || items.isEmpty()) {
            throw new DomainException("订单必须包含至少一个商品");
        }
        this.id = id;
        this.userId = userId;
        this.items = List.copyOf(items);  // 防御性拷贝
        this.amount = calculateTotal(items);
        this.status = OrderStatus.CREATED;
    }

    public static Order create(UserId userId, List<OrderItem> items) {
        return new Order(OrderId.generate(), userId, items);
    }

    // 业务行为内聚在领域对象里
    public void applyDiscount(DiscountPolicy policy, User user) {
        if (this.status != OrderStatus.CREATED) {
            throw new DomainException("只有待支付订单可以打折");
        }
        this.amount = policy.calculate(this.amount, user, this.items);
    }

    public void pay(PaymentId paymentId) {
        if (this.status != OrderStatus.CREATED) {
            throw new DomainException("订单状态不允许支付");
        }
        this.status = OrderStatus.PAID;
        // 发布领域事件...
    }

    private Money calculateTotal(List<OrderItem> items) {
        return items.stream()
                    .map(OrderItem::subtotal)
                    .reduce(Money.ZERO, Money::add);
    }
    
    // 只暴露必要的行为,不暴露 setter
    public Money amount() { return amount; }
    public OrderStatus status() { return status; }
}

核心差异:

  • 贫血模型:对象是名词,逻辑是动词,散落各处
  • 充血模型:对象既是名词也是动词,逻辑内聚

2.3 聚合边界的源码级判断

如何判断两个实体是否应该在同一聚合?看这个经验法则:

一个事务只修改一个聚合。

比如"下单扣库存"这个场景,很多同学会把 Order 和 Inventory 放在一个聚合里,导致:

// ❌ 错误:跨聚合事务,锁竞争严重
@Transactional
public void createOrder(...) {
    Order order = Order.create(...);
    orderRepository.save(order);
    inventory.decrease(order.items());  // 同一个事务里改库存
}

正确做法是通过领域事件最终一致:

// ✅ 正确:Order 聚合只负责自己
@Transactional
public void createOrder(...) {
    Order order = Order.create(...);
    orderRepository.save(order);
    // 领域事件在事务提交后发布
    eventPublisher.publish(new OrderCreatedEvent(order.id(), order.items()));
}

// 库存服务订阅事件,独立事务处理
@EventHandler
public void on(OrderCreatedEvent event) {
    inventoryService.decrease(event.items());
}

三、实战代码:三个完整可运行示例

示例 1:完整的订单聚合 + 值对象(Java)

// ============ 值对象:Money ============
package com.example.domain.shared;

import java.math.BigDecimal;
import java.util.Currency;
import java.util.Objects;

/**
 * 值对象:金额
 * 特性:不可变、无身份、可替换
 */
public final class Money {
    public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("CNY"));
    
    private final BigDecimal amount;
    private final Currency currency;

    private Money(BigDecimal amount, Currency currency) {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金额不能为负");
        }
        this.amount = amount.setScale(2, java.math.RoundingMode.HALF_UP);
        this.currency = Objects.requireNonNull(currency);
    }

    public static Money of(BigDecimal amount) {
        return new Money(amount, Currency.getInstance("CNY"));
    }

    public static Money of(double amount) {
        return of(BigDecimal.valueOf(amount));
    }

    public Money add(Money other) {
        assertSameCurrency(other);
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money multiply(BigDecimal rate) {
        return new Money(this.amount.multiply(rate), this.currency);
    }

    private void assertSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("币种不一致");
        }
    }

    public BigDecimal value() { return amount; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount.compareTo(money.amount) == 0 && currency.equals(money.currency);
    }

    @Override
    public int hashCode() { return Objects.hash(amount, currency); }
}
// ============ 聚合根:Order ============
package com.example.domain.order;

import com.example.domain.shared.Money;
import com.example.domain.shared.DomainEvent;
import com.example.domain.shared.DomainException;

import java.time.Instant;
import java.util.*;

/**
 * 订单聚合根
 * - 唯一入口:所有对订单的修改必须通过 Order 的方法
 * - 不变量:金额非负、状态机合法、至少一个商品
 */
public class Order {
    private final OrderId id;
    private final UserId userId;
    private final List<OrderItem> items;
    private Money amount;
    private OrderStatus status;
    private final Instant createdAt;
    private final List<DomainEvent> events = new ArrayList<>();

    private Order(OrderId id, UserId userId, List<OrderItem> items) {
        if (items == null || items.isEmpty()) {
            throw new DomainException("订单必须包含至少一个商品");
        }
        this.id = Objects.requireNonNull(id);
        this.userId = Objects.requireNonNull(userId);
        this.items = List.copyOf(items);
        this.amount = items.stream().map(OrderItem::subtotal)
                            .reduce(Money.ZERO, Money::add);
        this.status = OrderStatus.CREATED;
        this.createdAt = Instant.now();
        this.events.add(new OrderCreatedEvent(this.id, this.userId, this.amount));
    }

    public static Order create(UserId userId, List<OrderItem> items) {
        return new Order(OrderId.generate(), userId, items);
    }

    /** 应用折扣策略 —— 策略通过参数传入,聚合不依赖具体实现 */
    public void applyDiscount(DiscountPolicy policy, UserContext ctx) {
        requireStatus(OrderStatus.CREATED, "应用折扣");
        Money discounted = policy.calculate(this.amount, ctx, this.items);
        if (discounted.value().compareTo(Money.ZERO.value()) < 0) {
            throw new DomainException("折扣后金额不能为负");
        }
        this.amount = discounted;
        this.events.add(new OrderDiscountedEvent(this.id, this.amount));
    }

    public void pay(PaymentId paymentId) {
        requireStatus(OrderStatus.CREATED, "支付");
        this.status = OrderStatus.PAID;
        this.events.add(new OrderPaidEvent(this.id, paymentId, this.amount));
    }

    public void cancel(String reason) {
        if (status == OrderStatus.PAID) {
            throw new DomainException("已支付订单需走退款流程");
        }
        this.status = OrderStatus.CANCELLED;
        this.events.add(new OrderCancelledEvent(this.id, reason));
    }

    private void requireStatus(OrderStatus expected, String action) {
        if (this.status != expected) {
            throw new DomainException(
                String.format("订单当前状态[%s]不允许[%s]", status, action));
        }
    }

    /** 拉取领域事件(事务提交后清空) */
    public List<DomainEvent> pullEvents() {
        List<DomainEvent> copy = List.copyOf(events);
        events.clear();
        return copy;
    }

    public OrderId id() { return id; }
    public Money amount() { return amount; }
    public OrderStatus status() { return status; }
    public List<OrderItem> items() { return items; }
}
// ============ 端口:Repository ============
package com.example.domain.order;

/**
 * 端口(Port):领域层定义,基础设施层实现
 * 注意:接口在领域层,实现在基础设施层 —— 依赖倒置
 */
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
    List<Order> findByUserId(UserId userId);
}

这段代码的关键设计点:

  1. Order 没有 setter,外部只能通过业务方法修改
  2. applyDiscount 接收 DiscountPolicy 接口而非具体实现,领域层不依赖任何框架
  3. pullEvents() 实现领域事件的延迟发布,保证事务一致

示例 2:六边形架构的应用服务 + 适配器(Java + Spring)

// ============ 应用层:编排用例 ============
package com.example.application;

import com.example.domain.order.*;
import com.example.domain.shared.DomainEvent;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 应用服务:只做编排,不写业务规则
 * 职责:加载聚合 → 调用领域方法 → 保存 → 发布事件
 */
@Service
public class OrderApplicationService {

    private final OrderRepository orderRepository;
    private final UserQueryService userQueryService;
    private final DiscountPolicyFactory discountPolicyFactory;
    private final DomainEventPublisher eventPublisher;

    public OrderApplicationService(OrderRepository orderRepository,
                                   UserQueryService userQueryService,
                                   DiscountPolicyFactory discountPolicyFactory,
                                   DomainEventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.userQueryService = userQueryService;
        this.discountPolicyFactory = discountPolicyFactory;
        this.eventPublisher = eventPublisher;
    }

    @Transactional
    public OrderId createOrder(CreateOrderCommand cmd) {
        // 1. 调用领域工厂创建聚合
        List<OrderItem> items = cmd.items().stream()
                .map(i -> new OrderItem(i.skuId(), i.price(), i.quantity()))
                .toList();
        Order order = Order.create(new UserId(cmd.userId()), items);

        // 2. 查询上下文(跨聚合查询走读模型,不加载整个 User 聚合)
        UserContext ctx = userQueryService.loadContext(cmd.userId());

        // 3. 调用领域行为
        DiscountPolicy policy = discountPolicyFactory.resolve(ctx);
        order.applyDiscount(policy, ctx);

        // 4. 持久化
        orderRepository.save(order);

        // 5. 事务提交后发布事件(通过 TransactionSynchronization 保证)
        order.pullEvents().forEach(eventPublisher::publishAfterCommit);

        return order.id();
    }
}
// ============ 适配器层:REST Controller ============
package com.example.adapter.web;

import com.example.application.OrderApplicationService;
import com.example.application.CreateOrderCommand;
import org.springframework.web.bind.annotation.*;

/**
 * 入站适配器(Driving Adapter)
 * 职责:协议转换(HTTP → Command),不包含业务逻辑
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderApplicationService orderService;

    public OrderController(OrderApplicationService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ApiResponse<String> create(@RequestBody CreateOrderRequest req) {
        // 只做 DTO → Command 转换
        CreateOrderCommand cmd = new CreateOrderCommand(
                req.userId(),
                req.items().stream()
                   .map(i -> new CreateOrderCommand.Item(
                       i.skuId(), i.price(), i.quantity()))
                   .toList()
        );
        String orderId = orderService.createOrder(cmd).value();
        return ApiResponse.ok(orderId);
    }
}
// ============ 适配器层:Repository 实现 ============
package com.example.adapter.persistence;

import com.example.domain.order.*;
import org.springframework.stereotype.Repository;

/**
 * 出站适配器(Driven Adapter)
 * 职责:领域对象 ↔ 数据库表 的映射
 */
@Repository
public class OrderRepositoryImpl implements OrderRepository {

    private final OrderJpaMapper jpaMapper;

    public OrderRepositoryImpl(OrderJpaMapper jpaMapper) {
        this.jpaMapper = jpaMapper;
    }

    @Override
    public void save(Order order) {
        OrderPO po = OrderConverter.toPO(order);
        jpaMapper.save(po);
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return jpaMapper.findById(id.value())
                        .map(OrderConverter::toDomain);
    }

    @Override
    public List<Order> findByUserId(UserId userId) {
        return jpaMapper.findByUserId(userId.value())
                        .stream()
                        .map(OrderConverter::toDomain)
                        .toList();
    }
}

分层职责一览:

层 职责 依赖方向
Adapter(适配器) 协议转换、数据映射 依赖 Application
Application(应用) 用例编排、事务边界 依赖 Domain
Domain(领域) 业务规则、不变量 零依赖
Infrastructure 技术实现(DB、MQ、RPC) 实现 Domain 端口

示例 3:领域事件 + 最终一致性(Go 版本)

换个语言,展示 DDD 不是 Java 专属:

package order

import (
	"errors"
	"time"
)

// Order 聚合根
type Order struct {
	id        OrderID
	userID    UserID
	items     []OrderItem
	amount    Money
	status    Status
	events    []DomainEvent
	createdAt time.Time
}

// 工厂方法,禁止直接构造
func NewOrder(userID UserID, items []OrderItem) (*Order, error) {
	if len(items) == 0 {
		return nil, errors.New("订单必须包含至少一个商品")
	}
	total := ZeroMoney()
	for _, it := range items {
		total = total.Add(it.Subtotal())
	}
	o := &Order{
		id:        NewOrderID(),
		userID:    userID,
		items:     items,
		amount:    total,
		status:    StatusCreated,
		createdAt: time.Now(),
	}
	o.record(OrderCreatedEvent{OrderID: o.id, Amount: total})
	return o, nil
}

// 业务行为
func (o *Order) ApplyDiscount(policy DiscountPolicy, ctx UserContext) error {
	if o.status != StatusCreated {
		return errors.New("只有待支付订单可以打折")
	}
	newAmount, err := policy.Calculate(o.amount, ctx, o.items)
	if err != nil {
		return err
	}
	o.amount = newAmount
	o.record(OrderDiscountedEvent{OrderID: o.id, NewAmount: newAmount})
	return nil
}

func (o *Order) Pay(paymentID PaymentID) error {
	if o.status != StatusCreated {
		return errors.New("订单状态不允许支付")
	}
	o.status = StatusPaid
	o.record(OrderPaidEvent{OrderID: o.id, PaymentID: paymentID})
	return nil
}

func (o *Order) record(e DomainEvent) {
	o.events = append(o.events, e)
}

// PullEvents 拉取并清空事件
func (o *Order) PullEvents() []DomainEvent {
	evts := o.events
	o.events = nil
	return evts
}

// 端口定义
type Repository interface {
	Save(o *Order) error
	FindByID(id OrderID) (*Order, error)
}

四、方案对比:DDD 与传统分层架构

维度 传统三层架构 DDD + 六边形架构
业务逻辑位置 Service 层(贫血) 领域对象(充血)
对象职责 数据载体 数据 + 行为
依赖方向 Controller → Service → DAO Adapter → Application → Domain ← Infrastructure
可测试性 需启动容器 领域层纯单元测试
业务规则复用 差(散落) 好(内聚)
学习成本 低 高
适用场景 CRUD、简单业务 复杂业务、长期演进系统

与其他架构的对比

1. DDD vs 事务脚本(Transaction Script)

事务脚本适合简单场景,比如后台管理系统的增删改查。强行上 DDD 是过度设计。判断标准:如果一个业务规则能用 3 行 if-else 说清楚,就不要用聚合。

2. DDD vs 洋葱架构(Onion Architecture)

洋葱架构和六边形架构是同一思想的不同表达。区别在于:

  • 六边形强调端口/适配器(技术视角)
  • 洋葱强调依赖同心圆(分层视角)

实战中两者常常混用,本质都是"领域核心,依赖倒置"。

3. DDD vs CQRS

CQRS 不是 DDD 的替代,而是补充。DDD 解决"写模型"的复杂业务,CQRS 解决"读模型"的高并发查询。常见组合:写侧用 DDD 聚合,读侧用扁平化 View。

graph LR A[客户端] --> B[Command 入口] A --> C[Query 入口] B --> D[Order 聚合
充血模型] D --> E[写库] E -.事件同步.-> F[读库
宽表/ES] C --> F style D fill:#ffcccc style F fill:#ccffcc

五、最佳实践与避坑指南

✅ 最佳实践

  1. 聚合尽量小:一个聚合只保护真正需要强一致的不变量。Order + OrderItem 是合理的,Order + User 就是灾难。
  1. 通过 ID 引用其他聚合:Order.userId 而不是 Order.user。跨聚合加载会破坏事务边界。
  1. 领域层零框架依赖:Domain 模块的 pom.xml 里不应该有 Spring、MyBatis。这是检验架构是否干净的硬指标。
  1. 领域事件延迟发布:用 TransactionSynchronizationManager 保证事务提交后才发 MQ,避免"消息发了但事务回滚"。
  1. DTO 与领域对象分离:Controller 的 Request 和 Domain 的 Entity 必须分开,防止外部直接操作领域对象。

❌ 常见坑

坑 1:把聚合根当成万能容器

// ❌ 聚合过大:Order 里塞了 User、Coupon、Promotion
public class Order {
    private User user;          // 跨聚合!
    private List<Coupon> coupons;  // 跨聚合!
    private Promotion promotion;    // 跨聚合!
}

后果:加载一个订单要 join 十张表,锁竞争剧烈。

坑 2:领域服务滥用

领域服务只应该处理"跨多个实体的业务规则",比如"转账"涉及两个 Account。如果只有一个实体参与,写在实体内部。

坑 3:Repository 里写业务逻辑

// ❌ 错误
public interface OrderRepository {
    List<Order> findExpiredOrders();  // 什么算"过期"?这是业务规则!
}

// ✅ 正确
public interface OrderRepository {
    List<Order> findByStatusAndCreatedBefore(OrderStatus status, Instant time);
}

坑 4:贫血模型 + DDD 命名 = 伪 DDD

很多团队号称用了 DDD,代码里却全是 OrderDTO、OrderService.updateStatus()。判断标准:领域对象有没有行为方法?没有就是贫血。

坑 5:忽略防腐层(ACL)

对接外部系统时,不要直接把对方的 DTO 引入领域层。用适配器 + 转换器隔离:

// ✅ 防腐层
public class ExternalPaymentAdapter implements PaymentGateway {
    private final ThirdPartyPaymentClient client;  // 三方 SDK
    
    @Override
    public PaymentResult pay(Order order, Money amount) {
        // 转换:领域模型 → 三方请求
        ThirdPartyRequest req = new ThirdPartyRequest(
            order.id().value(), amount.value().toString());
        ThirdPartyResponse resp = client.invoke(req);
        // 转换:三方响应 → 领域模型
        return resp.success() 
            ? PaymentResult.success(resp.transactionId())
            : PaymentResult.fail(resp.errorMsg());
    }
}

六、总结与延伸思考

核心要点回顾

  1. DDD 的本质是"把业务规则放回它该在的地方"——领域对象。贫血模型是绝大多数业务失控的根源。
  1. 聚合是事务边界,不是对象容器。一个事务只改一个聚合,跨聚合用领域事件最终一致。
  1. 六边形架构的核心是依赖倒置。领域层零依赖,所有外部能力通过端口(接口)注入。
  1. DDD 不是银弹。CRUD 系统上 DDD 是自寻烦恼,复杂业务系统不用 DDD 是慢性自杀。

延伸思考

  • DDD 与微服务的边界如何对齐? 一个限界上下文(Bounded Context)通常对应一个微服务,但并非绝对。如果两个上下文共享同一团队、同一发布节奏,合并为模块化单体反而更好。
  • 事件溯源(Event Sourcing)值得上吗? 只有在需要完整审计、时间旅行查询时才考虑。大多数场景,传统持久化 + 领域事件足够。
  • 如何度量 DDD 落地效果? 看两个指标:领域层单元测试覆盖率(应 > 80%)、领域模块的依赖数(应为 0)。

架构的演进从来不是一蹴而就的。我建议的落地路径是:先在核心域试点一个聚合 → 团队达成共识 → 逐步推广到支撑域 → 通用域保持简单 CRUD。切忌一步到位,那是架构师最大的傲慢。

愿你的领域模型,永远清晰如初。