适配器模式实现:类适配器、对象适配器、接口适配器、双向适配器

适配器模式,说白了就是让两个不兼容的接口能一起工作。我刚开始接触设计模式时,觉得这玩意儿不就是加个中间层嘛,有什么好讲的?直到我在一个老系统改造项目里被狠狠教育了一回——新旧接口不匹配,改哪边都牵一发动全身。嗯,从那以后,我再也不敢小看适配器了。

适配器模式有四种常见实现方式:类适配器、对象适配器、接口适配器、双向适配器。每种都有各自的适用场景和坑。咱们一个一个来看。

类适配器(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()。用双向适配器,两边都能通过同一个适配器互相调用,代码结构非常清晰。

注意:双向适配器容易让代码变得复杂。我建议只在两个系统需要频繁双向交互时才使用。如果只是单向适配,用对象适配器就够了。别为了炫技而过度设计。

四种适配器的选择策略

说了这么多,到底怎么选?我总结了一个简单的决策流程:

  1. 需要重写源类的方法?→ 类适配器(但注意封装性问题)
  2. 需要灵活切换源实例?→ 对象适配器(首选方案)
  3. 接口方法太多,只用其中几个?→ 接口适配器
  4. 需要双向适配?→ 双向适配器(慎用)

个人经验:如果你不确定用哪种,先选对象适配器。它是最通用的方案,后续重构也方便。我见过太多人一上来就用类适配器,结果后面需求变了,改起来特别痛苦。对象适配器虽然代码多几行,但值得。

适配器模式四种实现对比 类适配器 继承源类 实现目标接口 编译时绑定 对象适配器 持有源类实例 实现目标接口 运行时绑定 接口适配器 抽象类实现接口 提供默认空方法 子类按需重写 双向适配器 实现双方接口 双向互相适配 系统迁移场景 ⭐ 最推荐使用 选择建议 • 不确定时,优先选对象适配器——灵活、可测试、易维护 • 需要重写源类方法时,考虑类适配器(注意封装性) • 接口方法太多时,用接口适配器简化子类实现 • 系统双向迁移时,才考虑双向适配器(慎用) • 适配器不是银弹——如果接口差异太大,考虑重构而非适配 注:类适配器在Java中受限于单继承,实际使用较少;C++开发者可灵活使用。

适配器模式看起来简单,但用好了能解决大问题。我见过太多项目因为接口不兼容,硬生生在业务代码里写满了if-else判断。其实一个适配器就能搞定的事,何必搞得那么复杂?

嗯,适配器模式就讲到这里。记住一点:适配器是为了让不兼容的接口协作,而不是为了让你绕开设计问题。如果两个接口本身设计就有问题,先重构接口,别拿适配器当遮羞布。