1 适配器模式的定义
适配器模式:将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。适配器模式又叫包装模式。
适配器模式的通用类图:
- Target目标角色
目标角色Target定义把其他类转换为何种接口,也就是我们期望的接口。一般是已经存在并且正在运行的角色,是一个接口或者抽象类(通常不应该是一个实现类)。 - Adaptee源角色
目标角色需要的信息在源角色中,但是又不能直接使用的角色。一般是已经存在的、运行良好的类或者对象(多为普通的实现类),需要经过适配器角色的包装,成为我们期待的目标角色。 - Adapter适配器角色
适配器模式的核心角色。Target和Adaptee都是已经存在的角色,而适配器角色是需要新建立的。适配器角色的职责是把源角色转换为目标角色,一般是通过继承或者关联的方式进行转换。
2 适配器模式通用代码示例
- 目标角色
- 目标角色接口
一般是已经存在并且正在运行的角色,是一个接口或者抽象类(通常不应该是一个实现类)。
public interface Target {
/**
* 目标角色的方法
*/
void request();
}
- 目标角色实现类
@Slf4j
public class ConcreteTarget implements Target {
@Override
public void request() {
log.info("目标角色的具体业务实现。{}", this.getClass().getSimpleName());
}
}
- 源角色
一般是已经存在的、运行良好的类或者对象(多为普通的实现类)。
@Slf4j
public class Adaptee {
/**
* 原有的业务逻辑
*/
public void provider() {
log.info("原有的业务逻辑,{}", this.getClass().getSimpleName());
}
}
- 适配器角色
一般继承源角色,实现目标角色的接口。
@Slf4j
public class Adapter extends Adaptee implements Target {
@Override
public void request() {
log.info("我是装饰器:{}", this.getClass().getSimpleName());
super.provider();
}
}
- 场景类
@Slf4j
public class Client {
public static void main(String[] args) {
//原有业务逻辑
Target target = new ConcreteTarget();
target.request();
//增加了适配器角色后的业务逻辑
Target targetAdapter = new Adapter();
targetAdapter.request();
}
}
运行结果:
18:08:08.747 [main] INFO com.jerry.adapter.ConcreteTarget - 目标角色的具体业务实现。ConcreteTarget
18:08:08.747 [main] INFO com.jerry.adapter.Adapter - 我是装饰器:Adapter
18:08:08.747 [main] INFO com.jerry.adapter.Adaptee - 原有的业务逻辑,Adapter
3 适配器模式的优缺点
- 优点
- 可以让两个没有任何关系的类在一起工作,只要适配器能胜任就行。
- 增加类的透明性
高层模块只需要访问目标角色接口,不需要关心具体实现。具体的实现都通过适配器委托给源角色执行。 - 提高了类的复用度
源角色在原有系统中可以继续使用,通过适配器也可以在目标角色实现中继续发挥作用。 - 灵活性好
需要的时候,根据需要灵活添加适配器;不需要的时候,直接删掉适配器即可,不需要对原有的业务和代码进行修改。
- 缺点
使用适配器模式,需要系统原有的设计符合依赖倒置原则和里氏替换原则,否则即使在适用适配器模式的场景中,也很难使用,需要对原有系统进行改造。
4 适配器模式的应用场景
- 当需要修改一个已经投产的接口时,适配器模式可以优先考虑。
- 系统扩展时,需要使用一个已有或新建立的类,但是这个类不符合系统的接口。
- 与二方或三方系统进行数据交互的时候,双方使用的对象不同,需要进行转换的时候。
注意
在项目详细设计阶段最好不需要考虑适配器模式,它不是为了解决还处于开发阶段的问题,而是解决正在线上运行的项目问题。多应用在系统应用扩展中,扩展应用不符合原有设计,通过适配器模式可以减少代码修改带来的风险。
5 适配器模式的扩展
5.1 对象适配器
以上2实例中,目标角色只有一个接口, 源角色也只有一个,我们通过适配器实现目标角色接口并继承源角色来实现适配,这种方式叫类适配器。
如果有三个源角色(功能各不相同),而Java不支持多继承,此时适配器可以通过类关联(或叫类聚合)的方式而不是继承来适配多个源角色。这种适配方式叫做对象适配器。
对象适配器的通用类图:
5.2 代码示例
对象适配器中,目标角色不变。下面只对有变化的源角色、适配器和场景类进行说明。
- 源角色
- 源角色接口A
public interface IAdapteeA {
/**
* 源角色接口A
*/
void providerA();
}
源角色接口A的具体类:
@Slf4j
public class AdapteeA implements IAdapteeA {
/**
* 原有的业务逻辑A
*/
@Override
public void providerA() {
log.info("原有的业务逻辑A,{}", this.getClass().getSimpleName());
}
}
- 源角色接口B
public interface IAdapteeB {
/**
* 源角色接口B
*/
void providerB();
}
源角色接口B的具体类:
@Slf4j
public class AdapteeB implements IAdapteeB {
/**
* 原有的业务逻辑B
*/
@Override
public void providerB() {
log.info("原有的业务逻辑B,{}", this.getClass().getSimpleName());
}
}
- 适配器
public class ObjectAdapter implements Target {
/**
* 源角色对象
*/
private IAdapterA adapteeA = null;
private IAdapterB adapteeB = null;
/**
* 构造函数传递源角色对象
*/
public ObjectAdapter(IAdapteeA adapteeA, IAdapteeB adapteeB) {
this.adapteeA = adapteeA;
this.adapteeB = adapteeB;
}
@Override
public void request() {
this.adapteeA.providerA();
this.adapteeB.providerB();
}
}
- 场景类
public class ObjectClient {
public static void main(String[] args) {
IAdapteeA adapteeA = new AdapteeA();
IAdapteeB adapteeB = new AdapteeB();
ObjectAdapter adapter = new ObjectAdapter(adapteeA, adapteeB);
adapter.request();
}
}
运行结果:
19:00:38.795 [main] INFO com.jerry.adapter.object.AdapteeA - 原有的业务逻辑A,AdapteeA
19:00:38.811 [main] INFO com.jerry.adapter.object.AdapteeB - 原有的业务逻辑B,AdapteeB
5.3 类适配器和对象适配器的区别
- 类适配器
适配器和源对象(一般只有一个源对象)之间的关系是类间继承。 - 对象适配器
适配器和源对象(一般有多个源对象)之间的关系是关联(或叫聚合)关系。
参考
- 设计模式之禅