设计模式--适配器模式

发布于:2025-07-15 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

想象这个真实场景:

  1. 你有一个新买的、超酷的蓝牙音箱(目标接口:蓝牙连接)。 你只想用手机蓝牙轻松连上它放音乐。

  2. 你还有一个珍藏多年的老式磁带播放机(需要适配的对象:Adaptee)。 它音质很棒,但它只有3.5mm耳机孔(旧接口),没有蓝牙功能。

  3. 问题来了: 你的手机(只有蓝牙)想播放音乐,但无法直接连接老式磁带机(只有3.5mm孔)。你想用磁带机放手机里的歌,怎么办?

解决方案(没有适配器):

  • 方案A(改磁带机): 你是个电子高手,拆开磁带机,焊上一个蓝牙模块。缺点: 风险大(可能弄坏)、成本高、复杂、破坏了原始设备。

  • 方案B(换手机): 买个带3.5mm耳机孔的老手机。缺点: 为了用旧设备换新手机?荒谬!成本更高,牺牲了新手机的功能。

  • 方案C(不用磁带机): 放弃你心爱的磁带机。缺点: 浪费资源,你失去了一个好音源。

解决方案(使用适配器):

  • 你买了一个“蓝牙音频接收器”(这就是适配器!)。 这个小设备:

    • 有一头是 蓝牙(实现目标接口),可以被你的新手机搜索并连接。

    • 另一头是 3.5mm耳机孔(连接Adaptee),可以插到你老磁带机的“AUX IN”或“LINE IN”接口上。

核心思想

适配器模式的核心目的是 解决接口不兼容的问题。它允许两个原本因为接口不同而无法协同工作的类能够一起工作。

        为了便于理解我们把上边的例子换成插头:你有一个欧洲标准的插头(三脚圆形【EuropeanPlug】),但你的插座是美标的(两脚扁平【AmericanSocket】)。为了让插头能插进插座正常工作,你需要一个转换插头(适配器【Adapter】)。适配器模式解决的问题与此类似:它充当两个不兼容接口之间的桥梁,将一个类的接口转换成客户期望的另一个接口。它让原本由于接口不兼容而无法一起工作的类可以协同工作

在软件设计中

  • 目标接口 (Target Interface): 你希望客户端使用的接口。它定义了客户端期望的方法。(对应 AmericanSocket)

  • 需要适配的类 (Adaptee): 已经存在的类,它拥有客户端需要的功能,但它的接口与目标接口不兼容。(对应 EuropeanPlug)

  • 适配器 (Adapter): 一个类,它实现了目标接口,并持有一个需要适配的类(Adaptee)的实例。适配器将目标接口的调用转换(适配) 成对 Adaptee 已有方法的调用。(对应电源转换器本身)

两种主要实现方式

适配器模式在 Java 中主要有两种实现方式:类适配器(通过继承)和对象适配器(通过组合)。对象适配器更常用且更灵活,是推荐的实践。

1. 对象适配器 (推荐 - 使用组合)

// 1. 目标接口 (Target Interface) - 客户端期望的接口
public interface AmericanSocket {
    void plugIntoAmericanOutlet();
}

// 2. 添加原生美式插头实现
public class AmericanPlug implements AmericanSocket {
    @Override
    public void plugIntoAmericanOutlet() {
        System.out.println("美式插头直接插入美式插座...");
    }
}

// 3. 需要适配的类 (Adaptee) - 已有的功能,但接口不兼容
public class EuropeanPlug {
    public void plugIntoEuropeanOutlet() {
        System.out.println("欧洲插头插入了欧洲插座...");
    }
}

// 4. 适配器 (Adapter) - 实现目标接口,持有Adaptee实例,进行转换
public class EuropeanToAmericanAdapter implements AmericanSocket {

    private EuropeanPlug europeanPlug; // 组合 - 持有Adaptee实例

    public EuropeanToAmericanAdapter() {
        this.europeanPlug = new EuropeanPlug();
    }

    @Override
    public void plugIntoAmericanOutlet() {
        System.out.println("适配器将美式插座接口转换为欧式接口...");
        europeanPlug.plugIntoEuropeanOutlet(); // 调用Adaptee的方法实现功能
    }
}

// 5. 客户端代码 (Client) - 只依赖目标接口 客户端现在可以自由选择
public class Client {
    public static void main(String[] args) {
        // 使用原生美式插头
        AmericanSocket american = new AmericanPlug();
        american.plugIntoAmericanOutlet();
        
        // 使用欧式插头(通过适配器)
        AmericanSocket europeanAdapter = new EuropeanToAmericanAdapter();
        europeanAdapter.plugIntoAmericanOutlet();
        
        // 甚至可以混合使用
        List<AmericanSocket> devices = Arrays.asList(
            new AmericanPlug(),              // 原生美式
            new EuropeanToAmericanAdapter()  // 适配的欧式
        );
        
        devices.forEach(AmericanSocket::plugIntoAmericanOutlet);
    }
}

输出:

        美式插头直接插入美式插座... 
        适配器将美式插座接口转换为欧式接口... 
        欧洲插头插入了欧洲插座... 
        美式插头直接插入美式插座... 
        适配器将美式插座接口转换为欧式接口... 
        欧洲插头插入了欧洲插座...

关键点:

  • 适配器 EuropeanToAmericanAdapter 实现了 AmericanSocket 接口。

  • 适配器持有一个 EuropeanPlug 实例(通过组合)。

  • 当客户端调用 plugIntoAmericanOutlet()(目标接口方法)时,适配器内部调用 europeanPlug.plugIntoEuropeanOutlet()(Adaptee的方法)来完成实际工作,并可能进行必要的转换逻辑(这里是打印一条消息)。

2. 类适配器 (使用继承 - 在Java中通常指通过继承Adaptee并实现Target接口)

// 1. 目标接口 (Target Interface) - 客户端期望的接口
public interface AmericanSocket {
    void plugIntoAmericanOutlet();
}

// 2. 添加原生美式插头实现
public class AmericanPlug implements AmericanSocket {
    @Override
    public void plugIntoAmericanOutlet() {
        System.out.println("美式插头直接插入美式插座...");
    }
}

// 3. 需要适配的类 (Adaptee) - 已有的功能,但接口不兼容
public class EuropeanPlug {
    public void plugIntoEuropeanOutlet() {
        System.out.println("欧洲插头插入了欧洲插座...");
    }
}

// 4. 适配器 (Adapter) - 实现目标接口,持有Adaptee实例,进行转换
public class EuropeanToAmericanAdapter extends EuropeanPlug implements AmericanSocket {


    @Override
    public void plugIntoAmericanOutlet() {
        System.out.println("适配器将美式插座接口转换为欧式接口...");
        super.plugIntoEuropeanOutlet(); // 直接调用继承自Adaptee的方法
    }
}

// 5. 客户端代码 (Client) - 只依赖目标接口 客户端现在可以自由选择
public class Client {
    public static void main(String[] args) {
        // 使用原生美式插头
        AmericanSocket american = new AmericanPlug();
        american.plugIntoAmericanOutlet();
        
        // 使用欧式插头(通过适配器)
        AmericanSocket europeanAdapter = new EuropeanToAmericanAdapter();
        europeanAdapter.plugIntoAmericanOutlet();
        
        // 甚至可以混合使用
        List<AmericanSocket> devices = Arrays.asList(
            new AmericanPlug(),              // 原生美式
            new EuropeanToAmericanAdapter()  // 适配的欧式
        );
        
        devices.forEach(AmericanSocket::plugIntoAmericanOutlet);
    }
}

关键点:

  • 适配器 EuropeanToAmericanAdapter 继承了 EuropeanPlug (Adaptee) 并 实现了 AmericanSocket (Target) 接口。

  • 适配器直接重用了 EuropeanPlug 的方法(通过继承)。

  • 当客户端调用 plugIntoAmericanOutlet() 时,适配器调用其继承来的 plugIntoEuropeanOutlet() 方法。

为什么对象适配器更常用?

  1. 灵活性: 对象适配器使用组合,一个适配器可以适配任何 EuropeanPlug 的子类(只要接口一致)。类适配器在编译时就固定了它只能适配 EuropeanPlug 或它的特定子类。

  2. 解耦: 对象适配器将适配器与 Adaptee 的实现解耦。适配器只依赖于 Adaptee 的接口。类适配器直接继承了 Adaptee 的实现,耦合度更高。

  3. 遵循“组合优于继承”原则: 组合通常比继承提供更大的灵活性和更少的副作用。

  4. Adaptee 是类或接口均可: 对象适配器也能适配实现了某个接口的 Adaptee。类适配器要求 Adaptee 必须是具体类(Java 不支持多继承,所以 Adaptee 必须是类)。

适配器模式的应用场景

  1. 集成第三方库或遗留代码: 当你需要使用一个功能强大的类库,但其接口与你项目的现有接口不匹配时,创建一个适配器来封装库的调用。

  2. 系统升级/重构: 新版本组件接口改变了,但旧客户端代码仍需调用新组件。可以编写适配器让旧接口调用新组件。

  3. 统一多个类的接口: 系统中存在多个功能类似但接口不同的类,客户端希望用统一的接口调用它们。可以为每个不同的类创建适配器,让它们都实现同一个目标接口。

  4. 创建可复用的类: 设计一个类,期望它能与未来可能出现的、接口未知的类协同工作。可以先定义好目标接口,未来通过适配器来适配新类。

适配器模式的优缺点

  • 优点:

    • 提高类的复用性: 让原本不兼容的类可以一起工作,复用已有的功能。

    • 提高灵活性: 通过更换不同的适配器,可以灵活地使用不同的 Adaptee。

    • 目标与实现解耦: 客户端代码只依赖目标接口,与 Adaptee 的具体实现解耦。

    • 符合开闭原则: 引入新的 Adaptee 类型时,只需添加新的适配器类,无需修改现有客户端代码和目标接口。

  • 缺点:

    • 增加复杂性: 引入了额外的适配器类,增加了系统的类和对象的数量。

    • 过度使用可能导致混乱: 如果系统中适配器过多,可能会使代码变得难以理解和维护。

    • 可能降低效率 (微乎其微): 多了一层间接调用,理论上会有轻微性能开销(但在绝大多数场景下可忽略不计)。

总结

        适配器模式是 Java 中解决接口不兼容问题的强大工具,它像一座桥梁连接了两个不匹配的世界。对象适配器(使用组合) 是更通用、更推荐的方式。在需要集成旧系统、使用第三方库或统一不同接口时,适配器模式能显著提高代码的复用性、灵活性和可维护性。理解其核心思想(转换接口)和两种实现方式的区别(组合 vs 继承)是应用好该模式的关键。


网站公告

今日签到

点亮在社区的每一天
去签到