vlambda博客
学习文章列表

总结适配器设计模式——看Netty是如何转换的Callable和Runnable接口



目录

类的适配器

对象的适配器

使用场景

优缺点

实际开发中该如何选择使用哪一种适配器

适配器模式和外观模式区别

适配器模式和装饰模式的区别

适配器模式和代理模式的区别

JDK 中使用适配器的例子

Netty中使用适配器的例

本篇文章大概3000字,阅读时间大约10分钟


本文重点总结了适配器设计模式的理论和用法,并且以Netty源码为契机,看看该模式是如何被使用的。


适配器模式的定义很简单,就是做一个中间转换,类似港版的iPad,iPhone 等电源插头,需要一个转换器才能使用大陆的插座。类似的,适配器模式就是做转换用的,是一个比较常用的结构型设计模式,它有两种实现方式,一个是类的适配器,一个是对象的适配器。

总结适配器设计模式——看Netty是如何转换的Callable和Runnable接口

01

类的适配器


被适配的类——Adaptee ,可以看成是一个旧系统里的稳定的类:

public class Adaptee { // 被适配的类 public void request() { System.out.println("被适配的类:request"); }}

下面是新接口,或者说需要适配的目标接口ITarget:

public interface ITarget { // 新的接口 void doTargetRequest();}//////////////////////////////////////////////////////public class NewTarget implements ITarget { @Override public void doTargetRequest() { System.out.println("NewTarget的目标方法:doRequest"); }}

类的适配器通过继承实现,下面是适配器类Adapter,通过继承被适配者——Adaptee,把被适配者的API—— request适配给目标接口ITarget的 API——doTargetRequest:

public class Adapter extends Adaptee  implements ITarget {  // 类的适配器:Adapter(通过继承实现) @Override public void doTargetRequest() {        // 这里可以写一些业务代码,如果有参数,就是对参数的转换过程        // 通过 Adapter,把被适配者的API——request,        // 适配给了目标接口——doTargetRequest。实现了对既有代码的复用        super.request();  // 这里可以写一些业务代码 }}

客户端调用:

public class Main { public static void main(String[] args) { ITarget target = new NewTarget(); target.doTargetRequest(); // NewTarget的目标方法:doRequest ITarget iTarget = new Adapter(); iTarget.doTargetRequest(); // 被适配的类:request }}


类图如下:

总结适配器设计模式——看Netty是如何转换的Callable和Runnable接口


02

对象的适配器


类适配器通过继承被适配的类实现,而对象的适配器通过组合被适配类的引用实现。Itarget接口,Adaptee 类都不变,适配器类Adapter2如下:

public class Adapter2 implements ITarget { private Adaptee adaptee; // 通过组合的方式
public Adapter2(Adaptee adaptee) { this.adaptee = adaptee; }
@Override public void doTargetRequest() { // 这里可以写一些业务代码,如果有参数,就是对参数的转换过程 // 实现了对既有代码的复用,通过 Adapter, // 把被适配者的API——request,适配给了目标接口——doTargetRequest this.adaptee.request(); // 这里可以写一些业务代码 }}

客户端调用:

public class Main2 { public static void main(String[] args) { ITarget target = new NewTarget(); target.doTargetRequest(); // NewTarget的目标方法:doRequest Adaptee adaptee = new Adaptee(); ITarget iTarget = new Adapter2(adaptee); iTarget.doTargetRequest(); // 被适配的类:request }}


类图如下:

总结适配器设计模式——看Netty是如何转换的Callable和Runnable接口


03

适配器使用场景


可以将该模式看做是一个补偿机制,应用这种模式算是"无奈之举”,如果设计之初就能规避一些不兼容的问题,那么就不需要使用该模式,但是往往事与愿违,我们无法未卜先知,并且有时候还涉及到权限的问题,一些代码我们无法改变。主要场景如下:


1、增强老代码能力

如果一个类已经稳定存在,你不想(或者说没有权限,比如依赖库)修改它,但是还要为其增加新需求,且这个需求和类的现有接口不能匹配,那么就可以使用适配器模式实现接口的转换以便满足新需求


2、软件的升级

比如升级有缺陷的接口,且该接口已经提供给了用户,我们需要做到无感知升级


3、统一多个类的接口

比如某个功能的实现依赖多个外部系统(或者说类)。过适配器模式将它们的接口适配为统一的接口,然后就可以使用多态的特性来复用代码逻辑。


4、替换依赖的外部系统

比如当把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动


5、软件的废弃接口设计

一般我们会标记废弃接口为@Deprecated,此时可以将其内部实现逻辑委托为新的接口实现,让使用它的项目有个过渡期。比如,JDK1.0中遍历集合容器的类Enumeration。JDK2.0 对这个类进行了重构,将它改名为Iterator并对它做了优化。但考虑到如果将Enumeration直接从JDK2.0删除,那使用JDK1.0的项目如果切换到JDK2.0,就会编译不过。为了避免这种情况发生,JDK选择使用适配器模式对废弃接口进行适配。


6、适配不同格式的数据

适配器模式除了用于接口适配,还能用在不同格式的数据之间的适配。比如Java中的Arrays.asList()可以看作一种数据适配器,它将数组类型的数据转化为集合容器类型。


04

适配器优缺点


它的优点很直接,就是提高类的的稳定性,增强其复用性,在不改变现有类的前提下,拓展类的功能,且能解耦目标类和适配器类,符合了开闭原则。


缺点就是会增加系统复杂度,其实我个人觉得微不足道


05

实际开发中该如何选择使用哪一种适配器?


判断标准主要有两个,一个是Adaptee(被适配)接口的个数,另一个是Adaptee和ITarget(新接口)的契合程度,具体的:

1、如果Adaptee接口不多,两种实现方式都可以

2、如果Adaptee接口很多且Adaptee和ITarget接口定义大部分都相同,那推荐使用类适配器,因为Adaptor复用父类Adaptee的接口,比对象适配器的实现方式,代码要少一些

3、如果Adaptee接口很多且Adaptee和ITarget接口定义大部分都不相同,那推荐使用对象适配器,因为组合结构相对于继承更加灵活


06

适配器模式和外观模式的区别?


他俩都是结构型模式,外观模式是为旧接口(一般是比较复杂的接口)提供一个对外的简单接口,或者说更方便使用的访问入口。而适配器模式专注的是让两个已有的接口协同工作。


更一般的看,外观模式也是一种特殊的适配器,只不过它适配的是整个系统(粒度更大一些),为一个系统提供一个方便访问的对外接口,比如API网关就是典型的外观模式的应用场景。


07

适配器模式和装饰模式的区别?


适配器模式用来转换不同接口,使旧接口和新接口能无缝衔接,而装饰模式不会改变接口,只是给接口增加新功能,并且支持多个装饰器的嵌套使用,联想JDK的I/O包。


08

适配器模式和代理模式的区别?


代理模式是在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,屏蔽一些实现细节,而非加强功能,联想spring的AOP,以及RPC框架的动态代理。。。这是它跟装饰模式,以及适配器模式最大的不同


09

JDK中使用适配器的例子


最常见的有 I/O 包里的字符包装流,InputStreamReader、OutputStreamWriter 等,还有util包里的Arrays.asList() 方法。


10

Netty里使用对象适配器实现接口转换


文章:里,简单提到,Netty提交定时任务,支持带返回结果的Callable任务,也支持Runnable任务,但是底层确共用了一套定时任务保存和调度的逻辑,这其中就使用了适配器模式来让两个任务接口无缝衔接。


如下,只看核心的实现代码,最终Netty的NIO线程执行定时任务统一调用的是Callable的call方法,对于Runnable的run方法,使用了适配器进行适配,黄色1的Callable是新接口的角色,黄色3的Runnable需要被适配的接口,即老接口的角色:

总结适配器设计模式——看Netty是如何转换的Callable和Runnable接口

黄色2是适配器本身,显然,这是一个对象的适配器实现,通过组合的方式,在适配器里组合了需要被适配的Runnable,然后在新接口的API——call里,委托老接口的功能实现。


以上,就实现了适配功能,代码实现上确实不复杂。


END


点亮在看,你最好看

~

阅读原文,获得更多精彩内容