适配器模式实现:类适配器、对象适配器、接口适配器、双向适配器
适配器模式,说白了就是让两个不兼容的接口能一起工作。我刚开始接触设计模式时,觉得这玩意儿不就是加个中间层嘛,有什么好讲的?直到我在一个老系统改造项目里被狠狠教育了一回——新旧接口不匹配,改哪边都牵一发动全身。嗯,从那以后,我再也不敢小看适配器了。
适配器模式有四种常见实现方式:类适配器、对象适配器、接口适配器、双向适配器。每种都有各自的适用场景和坑。咱们一个一个来看。
类适配器(Class Adapter)
类适配器通过多重继承来实现。适配器同时继承目标接口和源类,把源类的方法“适配”成目标接口的样子。
核心特点:适配器直接继承源类,可以重写源类的方法。但Java不支持多重继承,所以类适配器在Java里只能用接口+继承单类的方式实现。
// 目标接口
public interface ITarget {
void request();
}
// 源类(需要被适配的类)
public class Adaptee {
public void specificRequest() {
System.out.println("源类的方法被调用");
}
}
// 类适配器
public class ClassAdapter extends Adaptee implements ITarget {
@Override
public void request() {
// 直接调用继承来的方法
this.specificRequest();
}
}
我在项目中遇到过这种情况:一个遗留的报表系统,它的数据查询接口是queryData(),但新框架要求用fetch()。我直接用类适配器,继承旧查询类,实现新接口,一行代码搞定。但要注意——类适配器把源类的方法直接暴露给子类,如果源类的方法签名变了,适配器也得跟着改。
避坑指南:我曾经在一个多线程环境下用类适配器,结果源类的成员变量被子类直接访问,导致并发问题。类适配器破坏了封装性,源类的内部状态对适配器完全可见。如果你对封装有严格要求,慎用类适配器。
对象适配器(Object Adapter)
对象适配器通过组合来实现。适配器持有源类的一个实例,把调用委托给这个实例。这是最常用、也最灵活的方式。
// 对象适配器
public class ObjectAdapter implements ITarget {
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
// 委托给源类实例
adaptee.specificRequest();
}
}
// 使用方式
Adaptee adaptee = new Adaptee();
ITarget target = new ObjectAdapter(adaptee);
target.request();
你想想看,对象适配器比类适配器好在哪?灵活。同一个适配器可以适配不同的源类实例,甚至可以在运行时切换。而且它不破坏封装性,源类的内部细节对适配器不可见。
| 对比维度 | 类适配器 | 对象适配器 |
|---|---|---|
| 实现方式 | 继承(extends) | 组合(持有引用) |
| 灵活性 | 低,编译时绑定 | 高,运行时绑定 |
| 封装性 | 破坏封装,子类可访问父类内部 | 保持封装,只通过公开接口交互 |
| 适用语言 | 支持多重继承的语言(C++) | 所有面向对象语言 |
| 代码量 | 较少,直接继承 | 稍多,需要维护引用 |
我的建议:除非你明确需要重写源类的方法(类适配器可以做到),否则优先选对象适配器。我在近十年的项目里,90%的适配场景都用对象适配器。它更干净,更容易测试,也更容易维护。
接口适配器(Interface Adapter)
接口适配器也叫缺省适配器。当一个接口定义了太多方法,而你只需要用到其中几个时,接口适配器就派上用场了。它提供一个默认实现(通常是空方法体),你只需要重写你需要的方法。
// 一个有很多方法的接口
public interface IWorker {
void work();
void eat();
void sleep();
void rest();
void meeting();
}
// 接口适配器——提供默认空实现
public abstract class WorkerAdapter implements IWorker {
@Override
public void work() {}
@Override
public void eat() {}
@Override
public void sleep() {}
@Override
public void rest() {}
@Override
public void meeting() {}
}
// 具体实现——只关心工作
public class Programmer extends WorkerAdapter {
@Override
public void work() {
System.out.println("写代码");
}
@Override
public void eat() {
System.out.println("吃泡面");
}
}
为什么会需要接口适配器?我举个例子。有一次我接手一个支付系统,对接了七八个第三方支付渠道。每个渠道的接口都定义了几十个方法,但实际用到的就三四个。如果每个渠道都去实现全部方法,代码里全是空方法体,看着就烦。接口适配器帮我省了不少事。
关键点:接口适配器本质上是抽象类,它把接口的方法全部实现为空的或默认的,让子类只关注自己需要的部分。这在Java的AWT/Swing事件监听中非常常见——MouseAdapter、KeyAdapter都是典型的接口适配器。
双向适配器(Two-Way Adapter)
双向适配器比较特殊。它同时实现了目标接口和源接口,让两边都能通过同一个适配器互相调用。说白了,就是适配器既能把A适配成B,也能把B适配成A。
// 源接口
public interface IAdaptee {
void specificRequest();
}
// 目标接口
public interface ITarget {
void request();
}
// 双向适配器
public class TwoWayAdapter implements ITarget, IAdaptee {
private IAdaptee adaptee;
private ITarget target;
// 适配成目标
public TwoWayAdapter(IAdaptee adaptee) {
this.adaptee = adaptee;
}
// 适配成源
public TwoWayAdapter(ITarget target) {
this.target = target;
}
@Override
public void request() {
// 把源适配成目标
adaptee.specificRequest();
}
@Override
public void specificRequest() {
// 把目标适配成源
target.request();
}
}
双向适配器在实际项目中用得不多,但一旦用上就特别巧妙。我记得在一个系统迁移项目里,新旧两个系统需要双向同步数据。旧系统的接口是push(),新系统是send()。用双向适配器,两边都能通过同一个适配器互相调用,代码结构非常清晰。
注意:双向适配器容易让代码变得复杂。我建议只在两个系统需要频繁双向交互时才使用。如果只是单向适配,用对象适配器就够了。别为了炫技而过度设计。
四种适配器的选择策略
说了这么多,到底怎么选?我总结了一个简单的决策流程:
- 需要重写源类的方法?→ 类适配器(但注意封装性问题)
- 需要灵活切换源实例?→ 对象适配器(首选方案)
- 接口方法太多,只用其中几个?→ 接口适配器
- 需要双向适配?→ 双向适配器(慎用)
个人经验:如果你不确定用哪种,先选对象适配器。它是最通用的方案,后续重构也方便。我见过太多人一上来就用类适配器,结果后面需求变了,改起来特别痛苦。对象适配器虽然代码多几行,但值得。
适配器模式看起来简单,但用好了能解决大问题。我见过太多项目因为接口不兼容,硬生生在业务代码里写满了if-else判断。其实一个适配器就能搞定的事,何必搞得那么复杂?
嗯,适配器模式就讲到这里。记住一点:适配器是为了让不兼容的接口协作,而不是为了让你绕开设计问题。如果两个接口本身设计就有问题,先重构接口,别拿适配器当遮羞布。