适配器模式:让不兼容的接口“握手言和”
适配器模式,说白了就是“转接头”。你想想看,Type-C 转 HDMI、两脚插头转三脚插座——现实世界里到处都是适配器。软件里也一样:你有一个老系统,接口是 A;新模块只认接口 B。硬改?成本太高。写个适配器?嗯,这才是正经路子。
我个人习惯把适配器模式分成三类:类适配器、对象适配器、双向适配器。别被名字吓到,核心就一句话——让原本不兼容的接口能一起工作。
1. 类适配器(继承)
类适配器用继承来实现。适配器继承目标接口,同时继承被适配者的实现。好处是代码少,坏处是 Java 单继承,不够灵活。
我记得有一次接手一个遗留的日志系统,它只输出到文件。新需求要求同时输出到网络。我不想动老代码,就写了个类适配器:
// 目标接口:新系统需要的日志接口
public interface Logger {
void log(String message);
}
// 被适配者:老系统的文件日志
public class FileLogger {
public void writeToFile(String msg) {
System.out.println("写入文件: " + msg);
}
}
// 类适配器:继承老类,实现新接口
public class LoggerAdapter extends FileLogger implements Logger {
@Override
public void log(String message) {
// 直接调用父类方法
writeToFile(message);
}
}
用起来很简单:
Logger logger = new LoggerAdapter();
logger.log("用户登录成功");
2. 对象适配器(组合)
对象适配器用组合来实现。适配器持有被适配者的引用,通过委托调用。这是我最推荐的方式——组合优于继承,这话我说了十年都不腻。
同样是上面的日志例子,用对象适配器改写:
// 对象适配器:持有被适配者实例
public class LoggerAdapter2 implements Logger {
private FileLogger fileLogger;
public LoggerAdapter2(FileLogger fileLogger) {
this.fileLogger = fileLogger;
}
@Override
public void log(String message) {
fileLogger.writeToFile(message);
}
}
用起来:
FileLogger oldLogger = new FileLogger();
Logger logger = new LoggerAdapter2(oldLogger);
logger.log("用户退出系统");
3. 双向适配器
双向适配器有点意思。它同时实现两个接口,既能当 A 用,也能当 B 用。说白了就是“两头通吃”。
场景:你有一个老系统的支付接口,一个新系统的支付接口。两边都想调用对方。写一个双向适配器:
// 老支付接口
public interface OldPayment {
void pay(double amount);
}
// 新支付接口
public interface NewPayment {
void processPayment(double amount);
}
// 双向适配器
public class PaymentAdapter implements OldPayment, NewPayment {
private OldPayment oldPayment;
private NewPayment newPayment;
// 适配老系统到新系统
public PaymentAdapter(OldPayment oldPayment) {
this.oldPayment = oldPayment;
}
// 适配新系统到老系统
public PaymentAdapter(NewPayment newPayment) {
this.newPayment = newPayment;
}
@Override
public void pay(double amount) {
// 老接口调用 -> 转成新接口
if (newPayment != null) {
newPayment.processPayment(amount);
} else {
oldPayment.pay(amount);
}
}
@Override
public void processPayment(double amount) {
// 新接口调用 -> 转成老接口
if (oldPayment != null) {
oldPayment.pay(amount);
} else {
newPayment.processPayment(amount);
}
}
}
4. 遗留系统接口适配案例
这是适配器模式最经典的战场。我参与过一个银行核心系统改造项目,老系统暴露了一堆 EJB 接口,新系统用 RESTful API。不改老系统,新系统又等不及——适配器模式救场。
老接口:
// 老系统:EJB 风格的账户查询
public interface LegacyAccountService {
AccountDTO getAccountByCustomerId(String customerId);
List<TransactionDTO> getTransactions(String accountId, Date from, Date to);
}
新接口:
// 新系统:REST 风格的账户查询
public interface AccountService {
AccountVO queryAccount(String customerId);
List<TransactionVO> queryTransactions(String accountId, String fromDate, String toDate);
}
适配器:
public class AccountServiceAdapter implements AccountService {
private LegacyAccountService legacyService;
public AccountServiceAdapter(LegacyAccountService legacyService) {
this.legacyService = legacyService;
}
@Override
public AccountVO queryAccount(String customerId) {
AccountDTO dto = legacyService.getAccountByCustomerId(customerId);
// 转换 DTO 到 VO
return convertToVO(dto);
}
@Override
public List<TransactionVO> queryTransactions(String accountId, String fromDate, String toDate) {
Date from = parseDate(fromDate);
Date to = parseDate(toDate);
List<TransactionDTO> dtos = legacyService.getTransactions(accountId, from, to);
return dtos.stream().map(this::convertToVO).collect(Collectors.toList());
}
private AccountVO convertToVO(AccountDTO dto) {
// 字段映射逻辑
return new AccountVO(dto.getId(), dto.getName(), dto.getBalance());
}
private TransactionVO convertToVO(TransactionDTO dto) {
return new TransactionVO(dto.getId(), dto.getAmount(), dto.getDate().toString());
}
private Date parseDate(String dateStr) {
// 简单日期解析
return new Date();
}
}
5. 适配器模式 vs 其他模式
| 模式 | 目的 | 结构 | 典型场景 |
|---|---|---|---|
| 适配器 | 接口转换 | 适配器包装被适配者 | 遗留系统集成 |
| 代理 | 控制访问 | 代理持有真实对象 | 延迟加载、权限控制 |
| 装饰器 | 增强功能 | 递归包装 | 动态添加职责 |
| 外观 | 简化接口 | 门面封装子系统 | 复杂系统简化调用 |
你可能会问:适配器和代理看起来好像?区别在于意图。适配器是为了让接口匹配,代理是为了控制访问。适配器改接口,代理不改接口。
6. 适配器模式的核心结构
下面这张图帮你理清适配器模式的核心逻辑。我画了对象适配器的结构,因为这是最常用的形式:
7. 总结与避坑
适配器模式不是什么高深技术,但用好了能省很多事。我总结几条实战经验:
- 优先用对象适配器:组合比继承灵活,尤其 Java 单继承限制下。
- 适配器里别写业务逻辑:只做接口转换和数据映射。业务逻辑一进去,适配器就变成了“四不像”。
- 注意性能损耗:每次调用都经过适配器,会有轻微性能开销。高并发场景下要评估。
- 不要过度适配:如果接口差异太大,适配器会变得臃肿。这时候考虑重构被适配者或目标接口。
适配器模式,说白了就是“让旧代码活过来”的魔法。你不需要重写老系统,也不需要等新系统重构。写个适配器,两边都能跑。嗯,这就是我为什么一直推荐它的原因。
公众号:蓝海资料掘金营,微信deep3321