适配器模式【Adapter Pattern】,什么是适配器模式?优缺点?模式分类?应用场景?类适配器?对象适配器?

目录


设计模式专栏目录(点击进入…)



什么是适配器模式?

适配器模式(Adapter Pattern)是一种结构型设计模式(Structural Pattern),通过将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作。其核心思想是通过一个中间的“适配器”类,将一个类的接口转换成客户端所期待的另一种接口形式,从而使得原本因接口不兼容而不能一起工作的类能够协同工作。

主要用于使不兼容的接口能够一起工作。通过引入适配器类,客户端可以使用不兼容的类或接口而无需修改现有代码。

在现实生活中,国际旅行中常用的电源转换器,就是将不同国家的插座标准转换为你的电子设备所支持的充电接口,使设备得以顺利充电。


适配器模式优缺点

优点

(1)提高了类的复用性

通过适配器模式,原本不兼容的类可以被一起使用,避免了重写代码的需求,从而提高了代码的复用性。

(2)灵活性和扩展性

可以通过编写新的适配器,轻松地将现有类与新接口或新系统集成在一起,增加系统的灵活性和扩展性。

(3)遵循开闭原则

适配器模式允许扩展系统而不改变现有的代码结构,符合面向对象设计的开闭原则(对扩展开放,对修改关闭)。

(4)解耦

适配器模式将客户端与被适配的类解耦,使客户端不需要关心被适配类的接口细节,只需要使用适配器提供的接口。

缺点

(1)增加了系统的复杂性

如果过度使用适配器模式,可能会导致系统中出现大量的适配器类,增加了代码的复杂性和维护难度。

(2)性能开销

由于适配器模式涉及对象的间接调用,可能会导致一定的性能开销,特别是在高性能要求的系统中。

(3)可能会掩盖真实的系统设计缺陷

适配器模式虽然解决了接口不兼容的问题,但它可能会掩盖一些设计上的问题。如果系统设计时能够提前规划好接口,这样的适配可能本不需要。

(4)可能影响代码的可读性

引入适配器后,系统的结构变得更加复杂,可能会让代码的可读性下降,特别是对于不熟悉这种模式的开发人员。


适配器模式结构

目标接口(Target):这是客户端所期待的接口,即客户端通过此接口来调用所需的业务逻辑。

适配者(Adaptee):需要适配的类或接口,通常其现有的接口与客户端所需的接口不兼容。

适配器(Adapter):这是适配器模式的核心,它继承适配者或者持有一个适配者的引用,并通过实现目标接口,将适配者的接口转换为客户端所期望的接口。


适配器模式分类

根据适配器如何适配适配者,适配器模式可以分为两种类型:类适配器模式和对象适配器模式。

(1)类适配器模式

类适配器通过实现目标接口的同时继承适配者的方式,来实现在内部使用适配者的方法来实现目标接口的的方法。

(2)对象适配器模式

对象适配器模式则是通过组合的方式来实现接口的适配。适配器类持有一个适配者的引用,并通过该引用来调用源角色的方法,从而实现目标接口的方法。


适配器模式应用场景

适配器模式广泛应用于各种软件开发场景中,尤其是当面临不同系统、不同库或不同版本之间的接口不兼容问题时,适配器模式往往能够提供优雅的解决方案。

以下是一些典型的应用场景:

(1)旧系统迁移

在将旧系统迁移到新系统时,新系统可能不支持旧系统的某些接口或方法。此时,可以通过适配器模式将旧系统的接口适配为新系统所需的接口,从而保持旧系统功能的可用性。

(2)第三方库集成

在集成第三方库时,如果该库的接口与我们的系统接口不兼容,可以通过适配器模式来封装第三方库的接口,使其符合我们的系统架构和接口规范。

(3)接口升级

在软件系统的演进过程中,有时需要对接口进行升级或重构,但升级后的接口可能与旧客户端不兼容。此时,可以通过适配器模式为旧客户端提供一个兼容层,使它们能够继续访问系统而不必立即进行更新。

(4)多平台支持

在开发跨平台应用程序时,不同平台可能提供不同的API接口。通过适配器模式,可以将各个平台的API适配为统一的接口,从而简化开发工作和代码维护。


适配器模式实现

类适配器

在笔记本上的那个拖在外面的黑盒子就是个适配器,一般你在中国能用,在日本也能用,虽然两个国家的的电源电压不同,中国是 220V,日本是 110V,但是这个适配器能够把这些不同的电压转换为你需要的 36V 电压,保证你的笔记本能够正常运行。

那我们在设计模式中引入这个适配器模式是不是也是这个意思呢?
是的,一样的作用,两个不同接口,有不同的实现,但是某一天突然上帝命令你把 B 接口转换为 A 接口,怎么办?继承,能解决,但是比较傻,而且还违背了 OCP 原则,怎么办?好在还有适配器模式。

案例:
一个人力资源管理项目分为三大模块:人员信息管理,薪酬管理,职位管理,其中人员管理这块就用到了适配器模式,是怎么回事呢?当时开发时明确的指明:人员信息简管理的对象是所有员工的所有信息。

类图:
设计还是比较简单的,有一个对象 UserInfo 存储用户的所有信息(实际系统上还有很多子类)
在这里插入图片描述

后续开始使用借聘人员的方式招聘人员,从一个人力资源公司借用了一大批的低技术、低工资的人员,分配到各个子公司,要增加一个功能借用人员管理;人力资源公司有一套自己的人员管理系统,需要把我们使用到的人员信息传输到我们的系统中,系统之间的传输使用 RMI(Remote Method Invocation,远程对象调用)的方式,但是有一个问题人力资源公司的人员对象和我们系统的对象不相同呀;人员资源公司是把人的信息分为了三部分:基本信息,办公信息和个人家庭信息,并且都放到了 HashMap中,比如人员的姓名放到 BaseInfo 信息中,家庭地址放到 HomeInfo 中。

在这里插入图片描述

两个对象都不在一个系统中,如何使用呢?
只要有接口,就可以把远程的对象当成本地的对象使用。通过适配器,把 OuterUser 伪装成我们系统中一个 IUserInfo 对象,这样系统基本不用修改什么程序,所有的人员查询、调用跟本地一样样的。

在这里插入图片描述

1、当前系统用户信息接口和实现

package com.uhhe.common.design.adapter;

/**
 * 获取用户信息接口
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:03
 */
public interface IUserInfo {

    /**
     * 获得用户姓名
     *
     * @return 姓名
     */
    String getUserName();

    /**
     * 获得家庭地址
     *
     * @return 家庭地址
     */
    String getHomeAddress();

    /**
     * 手机号码,这个太重要,手机泛滥呀
     *
     * @return 手机号码
     */
    String getMobileNumber();

    /**
     * 办公电话,一般式座机
     *
     * @return 办公电话
     */
    String getOfficeTelNumber();

    /**
     * 这个人的职位是啥
     *
     * @return 职位
     */
    String getJobPosition();

    /**
     * 获得家庭电话,这个有点缺德,我是不喜欢打家庭电话讨论工作
     *
     * @return 获得家庭电话
     */
    String getHomeTelNumber();

}
package com.uhhe.common.design.adapter;

/**
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:04
 */
public class UserInfo implements IUserInfo {

    @Override
    public String getHomeAddress() {
        System.out.println("这里是员工的家庭地址....");
        return null;
    }

    @Override
    public String getHomeTelNumber() {
        System.out.println("员工的家庭电话是....");
        return null;
    }

    @Override
    public String getJobPosition() {
        System.out.println("这个人的职位是BOSS....");
        return null;
    }

    @Override
    public String getMobileNumber() {
        System.out.println("这个人的手机号码是0000....");
        return null;
    }

    @Override
    public String getOfficeTelNumber() {
        System.out.println("办公室电话是....");
        return null;
    }

    @Override
    public String getUserName() {
        System.out.println("姓名叫做...");
        return null;
    }

}

2、外系统的人员信息接口和实现

package com.uhhe.common.design.adapter;

import java.util.Map;

/**
 * 外系统的人员信息
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:08
 */
public interface IOuterUser {

    /**
     * 基本信息,比如名称,性别,手机号码了等
     *
     * @return Map
     */
    Map<String, Object> getUserBaseInfo();

    /**
     * 工作区域信息
     *
     * @return Map
     */
    Map<String, Object> getUserOfficeInfo();

    /**
     * 用户的家庭信息
     *
     * @return Map
     */
    Map<String, Object> getUserHomeInfo();

}
package com.uhhe.common.design.adapter;

import java.util.HashMap;
import java.util.Map;

/**
 * 外系统的用户信息的实现类
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:09
 */
public class OuterUser implements IOuterUser {

    @Override
    public Map<String, Object> getUserBaseInfo() {
        Map<String, Object> baseInfoMap = new HashMap<>(16);
        baseInfoMap.put("userName", "这个员工叫混世魔王....");
        baseInfoMap.put("mobileNumber", "这个员工电话是....");
        return baseInfoMap;
    }


    @Override
    public Map<String, Object> getUserHomeInfo() {
        Map<String, Object> homeInfo = new HashMap<>(16);
        homeInfo.put("homeTelNumber", "员工的家庭电话是....");
        homeInfo.put("homeAddress", "员工的家庭地址是....");
        return homeInfo;
    }

    @Override
    public Map<String, Object> getUserOfficeInfo() {
        Map<String, Object> officeInfo = new HashMap<>(16);
        officeInfo.put("jobPosition", "这个人的职位是BOSS...");
        officeInfo.put("officeTelNumber", "员工的办公电话是....");
        return officeInfo;
    }

}

3、将外部人员信息适配为内部人员信息(适配器)

那怎么把外系统的用户信息包装成我们公司的人员信息呢?看下面的 OuterUserInfo 类源码,也就是
适配器

package com.uhhe.common.design.adapter;

import java.util.Map;

/**
 * 把OuterUser包装成UserInfo
 * <p>
 * 有很多的强制类型转换,就是(String)这个东西,如果使用泛型的话,完全就可以避免这个转化
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:12
 */
public class OuterUserInfo extends OuterUser implements IUserInfo {

    /**
     * 员工的基本信息
     */
    private final Map<String, Object> baseInfo = super.getUserBaseInfo();

    /**
     * 员工的家庭信息
     */
    private final Map<String, Object> homeInfo = super.getUserHomeInfo();

    /**
     * 工作信息
     */
    private final Map<String, Object> officeInfo = super.getUserOfficeInfo();

    @Override
    public String getHomeAddress() {
        String homeAddress = (String) this.homeInfo.get("homeAddress");
        System.out.println(homeAddress);
        return homeAddress;
    }

    @Override
    public String getHomeTelNumber() {
        String homeTelNumber = (String) this.homeInfo.get("homeTelNumber");
        System.out.println(homeTelNumber);
        return homeTelNumber;
    }

    @Override
    public String getJobPosition() {
        String jobPosition = (String) this.officeInfo.get("jobPosition");
        System.out.println(jobPosition);
        return jobPosition;
    }

    @Override
    public String getMobileNumber() {
        String mobileNumber = (String) this.baseInfo.get("mobileNumber");
        System.out.println(mobileNumber);
        return mobileNumber;
    }

    @Override
    public String getOfficeTelNumber() {
        String officeTelNumber = (String) this.officeInfo.get("officeTelNumber");
        System.out.println(officeTelNumber);
        return officeTelNumber;
    }

    @Override
    public String getUserName() {
        String userName = (String) this.baseInfo.get("userName");
        System.out.println(userName);
        return userName;
    }

}

上面有很多的强制类型转换,就是(String)这个东西,如果使用泛型的话,完全就可以避免这个转化,这个适配器的作用就是做接口的转换。

4、使用适配器

package com.uhhe.common.design.adapter;

/**
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:14
 */
public class Application {

    /**
     * 适配器模式【Adapter Pattern】:
     * <p>
     * 比如老板要查所有的20-30的女性信息
     * <p>
     * 使用了适配器模式只修改了一句话,其他的业务逻辑都不用修改就解决了系统对接的问题,
     * 而且在实际系统中只是增加了一个业务类的继承,就实现了可以查本公司的员工信息,也可以查人力资源公司的员工信息,
     * 尽量少的修改,通过扩展的方式解决了该问题。适配器模式分为类适配器和对象适配器,这个区别不大
     * <p>
     * 上边的例子:类适配器,那对象适配器是什么样子呢?
     * <p>
     * 适配器模式不适合在系统设计阶段采用,没有一个系统分析师会在做详设的时候考虑使用适配器模式,
     * 这个模式使用的主要场景是扩展应用中,就像我们上面的那个例子一样,系统扩展了,不符合原有设计的
     * 时候才考虑通过适配器模式减少代码修改带来的风险。
     */
    public static void main(String[] args) {
        // 没有与外系统连接的时候,是这样写的
        //IUserInfo youngGirl = new UserInfo();

        // 老板一想不对呀,兔子不吃窝边草,还是找人力资源的员工好点
        // 只修改了这一句好
        IUserInfo youngGirl = new OuterUserInfo();
        //从数据库中查到101个
        for (int i = 0; i < 101; i++) {
            youngGirl.getMobileNumber();
        }
    }

}

对象适配器

假设我们有一个音频播放器系统,该系统支持多种音频格式的播放,但现在我们需要接入一个新的音频设备,该设备只支持MP3格式的音频。然而,我们的音频库中存在一些非MP3格式的音频文件(如WAV格式),我们需要将这些非MP3格式的音频文件转换为MP3格式后才能在该设备上播放。

1、定义视频播放器接口

package com.uhhe.common.design.adapter;

/**
 * 视频播放器接口
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:14
 */
public interface AudioPlayer {

    /**
     * 播放
     *
     * @param audioType 视频类型
     * @param fileName  文件名称
     */
    void play(String audioType, String fileName);

}

2、定义一个视频播放适配者

package com.uhhe.common.design.adapter;

/**
 * MP3播放器
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:14
 */
public class Mp3Player {

    /**
     * 播放mp3
     *
     * @param fileName 文件名称
     */
    public void playMp3(String fileName) {
        System.out.println("Playing mp3 file. Name: " + fileName);
    }

}

注意:这里Mp3Player并没有直接提供play方法,而是提供了针对特定格式的播放方法,如playMp3。

3、定义视频播放适配器

现在,我们需要创建一个适配器类AudioAdapter,它实现了AudioPlayer接口,并持有一个Mp3Player的实例,以便将非MP3格式的音频文件转换为MP3格式(在这个例子中,为了简化,我们假设转换逻辑已经内置在AudioAdapter中,实际上可能需要外部转换工具或服务)。

然而,为了保持示例的简洁性和集中讨论适配器模式的核心思想,我们将省略实际的转换逻辑,而是直接调用相应的播放方法,并输出一个假设的转换过程。

package com.uhhe.common.design.adapter;

/**
 * 视频播放适配器
 * 
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:14
 */
public class AudioAdapter implements AudioPlayer {

    private final Mp3Player mp3Player;

    public AudioAdapter(Mp3Player mp3Player) {
        this.mp3Player = mp3Player;
    }

    @Override
    public void play(String audioType, String fileName) {
        if ("wma".equalsIgnoreCase(audioType)) {
            // 假设这里进行了wma到MP3的转换
            System.out.println("Converting wma to mp3 format...");
        } else if ("wav".equalsIgnoreCase(audioType)) {
            // 假设这里进行了wav到MP3的转换
            System.out.println("Converting wav to mp3 format...");
        }
        mp3Player.playMp3(fileName);
    }

}

注意,上述代码中的play方法通过检查audioType参数来确定要播放的音频格式,并假设进行了一些格式转换。这里的关键点是适配器类AudioAdapter如何将一个不直接支持的接口(在这个例子中是play方法,其期望接收音频类型和文件名)适配为另一个接口(Mp3Player中的playMp3方法)。

4、使用适配器

package com.uhhe.common.design.adapter;

/**
 * 使用视频播放器
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 16:14
 */
public class AudioPlayerTest {

    public static void main(String[] args) {
        AudioPlayer audioPlayer = new AudioAdapter(new Mp3Player());
        audioPlayer.play("mp3", "music.mp3");
        audioPlayer.play("wma", "music.wma");
        audioPlayer.play("wav", "music.wav");
    }

}

5、运行结果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未禾

您的支持是我最宝贵的财富!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值