欢迎光临
我们一直在努力

DDD重构:从贫血模型到充血模型的实战之旅

摘要:本文从一段典型的 MVC 三层架构下单代码入手,剖析贫血模型导致的四个核心问题:业务规则散落、对象缺乏行为、难以单元测试、修改牵一发动全身。随后用 DDD 战术设计逐步重构,将订单规则归还 Order 实体、用 Money 值对象替代裸 BigDecimal、跨对象规则收进领域服务、编排职责交给应用服务、通过 Repository 接口实现依赖倒置。重构完成后系统梳理实体、值对象、聚合、领域服务与领域事件等核心概念,并对比四层架构与 MVC 的关系。最后从战略设计视角介绍限界上下文与核心域划分,结合业务规则密度分析充血与贫血模型的适用场景,给出常见误区与标准包结构参考。

第一次听到 DDD(Domain-Driven Design,领域驱动设计),我以为是某个新框架,就像当年 Spring 干掉 Struts 那样,要来干掉 MVC 了。

后来发现不是。再后来发现它和 MVC 根本不在同一个维度。然后我就彻底懵了。

市面上大部分 DDD 教程的问题是:一上来就抛出一堆概念——实体、值对象、聚合、限界上下文……每个词都认识,连起来不知道在说什么。概念背完了,回到自己的项目,Service 还是照样写。

所以这篇我换个讲法:先给你看一段你绝对写过的代码,再用 DDD 的思路一步步重构它。 概念在重构过程里自然出现,而不是先背定义再找例子。

读完这篇,你应该能回答三个问题:

  • 我的胖 Service 到底"胖"在哪?

  • DDD 拆开之后,每一块代码去了哪里?

  • 我的项目到底要不要用 DDD?


  • 一、先看一段你很可能写过的代码

    电商下单,用传统三层架构 + 贫血模型写出来,大概长这样(为了篇幅做了精简,但结构你一定眼熟):

    @Service
    public class OrderService {

       @Autowired
       private OrderMapper orderMapper;
       @Autowired
       private UserMapper userMapper;
       @Autowired
       private ProductMapper productMapper;
       @Autowired
       private CouponMapper couponMapper;
       @Autowired
       private RabbitTemplate rabbitTemplate;

       @Transactional
       public Long placeOrder(Long userId, Long productId, Integer quantity, Long couponId) {
           // 1. 查用户,校验状态
           UserPO user = userMapper.selectById(userId);
           if (user == null) {
               throw new RuntimeException("用户不存在");
          }
           if (user.getStatus() == 0) {
               throw new RuntimeException("用户已被禁用");
          }

           // 2. 查商品,校验库存
           ProductPO product = productMapper.selectById(productId);
           if (product == null || product.getStatus() != 1) {
               throw new RuntimeException("商品不存在或已下架");
          }
           if (product.getStock() < quantity) {
               throw new RuntimeException("库存不足");
          }

           // 3. 算价格:会员折扣 + 优惠券 + 满减
           BigDecimal total = product.getPrice().multiply(new BigDecimal(quantity));
           if (user.getLevel() == 2) {                       // VIP 打 9 折
               total = total.multiply(new BigDecimal("0.9"));
          }
           if (couponId != null) {
               CouponPO coupon = couponMapper.selectById(couponId);
               if (coupon.getStatus() != 1) {
                   throw new RuntimeException("优惠券不可用");
              }
               if (total.compareTo(new BigDecimal("100")) < 0) {   // 满 100 才能用券
                   throw new RuntimeException("订单金额不足 100 元,无法使用优惠券");
              }
               total = total.subtract(coupon.getAmount());
          }
           if (total.compareTo(new BigDecimal("200")) >= 0) {      // 满 200 减 30
               total = total.subtract(new BigDecimal("30"));
          }
           if (total.compareTo(BigDecimal.ZERO) < 0) {
               total = BigDecimal.ZERO;
          }

           // 4. 组装订单对象,落库
           OrderPO order = new OrderPO();
           order.setOrderNo("ORD" + System.currentTimeMillis());
           order.setUserId(userId);
           order.setProductId(productId);
           order.setQuantity(quantity);
           order.setTotalAmount(total);
           order.setStatus(0);   // 0 = 待支付
           orderMapper.insert(order);

           // 5. 扣库存
           product.setStock(product.getStock() – quantity);
           productMapper.updateById(product);

           // 6. 发消息通知
           rabbitTemplate.convertAndSend("order.created", order.getId());

           return order.getId();
      }
    }

    这段代码能跑,甚至跑得挺好。但如果你是 MVC 老手,看到它的反应大概是:"这不就是我每天都在写的东西吗?"

    对。问题就在这。


    二、这段代码到底哪里出了问题

    很多人说问题是"太长了"。不对,长只是结果,不是原因。真正的问题有四个:

    2.1 业务规则没有"家"

    "满 100 才能用券"这条规则写在哪?写在 placeOrder 方法的第 3 段里。那取消订单的时候要退券,规则写在哪?写在 cancelOrder 里。定时任务扫描超时订单,规则写在哪?写在 OrderTimeoutJob 里。

    同一条业务规则,散落在 N 个方法里,没有一处是它的"正主"。 改规则的时候全靠全局搜索和记性——搜 new BigDecimal("100"),祈祷没有漏。

    2.2 对象只是"数据袋"

    OrderPO 有什么?一堆字段 + getter/setter。它不知道"待支付才能支付",不知道"已完成不能取消",它什么都不知道。所有关于订单的知识全在 Service 里。

    这样的对象跟一个 Map<String, Object> 没有本质区别。我们把这叫贫血模型——对象有血(数据)无肉(行为)。

    2.3 没法单独测业务逻辑

    想测"VIP + 优惠券 + 满减叠加时价格算得对不对"?对不起,你得先把 userMapper、couponMapper、rabbitTemplate 全部 mock 掉,才能跑到算价格那几行。业务规则和 MyBatis、RabbitMQ 焊死在了一起。

    2.4 改一处,慌全身

    下个迭代产品说"满 200 减 30 改成阶梯满减",你要动这个方法;再下个迭代说"加积分抵扣",还是动这个方法。一个方法被十个需求轮流改,merge 冲突家常便饭。

    MVC 没有导致这些问题,但 MVC 也没管这些问题。 MVC 只规定了"展示和逻辑分开",至于 Model 内部怎么组织,它一句话都没说。DDD 管的恰恰是这件事。

    记住这个定位,后面所有的概念都是围绕它展开的。


    三、用 DDD 的思路重构一遍

    重构的原则只有一句话:让每条业务规则待在它真正属于的地方。

    我们一步步来。

    第 1 步:把"订单自己的规则"还给 Order

    问自己一个问题:"待支付状态才能支付"这条规则,是属于某个 Service 的,还是属于订单本身的?

    显然属于订单本身——不管你从哪个入口进来(App、小程序、定时任务),这条规则都不变。那它就该写在 Order 里:

    public class Order {
       private Long id;
       private String orderNo;
       private Long userId;
       private List<OrderItem> items;
       private Money totalAmount;       // 注意这里,下一节讲
       private OrderStatus status;

       // 规则:待支付才能支付
       public void pay() {
           if (this.status != OrderStatus.PENDING_PAYMENT) {
               throw new BizException("只有待支付状态才能支付");
          }
           this.status = OrderStatus.PAID;
      }

       // 规则:已发货、已完成的订单不能取消
       public void cancel() {
           if (this.status == OrderStatus.SHIPPED || this.status == OrderStatus.COMPLETED) {
               throw new BizException("当前状态不允许取消");
          }
           this.status = OrderStatus.CANCELLED;
      }

       // 规则:订单总价必须等于明细之和——没人能绕过
       public void addItem(OrderItem item) {
           this.items.add(item);
           recalcTotal();
      }

       private void recalcTotal() {
           this.totalAmount = items.stream()
                  .map(OrderItem::subtotal)
                  .reduce(Money.ZERO, Money::add);
      }
    }

    对比一下:原来状态流转的 if (status != 1) throw … 散落在各个 Service 方法里,现在收进了 Order 内部,外部想绕过都绕不过——因为根本没有 setStatus() 给你调。

    这就是 DDD 说的充血模型:实体既有数据也有行为,自己守护自己的规则。

    注意一个细节:pay() 里没有 orderMapper.update(this)。实体不认识 Mapper,不认识数据库。它只管业务规则。持久化是别人的事——谁的?后面讲。

    第 2 步:把"10 块钱"变成 Money,而不是 BigDecimal

    原代码里金额计算全是裸的 BigDecimal:multiply、subtract、compareTo(ZERO) 满天飞。这里藏着 bug 温床:

    • 谁保证金额不会算出负数?(原代码里特意写了一行 if (total < 0) total = 0,这就是在为裸类型擦屁股)

    • 谁保证不会把人民币和美元加在一起?

    • new BigDecimal("0.9") 这种魔法数字散落各处。

    DDD 的做法:金额是一个值对象(Value Object),把和钱有关的规则收进 Money 类:

    public class Money {
       public static final Money ZERO = new Money(BigDecimal.ZERO, "CNY");

       private final BigDecimal amount;
       private final String currency;

       public Money(BigDecimal amount, String currency) {
           if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
               throw new IllegalArgumentException("金额不能为负");
          }
           this.amount = amount;
           this.currency = currency;
      }

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

       public Money subtract(Money other) {
           checkCurrency(other);
           BigDecimal result = this.amount.subtract(other.amount);
           // 减成负数?归零——规则内聚在这里,而不是散落在每个调用点
           return result.compareTo(BigDecimal.ZERO) < 0
                   ? ZERO
                  : new Money(result, this.currency);
      }

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

       public boolean greaterThanOrEqual(Money other) {
           checkCurrency(other);
           return this.amount.compareTo(other.amount) >= 0;
      }

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

       // 值对象没有 id,相等与否看值本身
       @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.stripTrailingZeros(), currency);
      }
    }

    几个关键点:

    • 不可变(immutable):所有字段 final,运算返回新对象。钱不会被"改掉",只会被"换掉"。

    • 构造即校验:负数金额在 new 出来的那一刻就死了,不可能流进系统。

    • 没有 id:你兜里的 10 块和我兜里的 10 块等价,判断相等看值,不看身份。

    这就是实体和值对象的本质区别,先记住代码感觉,第五节再总结判断方法。

    第 3 步:跨对象的规则,放进领域服务

    第 1 步把订单自己的规则收进了 Order。但"算最终价格"这条规则有点尴尬:它要用到用户等级(User)、商品单价(Product)、优惠券(Coupon)——不属于任何一个单独的对象。

    塞给 Order?Order 不该认识 Coupon。塞回应用服务?那又回到老路了。

    DDD 给这类规则准备的地方叫领域服务(Domain Service):

    // 领域层:只封装纯业务规则,不碰数据库、不碰 MQ
    public class PricingDomainService {

       private static final Money COUPON_THRESHOLD = new Money(new BigDecimal("100"), "CNY");
       private static final Money FULL_REDUCE_THRESHOLD = new Money(new BigDecimal("200"), "CNY");
       private static final Money FULL_REDUCE_OFF = new Money(new BigDecimal("30"), "CNY");

       public Money calculateFinalPrice(User user, Product product, int quantity, Coupon coupon) {
           Money total = product.getPrice().multiply(new BigDecimal(quantity));

           if (user.isVip()) {
               total = total.multiply(new BigDecimal("0.9"));
          }
           if (coupon != null) {
               coupon.checkUsable(total);   // "满100可用"的规则收进 Coupon 自己
               total = total.subtract(coupon.getAmount());
          }
           if (total.greaterThanOrEqual(FULL_REDUCE_THRESHOLD)) {
               total = total.subtract(FULL_REDUCE_OFF);
          }
           return total;
      }
    }

    注意两件事:

  • "满 100 才能用券"这条规则变成了 coupon.checkUsable(total)——它属于优惠券自己,收进了 Coupon 实体。这就是第 1 步的思路在 Coupon 上的复用。

  • PricingDomainService 里没有任何 @Autowired、没有 Mapper、没有 MQ。你可以 new 一个出来,直接单测所有价格组合,不用 mock 任何东西。2.3 节说的"没法测"的问题,到这里解决了。

  • 怎么判断一段逻辑该不该放领域服务?一句话:把数据库换了、把框架换了、把 Web 换成命令行,这段逻辑还要不要原样存在?要,它就是业务规则,放领域层。

    第 4 步:应用服务只做编排

    现在回头看原来那个胖 Service 干的事,拆完三块之后还剩什么?

    剩下的是"步骤":先查用户,再查商品,算价,建单,保存,发消息。步骤本身不是业务规则,是流程编排。 这部分交给应用服务(Application Service):

    // 应用层:只做流程编排,不含业务规则
    @Service
    public class OrderAppService {

       private final UserRepository userRepository;
       private final ProductRepository productRepository;
       private final CouponRepository couponRepository;
       private final OrderRepository orderRepository;
       private final PricingDomainService pricingDomainService;
       private final DomainEventPublisher eventPublisher;

       // 构造器注入略

       @Transactional
       public Long placeOrder(PlaceOrderCommand command) {
           // 1. 把需要的聚合取出来
           User user = userRepository.findById(command.getUserId());
           Product product = productRepository.findById(command.getProductId());
           Coupon coupon = command.getCouponId() == null
                   ? null
                  : couponRepository.findById(command.getCouponId());

           // 2. 业务规则全部委托给领域对象
           product.checkStock(command.getQuantity());
           Money finalPrice = pricingDomainService.calculateFinalPrice(
                   user, product, command.getQuantity(), coupon);
           Order order = Order.create(user.getId(), product, command.getQuantity(), finalPrice);

           // 3. 持久化 + 发布事件
           orderRepository.save(order);
           eventPublisher.publish(new OrderCreatedEvent(order.getId()));

           return order.getId();
      }
    }

    对比第二节的胖 Service,变化一目了然:

    • "库存不足抛异常"从 if (stock < quantity) 变成了 product.checkStock(…)——规则进了 Product;

    • 算价的一整坨 if-else 变成了一行 pricingDomainService.calculateFinalPrice(…)——规则进了领域服务;

    • 用户禁用校验没了?不,它在 User 里(比如 userRepository.findById 返回前先校验,或者 Order.create 里调 user.checkActive())。

    应用服务变薄了,但逻辑一行没少——只是每条规则都回到了自己家。 这就是 DDD 重构的本质:不是消灭复杂度,而是给复杂度归位。

    回答那个 MVC 用户最困惑的问题——"原来的 Service 层去哪了?" 它被拆成了三块:

    原来胖 Service 里的成分去了哪里
    业务规则判断(库存够不够、状态能不能变、券能不能用) 领域层:实体 / 值对象 / 领域服务
    流程步骤编排(先查什么再调什么) 应用层:应用服务
    数据库、MQ、第三方接口的技术实现 基础设施层

    第 5 步:Repository——领域层只要接口,不关心实现

    你可能注意到了,第 4 步用的是 OrderRepository,不是 OrderMapper。这不是改个名字装高级,区别是实打实的:

    // ===== domain 层:只定义接口 =====
    public interface OrderRepository {
       Order findById(Long id);     // 返回的是领域对象,不是 PO
       void save(Order order);      // 接收的是领域对象
    }

    // ===== infrastructure 层:具体实现 =====
    @Repository
    public class MybatisOrderRepository implements OrderRepository {

       private final OrderMapper orderMapper;   // Mapper 在这里才出现

       @Override
       public Order findById(Long id) {
           OrderPO po = orderMapper.selectById(id);
           return OrderConverter.toDomain(po);   // PO → 领域对象
      }

       @Override
       public void save(Order order) {
           OrderPO po = OrderConverter.toPO(order);   // 领域对象 → PO
           orderMapper.insertOrUpdate(po);
      }
    }

    对比一下:

    • DAO / Mapper 面向数据库,操作的是表和 PO,说的是"insert、update、select"这套数据库语言;

    • Repository 面向领域,操作的是实体和聚合,说的是"给我一个订单、存下这个订单"这套业务语言。DAO 只是 Repository 内部的一个实现细节。

    为什么要多这一层?这就是 DDD 四层架构里最关键的设计原则——依赖倒置:

    • 领域层定义 OrderRepository 接口,但它不知道 MyBatis 存在;

    • 基础设施层实现这个接口,依赖方向是 infrastructure → domain,反过来了;

    • 领域层因此不依赖任何技术细节。MySQL 换 MongoDB,领域代码一行不动;写单测时用一个内存版 InMemoryOrderRepository,领域层照样跑。

    到此,第二节说的四个问题全部有了着落:规则有家了(实体/值对象/领域服务)、对象有肉了(充血模型)、能单测了(领域层零外部依赖)、改规则只动一处(规则内聚)。


    四、现在回头看概念:实体、值对象、聚合

    第三节已经把代码看完了,这一节把概念正式收个尾。你会发现每个概念你都已经在代码里见过了。

    4.1 实体(Entity):有身份的对象

    Order 是实体:它有 id / orderNo,状态从"待支付"一路变到"已完成",但它始终是那一张订单。判断两个实体是否相等,看身份标识,不看属性。

    4.2 值对象(Value Object):只看值的对象

    Money 是值对象:没有 id,不可变,相等看值。

    给你一个实用的判断方法——"换一个问题":

    • 把订单的收货地址从"北京"换成"上海",这还是原来那张订单吗?→ 是 → 订单是实体,地址是订单身上的一个值。

    • 把一张 100 元钞票换成另一张 100 元钞票,你的钱变了吗?→ 没变 → 金额是值对象。

    有个经典坑要注意:同一个概念,在不同场景下身份不同。 比如地址:

    • 在订单上下文里,地址只是订单的一个快照属性 → 值对象;

    • 在"用户收货地址簿"里,每个地址有 id,能被单独编辑、设为默认 → 它是实体。

    没有天生的实体或值对象,只有在你的业务边界里它是什么。这就引出了限界上下文,第六节讲。

    4.3 聚合(Aggregate)与聚合根:一致性边界

    回到第三节的 Order.addItem():为什么加商品明细必须通过 Order,而不能直接 new 一个 OrderItem 塞进数据库?

    因为"订单总价必须等于明细之和"这条规则,只有 Order 能保证。如果外部能绕过 Order 直接改 OrderItem,总价和明细就对不上了。

    聚合就是一组对象的访问边界,聚合根(这里是 Order)是唯一的入口。 类比快递柜:你只能通过柜门取件,不能把手伸进柜子。

    设计聚合时记住两条原则,能避开新手 90% 的坑:

  • 一致性边界原则:聚合是强一致性的最小单元。聚合内的规则(总价=明细之和)由聚合根在同一个事务里保证;聚合之间(订单和库存、订单和积分)通过领域事件或应用服务协调,做到最终一致即可。

  • 最小化原则:聚合越小越好。只把"生命周期一致、规则强绑定"的对象放进去。订单需要知道商品叫什么、多少钱——存个商品 ID + 当时的价格快照就够了,别把 Product 整个塞进来,更别把 User 塞进来。为了"查询方便"把聚合撑大,最后边界形同虚设,代码重新变回一锅粥。

  • 4.4 领域服务 vs 应用服务:一句话区分

    这两个最容易混,记住这句:

    • 领域服务:把数据库、框架、前端全换掉,这段逻辑依然成立 → 它是业务规则(如算价);

    • 应用服务:它描述的是"这个用例的步骤" → 先查再算再存再通知,是流程编排(如下单)。

    再简单点:领域服务里不该出现 @Transactional、@Autowired Mapper、HTTP、MQ;应用服务里不该出现业务规则的 if-else。

    4.5 领域事件(Domain Event):顺便提一句

    第三节末尾的 eventPublisher.publish(new OrderCreatedEvent(…)) 就是领域事件:订单聚合说"我被创建了",至于谁关心(发积分、发通知、推大数据),让订阅者自己去处理。聚合之间靠事件解耦,而不是互相直接调用。


    五、DDD 四层架构,和 MVC 摆在一起看

    现在可以把全貌亮出来了。DDD 在后端常见的落地方式是四层架构:

    架构层级核心职责对应传统 MVC/三层的哪部分里面有什么
    接口层(Interfaces) 接收请求、参数校验、组装响应 View + Controller Controller、DTO、VO 返回模型
    应用层(Application) 用例流程编排,无业务规则 胖 Service 的"流程"部分 应用服务、Command/Query 对象
    领域层(Domain) 核心业务规则,系统的心脏 胖 Service 的"规则"部分 + 数据模型 实体、值对象、聚合、领域服务、Repository 接口
    基础设施层(Infrastructure) 技术实现支撑 DAO + 外部调用 Repository 实现、Mapper、远程调用、MQ

    两个要点:

  • MVC 没有被干掉,它被收编了。 Controller 和 View 大体落在接口层;原来 Model/Service/DAO 里混成一团的职责,被拆进了应用层、领域层、基础设施层。MVC 管"请求怎么进来怎么出去",DDD 管"请求进来之后业务逻辑怎么组织"。不同维度,互补关系,可以共存。

  • 调用方向和依赖方向是两回事。 请求从接口层 → 应用层 → 领域层往下调用,但依赖关系通过依赖倒置保护着领域层:领域层只握有 Repository 接口,实现由基础设施层提供。领域层是四层里唯一不知道数据库存在的层。


  • 六、战略设计:代码之外的另一半 DDD

    前面五节讲的都是战术设计——边界之内代码怎么写。DDD 还有另一半叫战略设计,回答的是更宏观的问题:系统怎么拆,边界怎么划。

    很多教程把 DDD 讲成"充血模型 + 四层架构",其实是丢掉了一半。入门阶段战略设计记住两个概念就够。

    6.1 限界上下文(Bounded Context):同一个词,不同的意思

    4.2 节留了个伏笔:地址在不同场景下身份不同。推广开——"用户"这个词,在你们系统里真的是同一个东西吗?

    • 在会员中心,用户有等级、积分、优惠券;

    • 在订单中心,用户就是收货地址 + 联系方式;

    • 在财务中心,用户是开票信息 + 账单。

    如果你试图建一张"万能用户表"通吃所有场景,字段会越加越多,每个模块只用其中 20 个字段,规则互相打架。限界上下文就是给每个业务领域画一个圈:圈内,每个概念有唯一明确的含义;圈与圈之间,通过明确的接口(或防腐层)协作,互不干涉内部实现。

    它是后来拆微服务的重要参考——但注意,只是参考,不是一一对应(见第八节误区一)。

    6.2 核心域 / 支撑域 / 通用域:好钢用在刀刃上

    战略设计还会把子域分三类,用来分配研发资源:

    • 核心域:你的业务护城河,比如电商的交易履约、定价策略 → 精细建模,上完整的 DDD;

    • 支撑域:辅助核心域运转,比如商品资料管理 → 有规则但非核心竞争力,适度建模;

    • 通用域:行业通用能力,比如短信、权限、日志 → 买现成或用开源,别造轮子。

    这个分类本身就是 DDD 最重要的实践价值之一:它明确告诉你哪里值得精细设计,哪里简单 CRUD 就够——避免全系统过度设计,也避免核心业务被草率对待。


    七、冷静一下:充血 vs 贫血,不是信仰之争

    讲完充血模型必须泼一盆冷水:贫血模型不是"错",它是一种合理的默认选择。

    如果你的业务就是"把表单存进表,再从表里查出来",贫血模型 + 三层架构简单直接,没有任何问题。充血模型在这种情况下反而是负担——给只有 CRUD 的对象硬造行为,纯属戏精上身。

    真正的判断标准是业务规则的密度:

    • 规则少 → 放 Service 里没什么成本,贫血够用;

    • 规则多、规则之间有关联、规则会被多个入口复用 → 贫血的代价开始显现(散落、重复、不可测),这时充血模型的收益才盖过成本。

    第三节的重构之所以值得做,是因为"下单"这个场景规则密集:状态机、库存、价格、优惠券。不是因为贫血模型政治不正确。


    八、什么时候用 DDD,什么时候别用

    适合:

    • 业务规则复杂,不是 CRUD 能搞定的(状态机、复杂计算、多规则叠加);

    • 系统要长期维护迭代,今天省的事明天要还;

    • 团队有规模,需要靠代码结构传递业务知识。

    不适合:

    • 简单后台管理系统(就是增删改查);

    • 快速原型 / MVP(先跑起来验证,活下去再说);

    • 业务逻辑本来就薄的项目。

    一个粗糙但实用的信号:你的某个 Service 超过 500 行,改一条业务规则要动三个以上的文件,新人来了问"XX 规则写在哪"你答不上来——出现这些,就该考虑 DDD 了。数字不是硬标准,是复杂度的体温计。

    常见误区避坑

    误区一:DDD = 微服务。 DDD 是设计思想,微服务是部署架构。限界上下文是拆服务的重要参考,但简单业务里多个上下文跑在一个单体里完全没问题。

    误区二:充血模型 = 把所有逻辑塞进实体。 那只是把胖 Service 换成了胖实体。实体只管自己的状态和自身的规则;跨对象的规则去领域服务;流程编排去应用服务。

    误区三:Repository 就是 DAO 改名。 见第 5 步:DAO 面向数据库说 insert/update,Repository 面向领域说"给我订单/存订单"。DAO 是 Repository 的实现细节,两者不在一个层级。

    误区四:所有项目都要上全套 DDD。 见第七节。简单项目硬套 DDD,是拿复杂度治复杂度,得不偿失。

    误区五:一上来就追求完美分层。 更务实的路径是:先在最痛的那个核心模块里,把规则下沉到实体,把 Repository 接口抽出来,跑通了再推广。大爆炸式重构死多活少。


    九、标准包结构参考

    以 Java 电商订单域为例,可直接抄:

    com.xxx.order
    ├── interfaces         // 接口层:对外暴露能力
    │   ├── controller     // HTTP 接口
    │   ├── dto             // 请求/响应传输对象
    │   ├── response       // 返回模型(别和领域值对象混用)
    │   └── facade         // 对外 RPC 接口实现
    ├── application         // 应用层:流程编排,无业务规则
    │   ├── service         // 应用服务
    │   ├── command         // 写操作命令对象(如 PlaceOrderCommand)
    │   └── query           // 读操作查询对象
    ├── domain             // 领域层:核心业务规则
    │   ├── entity         // 实体、聚合根
    │   ├── valueobject     // 值对象(Money、Address…)
    │   ├── service         // 领域服务(PricingDomainService…)
    │   ├── event           // 领域事件
    │   └── repository     // 仓储接口(只有接口!)
    └── infrastructure     // 基础设施层:技术实现
      ├── repository     // 仓储实现(MybatisOrderRepository…)
      ├── dao             // Mapper / DAO + PO
      ├── remote         // 第三方接口调用
      ├── mq             // 消息收发实现
      └── config         // 技术配置

    一个自检方法:打开 domain 包,全文搜索 import,不应该出现任何 org.apache.ibatis、javax.servlet、com.aliyun 之类的技术包。 出现了,说明依赖漏进来了。


    十、总结

    回到开头的三个问题:

    1. 胖 Service 胖在哪? 不是行数,是规则没有家:散落各处、对象贫血、和技术实现焊死、改一处慌全身。

    2. 拆完去哪了? 业务规则 → 实体/值对象/领域服务;流程编排 → 应用服务;数据库和外部接口 → 基础设施层。MVC 的 Controller 和 View 没变,变的是 Model 内部的组织方式。

    3. 要不要用? 看规则密度。CRUD 项目别硬套,复杂业务别硬扛。

    一句话版本:

    MVC 解决的是"展示和逻辑分离",DDD 解决的是"业务逻辑内部怎么组织"。它们不在同一个维度,不是替代关系,是互补关系。DDD 做的事情,说穿了就一件:让每条业务规则待在它真正属于的地方。


    十一、下一步

    概念和重构思路都有了,但还差最后一步:从零开始写。 下一篇我们用一个完整可运行的电商订单模块,从建表到四层代码全部落地,包括事务怎么处理、领域事件怎么发、PO 和领域对象怎么转换——这些教程里通常语焉不详的工程细节,才是真正卡住人的地方。

    赞(0)
    未经允许不得转载:171主机测评 » DDD重构:从贫血模型到充血模型的实战之旅
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址