大家好,又见面了。

不知道下面这玩意大家有没有见过或者使用过?这是一个插座转换器。我们都知道日常使用的是220v​的交流电,而国外不同国家使用的电流电压是不一样的(比如日本使用的是110v)、且插座的接口样式也是各不相同的(比如欧洲国家使用的是两个小圆柱状的插头接口),如果我们到别的国家去旅行的时候,借助这个插座转换器,就可以让我们的手机充电器在国外也能正常使用了。


(资料图片仅供参考)

当然,除了使用插座转换器,还有个方法也可以让我们出国之后正常的使用各种电子产品,那就是在当地重新买一套!显然,这样的成本就会非常巨大,明显不符合我们 勤(nang)俭(zhong)持(xiu)家(se) 的特征。

看过我前面的文章的小伙伴应该知道,我的文章中一直反复的在阐述自己的一个观念,即“编码源于生活” ,这里依旧不例外。现实生活中的朴素哲学思维,在代码世界中其实也无时无刻不在体现着。上面举的例子,在我们的项目中又何尝不是在频繁上演此类情况呢?

我们先按照原有的业务逻辑实现了一套代码,后来又来了个新的需求,如果重新开发一套需要投入大量的人力物力,所以首选方案就是去思考如何去复用已有的逻辑,以最小的代价将业务对接适配使用现有的逻辑去实现。

本篇文章中,我们就从这个“插座转换器”来作为切入点,聊一聊在软件系统中无处不在的“插座转换器” —— 编码中的适配器(Adapter​)。选定以Adapter​为题材进行阐述,并非是因为Adapter​在技术实现上有多复杂,其实Adapter真正实现起来是非常简单的,而且很多人有意或无意中其实也都在使用。更多的是想一起探讨这种借助Adapter来复用与兼容已有逻辑的思路,以及如何利用Adapter来践行OCP(开闭原则)的系统架构设计理念。

Adapter的百媚千姿新瓶旧酒:复用现成的实现逻辑

新瓶装旧酒,在我们的系统里面是一个很“节省”的操作,可以让我们基于一个现有的能力快速的封装提供出一个全新的业务功能,当然有的时候,系统现有的能力可能会某些方面无法完全满足新业务的需求,需要做一些转换适配处理。

举个例子:

一个视频网站,原先已有一个评论能力,用户可以在视频下方发表评论,然后评论内容以列表的形式展示在视频下方页面上。现在需要开发一个新功能,支持视频发送弹幕能力,并将弹幕显示在视频播放画面上。

从需求功能上来说,评论与弹幕有很多相似之处。对后端而言,其处理逻辑与存储数据结构几乎都是相同的,只是在数据列表API​实现的时候,需要过滤出评论信息展示到评论区、或者过滤出弹幕信息显示到视频画面上。但是由于弹幕信息有一些特殊的属性,又没法直接完全使用现有的评论接口,比如弹幕可能会设置显示在屏幕的位置、弹幕的字体颜色等等。这种情况下,我们可以通过构造个Adapter适配器的方式,在复用已有评论能力的基础上,顺便扩展实现需要的弹幕新特性。

如上图所示,我们可以在Adapter中封装扩展弹幕需要的新特性,然后对于数据存储等逻辑则直接复用已有的评论功能处理逻辑,这样就可以大大减少我们的开发工作量、后续也只需要维护一套主体代码即可。

负重前行:兼容历史版本

和上面讨论的场景相反,实际开发中还有一种非常常见的情况,就是原先的时候实现了一套业务逻辑,然后因为业务变化或者系统重构,需要对底层具体实现逻辑进行大改。这种情况下,为了保证此前调用该API的业务可以正常使用,通常有两种思路:

保持原先的内容不动,完全另起炉灶全新实现一套,然后两套逻辑并存,同时维护;

按照新的逻辑去实现,并将原先的对外API适配转换对接使用新逻辑实现。

显然,从成本与可维护性层面考虑,思路2更为可行、更加经济。

对比我们文首举的那个“插头转换器”的例子,我们可以把图中V1版本业务逻辑​当做我们国内的手机充电插头,而图中绿色部分的V2新版本依赖逻辑​,则是欧洲地区的圆孔墙面插座,那么如何让国标的扁口插头能用上欧标的圆孔插座呢?关键就是那个插头转换器(Adapter)。

另类心机:屏蔽开源协议传染

大家可以回想下,曾经是否也有过从github上“借鉴”一些代码放进了自己的项目中,然后简单修改为符合自己诉求的逻辑,便当做是自研代码去正常使用了?不知道你是否有关注过你所拷贝的代码所对应的开源协议呢?要小心啦、这个看似平常的操作,也许会给项目埋下致命隐患!

为什么说的这么危言耸听呢?因为有一些不太友好的开源协议(比如GPL协议),会要求使用了其代码的项目如果商用就必须要开源其全部源码!而对于很多软件公司而言,源码便是公司的核心资产,是公司最为核心的竞争力,将源码开源无异于是要了老板和公司的命。也许有人会对此很不屑,大家都这么干,似乎并没有发现有人来追责呢?有个词叫做“树大招风”,只要你的产品做的够大,就一定会被盯上 —— 你品,你细品。在当前知识版权保护越来越强的情况下,我们还是应该关注并提前做好应对这种危机出现的可能,避免埋下隐患。

这种情况下,可以基于Adapter的机制,实现弃卒保车的效果。即构建一个适配层,然后仅将适配层进行开源,而核心的模块代码中,则通过接口调用的方式使用适配层即可,这样避免了核心模块代码被开源协议传染。由于核心模块中并没有集成被二次改动后的开源源码,所以也不具有开放源码的义务、而Adapter层没有任何核心业务逻辑,即使开源对公司、对项目也没有影响。

基于Adapter适配层​的方式来切断开源协议传染的成功实践,最典型的莫过于Android​项目(AOSP)了。因为AOSP是基于Linux kernel​内核进行构建的,而Linux Kernel​使用的是GPL​协议,那么按照要求,AOSP也需要开源其源码。但是问题来了,如果AOSP开源源码了,势必导致所有基于Android定制的各个硬件厂商底层的设备驱动相关的代码也都要全部开源,显然不会有公司愿意这么干。

为了让各个公司可以放心的基于Android​去开发自己的产品,AOSP​将自己的协议搞成了Apache​开源协议,这样对产商而言就非常友好了,无需将自己的核心源码开源。那么Google是如何做到将本来需要以GPL协议开源的AOSP给变为使用Apache协议开源的呢?其实就是做了一个Adapter​—— 也即HAL(Hardware Abstract Layer,硬件抽象层)。

Adapter是一种理念

关于编码中的Adapter​,常规的文档或者资料中,往往都是指的狭义上的适配器,也就是代码class类维度的Adapter。

我们跳出纯粹的编码层面,站到全局系统架构视角去审视的时候,其实Adapter​在系统架构与编码设计中是一个比较宽泛的概念。我个人更愿意Adapter看做是一种问题解决的思想、一种方案设计的理念。

根据要解决的问题level与范围​的不同,Adapter对应的粒度与呈现形态也会有差异。

服务型Adapter

如果是在一个分布式微服务系统中,消息推送能力可以预见的会提供给很多不同的服务节点去调用,则可以将消息推送能力也封装为一个对外微服务,业务通过RPC或者HTTP等方式进行远程调用。

这种是一种相对High Level的Adapter抽象使用(但抽象为服务独立部署后,其实也不仅仅是个Adapter了),广泛的应用于系统架构层面,是解决系统功能复用、业务解耦的一种有效手段。

在我此前的一篇文章中,介绍了一个构建通用在线文档预览服务的实际案例,里面对“预览编辑服务”的定位就是一个典型的服务型Adapter,如下图所示。通过预览编辑服务这个Adapter,将文档预览能力所涉及的后端对接OnlyOffice​或者对接kkFileView等细节逻辑给屏蔽掉,业务服务通过Adapter进行调用,大大简化了业务的使用复杂度,也保持了业务模块与文档预览服务内部模块之间的耦合。

服务型Adapter着眼解决的是系统进程层面的适配与统一封装,自身既是一个Adapter,又是一个独立的服务,封装内部细节差异化的实现,保证其它进程服务相对简单的调用逻辑。

依赖库型Adapter

在一些中小型项目中,会有若干个业务模块中会用到消息发送的能力,但是整体体量与业务规划层面而言,却也无需单独部署一个专门的消息推送服务进程,这种情况下,可以将其封装为一个依赖库,比如JAVA​中的一个jar包,或者C++​中的一个so库文件,亦或是C#中的dll库文件。这样各个业务模块可以集成此库文件,直接进行API调用即可。

此种类型的Adapter实现,在很多的框架中非常常见。比如在JAVA中的SpringBoot​中的日志框架,底层可以选择是使用logback​,也可以选择切换到log4j。

代码类Adapter

在单个项目模块中,我们为了保持业务逻辑的清晰与独立,也会通过Adapter类​的方式,来解耦具体的业务逻辑。比如这里的消息推送服务,如果仅当前模块需要使用,则可以创建一个独立的Adapter类,提供接口供其他类调用,在Adapter类中完成具体逻辑的封装实现。

还是以前面举的告警通知消息发送的例子来说明,使用Adapter方式隔离消息通道与业务逻辑的实现UML图如下:

代码类的Adapter在实际项目中使用的场景非常的广泛,是用于屏蔽代码底层差异化逻辑的不二选择。在总结各种实际使用场景与优秀实践的基础上,演进为23种设计模式之一的适配器模式。

下面我们一起聊一聊适配器模式。

Adapter是一种设计模式

所谓设计模式,便是将常规代码编码中常遇到的一些场景的处理方式进行了总结与抽象,固化成一个优秀实践范例模板​,使其整体实现更符合设计原则的要求。也就是说:设计模式并非是凭空捏造的,其实就是来源于常规的编码实践总结。

按照通俗意义上对代码设计模式的理解,适配器模式也可以分为2种形式,即类适配器模式与对象适配器模式。

下面分别阐述下。

类适配器模式

类适配器模式整体非常的简单,涉及的角色也很少。类适配器模式中,Adapter​与被适配的Adaptee​之间,通过继承的方式来实现,其UML图如下所示。

主要角色说明如下:

Adaptee:原始被适配的类,即不符合诉求需要由Adapter进行适配的原始接口。Adapter:适配器本身,也是类适配器模式的核心,用于将Adaptee适配为目标的Target。Target:期待获取到的目标结果。也即Adaptee经由Adapter适配后得到的统一的目标接口。

还是以前面的告警通知发送的场景为例,我们按照聚合的方式,演示下对应的Adapter实现逻辑。

@Servicepublic class MsgSendAdapter extends SmsSender implements IMsgSender {    @Override    public void send(AlarmDetail detail) {        // detail转SMS请求体的逻辑        SmsContent sms = convertToSmsContent(detail);        super.sendSms(sms);    }}

上述代码中,MsgSendAdapter​继承了SmsSender​类并且实现了IMsgSender​接口,将父类SmsSender​中的sendSms​接口转换为了IMsgSender​接口提供的目标接口send()​,业务可以调用IMsgSender.send()​接口,实现对SmsSender.sendSms()逻辑的调用。

对象适配器模式

对象适配器模式与类适配器模式类似,区别点在于Adapter​与被适配的Adaptee​之间非继承关系,而是对象组合关系。其UML图如下:

按照对象适配器的设计思路,其代码可以如下方式来实现:

@Servicepublic class MsgSendAdapter implements IMsgSender {    @Autowired    private SmsSender smsSender;    @Override    public void send(AlarmDetail detail) {        // detail转SMS请求体的逻辑        SmsContent sms = convertToSmsContent(detail);        smsSender.sendSms(sms);    }}

上述代码中,MsgSendAdapter​类中以组合的方式持有SmsSender​对象(Adaptee),相比较类适配器的继承逻辑,灵活性更高,所以对象适配器要更加的灵活与实用(其实在架构设计领域也一直有一种观点叫“组合优于继承”,因为继承打破了面向对象的封装)。

总结回顾

好啦,关于Adapter​相关的讨论与个人的理解,这里就给大家分享到这里。Adapter不仅是一个简单的具体实现类,也不仅仅是23种设计模式之一,更是一种问题解决的思想、一种方案设计的理念。

关于本篇文档中的内容,不知道屏幕前的各位小伙伴是否在项目中有使用过Adapter或者Adapter模式来帮助自己实现某些功能呢?是否对Adapter还有一些别的独到见解呢?

推荐内容