本篇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();
}
}
4 客户端调用者
//客户端代码
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();
}
}
4 客户端调用者
//客户端代码
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();
}
}
调用结果如下:
模式扩展
如果存在一种情况,当前系统中新旧接口两套并用,并且互相想要使用对方的实现怎么办?例如我既拥有日本笔记本,又拥有中国笔记本,想要在中国日本往返时都能给两个笔记本供电怎么办?可以使用双向适配器模式:
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();
}
}
打印结果如下:
总结一下
适配器模式理解起来非常容易,其实就是一个转接口,当我们在现有业务逻辑下调用新定义的接口时,如果系统中已经存在该接口的实现对象,只是接口定义上略有不同,我们没有办法修改现存对象接口实现对应的接口定义(该接口定义可能已经广泛的被上下游使用了)来匹配新定义的接口,此时使用适配器模式比较合适,有了适配器,客户端只和目标接口绑定而不是和实现绑定(适配过程被解耦并封装到了适配器类中),需要适配哪种现存对象实现就增加哪种适配器类。但长期看能不适配就尽量不适配,最好通过重构使系统代码更优雅。