防腐层哪里使用
按模型差异与维护成本判断防腐层适用范围,区分领域翻译、适配器、Repository 与普通工具包装。
#howto
#evergreen
#dev
[!info] related notes
防腐层哪里使用
先找出两边模型的差异,再判断是否需要防腐层。外部依赖存在,并不足以说明应该增加一层封装。
判断顺序
- 外部状态、单位、规则是否与内部业务含义不同?
- 这些差异是否已经散落到业务代码中,或会随供应商变化反复修改?
- 能否定义一个内部确实需要的接口,并完整表达成功、失败和不确定结果?
- 转换与维护的收益是否高于新增代码、延迟和运维成本?
六类场景
| 场景 | 适用做法与边界 |
|---|---|
| 第三方支付、短信、存储等 | 将供应商模型翻译为业务需要的能力与结果;仅改接口名字不代表已隔离语义 |
| 基础工具库 | 简单包装可减少导入耦合,但通常只是 adapter 或 wrapper;不要为所有库建立万能工具类 |
| 持久化 | Repository / Mapper 可隔离存储表示;事务、一致性和查询能力仍可能影响业务,不保证数据库可无成本替换 |
| 限界上下文集成 | 用消费方模型理解生产方的数据和事件,避免直接共享整个领域实体 |
| 前端数据适配 | 将后端状态码、单位等转换成前端可用模型;纯显示格式与本地化可留在视图层,不必全放进领域实体 |
| 遗留系统迁移 | 在新旧模型间转换;可配合渐进替换的 Strangler Fig 策略,但防腐层和迁移策略不是同一种模式 |
支付例子:别把创建意图当成扣款
旧示例将 stripe.paymentIntents.create() 包装为 charge(): Promise<string>。它隐藏了需要用户操作、处理中、失败等状态,拿到 ID 并不证明扣款成功。
内部接口应先确定业务动作是“发起支付”还是“查询已确认结果”,然后映射供应商状态、金额、币种与错误。还要明确幂等键和超时后的查询方式,不能靠接口名称制造成功语义。
工具替换仍需检查契约
- 将 Moment 换成 Day.js、Luxon 或 Temporal 时,要检查时区、可变性、日期运算和无效输入行为;Moment 处于维护模式,不应简单写成“停止维护”。
- UUID 与 Nano ID 的格式、长度、唯一性假设和兼容协议不同,
type Uuid = string不会提供运行时校验,也不能让二者自动等价。 - 时钟接口可用于固定测试时间;没有这种需求时,不必仅为未来可能换库增加静态
DateUtils。
上下文映射例子
用户系统可能提供账号资料,订单系统只需要客户标识与收货信息。应按订单语义决定是引用当前地址,还是保存下单时地址快照。
仅把 UserDTO.address 复制为 Customer.shippingAddress 还不能证明模型正确;要确认地址归属、有效期和缺失情况。不要因为字段叫 Customer 就默认它一定是值对象。
如何验证
用实际契约样例验证状态、缺字段、未知值、单位和错误映射;必要时增加供应商契约测试。对无法可靠转换的输入显式报错或进入待处理状态,不默默丢失含义。
即使简单 CRUD 不需要完整 ACL,也仍需控制响应字段、权限和外部输入,不能把数据库记录不加区分地暴露给客户端。