每日一个设计模式之【适配器模式】
☁️前言🎉🎉🎉
大家好✋,我是知识汲取者😄,今天给大家带来一篇有关适配器模式的学习笔记。众所周知能够熟练使用设计模式是一个优秀程序猿的必备技能,当我们在项目中选择一个或多个合适的设计模式,不仅能大大提高项目的稳健性、可移植性、可维护性,同时还能让你的代码更加精炼,具备艺术美感。
适配器相信大家在生活中一定有经常见到吧,手机的充电器的那个大头就是,它将220V电压转成了手机需要的电压,让手机能够适配220V电压,还有USB接口转接口也是,让不同的插头进行了适配。而在计算机世界中也存在适配器,比如在Java中适配器不是一个硬件,而是一个类,它能够让一个接口转成适配另一个类,从而通过这个接口调用被适配类中的方法,是不是感觉很神奇,现在就让我们开始学习吧(●ˇ∀ˇ●)
推荐阅读:
🌻适配器模式概述
-
什么是适配器模式?
设配器模式(Adapter Pattern)属于结构型模式,它将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器模式(Wrapper Pattern)
-
适配器模式的作用:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
-
适配器模式的优缺点
-
优点:
- 提高系统的兼容性,可以让任何两个没有关联的类一起运行
- 提高了类的透明度1,能够面对抽象编程,满足了开闭原则,有利于系统的维护
- 提高了类的复用,一个接口可以直接通过适配器调用另一个类的行为,不用再去重新创建一个新的类
- 提高了程序的灵活性,一个类的接口想要转换成另一个接口,直接创建一个适配器就可以了,很灵活、方便
……
-
缺点:
- 提高了系统的复杂度,过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现(这也是透明度带来的缺点),一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构(要遵循中庸之道)
- 具有一定的局限性,对于两个功能完全不同的Adaptee和Target的时,适配器模式是无法使用的
……
-
-
适配器模式的适用场景:
- 一个接口想要使用另一个类中的行为,可使用适配器模式
- 想建立一个可以重复使用的类,用于一些彼此之间没有太大关联的一些类,可使用适配器模式
……
生活中的应用:手机充电器、USB转接口
Spring中的应用:Spring中的通知就有用到适配器模式,通知(Advice)使用了适配器
AdviceAdapter
,这是适配器接口,对于每一个通知都拥它们各自的适配器,比如前置通知(BeforeAdvice)使用的适配器是BeforeAdviceAdapter
,被适配的对象是拦截器,因为通知的本质对请求进行拦截,然后利用代理模式添加扩展代码,增强程序的功能(具体可以去参考Spring的源码)适配器模式角色划分:
- 目标接口(Target):客户所需要的接口,是由适配器得到的最终产物,用于和客户进行交互,可以是抽象类、接口
- 被适配者(Adaptee):被适配的目标,用于被适配器进行转换,是Target对象的实际运行者,一般会是一个具体的类
- 适配器(Adapter):适配器模式的核心角色,它能够将Adaptee转成Target,是一个具体的类
-
适配器模式的分类:
- 类适配器模式:适配器由一个类组成,通过继承Adaptee,然后实现Target的方法,最终完成适配器模式
- 对象适配器模式:适配器由一个类组成,该类实现Target的方法,内置一个Adaptee对象,最终完成适配器模式
- 接口适配器模式:适配器由一个抽象类组成,该抽象类同时实现Adapter接口和Target接口,最终完成适配器模式
🌱适配器模式的实现
🐳类适配器模式
- 实现方式:让Adapter先继承Adaptee,然后实现Target,核心是使用继承
- 解决的问题:将一个类的接口转换成客户希望的另外一个接口
- 存在的问题:由于类适配器模式需要使用继承Adaptee,而在Java中只允许单一继承,这就导致Target必须是接口,限制了Target类型;同时继承也导致一个适配器只能去适配一个类,这样每次想要适配一个类就需要去创建一个适配器,这样很容易增加系统的复杂度。出现这个问题的本质就是该模式不符合面对对象基本设计原则==“多用组合,少用继承”==原则
示例:
问题描述:机器人拥有自己的行为,但是我们想要机器人能够拥有动物的行为
-
Step1:创建Adaptee
1)Dog:
package com.hhxy.adaptee.imp; import com.hhxy.adaptee.Animal; /** * @author ghp * @date 2022/9/30 * @title * @description */ public class Bird implements Animal { /** * 鸟发出叫声的方法 */ @Override public void cry() { System.out.println("鸟发出叽叽喳喳的叫声"); } /** * 鸟飞的方法 */ @Override public void run() { System.out.println("鸟在天空中飞"); } }
2)Bird:
package com.hhxy.adaptee.imp; import com.hhxy.adaptee.Animal; /** * @author ghp * @date 2022/9/30 * @title * @description */ public class Dog implements Animal { /** * 狗发出叫声的方法 */ @Override public void cry() { System.out.println("狗发出汪汪汪的叫声"); } /** * 狗跑的方法 */ @Override public void run() { System.out.println("狗在地上跑"); } }
-
Step2:创建Target
1)RobotClass:
package com.hhxy.target; /** * @author ghp * @date 2022/9/30 * @title 机器人(用于类适配器模式) * @description */ public interface RobotClass { /** * 机器人发声的方法 */ void cry(); /** * 机器人跑的方法 */ void run(); }
2)RobotClassImp:
package com.hhxy.target.imp; import com.hhxy.target.RobotClass; /** * @author ghp * @date 2022/10/7 * @title 机器人的实现类(类适配器模式) * @description */ public class RobotClassImp implements RobotClass { /** * 机器人发声的方法 */ @Override public void cry() { System.out.println("机器人叫"); } /** * 机器人跑的方法 */ @Override public void run() { System.out.println("机器人跑"); } }
-
Step3:创建Adapter
1)DogAdapter:
package com.hhxy.adapter.classAdapter.imp; import com.hhxy.target.RobotClass; import com.hhxy.adaptee.imp.Dog; /** * @author ghp * @date 2022/9/30 * @title 模仿狗行为的适配器 * @description 用于提供机器人模仿狗的行为的方法 */ public class DogAdapter extends Dog implements Adapter,RobotClass { /** * 机器人模仿狗发声的方法 */ @Override public void cry() { System.out.print("机器人模仿"); super.cry(); } /** * 机器人模仿狗跑的方法 */ @Override public void run() { System.out.print("机器人模仿"); super.run(); } }
2)BirdAdapter:
package com.hhxy.adapter.classAdapter.imp; import com.hhxy.target.RobotClass; import com.hhxy.adaptee.imp.Bird; /** * @author ghp * @date 2022/9/30 * @title 模仿鸟行为的适配器 * @description 用于机器人模仿鸟行为的方法 */ public class BirdAdapter extends Bird implements Adapter, RobotClass { /** * 机器人模仿鸟发声的方法 */ @Override public void cry() { System.out.print("机器人模仿"); super.cry(); } /** * 机器人模仿鸟飞的方法 */ @Override public void run() { System.out.print("机器人模仿"); super.run(); } }
-
Step4:创建配置文件
class-adapter-config.xml:
这个配置文件主要是用来得到Adapter对象的,适用配置文件获取Adapter对象能够完美符合开闭原则,实现代码解耦,同时很方便测试、维护
<?xml version="1.0" encoding="UTF-8" ?> <config> <animal>com.hhxy.adapter.classAdapter.imp.DogAdapter</animal> <animal>com.hhxy.adapter.classAdapter.imp.BirdAdapter</animal> <animal>test</animal> </config>
-
Step5:编写配置文件读取类
ReadClassAdapterConfig:
package com.hhxy.read; import com.hhxy.target.RobotClass; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.lang.reflect.Constructor; /** * @author ghp * @date 2022/9/30 * @title * @description 用于读取配置文件并获取一个适配器对象(类适配器模式使用) */ public class ReadClassAdapterConfig { public static RobotClass getRobot(){ try{ //1、将配置文件加载到内存中,获取DOM对象 //1.1 获取DOM解析器工厂对象DocumentBuilderFactory,用于创建DOM解析器 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); //1.2 获取DOM解析器DocumentBuilder DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); //1.3 加载配置文件 // Document document = documentBuilder.parse(new FileInputStream("day05_Adapter/src/class-adapter-config.xml")); //让代码和模块名进行解耦,比上面那种方法更优 Document document = documentBuilder.parse(ReadClassAdapterConfig.class.getResourceAsStream("/class-adapter-config.xml")); //2、获取配置文件中的数据 //2.1 从DOM中获取指定的结点的结点列表 NodeList nodeList = document.getElementsByTagName("adapterName"); //2.2 获取指定位置的结点 Node classNode = nodeList.item(1).getFirstChild(); //2.3 获取指定结点中的数据(排除空格) String adapterName = classNode.getNodeValue().trim(); //3、使用反射获取获取Adapter对象 //3.1 获取类对象 Class cls = Class.forName(adapterName); //3.2 获取该类对象的构造器对象 Constructor constructor = cls.getDeclaredConstructor(); //3.3 暴力反射,防止构造器私有化导致无法创建对象 constructor.setAccessible(true); //3.4 获取配置器对象(使用多态的方式,提高兼容性) RobotClass robot = (RobotClass) constructor.newInstance(); //4、返回通过配置文件获取的配置器对象 return robot; } catch (Exception e) { //如果异常就打印异常信息,同时返回一个空 e.printStackTrace(); throw new RuntimeException("未找到该适配器,请检查配置文件或者添加一个适配器!"); } } }
-
Step6:测试
package com.hhxy.test; import com.hhxy.read.ReadClassAdapterConfig; import com.hhxy.target.RobotClass; import com.hhxy.target.imp.RobotClassImp; /** * @author ghp * @date 2022/9/30 * @title 测试类1 * @description 用于测试类适配器 */ public class Test1 { public static void main(String[] args) { /* 方式一:直接new //机器人自己的行为 RobotClass robot = new RobotClassImp(); robot.cry(); robot.run(); System.out.println("-----------------------"); //机器人使用了适配器后能拥有狗的行为 RobotClass robotDog = new DogAdapter(); robotDog.cry(); robotDog.run(); System.out.println("-----------------------"); */ //方式二:通过读取配置文件获取适配器对象,实现了解耦,同时也很方便测试 //机器人自己的行为 RobotClass robot = new RobotClassImp(); robot.cry(); robot.run(); System.out.println("-----------------------"); //机器人使用了适配器后能拥有动物的行为(如果想要让机器人换种动物模仿,只需要修改配置文件或者配置文件读取类) RobotClass robotAnimal = ReadClassAdapterConfig.getRobot(); robotAnimal.cry(); robotAnimal.run(); } }
测试结果:
可以查看以下执行方法时的时序图加深理解:
从上面可以看到DogAdapter,就是Robot和Dog的转接口,它将Dog的行为给了Robot,让Robot能够具有Dog的行为,这和我们生活中的适配器是同样的效果,我们手机的充电器就是一个适配器,它能够将220V的电压转成我们手机所需要的电压,这里DogAdapter也是一样,将Dog的行为转成了Robot的行为,而它实现的基础是Java三大基本特性之一的多态
🐳对象适配器模式
- 实现方式:让Adapter实现Target,然后再Adapter内部创建一个Adaptee对象,核心是使用聚合
- 解决的问题:每次新增适配类,无需去创建适配器,提高了代码的复用;符合“多用组合,少用聚合”原则,降低了系统复杂度
- 存在的问题:不够灵活,因为我们是使用Adapter实现Target接口的,这就导致Adapter必须实现Target的所有方法,但是有时候我们并不需要使用所有Target中的所有方法
示例:
问题描述:机器人拥有自己的行为,但是我们想要机器人能够拥有动物的行为
-
Step1:创建Adaptee
和类适配器中的代码一致,略…… -
Step2:创建Target
和类适配器的代码类似,略…
-
Step3:创建Adapter
AnimalAdapter:
之前需要使用BirdAdapter和DogAdapter两个适配器,而现在只需要一个适配器就能实现类适配器模式两个适配器的作用了,大大地提高了代码的复用
package com.hhxy.adapter.objectAdapter; import com.hhxy.adaptee.Animal; import com.hhxy.target.RobotObject; /** * @author ghp * @date 2022/9/30 * @title 对象适配器模式 * @description 用于模仿动物的行为 */ public class AnimalAdapter implements RobotObject { private Animal animal; public AnimalAdapter(Animal animal) { this.animal = animal; } /** * 机器人发声的方法 */ @Override public void cry() { System.out.print("机器人模仿"); animal.cry(); } /** * 机器人跑的方法 */ @Override public void run() { System.out.print("机器人模仿"); animal.run(); } }
-
Step4:编写配置文件
object-adapter-config.xml:
<?xml version="1.0" encoding="UTF-8" ?> <config> <!--用于反射创建Animal对象--> <animalName>com.hhxy.adaptee.imp.Bird</animalName> <animalName>com.hhxy.adaptee.imp.Dog</animalName> <animalName>test</animalName> <!--用于反射创建适配器对象--> <adapterName>com.hhxy.adapter.objectAdapter.AnimalAdapter</adapterName> <adapterName>test</adapterName> </config>
-
Step5:编写配置文件读取类
package com.hhxy.read; import com.hhxy.adaptee.Animal; import com.hhxy.target.RobotObject; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.lang.reflect.Constructor; /** * @author ghp * @date 2022/10/7 * @title * @description 用于读取配置文件并获取一个适配器对象(对象配器模式使用) */ public class ReadObjectAdapterConfig { public static RobotObject getRobot() { try { //1、将配置文件加载到内存中,获取DOM对象 //1.1 获取DOM解析器工厂对象DocumentBuilderFactory,用于创建DOM解析器 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); //1.2 获取DOM解析器DocumentBuilder DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); //1.3 加载配置文件 // Document document = documentBuilder.parse(new FileInputStream("day05_Adapter/src/class-adapter-config.xml")); //让代码和模块名进行解耦,比上面那种方法更优 Document document = documentBuilder.parse(ReadClassAdapterConfig.class.getResourceAsStream("/object-adapter-config.xml")); //2、获取配置文件中的数据 //2.1 从DOM中获取指定的结点的结点列表 NodeList nodeListAnimal = document.getElementsByTagName("animalName"); NodeList nodeListAdapter = document.getElementsByTagName("adapterName"); //2.2 获取指定位置的结点 Node classNodeAnimal = nodeListAnimal.item(0).getFirstChild(); Node classNodeAdapter = nodeListAdapter.item(0).getFirstChild(); //2.3 获取指定结点中的数据(排除空格) String animalName = classNodeAnimal.getNodeValue().trim(); String adapterName = classNodeAdapter.getNodeValue().trim(); //3、使用反射获取获取Animal对象 //3.1 获取类对象 Class clsAnimal = Class.forName(animalName); Class clsAdapter = Class.forName(adapterName); //3.2 获取该类对象的构造器对象 Constructor constructorAnimal = clsAnimal.getDeclaredConstructor(); Constructor constructorAdapter = clsAdapter.getDeclaredConstructor(Animal.class); //3.3 暴力反射,防止构造器私有化导致无法创建对象 constructorAnimal.setAccessible(true); constructorAdapter.setAccessible(true); //3.4 获取配置器对象(使用多态的方式,提高兼容性) Animal animal = (Animal) constructorAnimal.newInstance(); RobotObject robot = (RobotObject) constructorAdapter.newInstance(animal); //4、返回通过配置文件获取的配置器对象 return robot; } catch (Exception e) { //如果异常就打印异常信息,同时返回一个空 e.printStackTrace(); throw new RuntimeException("未找到该适配器,请检查配置文件或者添加一个适配器!"); } } }
-
Step6:测试
package com.hhxy.test; import com.hhxy.read.ReadObjectAdapterConfig; import com.hhxy.target.RobotObject; import com.hhxy.target.imp.RobotObjectImp; /** * @author ghp * @date 2022/10/7 * @title 测试类2 * @description 用于测试对象适配器模式 */ public class Test2 { public static void main(String[] args) { /* 方式一:通过new获取适配器对象 //机器人自己的行为 RobotObject robot = new RobotObjectImp(); robot.cry(); robot.run(); System.out.println("-----------------------"); //机器人使用了适配器后能拥有鸟的行为 RobotObject robotAnimal = new AnimalAdapter(new Dog()); robotAnimal.cry(); robotAnimal.run(); */ //方式二:通过读取配置文件获取适配器对象,实现了解耦,同时也很方便测试 //机器人自己的行为 RobotObject robot = new RobotObjectImp(); robot.cry(); robot.run(); System.out.println("-----------------------"); RobotObject robotAnimal = ReadObjectAdapterConfig.getRobot(); robotAnimal.cry(); robotAnimal.run(); } }
测试结果:
🐳接口适配器模式
- 实现方式:将Adapter抽象成一个接口,然后使用用一个AbstractAdapter去实现它,再通过委托的方式进行实现,本质上还是属于对象适配器的一种特殊方式,只是比对象适配器模式多了一个抽象层
- 解决的问题:十分灵活,适配器实现类不光可以任意实现Robot接口中的方法,而且还可以通过匿名内部类重写方法,非常方便
示例:
问题描述:我们想要机器人拥有Bird的cry行为,同时拥有Dog的run行为
思考:如果使用第一种方式需要同时创建两个机器人(RobtClass)对象,显然不符合题目描述;使用第二种方式,同样的也需要创建两个机器人(RobotObject)对象,但是可以通过改造Adapter来实现,我们可以通过在Adapter中直接创建一个Dog、Bird对象,然后通过两个调用来实现,这是一种实现方式。
PS:在这里显然是无法体现到接口适配器的厉害之处的,这是因为在Robot接口存在的方法太少了,在实际项目中一般Target接口中会存在大量的方法(实际项目中这种适配器模式是用的最多的!),而有时我们只想使用Adapter让Traget的部分方法与Adaptee进行适配,这时候如果来使用前面两种方式实现Adapter,就需要实现接口的所有方法,这是因为实现类都必须实现接口中的所有的方法,这就导致Adapter显得特别臃肿、混乱。
这个时候就可以使用接口适配器方式来实现了(●ˇ∀ˇ●)
-
Step1:创建Adaptee
上同,略……
-
Step2:创建Target
RobotInterface:
package com.hhxy.target; /** * @author ghp * @date 2022/10/7 * @title 机器人(拥有接口适配器) * @description */ public interface RobotInterface { /** * 机器人发声的方法 */ void cry(); /** * 机器人跑的方法 */ void run(); /** * 模拟Target中一些不需要进行适配的方法 */ void a(); void b(); void c(); void d(); }
-
Step3:创建Adapter
1)AbstractAdapter
package com.hhxy.adapter.interfaceAdapter; import com.hhxy.target.RobotInterface; /** * @author ghp * @date 2022/9/30 * @title * @description */ public abstract class AbstractAdapter implements RobotInterface { /** * 机器人发声的方法 */ @Override public void cry() { } /** * 机器人跑的方法 */ @Override public void run() { } /** * 模拟Target中一些不需要进行适配的方法 */ @Override public void a() { } @Override public void b() { } @Override public void c() { } @Override public void d() { } }
2)AnimalAdapter
package com.hhxy.adapter.interfaceAdapter; import com.hhxy.adaptee.Animal; import com.hhxy.adaptee.imp.Bird; import com.hhxy.adaptee.imp.Dog; /** * @author ghp * @date 2022/10/7 * @title * @description */ public class AnimalAdapter extends AbstractAdapter{ private Animal dog = new Dog(); private Animal bird = new Bird(); /** * 机器人发声的方法 */ @Override public void cry() { System.out.println("机器人模仿"); bird.cry(); } /** * 机器人跑的方法 */ @Override public void run() { dog.run(); } }
-
Step4:编写配置文件
略……
-
Step5:编写配置文件读取类
略……
-
Step6:测试
package com.hhxy.test; import com.hhxy.adapter.interfaceAdapter.AnimalAdapter; import com.hhxy.target.RobotInterface; /** * @author ghp * @date 2022/10/7 * @title * @description */ public class Test3 { public static void main(String[] args) { //方式一:通过new获取适配器对象 RobotInterface robot = new AnimalAdapter(); robot.cry(); robot.run(); //方式二:通过读取配置文件获取适配器对象,实现了解耦,同时也很方便测试 //略…… } }
🌲总结
- 类适配器:类适配器采用
继承+实现
的方式完成适配器,它拥有适配器的一般性功能,但是不符合“多用组合,少用继承”原则,导致系统具有较高复杂度,同时也限制了Target的类别 - 对象适配器:对象适配器采用
组合+实现
的方式完成适配器,它不仅拥有适配器的一般性功能,还满足了“多用组合,少用继承”原则,降低了系统复杂度,提高了程序的灵活性,但是由于它是直接实现Target接口,这就导致要适配器要实现Target的所有方法,包括一些无需适配的方法,这就会导致适配器很臃肿、混乱,所以面对一些大型系统就显得有点吃力 - 接口适配器:接口适配器是在对象适配器的基础上新增一个抽象类,让抽象类去重写Target的所有方法,然后适配器只需要继承抽象类就行了 ,从而大大提高了适配器的灵活性,也让适配器显得更加简洁,十分适合大型系统
总的而言,三种适配器模式从上而下使用范围逐渐扩大,灵活性也逐渐扩大
自此,文章就结束了,如果觉得本文对你有一丢丢帮助的话😄,欢迎点赞👍+评论✍,您的支持将是我写出更加优秀文章的动力O(∩_∩)O
上一篇:每日一个设计模式之【建造者模式】
下一篇:每日一个设计模式之【桥接模式】
参考文章:
- 适配器模式 | 菜鸟教程
- Java设计模式之结构型:适配器模式
- 【源码分析设计模式 10】SpringMVC中的适配器模式
- 设计模式-适配器模式(类适配器、对象适配器、接口适配器详解)_
- 计算机术语,“对…是透明的”怎样理解?
在次致谢
透明度:类中行为对于使用者的可见度,透明度越高,使用者越难以了解类的行为 ↩︎