防腐层哪里使用

看看在整个软件工程中,还有哪些地方是 **“必须”** 或 **“强烈建议”** 设立防腐层的。

#type / howto #status / evergreen #tech / dev

防腐层哪里使用

[!info] related notes

防腐层的核心作用只有两个:

  1. 隔离变化:外部怎么变,我不关心,我的核心逻辑不用改。
  2. 语义翻译:把“别人的语言”翻译成“我的语言”。

以下是 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 的标配。

我们在前面的对话中反复提到的 fromPersistencetoPersistence 就是典型的防腐层。

  • 场景:数据库表结构设计通常是为了“存储效率”或“范式”,而领域对象是为了“业务逻辑”。两者往往不一致。
  • 例子
    • DB 里:user_roles 表是一个多对多关联表。
    • Domain 里:User 对象里直接有一个 roles: Role[] 数组。
  • 防腐手段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” (格式化时间)。
  • 防腐手段View ModelEntity

不要在 React 组件的 JSX 里写 {data.user_status === 1 ? 'Active' : 'Inactive'}。这叫逻辑泄漏。

应该在防腐层(你的 AuthSessionClient 实体)里做掉:session.statusTextsession.isActive


6. 遗留系统迁移 (The Strangler Fig Pattern)

这是防腐层这个词最初诞生的场景。

  • 场景:你有一个 10 年前的老旧系统(Legacy),代码乱七八糟。你现在要开发新系统,但部分功能还得调那个老系统。
  • 防腐手段
    • 绝对不要让老系统的烂模型(比如拼音命名的字段 YongHuMing)进入你的新系统。
    • 建立一个 ACL Service,把老系统的烂数据“清洗”成新系统的漂亮数据,再放进来。

总结:何时该用?

你可以用一个**“不信任原则”**来判断:

  1. 我不信任外部 API:它可能会改,可能会挂,命名可能很烂 -> 加防腐层 (Adapter)
  2. 我不信任数据库结构:它可能会变,可能为了性能写得很怪 -> 加防腐层 (Repository)
  3. 我不信任其他微服务:它们给的数据太多太杂 -> 加防腐层 (DTO Mapping)
  4. 我不信任第三方库:它们可能会停止维护 -> 加防腐层 (Wrapper)

但是,切记不要过度设计:

如果你是在写一个极其简单的 CRUD(增删改查)后台管理系统,直接把 DB 数据吐给前端也没问题。防腐层是有代码量成本的,只有当系统复杂度较高、或者你需要长期的稳定性时,它的价值才会显现。

创建于 2026/1/27 更新于 2026/5/27