设计模式-适配器模式

文章目录

  • 一、概述
    • [1.1 结构与角色](#1.1 结构与角色)
    • [1.2 适用场景](#1.2 适用场景)
  • 二、实现方式
    • [2.1 类适配器](#2.1 类适配器)
    • [2.2 对象适配器](#2.2 对象适配器)
    • [2.3 接口适配器](#2.3 接口适配器)
    • [2.4 三种适配器对比](#2.4 三种适配器对比)
  • [三、JDK 源码中的适配器](#三、JDK 源码中的适配器)
    • [3.1 InputStreamReader / OutputStreamWriter](#3.1 InputStreamReader / OutputStreamWriter)
    • [3.2 Arrays.asList()](#3.2 Arrays.asList())
    • [3.3 Spring MVC HandlerAdapter](#3.3 Spring MVC HandlerAdapter)
  • 四、总结

一、概述

在软件开发中,经常会遇到这样的场景:系统中已有的接口与客户端需要的接口不一致,但又不方便修改已有代码(比如是第三方库或遗留系统)。这时候就需要一种"转换器",将一个类的接口转换成客户端所期望的另一种接口,从而使原本不兼容的类可以协同工作。

这种"转换器"就是------适配器模式(Adapter Pattern)。

适配器模式是 Java 中最常用的结构型设计模式之一,它作为两个不兼容接口之间的桥梁,让原本由于接口不兼容而不能一起工作的类可以一起工作。

生活中的适配器例子比比皆是:

  • 电源适配器:中国的电压是 220V,但手机充电需要 5V,电源适配器就是将 220V 的交流电转换为 5V 的直流电
  • Type-C 转接头:老式 USB 设备无法直接插入 Type-C 接口,需要一个转接头进行适配
  • SD 卡读卡器:笔记本电脑没有 SD 卡槽,通过 USB 读卡器将 SD 卡接口适配为 USB 接口

核心:将一个类的接口转换成客户端所期望的另一个接口,使得原本不兼容的类可以协同工作

1.1 结构与角色

适配器模式包含以下角色:
实现
持有/继承
调用
Client 客户端
Target 目标接口
Adapter 适配器
Adaptee 被适配者

  • Target(目标接口):客户端所期望的接口,可以是抽象类或接口
  • Adapter(适配器):实现目标接口,并持有或继承被适配者的引用,将客户端的请求转换为对被适配者的调用
  • Adaptee(被适配者):已有的、需要被适配的类,其接口与目标接口不兼容
  • Client(客户端):通过目标接口与对象交互

1.2 适用场景

  • 已有的类,其方法和需求不匹配,但又不方便修改源码
  • 想要创建一个可以复用的类,用于与一些彼此之间没有太大关联的类(包括将来可能引入的类)一起工作
  • 需要使用几个现有的子类,但通过对每个子类进行子类化来适配它们的接口是不现实的

二、实现方式

适配器模式有三种实现方式:类适配器对象适配器接口适配器,下面逐一介绍。

2.1 类适配器

类适配器通过继承被适配者来实现适配。适配器同时继承被适配者类并实现目标接口。

以手机充电为例,家用电压是 220V(被适配者),手机充电需要 5V(目标接口),通过适配器将 220V 转换为 5V:
实现
继承
Phone 客户端
IVoltage5V 目标接口
VoltageAdapter 适配器
Voltage220V 被适配者

(1)被适配者------220V 电压

java 复制代码
/**
 * 被适配者:家用 220V 交流电
 */
public class Voltage220V {

    /**
     * 输出 220V 电压
     */
    public int output220V() {
        int src = 220;
        System.out.println("电压 = " + src + "V");
        return src;
    }
}

(2)目标接口------5V 电压

java 复制代码
/**
 * 目标接口:5V 直流电
 */
public interface IVoltage5V {

    /**
     * 输出 5V 电压
     */
    int output5V();
}

(3)类适配器

java 复制代码
/**
 * 类适配器:继承被适配者,实现目标接口
 */
public class VoltageClassAdapter extends Voltage220V implements IVoltage5V {

    @Override
    public int output5V() {
        // 获取被适配者的输出
        int srcV = output220V();
        // 转换为目标接口的输出
        int dstV = srcV / 44;
        System.out.println("适配器将 " + srcV + "V 转换为 " + dstV + "V");
        return dstV;
    }
}

(4)客户端------手机充电

java 复制代码
/**
 * 客户端:手机
 */
public class Phone {

    /**
     * 充电
     *
     * @param iVoltage5V 5V 电压接口
     */
    public void charging(IVoltage5V iVoltage5V) {
        int voltage = iVoltage5V.output5V();
        if (voltage == 5) {
            System.out.println("电压为 " + voltage + "V,可以充电");
        } else if (voltage > 5) {
            System.out.println("电压为 " + voltage + "V,超过额定电压,无法充电");
        }
    }
}

(5)客户端调用

java 复制代码
public class ClassAdapterDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 使用类适配器充电
        phone.charging(new VoltageClassAdapter());
        // 电压 = 220V
        // 适配器将 220V 转换为 5V
        // 电压为 5V,可以充电
    }
}

类适配器的局限性

  • Java 不支持多继承,所以类适配器只能继承一个被适配者
  • 适配器与被适配者是继承关系,耦合度较高
  • 被适配者的所有方法都会暴露给适配器,即使不需要

2.2 对象适配器

对象适配器通过组合(持有被适配者的引用)来实现适配,而不是继承。这种方式更灵活,也更为常用。
实现
组合持有
Phone 客户端
IVoltage5V 目标接口
VoltageObjectAdapter 适配器
Voltage220V 被适配者

(1)对象适配器

java 复制代码
/**
 * 对象适配器:持有被适配者的引用,实现目标接口
 */
public class VoltageObjectAdapter implements IVoltage5V {

    private final Voltage220V voltage220V;

    public VoltageObjectAdapter(Voltage220V voltage220V) {
        this.voltage220V = voltage220V;
    }

    @Override
    public int output5V() {
        // 通过组合调用被适配者的方法
        int srcV = voltage220V.output220V();
        // 转换为目标接口的输出
        int dstV = srcV / 44;
        System.out.println("适配器将 " + srcV + "V 转换为 " + dstV + "V");
        return dstV;
    }
}

(2)客户端调用

java 复制代码
public class ObjectAdapterDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 使用对象适配器充电
        Voltage220V voltage220V = new Voltage220V();
        phone.charging(new VoltageObjectAdapter(voltage220V));
        // 电压 = 220V
        // 适配器将 220V 转换为 5V
        // 电压为 5V,可以充电
    }
}

对象适配器的优势

  • 使用组合代替继承,耦合度更低
  • 符合合成复用原则:尽量使用组合/聚合,而不是继承
  • 可以适配被适配者的子类,由于组合关系,传入子类对象依然可以工作
  • 更灵活,可以在运行时动态替换被适配者

2.3 接口适配器

接口适配器又称为缺省适配器,当一个接口定义了多个方法,但客户端只需要其中一部分方法时,直接实现该接口就需要实现所有方法(即使用不到),代码显得冗余。

接口适配器的做法是:提供一个抽象类实现该接口,并为每个方法提供默认实现(空实现),客户端继承该抽象类,只需重写需要的方法即可。
定义多个方法
默认空实现
继承
选择性重写
Interface 接口
AbstractClass 抽象适配器
ConcreteClass 具体实现
方法A

(1)接口------多个方法

java 复制代码
/**
 * 接口:定义了多个方法
 */
public interface ISensitiveWordFilter {

    /**
     * 过滤脏话
     */
    String filterBadWords(String text);

    /**
     * 过滤政治敏感词
     */
    String filterPoliticalWords(String text);

    /**
     * 过滤广告
     */
    String filterAds(String text);

    /**
     * 过滤色情内容
     */
    String filterPornographic(String text);
}

(2)抽象适配器------默认空实现

java 复制代码
/**
 * 抽象适配器:为接口的所有方法提供默认空实现
 */
public abstract class SensitiveWordFilterAdapter implements ISensitiveWordFilter {

    @Override
    public String filterBadWords(String text) {
        return text;
    }

    @Override
    public String filterPoliticalWords(String text) {
        return text;
    }

    @Override
    public String filterAds(String text) {
        return text;
    }

    @Override
    public String filterPornographic(String text) {
        return text;
    }
}

(3)具体实现------只重写需要的方法

java 复制代码
/**
 * 脏话过滤器:只重写过滤脏话的方法
 */
public class BadWordsFilter extends SensitiveWordFilterAdapter {

    @Override
    public String filterBadWords(String text) {
        return text.replace("脏话", "***");
    }
}
java 复制代码
/**
 * 广告过滤器:只重写过滤广告的方法
 */
public class AdsFilter extends SensitiveWordFilterAdapter {

    @Override
    public String filterAds(String text) {
        return text.replace("广告", "***");
    }
}

(4)客户端调用

java 复制代码
public class InterfaceAdapterDemo {
    public static void main(String[] args) {
        String text = "这是一句脏话,这里有一条广告";

        // 脏话过滤器------只过滤脏话
        ISensitiveWordFilter badWordsFilter = new BadWordsFilter();
        System.out.println(badWordsFilter.filterBadWords(text));
        // 这是一句***,这里有一条广告

        // 广告过滤器------只过滤广告
        ISensitiveWordFilter adsFilter = new AdsFilter();
        System.out.println(adsFilter.filterAds(text));
        // 这是一句脏话,这里有一条***
    }
}

说明:接口适配器的核心思想就是用一个抽象类为接口的所有方法提供默认实现,客户端继承该抽象类后只需要重写自己关心的方法,避免了直接实现接口时需要实现所有方法的冗余代码。


2.4 三种适配器对比

对比维度 类适配器 对象适配器 接口适配器
实现方式 继承被适配者 组合持有被适配者 抽象类默认实现
核心关系 继承 组合 继承
灵活性 低(单继承限制) 高(可适配子类) 高(选择性重写)
耦合度 高(继承强耦合) 低(组合松耦合)
适用场景 适配单个类 适配单个类或其子类 接口方法多,只需部分实现
符合原则 不符合合成复用原则 符合合成复用原则 符合

选型建议:

  • 大多数场景优先选择对象适配器,组合优于继承
  • 如果需要同时适配类和它的子类,使用对象适配器
  • 如果接口方法很多但只需实现部分方法,使用接口适配器
  • 类适配器一般较少使用,除非特殊需求

三、JDK 源码中的适配器

适配器模式在 JDK 及主流框架中有着广泛的应用,下面来看几个经典的例子。

3.1 InputStreamReader / OutputStreamWriter

java.io.InputStreamReaderjava.io.OutputStreamWriter 是最典型的适配器模式应用。

InputStreamReader 将字节流(InputStream)适配为字符流(Reader),OutputStreamWriter 将字节流(OutputStream)适配为字符流(Writer)。

java 复制代码
// InputStreamReader 的类层次结构
public class InputStreamReader extends Reader {

    private final StreamDecoder sd;

    /**
     * 构造方法:接受一个 InputStream 对象
     * 将 InputStream 适配为 Reader
     */
    public InputStreamReader(InputStream in) {
        super(in);
        sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
    }

    /**
     * 构造方法:接受 InputStream 和字符编码
     */
    public InputStreamReader(InputStream in, String charsetName)
            throws UnsupportedEncodingException {
        super(in);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
    }

    @Override
    public int read() throws IOException {
        return sd.read();
    }

    @Override
    public int read(char[] cbuf, int offset, int length) throws IOException {
        return sd.read(cbuf, offset, length);
    }
}

在这个例子中:

  • Target(目标接口)Reader(字符流)
  • Adaptee(被适配者)InputStream(字节流)
  • Adapter(适配器)InputStreamReader(将字节流适配为字符流)

使用方式:

java 复制代码
// 将字节流适配为字符流
InputStream inputStream = new FileInputStream("test.txt");
Reader reader = new InputStreamReader(inputStream, "UTF-8");

3.2 Arrays.asList()

java.util.Arrays.asList() 方法也是一种适配器的应用,它将数组类型适配为 List 类型。

java 复制代码
public class Arrays {

    /**
     * 将数组适配为 List
     *
     * @param a 数组
     * @return List 视图
     */
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    /**
     * 内部 ArrayList ------ 这是一个适配器
     * 它将数组适配为 List 接口
     */
    private static class ArrayList<E> extends AbstractList<E>
            implements RandomAccess, java.io.Serializable {

        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public E get(int index) {
            return a[index];
        }

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

        @Override
        public int size() {
            return a.length;
        }
    }
}

在这个例子中:

  • Target(目标接口)List<E>
  • Adaptee(被适配者)E[](数组)
  • Adapter(适配器)Arrays.ArrayList(内部类,将数组适配为 List)

注意Arrays.asList() 返回的 List 是固定大小的,不支持 add()remove() 操作,因为底层仍然是数组。

3.3 Spring MVC HandlerAdapter

在 Spring MVC 中,HandlerAdapter 是适配器模式的经典应用。不同类型的 Handler(Controller、Servlet、HttpRequestHandler 等)有着不同的调用方式,Spring MVC 通过 HandlerAdapter 将各种 Handler 适配为统一的调用接口。

java 复制代码
/**
 * HandlerAdapter 接口 ------ 目标接口
 */
public interface HandlerAdapter {

    boolean supports(Object handler);

    ModelAndView handle(HttpServletRequest request,
                       HttpServletResponse response,
                       Object handler) throws Exception;
}

Spring MVC 提供了多种适配器实现:

  • RequestMappingHandlerAdapter:适配 @RequestMapping 标注的方法
  • HttpRequestHandlerAdapter:适配 HttpRequestHandler
  • SimpleControllerHandlerAdapter:适配 Controller 接口
java 复制代码
/**
 * SimpleControllerHandlerAdapter ------ 适配 Controller 接口
 */
public class SimpleControllerHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof Controller);
    }

    @Override
    public ModelAndView handle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        return ((Controller) handler).handleRequest(request, response);
    }
}

这种设计使得 DispatcherServlet 不需要关心具体 Handler 的类型,只需要通过 HandlerAdapter 接口调用即可,非常优雅地实现了不同类型 Handler 的统一处理。


四、总结

适配器模式的核心思想是将一个类的接口转换成客户端所期望的另一个接口,使得原本不兼容的类可以协同工作。

优点:

  • 解决接口不兼容问题:让原本不兼容的类可以一起工作
  • 提高类的复用性:已有的类可以在新的系统中复用,无需修改源码
  • 符合开闭原则:通过新增适配器来扩展功能,无需修改已有代码
  • 符合合成复用原则(对象适配器):使用组合代替继承,降低耦合度
  • 接口适配器:解决了接口方法过多导致的实现冗余问题

缺点:

  • 增加系统复杂度:多了一层适配器,增加了类的数量
  • 类适配器耦合度高:继承导致适配器与被适配者强耦合
  • 过多使用适配器会使系统凌乱:如果不需要适配,就不应该使用适配器,而不是为了用模式而用模式

适用场景:

  • 已有的类,其接口与需求不匹配,但又无法修改源码(如第三方库)
  • 想要创建一个可以复用的类,与一些不兼容的类一起工作
  • 需要统一多个不同接口的调用方式
  • 接口方法很多,但只需实现其中部分方法

适配器 vs 代理 vs 装饰者 :这三种模式结构相似但意图不同------适配器模式的意图是接口转换,让不兼容的类协同工作;代理模式的意图是控制访问,为对象提供替代品;装饰者模式的意图是功能增强,动态地给对象添加职责。


参考博客:

适配器模式 | 菜鸟教程:https://www.runoob.com/design-pattern/adapter-pattern.html

相关推荐
Forget the Dream2 小时前
基于适配器模式的 Axios 封装实践
设计模式·typescript·axios·适配器模式
Java面试题总结2 小时前
【设计模式03】使用模版模式+责任链模式优化实战
设计模式·责任链模式
庞轩px2 小时前
Redis工具类重构——从臃肿到优雅的门面模式实践
数据库·redis·设计模式·重构·门面模式·可扩展性·可维护性
Supersist17 小时前
【设计模式03】使用模版模式+责任链模式优化实战
后端·设计模式·代码规范
geovindu18 小时前
go: Interpreter Pattern
开发语言·设计模式·golang·解释器模式
workflower19 小时前
从拿订单到看方向
大数据·人工智能·设计模式·机器人·动态规划
sensen_kiss1 天前
CPT304 SoftwareEngineeringII 软件工程 2 Pt.3 设计模式(上)
设计模式·软件工程
mit6.8241 天前
20种Agent 设计模式
人工智能·设计模式
workflower1 天前
企业酝酿数智化内驱力
大数据·人工智能·设计模式·机器人·动态规划