前言
做过第三方对接的兄弟都知道,接一个是享受,接三个是工作,接十个那就是灾难。
最近在负责公司核心的多渠道第三方登录与用户信息同步模块。我们面临的现状是:业务方要求接入七八个外部渠道(包括某头部电商、某保险平台、某社交巨头等)。
起初我觉得没啥,不就是调个 API 拿个 JSON 吗?结果拿到接口文档我就裂开了:
-
A 渠道:姓名分了"汉字"和"拼音/片假名"两个字段返回;性别用 1/2 表示。
-
B 渠道:姓名是一个合并的长字符串;性别直接给的是 male/female。
-
C 渠道:日期格式甚至是 YYYY/MM/DD,跟我们库里的 Date 完全不兼容。
如果直接在业务核心代码里写 if ("ChannelA".equals(type)) 然后去解析数据,那这块代码不出一个月就会变成无人敢动的"屎山"。
所以这里使用了适配器模式(Adapter Pattern)来构建一层防腐层(ACL, Anti-Corruption Layer)。
什么是防腐层?
我们就是DDD架构,DDD架构中的防腐层。就是防止外部的其他逻辑,污染内部的核心逻辑。你要学很多语言,不能学一个语言就写到家规里,你得有一个翻译官,翻译官就是防腐层。
其实说人话就是:别让外面的脏数据,污染了我们内部的代码。
外部渠道的数据结构是他们定的,千奇百怪;但我们系统内部的 User 模型是标准的。我需要一个中间人,把那些乱七八糟的数据,"清洗"成我们看着舒服的标准模样,然后再放进业务逻辑里。
这个"中间人",就是适配器。 适配器 + 转换数据的逻辑就是防腐层。
具体落地
第一步:定义通用的"转换器"标准
首先,利用 Java 的泛型,定义一个所有适配器都必须遵守的接口。 S 代表 Source(外部的脏数据),T 代表 Target(内部的标准数据)。
java
/**
* 外部用户信息适配器接口
* @param <S> Source: 外部渠道返回的原始对象
* @param <T> Target: 内部标准用户模型
*/
public interface ExternalUserInfoAdapter<S, T> {
/**
* 核心转换方法:把脏数据变成标准数据
*/
T adapt(S source);
}
第二步:编写具体的渠道适配器
以那个"姓名分段、性别用英文"的某电商渠道为例。我写了一个专门的 Adapter 类,把脏活累活全封装在这里。
注意这里加了 @Component,交给 Spring 管理。
java
@Component
public class ChannelAUserInfoAdapter implements ExternalUserInfoAdapter<ChannelAUserResponse, StandardUserDTO> {
@Override
public StandardUserDTO adapt(ChannelAUserResponse source) {
if (source == null) return null;
StandardUserDTO target = new StandardUserDTO();
// 1. 处理姓名:该渠道把名字拆成了两部分,我们需要分别映射
// 比如 source.getKanjiName() -> 映射到内部的 givenName
// source.getKanaName() -> 映射到内部的 localGivenName
target.setGivenName(source.getKanjiName());
target.setLocalGivenName(source.getKanaName());
// 2. 处理性别:把字符串转成内部枚举
// 外部是 "male"/"female",内部是 GenderEnum
if ("male".equalsIgnoreCase(source.getGenderStr())) {
target.setGender(GenderEnum.MALE);
} else if ("female".equalsIgnoreCase(source.getGenderStr())) {
target.setGender(GenderEnum.FEMALE);
} else {
target.setGender(GenderEnum.UNKNOWN);
}
// 3. 处理日期:字符串转 Date 对象
// 这里省略具体的 DateUtil 解析代码
target.setBirthday(DateUtil.parse(source.getBirthStr()));
return target;
}
}
第三步:在业务层清爽地调用
有了适配器,Service 层的代码简直干净得令人感动。我完全不需要关心对方是给的 male 还是 1,我只管拿标准对象。
java
@Service
public class ChannelAUserHandler {
@Autowired
private ChannelAUserInfoAdapter adapter; // 注入上面的适配器
public void syncUser(String token) {
// 1. 调用外部 RPC/API 拿到原始数据(脏数据)
ChannelAUserResponse rawData = thirdPartyClient.getUserInfo(token);
// 2. 一行代码完成"清洗"
// 业务层不需要任何 if-else 来判断性别格式,适配器都搞定了
StandardUserDTO standardUser = adapter.adapt(rawData);
// 3. 执行核心业务(入库、更新Session等)
userRepository.saveOrUpdate(standardUser);
}
}
进阶思考:适配器模式 vs 策略模式
在做代码 Review 的时候,有同事问我:"这玩意儿跟策略模式(Strategy Pattern)看着很像啊,都是接口+实现类,为啥叫适配器?"
这也是面试中常考的点,我是这么区分的:
- 意图不同(事后 vs 事前)
适配器(Adapter)是"事后补救":两边的接口(外部API vs 内部模型)已经定死了,谁都改不了,且死活对不上。这时候只能插一个适配器在中间做胶水。
策略(Strategy)是"事前规划":我要做一件事(比如登录),但有多种做法(A渠道登录、B渠道登录)。我定义一个策略接口,可以在运行时灵活切换算法。
- 职责不同(数据 vs 行为)
适配器关注"数据流转":看我的 adapt 方法,输入是 A,输出是 B。它只负责类型转换(Type Conversion),不应该包含复杂的业务逻辑(比如查库、发短信),它是无副作用的。
策略关注"业务行为":策略模式的 execute 方法通常包含具体的业务逻辑。
在我的项目中,ChannelAUserInfoAdapter 负责把数据变身(适配器),而 ChannelAUserHandler 负责具体的登录流程控制(这其实更像策略模式的使用场景)。一个负责变身,一个负责打仗。
拓展 - 怎么区别不同的渠道
在API设计层面,把对应的渠道标识放到请求路径里,通过@PathVariable 获取字符串。
每个渠道的策略器中实现接口方法,getChannel。
然后把每个策略放到Factory中,Factory中实际就维护一个Map<String,Adapter> 然后根据getChannel获取String,自动找到对应的适配器。
核心逻辑,Service层只有三行。
- 调对面接口;
- 适配器转换,转换成我们系统的格式;
- 入库等业务操作;
总结
通过引入这层防腐层,我们后续再接入新渠道时,核心业务代码一行都不用改,只需要新增一个 XxxAdapter 类即可。
经过测算,这一波重构将新渠道的接入效率提升了约 40%。更重要的是,它把脏数据隔离在了核心业务之外,这让代码维护起来心里踏实多了。