4.2 依赖倒置与整洁架构:把箭头扭过来


4.2 依赖倒置与整洁架构:把箭头扭过来

本节摘要:4.1 把楼层分好了,本节处理承重问题——依赖箭头的方向。传统分层里"上依赖下"的箭头,到了领域层必须扭过来:接口属于领域,实现属于基础设施,框架依赖业务而非相反。依赖倒置原则、端口与适配器、整洁架构三套说法讲的是同一件事;本节用青柚商城的真实改造展示箭头怎么扭、扭完哪些问题自动消失,以及倒置什么时候是过度设计。

一个具体的困境开场

4.1 的测试报告留下一个未解之谜:为什么领域逻辑测不了?把三层形态的 OrderService 拆开看 import 列表就明白了——它引用数据访问组件、引用远程调用客户端、引用消息框架。领域逻辑和这三样东西物理焊死:想测"券不可用就拒单",得先有一套能跑的数据访问环境。问题不在测试技术,在于依赖箭头画反了:按四层的本意,基础设施支撑领域,落实在代码上却是领域代码 import 基础设施——支撑关系与依赖关系是反的。

依赖倒置原则(Dependency Inversion Principle)给的反转条款有两句:高层模块(业务策略)不该依赖低层模块(技术细节),两者都该依赖抽象;抽象不该依赖细节,细节该依赖抽象。落到 DDD 的四层图上,就是一根被扭过来的箭头:领域层声明"我需要一个能存订单的东西",基础设施层提供实现——接口的归属权从低层移交给高层。3.3 的仓库接口之所以定义在领域层,执行的就是这一条;现在把它背后的原理说透。

改造现场:一根箭头怎么扭

青柚商城改造前的领域类长这样:

// 改造前:领域逻辑依赖具体技术——箭头指反了 public class CouponPolicy { public boolean usable(CouponEntity coupon, BigDecimal subtotal) { // 业务判断:券状态、门槛、有效期 if (!"ACTIVE".equals(coupon.getStatus())) return false; if (subtotal.compareTo(coupon.getMinSpend()) < 0) return false; // 技术渗入:直接调数据访问组件查"该用户已用张数" return couponDao.countUsedBy(coupon.getId(), currentUserId()) < 1; } }

两个病灶。业务判断与数据访问共存于一个方法,测试要起库;couponDao 是数据访问组件的类型,领域类被技术细节绑架。改造三步:先分离关注点——"已用张数"是个业务概念,给它在领域层一个抽象;再把接口所有权交给领域层;最后基础设施实现抽象:

// 改造第一步:领域层声明自己需要的抽象(端口) public interface CouponUsageLedger { // 领域语言:券核销台账 int timesUsedBy(CouponId couponId, BuyerId buyerId); } // 改造后的领域类:只依赖自己的语言 public class CouponPolicy { private final CouponUsageLedger ledger; // 领域声明的端口,不是技术的类型 public boolean usable(Coupon coupon, Money subtotal) { if (!coupon.active()) return false; if (!subtotal.meets(coupon.minSpend())) return false; return ledger.timesUsedBy(coupon.id(), coupon.holder()) < 1; } } // 基础设施层:实现领域的端口(适配器) public class CouponUsageLedgerImpl implements CouponUsageLedger { private final CouponDao dao; // 技术细节被关在实现里 @Override public int timesUsedBy(CouponId couponId, BuyerId buyerId) { return dao.countByCouponAndBuyer(couponId.value(), buyerId.value()); } }

改造后的可测性是立竿见影的:测 CouponPolicy 只需在测试里塞一个 timesUsedBy 返回固定值的假实现,业务规则的全部分支——未激活、不达门槛、超限用——都是毫秒级的纯内存断言。但这只是 immediate 的好处,长期的好处更值钱:领域层的语言稳定,技术的更替频繁。数据访问方案三年换了两次,CouponPolicy 一行没动——箭头扭过来之后,换技术从"手术"降级成"换插头"。

三张图,一件事

把箭头扭过来的思想,业界画过三张著名的图,讲的全是同一件事,只是强调的侧面不同。

**六边形架构(端口与适配器)**强调的是接入方式:领域核心住在一个六边形里,对外暴露两类端口——被调用的(仓库接口、外部系统能力接口)与主动发起的(应用服务的用例入口);每个端口配适配器。数据库是适配器,网页界面也是适配器,地位完全平等——核心不关心谁在敲它的门

整洁架构把同一件事画成同心圆,并加了一条铁律:源代码依赖只能由外向内,内环对外环一无所知。最内环是实体(业务规则),往外依次是用例(应用层)、接口适配器、框架与驱动。它比六边形多强调的一点是跨环传递的数据结构必须简单——别让外层的传输对象渗进内环。

洋葱架构与整洁架构同构,额外强调领域服务的地位:跨聚合的规则(3.2 的分摊服务)住在内环,与实体同级合法。

整洁架构同心圆

整洁架构同心圆

三张图对 DDD 的意义可以合成一句:它们是"领域模型是核心资产"在源代码层面的产权登记。资产的前提是独立——能脱离技术栈估值(单测)、搬运(换框架)、展示(原型)。箭头向内,资产才归你;箭头向外,你只是框架的租户。

什么时候不倒置

倒置有成本:每个端口多一层抽象、多一套假实现测试。以下三种情况,直连反而正确。其一,稳定且无业务语义的技术设施:日志、加密这类纯工具函数,领域层直接用,为它们造"日志端口"纯属仪式。其二,不承载业务规则的胶水层:应用层直接调消息客户端发事件没有问题——前提是事件本身的构造在领域层完成了,应用层只是搬运。其三,原型期:模型每周推翻两遍的阶段,先直连快跑,模型稳定后按本章方法一次性倒置——倒置的是稳定的抽象,摇摆不定的抽象不值得占用一个端口名。

判据依旧是那条:这段依赖里有没有业务语义。"查已用张数"是业务概念,倒置;"发一条日志"是技术动作,直连。语义在谁那边,箭头就该向谁收拢。

带走的判断

  • 支撑关系与依赖关系必须同向:基础设施支撑领域,依赖箭头也指向领域——接口的归属权是关键筹码。
  • 倒置的长期收益是换技术从手术降级为换插头;短期收益是业务规则获得毫秒级纯内存测试。
  • 六边形、整洁、洋葱三张图一个意思:业务规则的源代码不 import 任何框架。
  • 倒置有适用线:有业务语义的依赖才值得端口化,纯工具与原型期直连不丢人。

楼层的箭头都理顺了,只剩最后一个诱惑:模块都这么干净了,要不要干脆拆成微服务?4.3 给出青柚商城的答案。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U