读书笔记-设计模式-可复用版-Adapter 适配器模式
拖了有些日子了,设计模式第一次读的时候,尤其是碰到一些不常用到的,难免会出现一些 生涩难懂的情况,我个人又很不喜欢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模式结构图:
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