zoukankan      html  css  js  c++  java
  • 【设计模式(六)】适配器模式

    个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

    如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


    前言

    将一个类的接口转成客户期望的另外一个接口。适配器模式使得原本由于接口不匹配而不能一起工作的那些类可以一起工作。——Gang of Four

    适配器英文为adapter,如果你是一位Android开发工程师,一定不会对这个单词陌生,在Android开发中,我们经常需要设计适配器,对数据加以处理使其以合适的形式在view中展示出来。简而言之就是数据原先无法以合适的形式展示,我们需要适配器来处理。

    在生活中,我们最常见的适配器其实就是电源适配器(当然还有其他的),也就是usb充电器的插头、电器的电源等。因为usb线无法直接使用插座上的插口,我们需要一个插头(适配器)来处理一下,就可以了。

    原本不匹配而无法使用的接口,我们通过适配器处理后,使其能够正常匹配

    最常见的,jdbc就是我一种适配器,负责将数据库的数据源进行处理,最终以api的形式提供给Java客户端使用,无论我们是使用mybatis还是mybatis-plus最终都无法绕过jdbc,因为这就是我们Java客户端与数据源的适配器

    实际上我们的应用开发中会使用到各种各样的控件,以及各种各样的资源,我们不可能拒绝使用与我们规范不同的资源(苹果&Android:?有被冒犯),适配器就是此种情况下的一种解决方案。

    实际上就算是在统一规范下,也可能随着规范的升级迭代而产生差异,此时也可以通过适配器对其进行修正。


    请注意区分适配和扩展,适配仅做转换,已保证目标类可以被使用,而不生成新的功能,新的功能是扩展,建议直接继承进行扩展


    1.介绍

    使用目的:将一个类的接口转换成被期望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

    使用时机:需要的类接口不符合需求无法使用,或者需要使用的多个类没有统一格式的接口无法一起使用

    解决问题:原有的接口不符合需求

    分类:根据适配的目标不同分为两种

    • 适配器模式
    • 对象适配器模式

    其实还有一种缺省适配器模式,本质是属于适配器模式的变型,本文后面会介绍

    实现方法

    • 类适配器模式:采用多重继承方式实现,定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件
    • 对象适配器模式:釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。

    缺省适配模式为一个接口提供缺省实现,这样子类型可以从这个缺省实现进行扩展,而不必从原有接口进行扩展

    应用实例:

    • JDBC:JDBC连接数据源(通常为数据库),并提供API允许客户端访问数据库,从而使客户端能够对数据库进行增删改查等操作
    • Android中的ListView、RecycleView等组件,都需要配合适配器使用,适配器负责将数据转换为能够展示的view
    • 对接硬件设备(如摄像机),厂商会提供SDK,但大多数时候里面的API接口无法直接使用,比如自己编写适配器对接口进行调用

    优点

    1. 可以让两个没有关联的类一起运行:这也是初衷
    2. 提高了类的复用性:我们可以在适配器中对功能进行改装,使原本的功能更加完善甚至是创造新的功能
    3. 增加了类的透明度:有了适配器客户端就可以间接调用相关接口了
    4. 灵活度高:我们可以在适配器中适当自己调整功能

    缺点

    1. 由于Java中最多继承一个类,所以一个适配器至多只能适配一个目标
    2. 实现复杂,过度使用会导致系统混乱,且维护成本高,某些时候可能还不如重构系统

    注意事项:适配器是为了解决一个新的类或对象接口与原系统不匹配的情况,而非设计系统架构时的组件。

    换言之,在引入新的类或对象的接口不兼容时使用适配器模式,而在设计系统时的兼容性问题直接调整架构解决即可


    2.结构

    适配器模式包括四个主要角色:

    • 目标(Target)接口:即被期望的接口,可以是抽象类,或者接口
    • 适配者(Adaptee)接口:被访问和适配的组件的接口
    • 适配器(Adapter)类:转换器,负责将适配者接口转换为目标接口
    • 客户端(Client):与符合Target接口的对象协同

    3.类适配器

    类适配器需要继承适配者

    理论上一般是多重继承,即同时继承两个或多个适配者类,从而将其接口进行匹配

    Java只允许单重继承,所以只能继承单个适配者类,并将其转换为所需要的的接口,一般同时使用接口类(Interface)进行规范

    简而言之,Java只能将一个类的接口转换为目标,而c/c++可以将两个类的接口聚合在一起


    3.1.结构

    image-20201013142410633

    • 客户端使用目标接口
    • 目标接口实际上使用的是适配器中的接口
    • 适配器需要继承适配者类,才能使用其内部的功能
    • 同时适配器需要实现接口中的方法,作为目标接口使用

    3.2.实现

    这里举两个例子,分别是两种情况,实质上都是对原功能的扩展

    请注意这里有原功能才能扩展,没有原功能的话属于新增,跟适配器没有任何关系,这种需求请重新写一个类

    3.2.1.示例1

    模拟业务如下:

    • 旧版本的类使用hello()方法打招呼
    • 新版本打算使用greet()方法打招呼,其业务相同

    此时需要将hello()转换为greet()方法


    Adaptee适配者类(Adaptee)

    package com.company.test.adapter;
    
    public class Adaptee {
        public void hello() {
            System.out.println("HelloWorld!");
        }
    }
    

    适配者类,包括一个原方法,打印HelloWorld

    请注意此类属于原组件,不属于适配器的一部分,而是适配器的目标


    CusInterface自定义接口(Target)

    package com.company.test.adapter;
    
    public interface CusInterface {
        public void greet();
    }
    

    定义接口,用于规范目标接口


    ClassAdapter适配器类(Adapter)

    package com.company.test.adapter;
    
    public class ClassAdapter extends Adaptee implements CusInterface {
        @Override
        public void greet() {
            super.hello();
        }
    }
    

    此处继承适配者类,并实现接口,在此处实现接口的转换


    Client客户端类(Client)

    package com.company.test.adapter;
    
    public class ClassAdapterTest {
        public static void main(String[] args) {
            CusInterface adapter=new ClassAdapter();
            adapter.greet();
        }
    }
    

    运行结果

    image-20201015093926380


    3.2.2.示例2

    模拟业务如下:

    • 原组件提供加法接口int plus(int x,int y),即将两个数字相加
    • 现在要求目标接口实现加法String plus(String x,String y),即数字以字符串形式传递

    此时需要对旧的接口进行转换,使其达到目标接口效果

    仅给出示例代码,不再解释

    Adaptee

    package com.company.test.adapter.test2;
    
    public class Adaptee {
        public int plus(int x, int y) {
            return x + y;
        }
    }
    

    CusInterface(Target)

    package com.company.test.adapter.test2;
    
    public interface CusInterface {
        String plus(String x, String y);
    }
    

    ObjectAdapter(Adapter)

    package com.company.test.adapter.test2;
    
    public class ObjectAdapter extends Adaptee implements CusInterface {
        @Override
        public String plus(String x, String y) {
            return String.valueOf(super.plus(Integer.valueOf(x), Integer.valueOf(y)));
        }
    }
    

    ObjectAdapterTest(Client)

    package com.company.test.adapter.test2;
    
    public class ClassAdapterTest {
        public static void main(String[] args) {
            CusInterface adapter = new ObjectAdapter();
            String ans = adapter.plus("111", "233");
            System.out.println(ans);
        }
    }
    

    运行结果

    image-20201015113412791

    3.3.小结

    适配器通过继承适配者,对旧接口进行转换,通常是改变参数类型以使其匹配,通过super即可直接调用旧接口


    4.对象适配器

    对象适配器直接持有适配者类的实例对象

    对象持有数量没有限制,所以就没有适配者数量的限制,也就可以将多个适配者同时进行适配

    因而对象适配器实际使用中远远多于类适配器

    4.1.结构

    image-20201015120557506

    • Adapter直接持有Adaptee对象,从而可以使用原组件的功能
    • Adapter里的方法直接调用Adaptee对象的方法,进而对其实现转换
    • 一个Adapter也可以持有多个Adaptee,进而适配多个适配者

    4.2.实现

    4.2.1.实例1

    这里只修改适配器,其余代码一致,便于比(tou)较(lan)

    ClassAdapter

    public class ClassAdapter implements CusInterface {
        private Adaptee adaptee=new Adaptee();
        @Override
        public void greet() {
            adaptee.hello();
        }
    }
    

    4.2.2.实例2

    同样只修改适配器

    ObjectAdapter

    public class ObjectAdapter implements CusInterface {
        private Adaptee adaptee = new Adaptee();
    
        @Override
        public String plus(String x, String y) {
            return String.valueOf(adaptee.plus(Integer.valueOf(x), Integer.valueOf(y)));
        }
    }
    

    这里持有适配者对象的方法有三种

    • 创建局部变量,直接初始化一个适配者对象

      即上面使用的这种

    • 创建局部变量,在构造函数中自行初始化

    • 创建局部变量,通过构造函数从客户端中接收适配者对象

    4.2.3.实例3

    我们再测试一下同时适配多个类的情况

    • 现有两个播放组件,分别能够播放视频和音频
    • 需要一个播放接口,能够播放视频和音频

    Adaptee

    package com.company.test.adapter.test3;
    
    public class VideoPlayer {
        public void play(String fileName)  {
            //todo play the audit
            System.out.println("play video :" + fileName);
        }
    }
    
    package com.company.test.adapter.test3;
    
    public class AuditPlayer {
        public void play(String fileName){
            //todo play the audit
            System.out.println("play audit :"+fileName);
        }
    }
    

    Target

    package com.company.test.adapter.test3;
    
    public interface CusInterface {
        void play(String fileName);
    }
    

    Adapter

    package com.company.test.adapter.test3;
    
    public class ObjectAdapter implements CusInterface {
        private AuditPlayer auditPlayer = new AuditPlayer();
        private VideoPlayer videoPlayer = new VideoPlayer();
    
        @Override
        public void play(String fileName) {
            if (fileName.endsWith(".mp3")) {
                auditPlayer.play(fileName);
            } else if (fileName.endsWith(".mp4") || fileName.endsWith(".vcl")) {
                videoPlayer.play(fileName);
            } else {
                System.out.println("error:cannot recognize file " + fileName);
            }
        }
    }
    

    Client

    package com.company.test.adapter.test3;
    
    public class ObjectAdapterTest {
        public static void main(String[] args) {
            CusInterface adapter = new ObjectAdapter();
            adapter.play("hello.mp3");
            adapter.play("hello.mp4");
            adapter.play("hello.mp5");
        }
    }
    

    运行结果

    image-20201016135653693

    4.3.小结

    适配器直接持有适配者类,在适配器中定义所需要的的方法,根据需求使用合适的调整,可以直接使用适配者类中的方法


    5.缺省适配器模式

    缺省适配(Default Adapter)模式为一个接口提供缺省实现,这样子类型可以从这个缺省实现进行扩展,而不必从原有接口进行扩展。

    即,如果可能需要很多功能,但当前只实现了一部分,那么可以将未实现的功能缺省实现

    因为不这么做连编译都过不去。。。

    作为适配器模式的一个特例(或者说是变型),缺省是适配模式在JAVA语言中有着特殊的应用

    严格意义上,缺省适配器不属于适配器模式

    5.1.结构

    image-20201016161209388

    • 需要有一个接口类interface,来规范全部的功能
    • 适配器adapter对接口的所有方法进行空实现,或者其余默认实现
    • 目标类target集成adapter,重写那些需要实现的功能

    因此target未实现的功能,就由adapter提供空实现或者默认实现啦(实际应用不建议空实现,别强迫后人非得一行一行看你代码。。有些缺德。。)


    5.2.实现

    代码很简单,我就不分开贴了

    package com.company.test.adapter.test4;
    
    interface InterfaceTarget {
        void sayHi();
    
        void sayHello();
    
        void sayBye();
    }
    
    class Adapter implements InterfaceTarget {
    
        @Override
        public void sayHi() {
        }
    
        @Override
        public void sayHello() {
        }
    
        @Override
        public void sayBye() {
            System.out.println("you need to override this function first!");
        }
    }
    
    class Target extends Adapter {
        @Override
        public void sayHi() {
            System.out.println("Hi!");
        }
    }
    
    public class DefaultAdapterTest {
        public static void main(String[] args) {
            InterfaceTarget target = new Target();
            target.sayHi();
            target.sayHello();
            target.sayBye();
        }
    }
    
    1. 定义接口类InterfaceTarget,规范和提供所有接口

    2. 定义适配器Adapter,实现接口类,并将所有接口进行空实现或者默认实现

      示例中sayBye()方法进行了默认实现,其余两个进行了空实现

    3. 定义目标类Target,继承适配器,并重写我们实际实现的功能

    4. 客户端就可以直接使用啦~所有方法都不会报错

    运行结果

    image-20201016162743484

    5.3.小结

    实际上缺省适配器模式就是提供了接口的默认实现而已,这样就保证所有方法都被实现了,目标类就不会报错了

    简而言之,在我们不需要实现所有方法时,使用缺省适配器,给出平庸实现


    6.总结

    6.1.不同适配器的优缺点

    类适配器

    • 优点:由于适配器是适配者的子类,因此可以在适配器中重写部分方法,使得适配器灵活性更强

      但请注意,这种操作并不属于适配器的范畴,请不要将实际应用与理论基础混淆

    • 缺点

      • 对于Java等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;

      • 适配者类不能为最终类,即Java中的final类;

      • 类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性

        若使用类,那么构造目标类的时候不会初始化子类

    对象适配器

    • 优点

      • 可以同时适配多个适配者类

      • 适配器直接持有适配者类,那么根据“里氏代换原则”,适配器对适配者的子类同样适用

        适配器如果从客户端接收适配者对象,那么同样可以接受适配者子类对象

    • 缺点:如果需要置换掉适配者中的方法,过程较为复杂

      类适配器直接在适配器中重写适配者的方法即可

      对象适配器需要先建立适配者的子类,重写方法,再将适配器中的对象更换为其子类


    6.2.适配器的意义

    虽然目的都是为了让目标类能够与当前环境或者其他类进行匹配,但其用意不同

    • 类/对象适配器是为了改变源接口,使其能够与系统相容
    • 缺省适配器是为了让未完全实现的接口,与系统相容(其实就是为了不报错。。。)

    后记

    设计模式是总结的经验,并非需要严格遵守的方案

    实际使用中更多的是使用变型和衍生方案

    例如适配器模式实际使用中很少完全符合方案,也因此导致很多前辈总结的时候会将变型误解为设计模式本身(查阅的资料和博客很多地方都出现基于实践的误解和偏差)

    虽然实战中未必严格遵守方案,但除去根据需求的变更,应尽量符合设计模式的方案



    作者:Echo_Ye

    WX:Echo_YeZ

    EMAIL :echo_yezi@qq.com

    个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

  • 相关阅读:
    K8s 使用 nfs-client-provisioner
    MySQL IF CASE 例子
    Nginx 限速
    Python 元组操作
    Python if, while,for,continue,break,三目运算符
    Centos7 安装 pyenv
    MySQL 查看大事务
    Tomcat 修改日志路径及日志分割
    游戏攻略 美少女万华镜5
    自建远程桌面过程 vnc + frp
  • 原文地址:https://www.cnblogs.com/silent-bug/p/13827740.html
Copyright © 2011-2022 走看看