读书笔记-设计模式-可复用版-Adapter 适配器模式

发表于2019-05-27
评论0 3k浏览

拖了有些日子了,设计模式第一次读的时候,尤其是碰到一些不常用到的,难免会出现一些 生涩难懂的情况,我个人又很不喜欢GOF里,对设计模式概念定义的那一套(估计是翻译的问题),很抽象,虽然这是每个人学习都应该去读去理解的地方。


 

我更喜欢的是当提到某一个设计模式,可以用自己的语言描述出来它的特点,含义,并且能够立马想到一个经典的应用场景,比如Builder建造者模式,我会马上想到,对话框的经典实现,Prototype原型模式,涉及到深浅拷贝,可以应用场景是Unity中的Prefab,通过拷贝已有的原型,创建新的类型等等。


 

用一些自己的语言去描述设计模式,而不是完全照搬官方的定义,并多积累,多尝试,多总结不同的应用场景,这样才会有助于设计模式的理解。


 

概念只是抽象的定义,实际情况要灵活的运用。


 

比如很多时候,一个功能的实现,需要由多个其它的类来共同完成,指的并不是1:1


 

Adapter-适配器模式


 

即然要进行“适配“,一定是出现了不兼容的情况,需要处理兼容性的问题,但要注意的是,兼容不代表就是转换,也可以进行包装,比如2个独立的功能,我需要一个能够同时包含这两个独立功能的情况,通过包装,实现一个包含了这2个功能的类,也是适配。


 

适配的两个关键语:转换和包装


 

适配器又叫包装器Wrapper.


 

生活中可以举出很多这样的例子,比如:


 

1.语言不通,说着不同的语言,甚至是方言,需要翻译


 

2.电压不同,出国旅行,必须要带的一样物品-电压转换器


 

3.电脑一类的读卡器,接口转换器,如mac pro 最新款只有Type-C接口,需要适配器转换为USB,才可以连接USB的设备。


 

4.货币,不同国家的货币,汇率不同,通常无法直接进行交易,我们需要将货币换成对方可以接受的类型,这里Currency Exchange(货币兑换中心)就是Adapter,由他来兑换各个国家的货币。


 

5.跨平台调用,比如在Unity中,我们需要调用Android或是iOS的支付或是其它一些原生方法,无法直接进行调用,都需要使用由语言本身提供的特定方式,比如DllImport,AndroidJavaClass,这是从语言底层的角度,实现了适配器Adapter的思想(我可以调用Android,iOS原生的方法,但并不需要他们自身做出修改)


 

6.商户付款的二维码,不管我们是使用微信还是支付宝,都可以完成支付,但微信和支付宝是两个完全独立的接口,适配器在当中起到了作用!


 

7.Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面,Flutter并不会涉及到Android,iOS系统层面的修改,现有提供的接口就可以完成跨平台的运行的实现,这是最近Android的同学在做的事儿。


 

8.有了Flutter 自然就可以想到我们现在使用的游戏引擎,Unity,UE4均是一次编写,就可以发布到多个平台,游戏引擎自身,对平台进行了复杂的适配工作,并不需要被适配的平台本身为此做出修改。


 

其它的类似应用:


 

比如我有一套实现文本功能的类,但我现在实现一个支持Rich Text的功能,由于时间以及难度的问题,实现起来并不容易,但是发现项目里有一个插件提供了很棒的Rich Text功能,但他和我的接口并不兼容(面向接口编程,而不是具体的类),这时候,我们就可以通过Adapter适配器,外层依然是自己期待接口的实现类,内部实现则是插件里提供的Rich Text,但Adapter时常还要负责那些被匹配的类没有的功能,比如我需要拖动的功能,但R水ich Text并没有实现,所以这部分就要由Adapter自己去实现了,具体实现方式有两种:一种是多重继承,实现期望的接口以及私有继承Rich Text(禁止禁止子类再次重写),另一种是将Rich Text定义为实现类的一部分(组合)(实现方式会在下面有更多的说明)


 

这部分参考书上的关于绘图编辑器,TextShape和TextView之间的适配器。具体可以参考书中该章节。


 

从程序的角度来讲,就是原本互不兼容,无法一起工作的接口,通过“适配器”,可以让两个接口兼容起来,并能够一起工作。


 

第六条是最初认为是可行案例,但后来发现,实际情况要复杂得多,商户付款的问题,我们通过微信和支付宝去扫描一个二维码,都是要经过安全验证的,不是你随便一个二维码扫描,都可以跳转,实际测试可以发现,商户的二维码是要在微信的后台进行认证验证申请的地址,所以这里针对商户付款的业务也肯定是进行了一系列的处理,但如果不考虑现实安全性等因素,仅从思想和学术角度来讨论,也是符合适配器Adapter思想的。


 

“适配器”可以让两个不相干的接口兼容,一起工作,但依然保持原有的独立,只是进行了转换或是包装以重用现有的接口,以满足各种需求 ,且并不会涉及到两个接口本身的修改。


 

什么时候会出现这种情况?


 

通常是第三方库,大家在不同的开发规范下,肯定会有不同的参数,返回值,数据结构等等,一个很简单的例子,你要序列化游戏的数据,要用第三方的加密库,但你的游戏是以什么样的形式输出是由你自己定义的,比如你游戏导出的是二进制的数据,但第三方的加密库,需要是明文的json,这就是“不兼容”的情况,我们要做的就是提供一个适配器,将数据转换成json,这样就可以和第三方的库一起使用了  了。


 

如果在同样的规范下开发,出现了接口不兼容,无法一起工作的情况,那么就别想着去用适配器了,直接重构代码吧。


 

适配器是一个“亡羊补牢”的方案,必不得已的情况下,才去使用。


 

适配器有两种:


 

类适配器


 

通过多重继承来实现,这是在C++中可行的,因为只有C++支持多继承,而Java,C#这些后来者语言不支持,但可以通过多个接口来实现。

多继承在新的编程语言中已经被弃用了,至少可以证明这并不是一个好的设计,比如二义性,但也是有办法实现的,我们可以基于接口。


 

对象适配器


 

这是我们目适配器的实现的主要方式,通过继承和组合实现。


 

Adapter模式结构图:


 

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1


 


 

Client:就理解为调用的客户端就好。


 

Target:是客户所期待的接口(我们需要去兼容它,需要被兼容的接口),可以具体的或是抽象的类,也可以是接口。


 

Adapter:适配器,通过继承或实现Target类或接口,重写Target中的方法,通常扮演着一个转换和包装的角色,并不包含具体的转换和包装的过程,Adapter也可以是抽象类,当有多个子类的时候,由子类来继承实现。


 

Adaptee:被适配的类,他通常会在Adapter中去使用,因为Adpater继承或实现了 Target,具体的实现由Adaptee实现,即Adaptee用于实现兼容的操作。


 

实现Adapter适配器设计模式有两种方式:组合(compositon, has-a关系)和继承(inheritance,is-a关系)。


 

需要注意的,继承和组合都是为了复用代码,但组合的方式更加的灵活,比如可以运行中,动态的添加和删除功能,而继承是编译期间就确定了,是静态的,关于继承和组合的区别及优缺点可以找到很多。


 

不管是类适配器还是对象适配器,最后的目的都是相同的。不相干的两者,为了提高复用性,可以通过Adapter的思想进行兼容,可以达到一起工作的目的。


 

简略的代码:


 

class Target{

 public virtual void Request();

}

class Adapter:Target{

 public Adaptee adaptee = new Adaptee();

 public override void Request(){

  adaptee.specialRequest();

  }

}

class Adaptee{

 public specialRequest(){ }

}


 

Sample Code:


 

Target t = new Adapter();

t.Request();


 


 

要注意区分Adapter和Adaptee,一个是适配器,用于中转,另一个被适配者,用于去兼容Target所匹配的数据, 具体的适配部分的逻辑,要写在这里。


 

下面使用插头转换器来举一个例子,来进一步解释Adapter使用:


 

还原场景:


 

我拿着国内的插头在国外是无法使用的,我需要一个转换器(适配器)来进行兼容,可以让国内的电器在国外使用。


 

比如俄罗斯的插头是两个圆形小柱子,国内的插头的标准是肯定不符合的。


 

代码:


 

publicclassRussianOutlet

{

 //开始通电

    publicvirtualbool PowerUp(RussianStandard device)

    {

        return device.Connected();

     }

}


 

//俄罗斯标准备,所有的俄罗斯电器都要继承它

publicclassRussianStandard

{

    publicint standard = 1;


 

    publicvirtualbool Connected() {  returnfalse;  }

}


 

//中国标准,与俄罗斯并不兼容

publicclassChineseStandard

{

    publicint standard = 2;


 

    publicvirtualbool Connected() {  returnfalse;  }

}


 

//俄罗斯家用电器,继承自RussianStandard (俄罗斯标准)

publicclassRussianKettle : RussianStandard

{

    publicvoid KettleWorking()

    {

        Debug.Log("RussianKettle is working");

     }


 

 //当连通电源时,调用kettleWorking

    publicoverridebool Connected()

    {

        KettleWorking();

        returntrue;

     }

}


 

publicclassClient

{

    RussianOutlet russianOutlet = newRussianOutlet();


 

    publicClient()

    {

  //RussianKettle 俄罗斯电器

        russianOutlet.PowerUp(newRussianKettle());

  //ChineseKettleAdapter中国电器无法直接使用,需要通过Adapter进行转换,           才可以在俄罗斯标准下工作

        russianOutlet.PowerUp(newChineseKettleAdapter());

     }


 

}


 

publicclassChineseKettle:ChineseStandard

{

    publicvoid KettleWorking()

    {

        Debug.Log("ChineseKettle is working");

     }


 

    publicoverridebool Connected()

    {

        KettleWorking();

        returntrue;

     }

}


 

//Adapter 转换chineseKettle,具体的转换由ChineseKettleForRussian负责

publicclassChineseKettleAdapter: RussianStandard

{

    publicChineseKettleForRussian kettle;

    publicoverridebool Connected()

    {

  //进行转换

        kettle.adapterRussianStandard();

        kettle.KettleWorking();

        returntrue;

     }

}


 

//Adaptee 需要被转换的类中国的Kettle

publicclassChineseKettleForRussian

{

    publicChineseKettle chineseKettle = newChineseKettle();


 

    publicvoid adapterRussianStandard()

    {

        //do some stuff for chineseKettle

     }


 

    publicvoid KettleWorking()

    {

        chineseKettle.KettleWorking();

     }

}


 


 

再举一个例子,但就不给出代码了,比如你要存储一段数据,你找到了一个第三方的库,你要存储的数据就是一个使用某个特殊符号拼接的字符串,但第三方库是接收的一个类对象,然后对类进行序列化操作,那么你是无法直接使用第三方库的,你要做的就是将字符串包装成类对象,然后再提供给第三方库使用,这种情况也是适配器模式。


 

只要两个接口不兼容,不可以直接一起工作,就满足适配器模式的条件。


 

此外,最为典型的一个应用就是数据库操作的DataAdapter了,因为数据源可能来自于不同的数据库, Oracle,SQL Server,Access,DB2等等,这些数据在组织上会有不同之处,通过Adpter可以给我们提供统一的数据,格式,可见,这些数据库肯定是在不同的规范下开发的。


 

适配器模式可以用于转换和包装,上面的例子中是转换的场景,下面我们来模拟一个包装的场景。


 

场景还原:


 

我现在有两个接口:


 

1.从英文到中文的翻译 

2.从中文到英文的翻译


 

这两个接口是独立的。


 

每一次输入内容时,都需要选择哪一种翻译,现在我需要一个简单的需求,只要首字母是英文,就默认使用英到中的翻译接口,如果是首字母是中文,就默认使用中到英的翻译接口。


 

这时候,我们就需要通过Adapter,来对新的需求进行包装实现:


 

代码如下:


 

publicinterfaceITranslation

{

    string Translate(string context);

}


 

publicclassChineseToEnglish : ITranslation

{

    publicstring Translate(string context)

    {

        return"egnlish context";

     }

}


 

publicclassEnglishToChinese : ITranslation

{

    publicstring Translate(string context)

    {

        return"中文内容";

     }

}


 

publicclassAutoTranslation:ChineseToEnglish

{

    publicEnglishToChinese en_to_cn = newEnglishToChinese();


 

    publicnewstring Translate(string context)

    {

        return System.Text.Encoding.UTF8.GetBytes(context.Substring(0,1)).Length == 1 ? en_to_cn.Translate(context) : base.Translate(context);

     }

}


 

Sample Code:


 

AutoTranslation auto_translate = newAutoTranslation();

Debug.Log(auto_translate.Translate("这是一段中文"));

Debug.Log(auto_translate.Translate("Great minds think alike"));


 

通过添加AutoTranslation,继承自ChineseToEnglish(继承自EnglishToChinese)也可以,甚至是不使用继承,使用组合的形式。


 

通过判断第一个字符是中文还是英文字符,调用不同的类型的翻译。 


 

时间真是个奇怪的东西,有时候快如白驹过隙,有时候慢到度日如年,内心要有一个恒定的标准,认识到时间的珍贵价值,钢铁侠说:你玩弄时间,时间也会玩弄你。这与生活就是一面镜子,有着相同的含义,最近在读冯唐成事这本书,刚看到的几页里有一段是关于总结的,是要每日总结,每天都要抽出一些时间,对今天的工作,生活,情感等方方面面,与自己进行交谈,我以前喜欢按月去制定计划,这没有什么问题,但我并没有具体的执行到每周,每一天,到了月末,发现要完成的事儿还有很多很多,想补也来不及了,问题出在哪里?


 

如果每天都能够在“规矩”内办事,不达目的不摆休的态度,结果相信不会差,你对自己可以有多狠呢?


 

参考文献:

大话设计模式

GOF

https://blog.csdn.net/wujunyucg/article/details/78619097

https://www.runoob.com/design-pattern/adapter-pattern.html

  • 允许他人重新传播作品,但他人重新传播时必须在所使用作品的正文开头的显著位置,注明用户的姓名、来源及其采用的知识共享协议,并与该作品在磨坊上的原发地址建立链接
  • 可对作品重新编排、修改、节选或者以作品为基础进行创作和发布
  • 可将作品进行商业性使用

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引