本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是适配器模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:
- 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
- 模式结构:包含模式的角色定义及调用关系以及其模版代码
- 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
- 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
- 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
- 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构
接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构,本文的模式实践案例大多来自极客时间。
模式档案
在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的事例,这时需要第三者进行适配。例如,讲中文的人同讲英文的人对话时需要一个翻译,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题。
模式定义:将一个类的接口转换成客户端希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求调用者了解现有组件库中的相关组件的内部结构,所以应用相对较少些
模式特点:主要特点是能最大限度的保留原有对象不需要重写,仅仅通过适配的方式即能让现有对象继续在新系统中工作。
解决什么问题:主要解决在软件系统中,常常要将一些现存的对象放到新的环境中,而新环境要求的接口是现对象不能满足的问题。
优点:该模式的主要优点如下:
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,调用者不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
缺点:该模式的主要缺点如下:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构
- 于 JAVA 至多继承一个类,所以对于类结构型的适配器模式适配器类至多只能适配一个适配者类。
使用场景: 该模式通常适用于以下场景。
- 设计统一多个类的接口:两个类所做的事情相同或相似,但是具有不同接口的时候。我们希望统一定义接口,利用多态实现易维护的代码实现
- 封装有缺陷的外部接口:使用第三方组件,组件接口定义和自己定义的不同,不希望修改自己的接口,但是要使用第三方组件接口的功能。
以上场景可以使用适配器模式,但是最好在一定时间内做重构或按照提供方的接口定义进行接口定义。
模式结构
适配器模式(Adapter)包含以下三个主要角色。
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的类。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
角色的相互调用关系如下图所示:类适配器模式结构如下:
对象适配器模式结构如下:
模式实现
依据模式结构我们分别实现以下类适配器模式和对象适配器模式
类适配器模式实现
类适配器模式角色代码如下
1 目标接口
//目标接口 interface Target { public void request(); }
2 适配者类
//适配者接口 class Adaptee { public void specificRequest() { System.out.println("适配者中的业务代码被调用!"); } }
3 类适配器
适配器类会继承适配者类并获取它的接口方法
//类适配器类 class ClassAdapter extends Adaptee implements Target { public void request() { specificRequest(); } }
客户端调用
//客户端代码 public class ClassAdapterTest { public static void main(String[] args) { System.out.println("类适配器模式测试:"); Target target = new ClassAdapter(); target.request(); } }
调用结果如下:
对象适配器模式实现
对象适配器模式角色代码如下
1 目标接口
//目标接口 interface Target { public void request(); }
2 适配者类
//适配者接口 class Adaptee { public void specificRequest() { System.out.println("适配者中的业务代码被调用!"); } }
3 对象适配器
适配器类会使用适配者类并获取它的接口方法
//对象适配器类 @AllArgsConstructor class ClassAdapter implements Target { private Adaptee adaptee; public void request() { adaptee.specificRequest(); } }
客户端调用如下
//客户端代码 public class ClassAdapterTest { public static void main(String[] args) { System.out.println("对象适配器模式测试:"); Adaptee adaptee = new Adaptee(); Target target = new ClassAdapter(adaptee); target.request(); } }
调用结果如下:
模式实践
我们来看以下几种模式实践:
设计一个电压转换器
国内的充电电压是220V,日本的是110V,如果出差到日本,想用日本的充电器给中国的笔记本充电,需要使用110V转220电压的变压器转换使用。
package com.example.designpattern.adpator; import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter; import lombok.AllArgsConstructor; //目标接口 interface JapanElectricPressure { void japanPower(); } //适配者接口 class ChinaElectricPressure { public void chinaPower() { System.out.println("中国电器标准220V开始供电!"); } } //类适配器类 @AllArgsConstructor class ElectricPressureAdapter implements JapanElectricPressure { private ChinaElectricPressure chinaElectricPressure; public void japanPower() { System.out.println("电压转换成功,110V电压转为220V!"); chinaElectricPressure.chinaPower(); } } //笔记本充电 public class ClassAdapterTest { public static void main(String[] args) { ChinaElectricPressure chinaElectricPressure = new ChinaElectricPressure(); JapanElectricPressure japanElectricPressure = new ElectricPressureAdapter(chinaElectricPressure); System.out.println("日本充电器110V开始供电!"); japanElectricPressure.japanPower(); } }
调用结果如下:
设计一个外部缺陷接口封装方案
假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了
public class CD { //这个类来自外部sdk,我们无权修改它的代码 //... public static void staticFunction1() { //... } public void uglyNamingFunction2() { //... } public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... } public void lowPerformanceFunction4() { //... } } // 使用适配器模式进行重构 public class ITarget { void function1(); void function2(); void fucntion3(ParamsWrapperDefinition paramsWrapper); void function4(); //... } // 注意:适配器类的命名不一定非得末尾带Adaptor public class CDAdaptor extends CD implements ITarget { //... public void function1() { super.staticFunction1(); } public void function2() { super.uglyNamingFucntion2(); } public void function3(ParamsWrapperDefinition paramsWrapper) { super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...); } public void function4() { //...reimplement it... } }
设计一个多重过滤器敏感词过滤系统
某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码
public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口 //text是原始文本,函数输出用***替换敏感词之后的文本 public String filterSexyWords(String text) { // ... } public String filterPoliticalWords(String text) { // ... } } public class BSensitiveWordsFilter { // B敏感词过滤系统提供的接口 public String filter(String text) { //... } } public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口 public String filter(String text, String mask) { //... } } // 未使用适配器模式之前的代码:代码的可测试性、扩展性不好 public class RiskManagement { private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter(); private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter(); private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter(); public String filterSensitiveWords(String text) { String maskedText = aFilter.filterSexyWords(text); maskedText = aFilter.filterPoliticalWords(maskedText); maskedText = bFilter.filter(maskedText); maskedText = cFilter.filter(maskedText, "***"); return maskedText; } }
使用适配器模式进行改造:
// 使用适配器模式进行改造 public interface ISensitiveWordsFilter { // 统一接口定义 String filter(String text); } public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter { private ASensitiveWordsFilter aFilter; public String filter(String text) { String maskedText = aFilter.filterSexyWords(text); maskedText = aFilter.filterPoliticalWords(maskedText); return maskedText; } } //...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor... // 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统, // 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。 public class RiskManagement { private List<ISensitiveWordsFilter> filters = new ArrayList<>(); public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) { filters.add(filter); } public String filterSensitiveWords(String text) { String maskedText = text; for (ISensitiveWordsFilter filter : filters) { maskedText = filter.filter(maskedText); } return maskedText; } }
模式对比
什么情况下使用类适配器,什么情况下使用对象适配器呢?
类适配器模式和对象适配器模式
针对这两种实现方式,在实际的开发中,到底该如何选择使用哪一种呢?判断的标准主要有两个,一个是 Adaptee 接口的个数,另一个是 Adaptee 和 ITarget 的契合程度。
- 如果 Adaptee 接口并不多,那两种实现方式都可以。
- 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
- 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。
实际使用中对象适配器的应用场景更多。
模式扩展
如果存在一种情况,当前系统中新旧接口两套并用,并且互相想要使用对方的实现怎么办?例如我既拥有日本笔记本,又拥有中国笔记本,想要在中国日本往返时都能给两个笔记本供电怎么办?可以使用双向适配器模式:
package com.example.designpattern.adpator; import lombok.AllArgsConstructor; //目标接口 interface JapanElectricPressure { void japanPower(); } //适配者接口 interface ChinaElectricPressure { void chinaPower(); } //适配者实现 class JapanElectricPressureImpl implements JapanElectricPressure { public void japanPower() { System.out.println("日本电器标准110V开始供电!"); } } //适配者实现 class ChinaElectricPressureImpl implements ChinaElectricPressure { public void chinaPower() { System.out.println("中国电器标准220V开始供电!"); } } //类适配器类 @AllArgsConstructor class ElectricPressureAdapter implements JapanElectricPressure, ChinaElectricPressure { private JapanElectricPressure japanElectricPressure; private ChinaElectricPressure chinaElectricPressure; ElectricPressureAdapter(JapanElectricPressure japanElectricPressure) { this.japanElectricPressure = japanElectricPressure; } ElectricPressureAdapter(ChinaElectricPressure chinaElectricPressure) { this.chinaElectricPressure = chinaElectricPressure; } public void japanPower() { System.out.println("电压转换成功,110V电压转为220V!"); chinaElectricPressure.chinaPower(); } @Override public void chinaPower() { System.out.println("电压转换成功,220V电压转为110V!"); japanElectricPressure.japanPower(); } } //笔记本充电 public class ClassAdapterTest { public static void main(String[] args) { System.out.println("在日本给国产笔记本充电!"); ChinaElectricPressure chinaElectricPressureImpl = new ChinaElectricPressureImpl(); JapanElectricPressure japanElectricPressure = new ElectricPressureAdapter(chinaElectricPressureImpl); japanElectricPressure.japanPower(); System.out.println("==========================================================="); System.out.println("在中国给日本笔记本充电!"); JapanElectricPressure japanElectricPressureImpl = new JapanElectricPressureImpl(); ChinaElectricPressure chinaElectricPressure = new ElectricPressureAdapter(japanElectricPressureImpl); chinaElectricPressure.chinaPower(); } }
打印结果如下:
总结一下
适配器模式理解起来非常容易,其实就是一个转接口,当我们在现有业务逻辑下调用新定义的接口时,如果系统中已经存在该接口的实现对象,只是接口定义上略有不同,我们没有办法修改现存对象接口实现对应的接口定义(该接口定义可能已经广泛的被上下游使用了)来匹配新定义的接口,此时使用适配器模式比较合适,有了适配器,客户端只和目标接口绑定而不是和实现绑定(适配过程被解耦并封装到了适配器类中),需要适配哪种现存对象实现就增加哪种适配器类。但长期看能不适配就尽量不适配,最好通过重构使系统代码更优雅。