DDD(领域驱动设计)篇一:DDD 到底在解决什么问题?
1、先摆结构:两个架构长什么样
你熟悉的三层架构,和 mpc 中台(我们系统里的中台叫mpc)实际的 DDD 分层,长这样:
左边是三层:所有活挤在 Service 一层里。右边是 DDD:同样一次请求被拆成四层,注意"业务规则"住在领域层,锁和缓存这类技术细节被隔离在最底下。
看起来只是把 Service 拆成了三块?不。关键区别不是层数,是"业务逻辑搬家了"。
打个比方:三层架构像一家什么都管的便利店,一个店员(Service 方法)同时收银、煮关东煮、盘库存、修打印机。生意小没事,生意大了每个店员都得是全能选手,换一个店员(改需求),另三样活都可能被牵连。
DDD 像一家正规餐厅:应用层是领班——不做饭,只安排哪桌的菜、交给哪个厨师;领域层是厨师和菜谱——真正的手艺在这里;基础设施层是后厨设备——冰箱坏了换设备,菜谱不用改。
2、拿真代码对比一下
三层版,patient 模块下单 OrderServiceImpl.java 的createOrder(一千六百行大类中的一员),简化后:
@Transactional(rollbackFor = Exception.class) public OrderSeqVO createOrder(CreateOrderDTO dto) { // 业务校验:患者是否满足挂号条件 boolean checkHospitalValid = checkHospitalValid(consultationValidDTO); Validator.validateTrue(checkHospitalValid, "就诊人信息不满足条件!"); // 远程调用:查医院特色服务,决定走哪条分支 Map<String, DictVo> specialServiceMap = hospitalApi .getHospitalSpecialServerList(dto.getHospitalId()).getData()...; // 业务分支:续方购药走这、特色服务走那、在线复诊还要校验号源 if (serverTypeEnum == ServerTypeEnum.DOCTOR_CONTINUE_BUY) { ... } // 技术细节:分布式锁防重复下单 RLock lock = redissonClient.getLock(SysContextHolder.getUserId() + "createOrder"); if (!lock.tryLock(1, 30, TimeUnit.SECONDS)) { GlobalException.throwEx("请勿重复提交订单"); } // 再往后:号源智能分配、耗时统计、按业务类型决定是否重试…… }数一数,一个方法混了五种东西,而且变更节奏完全不同:
| 成分 | 变更频率 | 举例 |
|---|---|---|
| 业务规则 | 月月改、经常改 | 挂号条件、医生号源匹配、新的挂号类型 |
| 远程调用 | 偶尔改 | hospitalApi 查特色服务 |
| 技术细节 | 半年不动 | Redisson 锁、tryLock 参数 |
| 流程编排 | 随业务改 | 分支判断、重试 |
| 统计观测 | 几乎不动 | StopWatch 耗时统计 |
痛点不在"代码烂"——每段单拎出来都没毛病。痛点在五种节奏不同的东西拧在一起,改任何一个都得通读全文。
DDD 版,同一工程里 mpc 中台的订单取消,OrderBaseSalesRuntimeContext.java(应用层,就是那个"领班"):
@Transactional(rollbackFor = Exception.class) public Result<CancelOrderDto> cancelOrder(CancelOrderParam param) { // 1. 把订单实体从仓库捞出来 Optional<OrderBasedSalesEntity> optional = orderEntityRepo.findOrder(orderId, orderNo, sourceChannel, externalOrderNo); OrderBasedSalesEntity orderEntity = optional.get(); // 2. 加锁交给锁的适配器,不自己摸 Redisson return iDomainLockAdapter.logicWithLock(orderEntity.getOrderLockKey(), () -> { // 3. 触发"取消"事件,规则由状态机和 Handler 说话 EventModel cancelEvent = EventModelFactory.INSTANCE .getEventModel(SalesOrderEventConstants.cancel); cancelEvent.setValueObject(JSONUtil.parseObj(param)); orderEntity.onEvent(cancelEvent); // ← 核心就这一行 return Result.success(new CancelOrderDto()); }); }那"什么状态下能取消、取消后干啥"去哪了?——能不能取消由状态机描述符说了算,没注册的"状态+事件"组合框架直接抛异常;取消后干什么由 Handler 说了算;锁怎么加由 IDomainLockAdapter 说了算,领域层连 Redisson 的类都没 import 过。
3、逐项对比表
| 维度 | 三层架构 | DDD |
|---|---|---|
| 业务逻辑住在哪 | Service 方法里 | 领域对象 + 状态机 + Handler |
| Entity 的角色 | 数据袋子(贫血),只有 getter/setter | 有行为有规则(充血),如isOrderRefundable() |
| 状态流转怎么管 | 每个方法手写 if/setStatus | 状态机统一声明,非法流转自动拦截 |
| 改一条规则 | 全局搜、多处改、容易漏 | 通常只改一处 |
| 分布式锁/缓存 | 直接 import Redisson/RedisTemplate | 适配器隔离,领域层不知道 Redis 存在 |
| 单元测试 | 依赖一堆,mock 到手软 | 领域规则不碰中间件,纯逻辑好测 |
| 上手成本 | 低,应届生一周上手 | 高,概念多,需要人带 |
| 简单接口的代价 | 三个类搞定 | 可能要建实体、描述符、Handler 一套 |
最后两行是 DDD 的真实代价,不吹不黑。
4、到底什么时候用哪个
判断逻辑其实就两问:有没有复杂的状态流转?规则是不是常变 / 多团队共用?决策路径是这样的:
对应的场景表:
| 场景 | 建议 |
|---|---|
| 配置管理、字典、简单 CRUD | 三层,别折腾 |
| 复杂查询、报表、导出 | 三层(查询为主就该轻快) |
| 订单/支付/退款这类多状态流转 | DDD 收益明显 |
| 规则复杂且常变(计费、退费、风控) | DDD |
| 多团队共用的中台/平台 | DDD 划边界几乎是刚需 |
我们项目就是现成的印证:patient 模块面向 C 端单一业务,接口虽多但链路相对独立,三层够用,硬改 DDD 属于过度设计;mpc 中台是库存、订单、支付回调、IM 好几个业务方共用一套骨架,外部还有"京东极速咨询"这种多渠道接入(描述符里label = "jd_quickmed_service"),领域骨架让新业务接入变成"声明一个描述符 + 写几个 Handler"。
同一个工程、两种架构并存,这不是技术债,是按业务复杂度选型的正常状态。
5、一个容易踩的认知坑
很多人以为"DDD = 把 Service 的代码搬到 Entity 里"。不对。
搬家只是表象,前提是你先想清楚业务的边界和规则——哪些状态、哪些事件、哪些规则属于"订单"这个地盘。想不清楚就动手,结果只是把一锅粥从一个碗倒进另一个碗。这也是为什么 DDD 实践里"先和产品对语言、先画状态图"比"先写代码"重要得多。
6、思考题
翻出你上周写过的最复杂的一个 Service 方法(你自己心里有数是哪个),做三件事:
数一数里面混了几类职责:业务规则、远程调用、缓存、锁、事务、参数组装……
找出其中的"状态变更"代码:改了哪个字段?这个字段还有哪些别处也在改?
问自己:如果这个方法只保留业务规则,其他都挪走,它会变短多少?
带着答案进入下一篇——把项目里真实的"三层订单"和"DDD 订单"放在显微镜下对照。
DDD(领域驱动设计)篇三:实战对照——同一个订单业务,两种架构怎么写