2020 - 1024 = ?设计模式系列 — 适配器模式

996

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。

前言

23种设计模式快速记忆的请看上面第一篇,本篇和大家一起来学习适配器模式,适配器模式包含类的适配器模式对象的适配器模式

模式定义

将一个类的接口转换成客户端希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

适配器模式的形式分为:类的适配器模式 & 对象的适配器模式。

类的适配器模式

类的适配器模式是把适配的类的API转换成为目标类的API。

在上图中可以看出:

  • 冲突:Target期待调用operation方法,而Adaptee并没有(这就是所谓的不兼容了)。
  • 解决方案:为使Target能够使用Adaptee类里的SpecificOperation方法,故提供一个中间环节Adapter类(继承Adaptee & 实现Target接口),把Adaptee的API与Target的API衔接起来(适配)。

Adapter与Adaptee是继承关系,这决定了这个适配器模式是类的

使用步骤(代码解析)

步骤1: 创建Target接口

interface Target {
    //这是源类Adapteee没有的方法
    void operation();
}

步骤2: 创建源类(Adaptee)

class Adaptee {
    public void SpecificOperation() {

    }
}

步骤3: 创建适配器类(Adapter)

//适配器Adapter继承自Adaptee,同时又实现了目标(Target)接口。
class Adapter extends Adaptee implements Target {

    //目标接口要求调用operation()这个方法名,但源类Adaptee没有方法operation()
    //因此适配器补充上这个方法名
    //但实际上operation()只是调用源类Adaptee的SpecificOpertaion()方法的内容
    //所以适配器只是将SpecificOpertaion()方法作了一层封装,封装成Target可以调用的operation()而已
    @Override
    public void operation() {
        this.SpecificOperation();
    }

}

步骤4:定义具体使用目标类,并通过Adapter类调用所需要的方法从而实现目标

public class AdapterPattern {

    public static void main(String[] args) {
        Target mAdapter = new Adapter();
        mAdapter.operation();
    }
}

对象的适配器模式

与类的适配器模式相同,对象的适配器模式也是把适配的类的API转换成为目标类的API。

与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类。

在上图中可以看出:

  • 冲突:Target期待调用operation方法,而Adaptee并没有(这就是所谓的不兼容了)。
  • 解决方案:为使Target能够使用Adaptee类里的SpecificOperation方法,故提供一个中间环节Adapter类(包装了一个Adaptee的实例),把Adaptee的API与Target的API衔接起来(适配)。

Adapter与Adaptee是委派关系,这决定了适配器模式是对象的。

使用步骤(代码解析)

步骤1: 创建Target接口

interface Target {
    //这是源类Adapteee没有的方法
    void operation();
}

步骤2: 创建源类(Adaptee)

class Adaptee {
    
    public void SpecificOpertaion(){
    }
}

步骤3: 创建适配器类(Adapter)(不适用继承而是委派)

class Adapter implements Target{
    // 直接关联被适配类  
    private Adaptee adaptee;

    // 可以通过构造函数传入具体需要适配的被适配类对象  
    public Adapter (Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void operation() {
        // 这里是使用委托的方式完成特殊功能  
        this.adaptee.SpecificOpertaion();
    }
}  

步骤4:定义具体使用目标类,并通过Adapter类调用所需要的方法从而实现目标

public class AdapterPattern {
    public static void main(String[] args) {
        // 步骤4:定义具体使用目标类,并通过Adapter类调用所需要的方法从而实现目标
        //需要先创建一个被适配类的对象作为参数  
        Target mAdapter = new Adapter(new Adaptee());
        mAdapter.operation();
    }
}

两种适配器比较

  • 对象适配器: 使用组合的方式, 不仅能适配一个被适配者的类, 还可以适配它的任何一个子类;
  • 类适配器: 只能适配一个特定的类, 但是它不需要重新实现整个被适配者的功能. 而且它还可以重写被适配者的行为;
  • 对象适配器: 使用的是组合而不是继承, 通过多写几行代码把事情委托给了被适配者. 这样很灵活;
  • 类适配器: 需要一个适配器和一个被适配者, 只需要一个类就行;
  • 对象适配器: 对适配器添加的任何行为对被适配者和它的子类都起作用;
    ...

解决的问题

从模式的定义中,我们看到适配器模式就是用来转换接口,解决不兼容问题的。想想我们现实生活中的适配器,最常用的就是手机充电器了,也叫做电源适配器,它把家用交流强电转换为手机用的直流弱电。其中交流电就是被适配者,充电器是适配器,手机是用电客户

原本由于接口不兼容而不能一起工作的那些类可以在一起工作。

模式组成

组成(角色) 作用
客户(Client) 只能调用目标接口功能,不能直接使用被适配器,但可以通过适配器的接口转换间接使用被适配器。
目标接口(Target) 客户看到的接口,适配器必须实现该接口才能被客户使用。
适配器(Adapter) 适配器把被适配者接口转换为目标接口,提供给客户使用。
被适配者(Adaptee) 被适配者接口与目标接口不兼容,需要适配器转换成目标接口子类,才能被客户使用。

实例说明

在这里使用类适配器模式进行举例,对象适配器模式只是在适配类实现时将“继承”改成“在内部委派Adaptee类”而已。

实例概况

  • 背景:隔壁老王买了一个进口的电视机
  • 冲突:进口电视机要求电压(110V)与国内插头标准输出电压(220V)不兼容
  • 解决方案:设置一个适配器将插头输出的220V转变成110V

即适配器模式中的类的适配器模式

使用步骤

步骤1: 创建Target接口(期待得到的插头):能输出110V(将220V转换成110V)

 interface Target {

    //将220V转换输出110V(原有插头(Adaptee)没有的)
    void convert_110v();
}

步骤2: 创建源类(原有的插头)

class PowerPort220V{
    //原有插头只能输出220V
    public void output_220v(){
    }
}

步骤3:创建适配器类(Adapter)

class Adapter220V extends PowerPort220V implements Target{
    //期待的插头要求调用convert_110v(),但原有插头没有
    //因此适配器补充上这个方法名
    //但实际上convert_110v()只是调用原有插头的output_220v()方法的内容
    //所以适配器只是将output_220v()作了一层封装,封装成Target可以调用的convert_110v()而已

    @Override
    public void convert_110v(){
        this.output_220v();
    }
}

步骤4:定义具体使用目标类,并通过Adapter类调用所需要的方法从而实现目标(不需要通过原有插头)

//进口电视类
class ImportedMachine {

    @Override
    public void Work() {
        System.out.println("进口电视正常运行");
    }
}


//通过Adapter类从而调用所需要的方法
public class AdapterPattern {

    public static void main(String[] args) {
        Target mAdapter220V = new Adapter220V();
        ImportedMachine mImportedMachine = new ImportedMachine();

        //用户拿着进口电视插上适配器(调用Convert_110v()方法)
        //再将适配器插上原有插头(Convert_110v()方法内部调用Output_220v()方法输出220V)
        //适配器只是个外壳,对外提供110V,但本质还是220V进行供电
        mAdapter220V.convert_110v();
        mImportedMachine.Work();
    }
}

输出结果

进口电视正常运行

优点

  • 转换接口,适配器让不兼容的接口变成兼容
  • 让客户和实现的接口解耦。有了适配器,客户端每次调用不兼容的接口时,不用修改自己的代码,只要调用适合的适配器就可以了。
  • 使用了对象组合设计原则。以组合的方式包装被适配者,被适配者的任何子类都可以搭配着同一个适配器使用。
  • 体现了“开闭”原则。适配器模式把客户和接口绑定起来,而不是和具体实现绑定,我们可以使用多个配适器来转换多个后台类,也可以很容易地增加新的适配器。

缺点

  • 每个被适配者都需要一个适配器,当适配器过多时会增加系统复杂度,降低运行时的性能。
  • 实现一个适配器可能需要下一番功夫,增加开发的难度。

应用场景

  • 当要使用的两个类所做的事情相同或者相似,但是具有不同的接口时考虑使用配适器模式。
  • 当需要统一客户端调用接口的代码,而所调用的接口具有不兼容问题时使用适配器模式。这样客户端只有调用一个接口就行了,这样可以更简单、更直接、更紧凑。

建议尽量使用对象的适配器模式,多用合成/聚合、少用继承。

当然,具体问题具体分析,根据需要来选用合适的实现方式。

源码中的应用

#JDK
java.util.Arrays#asList()
java.util.Collections#list()
java.util.Collections#enumeration()
java.io.InputStreamReader(InputStream) (returns a Reader)
java.io.OutputStreamWriter(OutputStream) (returns a Writer)
java.util.collections#enumeration(),从Iterator到Enumeration的适配。

#Spring
org.springframework.context.event.GenericApplicationListenerAdapter

Arrays.asList()

使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

GenericApplicationListenerAdapter

spring架构体系中的事件模型,面向事件编程可以使你的应用扩展性更好,设计更优美,更有设计感,也是解耦最常用的方式,首先看下类图。



ApplicationListener 事件监听器接口,基于观察者模式实现。

GenericApplicationListener 处理基于通用的事件监听器接口,提供了一种基于事件类型的监测,如下:

boolean supportsEventType(ResolvableType eventType);

是SmartApplicationListener的改良版本。

SmartApplicationListener 基于事件的监听器接口,如下:

boolean supportsEventType(Class<? extends ApplicationEvent> eventType);

ApplicationListenerMethodAdapter GenericApplicationListener适配器实现,如下:

public class ApplicationListenerMethodAdapter implements GenericApplicationListener

可以看到是通过实现接口这种方式的适配器模式实现。

为什么实现接口这种方式比继承类这种实现扩展性更好,java是单继承,用实现接口这种方式可以间接的实现的多继承,扩展性更好。

SourceFilteringListener 基于GenericApplicationListener,SmartApplicationListener的装饰器模式实现,从指定的事件源筛选事件,调用它的委托侦听器来匹配应用程序事件对象。

GenericApplicationListenerAdapter GenericApplicationListener适配器模式实现。

PS:以上代码提交在 Githubhttps://github.com/Niuh-Study/niuh-designpatterns.git

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读,本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343