防腐层哪里使用
看看在整个软件工程中,还有哪些地方是 **“必须”** 或 **“强烈建议”** 设立防腐层的。
防腐层哪里使用
[!info] related notes
- 相关知识: 防腐层, DDD架构, ddd中领域服务和应用服务
防腐层的核心作用只有两个:
- 隔离变化:外部怎么变,我不关心,我的核心逻辑不用改。
- 语义翻译:把“别人的语言”翻译成“我的语言”。
以下是 6 个最经典、最实用的防腐层应用场景:
1. 第三方服务集成 (Third-Party Integrations)
这是最常见、最容易被忽视的地方。
很多项目直接在业务代码里调用 stripe.charges.create(...) 或者 aws.s3.upload(...)。一旦第三方 SDK 升级,或者你要从 AWS 换成阿里云,整个项目就炸了。
- 场景:支付、短信、云存储、地图服务。
- 防腐手段:定义自己的 Interface(适配器模式)。
❌ 错误做法(直接依赖):
TypeScript
// Service 层直接用 Stripe SDK
import Stripe from 'stripe';
class OrderService {
async pay(amount: number) {
// 你的业务代码被 stripe 的具体实现污染了
await stripe.paymentIntents.create({ amount, currency: 'usd' });
}
}
✅ 防腐层做法:
TypeScript
// 1. 定义防腐接口 (Domain Layer)
export interface IPaymentGateway {
charge(amount: number, currency: string): Promise<string>;
}
// 2. 实现防腐层 (Infrastructure Layer)
class StripeAdapter implements IPaymentGateway {
async charge(amount: number, currency: string) {
// 这里负责把你的语义翻译成 Stripe 的语义
const result = await stripe.paymentIntents.create({ ... });
return result.id;
}
}
// 3. 业务层只依赖接口
class OrderService {
constructor(private paymentGateway: IPaymentGateway) {}
async pay() {
await this.paymentGateway.charge(100, 'CNY'); // 干净!
}
}
2. 基础设施库与工具库 (Libraries & Utilities)
这是很多高级架构师会做,但初级开发者容易漏掉的细节。
正如我们刚才讨论的时间库、日志库。
- 场景:
- 时间:Moment.js 停止维护了,你想换 Day.js 或 Luxon。
- 日志:现在用 Winston,以后想换 Pino 或接入 ELK。
- UUID:现在用
uuid库,以后想用nanoid。
- 防腐手段:
- 类型别名:
type Uuid = string; - 包装函数/类:不要在业务里直接
import dayjs,而是封装一个DateUtils。
- 类型别名:
✅ 防腐层做法:
TypeScript
// libs/shared/src/utils/date.ts
import dayjs from 'dayjs'; // 只有这个文件知道我们用了 dayjs
export class DateProvider {
static now(): Date { return dayjs().toDate(); }
static addDays(date: Date, days: number): Date {
return dayjs(date).add(days, 'day').toDate();
}
}
// 业务代码里:
// DateProvider.addDays(user.expiresAt, 7);
// 如果明天换成 Temporal API,只需要改 DateProvider 一个文件。
3. 持久化层与数据库 (Repository Pattern)
这是 DDD 的标配。
我们在前面的对话中反复提到的 fromPersistence 和 toPersistence 就是典型的防腐层。
- 场景:数据库表结构设计通常是为了“存储效率”或“范式”,而领域对象是为了“业务逻辑”。两者往往不一致。
- 例子:
- DB 里:
user_roles表是一个多对多关联表。 - Domain 里:
User对象里直接有一个roles: Role[]数组。
- DB 里:
- 防腐手段:Repository + Mapper。
Repository 的作用就是:向 Domain 隐瞒 底层是 SQL、NoSQL 还是文件系统的相关事实。
4. 上下文映射 (Bounded Contexts)
这是微服务或大型单体中最重要的防腐层。
当你在 Monorepo 中,Order(订单)模块需要调用 User(用户)模块时。
- 场景:
- 用户上下文:
User对象包含:密码哈希、注册IP、最后登录时间、手机号、邮箱…(这都是用户域关心的)。 - 订单上下文:
Order模块其实只需要知道:用户的 ID、用户的收货地址、用户的名字。它不应该知道用户的密码或登录IP。
- 用户上下文:
- 防腐手段:DTO 转换。
✅ 防腐层做法:
订单服务不应该直接复用用户服务的 User 实体。
订单服务应该定义一个自己的值对象:OrderUser (或者叫 Customer)。
TypeScript
// Order Context 定义的需要的用户长这样
interface Customer {
id: string;
shippingAddress: Address;
}
// 防腐层 (Adapter)
// 负责调用 User 服务,把那个巨大的 User 对象裁剪成 Order 需要的 Customer 对象
function mapUserToCustomer(userDto: UserDTO): Customer {
return {
id: userDto.id,
shippingAddress: userDto.address
};
}
如果不做这层防腐,一旦 User 模块修改了 password 字段的定义,居然会导致 Order 模块报错,这就是典型的上下文泄漏。
5. 前端视图层 (ViewModel / Presentation Model)
这是你之前提到的 fromClientDTO 的延伸。
前端是最容易受后端变动影响的重灾区。
- 场景:
- 后端 API 返回:
{ "user_status": 1, "created_time_stamp": 123456789 } - 前端 UI 需要:显示 “Active” (绿色徽章) 和 “2024-01-01” (格式化时间)。
- 后端 API 返回:
- 防腐手段:View Model 或 Entity。
不要在 React 组件的 JSX 里写 {data.user_status === 1 ? 'Active' : 'Inactive'}。这叫逻辑泄漏。
应该在防腐层(你的 AuthSessionClient 实体)里做掉:session.statusText 或 session.isActive。
6. 遗留系统迁移 (The Strangler Fig Pattern)
这是防腐层这个词最初诞生的场景。
- 场景:你有一个 10 年前的老旧系统(Legacy),代码乱七八糟。你现在要开发新系统,但部分功能还得调那个老系统。
- 防腐手段:
- 绝对不要让老系统的烂模型(比如拼音命名的字段
YongHuMing)进入你的新系统。 - 建立一个 ACL Service,把老系统的烂数据“清洗”成新系统的漂亮数据,再放进来。
- 绝对不要让老系统的烂模型(比如拼音命名的字段
总结:何时该用?
你可以用一个**“不信任原则”**来判断:
- 我不信任外部 API:它可能会改,可能会挂,命名可能很烂 -> 加防腐层 (Adapter)。
- 我不信任数据库结构:它可能会变,可能为了性能写得很怪 -> 加防腐层 (Repository)。
- 我不信任其他微服务:它们给的数据太多太杂 -> 加防腐层 (DTO Mapping)。
- 我不信任第三方库:它们可能会停止维护 -> 加防腐层 (Wrapper)。
但是,切记不要过度设计:
如果你是在写一个极其简单的 CRUD(增删改查)后台管理系统,直接把 DB 数据吐给前端也没问题。防腐层是有代码量成本的,只有当系统复杂度较高、或者你需要长期的稳定性时,它的价值才会显现。