变量的适配器模式是通过封装转换逻辑,使业务代码面向统一新接口编程,而无需感知旧数据源(DTO/Map/JSON/XML等)的字段结构、命名、类型或嵌套差异;其核心是定义目标接口、识别旧数据形式、编写适配器类集中处理映射与标准化,并统一接入所有数据入口。

变量的适配器模式不是指“给变量加适配器”,而是指:当旧系统中数据载体(如 DTO、Map、JSON 字符串、XML 节点等)的字段结构、命名、类型或嵌套方式与新版接口契约不一致时,通过封装一层转换逻辑,让业务代码只面向统一的新接口编程,而无需感知底层数据来源的差异。
明确目标接口与旧数据结构
先定义新版接口所需的数据形态——比如一个标准的 UserInput 接口或 POJO:
- 字段名必须是
fullName、email、createdAt(ISO 格式字符串) - 不允许空值,
email必须含 @ 符号 -
createdAt若旧数据是毫秒时间戳,需转为 ISO 字符串
再确认旧数据常见形式,例如:
- 老 Map:
{"user_name": "张三", "mail": "zhang@old.com", "create_time": 1620000000000} - 老 JSON 字符串(字段下划线、时间数值型、邮箱字段名不统一)
- 老 XML 片段(含命名空间、CDATA、属性混合)
用对象适配器封装转换逻辑
写一个适配器类,接收原始数据,输出符合新版接口要求的对象。关键点是:转换发生在适配器内部,而非业务方法里。
- 构造器接收原始数据源(如
Map<String, Object>或String xml),不做校验就抛异常 - 提供
toUserInput()方法,集中做字段映射、类型转换、空值兜底、格式标准化 - 避免在业务层写
if (old.get("user_name") != null) dto.setFullName((String) old.get("user_name"))这类散落逻辑
示例片段:
public class LegacyUserAdapter {private final Map<String, Object> legacyData;
public LegacyUserAdapter(Map<String, Object> legacyData) {
this.legacyData = Objects.requireNonNull(legacyData);
}
public UserInput toUserInput() {
return new UserInput(
safeString("user_name"),
normalizeEmail("mail"),
formatTimestamp("create_time")
);
}
}
统一接入点,避免分支蔓延
所有旧数据入口(HTTP 参数、MQ 消息体、DB 查询结果)都走同一组适配器,而不是在每个服务方法里判断数据来源再手动转换。
- Controller 层接收原始请求后,立即交给对应适配器生成
UserInput - 新增一种旧数据源(如 YAML 配置文件导出的用户列表),只需新增一个
YamlUserAdapter,实现相同转换接口 - 业务 Service 层只依赖
UserInput,完全不知道“user_name”或“mail”这些旧字段名
这样既遵守开闭原则,也使单元测试可聚焦于“输入 → 输出”的纯转换验证,不耦合网络或数据库。
处理真实场景中的毛刺细节
实际迁移中,光字段对齐远远不够。适配器必须主动应对以下问题:
- 时间格式混乱:旧数据可能是秒级/毫秒级时间戳、"yyyy-MM-dd" 字符串、甚至 "2021-05-03T10:20:30+08:00" 带时区——适配器内统一解析为 Instant 再格式化
-
字段别名与缺失:老系统用
mobile,新接口叫phone;若老数据无该字段,适配器应设默认值或抛结构异常,而非让业务层 NPE -
嵌套结构扁平化:老数据是
{"profile": {"name": "张三"}},新版要fullName——适配器负责深取 + 合并,不靠业务代码get("profile").get("name") - 编码与空格:老字段值前后带不可见空格、全角字符、HTML 实体 —— 在适配器里 trim、normalize、unescape

















