
摘要本章先不急着讲 DDD 名词而是从一段看似正常的下单代码出发看清业务规则散落、技术细节污染、边界缺失和语言不一致如何把系统拖成大泥球。读完后你会知道 DDD 到底是在解决什么问题以及什么时候不该为了 DDD 而过度设计。本系列怎么学从问题到落地这套内容不按DDD 名词表来排而是按新手真正理解一件事的顺序来排先看问题再划边界再建模型最后落到代码、数据库和重构实践。第四段现实落地避免一上来推倒重来第三段工程落地让模型在项目里跑起来第二段战术建模把业务规则放进模型第一段战略设计先把业务边界看清楚问题引入划业务范围明确模型边界进入模型设计组成聚合内部对象安放业务行为处理跨对象协作连接存储和创建进入工程结构落实代码组织落到存储处理复杂查询回到真实项目1. 为什么需要 DDD先看到传统写法的问题2. 领域 / 子域 / 核心域判断哪里值得精细建模3. 限界上下文 / 统一语言确定模型在哪个边界内成立4. 上下文映射 / 集成解决边界之间怎么协作5. 实体 / 值对象先分清对象身份和属性6. 聚合 / 聚合根用一致性边界守业务规则7. 充血模型 / 贫血模型回答业务逻辑到底写在哪8. 领域服务 / 领域事件处理跨对象规则和副作用解耦9. 仓储 / 工厂解决模型的创建和存取10. DDD 分层架构明确代码依赖方向11. 代码模型 / 目录结构把概念落到 Spring Boot 包结构12. 数据库设计把领域模型映射成表13. CQRS把复杂读和规则写分开14. 落地实践 / 重构从老项目渐进式引入 DDD阶段章节新手要抓住的问题战略设计01-04系统为什么会乱业务边界怎么划上下文之间怎么协作战术建模05-09对象怎么设计规则放在哪聚合怎么守一致性模型怎么创建和保存工程落地10-13代码怎么分层目录怎么摆数据库怎么映射复杂查询怎么处理实践收口14老项目怎么渐进式改造什么时候该用/不该用 DDD一、先看一段很正常的代码我们的优选门店要做一个下单功能。按传统三层架构Controller → Service → DAO多数人会这么写ServicepublicclassOrderService{AutowiredprivateProductMapperproductMapper;AutowiredprivateStockMapperstockMapper;AutowiredprivateMemberMappermemberMapper;AutowiredprivateCouponMappercouponMapper;AutowiredprivateOrderMapperorderMapper;TransactionalpublicLongplaceOrder(PlaceOrderRequestreq){// 1. 查商品、算价格BigDecimaltotalBigDecimal.ZERO;for(OrderItemReqitem:req.getItems()){ProductpproductMapper.selectById(item.getProductId());if(pnull||p.getStatus()!1){thrownewRuntimeException(商品不可售);}totaltotal.add(p.getPrice().multiply(newBigDecimal(item.getQty())));}// 2. 用优惠券if(req.getCouponId()!null){CouponccouponMapper.selectById(req.getCouponId());if(c.getThreshold().compareTo(total)0){totaltotal.subtract(c.getAmount());}}// 3. 扣库存for(OrderItemReqitem:req.getItems()){introwsstockMapper.deduct(item.getProductId(),item.getQty());if(rows0)thrownewRuntimeException(库存不足);}// 4. 加会员积分MembermmemberMapper.selectById(req.getMemberId());m.setPoints(m.getPoints()total.intValue());memberMapper.updateById(m);// 5. 落订单OrderordernewOrder();order.setMemberId(req.getMemberId());order.setTotalAmount(total);order.setStatus(0);orderMapper.insert(order);returnorder.getId();}}这段代码能跑、能上线。问题是它会随着业务长大而一天天烂掉。二、它到底烂在哪传统架构的弊端弊端 1业务逻辑全堆在 Service没人守得住规则下单的核心业务规则哪些商品能买、满减怎么算、库存怎么扣、积分怎么加全部散落在一个方法里。Product、Order、Member这些类只有 getter/setter是纯粹的数据袋子这就是后面要讲的贫血模型。后果是同样的算订单总价逻辑购物车页面、确认页、下单接口各写了一遍规则一改三处都要改必漏一处。积分 实付金额取整这条规则写在OrderService里哪天做退款的同事根本不知道有这条规则积分就退错了。弊端 2技术细节和业务逻辑搅在一起productMapper.selectById数据库技术 和 “商品不可售就报错”业务规则写在同一个方法里。换 ORM、加缓存、做分库分表都要动这段业务代码。业务和技术互相污染改谁都怕碰坏对方。弊端 3没有边界牵一发动全身订单、库存、会员、促销全在一个事务、一个 Service 里。促销逻辑一复杂这个方法就膨胀到几百行。它有 5 个Mapper依赖意味着它和 5 个表强耦合。这就是导读里说的大泥球不同关注点的代码混在一起 → 难以识别领域逻辑 → 该内聚的分散、该解耦的纠缠 → 改业务伤技术、改技术伤业务 → 越维护越乱重复和不一致越来越多。弊端 4代码和业务对不上号产品经理说的是会员下单后冻结优惠券代码里却是couponMapper.updateStatus(id, 2)。2是什么意思没人知道。业务语言和代码语言对不上每次沟通都要翻译需求一复杂就翻译错。三、DDD 凭什么能治DDDDomain-Driven Design领域驱动设计的核心思想其实就一句话让代码围着业务领域来组织而不是围着数据库表或技术框架来组织。它给出两个层面的方法战略设计——先把大系统按业务边界切块。下单、库存、促销、会员各自是独立的限界上下文各有各的模型和边界。改促销物理上就碰不到积分的代码。战术设计——每块内部让业务规则有家可归。下单的规则属于订单这个聚合由它自己守护积分规则属于会员。Order不再是数据袋子而是有行为的对象order.place()、order.cancel()。用 DDD 重构后上面那段代码大致会变成ServicepublicclassPlaceOrderAppService{privatefinalProductRepositoryproductRepo;privatefinalOrderRepositoryorderRepo;privatefinalDomainEventPublishereventPublisher;TransactionalpublicOrderIdplaceOrder(PlaceOrderCommandcmd){// 应用层只做编排把活儿派给领域对象自己不写业务规则ListOrderLinelinescmd.toOrderLines(productRepo);// 取价、校验可售OrderorderOrder.place(cmd.memberId(),lines,cmd.coupon());// 业务规则在聚合内部orderRepo.save(order);eventPublisher.publish(order.domainEvents());// 发订单已创建事件库存/积分异步响应returnorder.getId();}}对比一下你能直观感受到的变化维度传统三层DDD业务规则在哪散落在 Service内聚在领域对象聚合里谁守护规则没人靠程序员自觉聚合根强制守护边界没有全耦合限界上下文物理隔离改促销会影响积分吗很可能不会物理隔离代码读起来像业务吗不像是 SQL 流水账像order.place()直接对应下单四、什么时候不该用 DDD避免过度设计DDD 不是银弹它有成本学习成本、代码量。别被高大上冲昏头简单 CRUD、后台管理、数据报表业务逻辑就是增删改查没什么规则要守护。直接三层架构甚至低代码DDD 反而是负担。业务逻辑极其简单的项目杀鸡用牛刀。真正适合 DDD 的业务规则复杂、会长期演进、多人协作的核心业务系统——比如我们的订单交易、促销引擎。判断口诀逻辑越复杂、越要长期演进、越值钱的业务越值得上 DDD。五、怎么验证一个模块是否真的需要 DDD别靠感觉判断拿一个真实模块按下面清单打分。命中越多越值得用 DDD命中越少越该保持简单。观察点适合 DDD 的信号不适合 DDD 的信号业务规则有状态流转、组合规则、例外规则只是字段增删改查变化频率需求经常变且每次都怕改坏长期稳定很少改业务价值核心收入、核心体验、核心风控后台配置、报表导出协作人数产品、业务、开发多人反复沟通一个人维护的小工具当前痛点Service 很长、规则重复、bug 频发代码简单直观可以在老项目里用命令做一个粗略体检# 找出超长 Service优先看是否有业务规则堆积findsrc/main/java-name*Service.java-print0|xargs-0wc-l|sort-nr|head# 找魔法状态值、散落的状态判断grep-Rstatus.*src/main/javagrep-RsetStatussrc/main/java# 找一个服务依赖了多少 Mapper/Repository依赖越多越可能是大泥球grep-RAutowired.*Mapper\|private final .*Mappersrc/main/java注意这些命令只能帮你发现可疑点不能替你下结论。最终还是要回到业务这块是不是复杂、值钱、长期演进。