定义
适配器模式(Adapter Pattern),它的功能是将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而导致无法在一起工作的两个类能够一起工作。属于结构型的设计模式。
原文:Convert the interface of a class into anotherinterface clients expect.Adapter lets classes worktogether that couldn't otherwise because ofincompatible interfaces.
大白话解释一下,当前系统存在两种接口A和B,但是客户端(金主爸爸)只认识A接口,不认识B接口。那这个时候就需要增加一个Adapter(中间人),将B接口的资源通过A接口返回出去,这样客户端就可以通过A接口来获取B接口的数据了。
在软件开发中,基本上任何问题度可以通过一个中间层来解决,适配器模式其实就是一个中间层。它的作用是将一种接口转化为另一种符合需求的接口。
应用场景
首先来看几个例子,电源适配器,显示器转接头,电源插转换头,相信大家都不陌生。
适配器模式就是提供一个适配器,将当前系统转换为客户端能够访问的接口。它适用于以下业务场景:
- 已经存在类,它的方法和需求不匹配(方法结果相同或者相似)的情况
- 适配器模式不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品,不同需求功能类似而接口不同的解决方案,有点亡羊补牢的感觉
适配器模式有3种形式:类适配器、对象适配器、接口适配器。下面将分别讨论
适配器模式一般包含3个角色:
- 目标角色(ITarget):我们期望的接口
- 源角色(Adaptee):存在于系统中,指满足要求(需转换)但接口不匹配的接口实例
- 适配器(Adapter):将Adaptee转化为目标角色ITarget的类实例
类适配器通用实现及UML图
场景:在中国,家用电都是220V的交流电,但手机充电器使用的都是5V的直流电,因此,我们就需要用到电源适配器,我们用代码来描述一下这个场景
表示220V交流电
创建ITarget角色DC5接口,表示5V直流电。
创建Adapter角色电源适配器PowerAdapter类,继承了AC220,并实现了DC5的方法
来看客户端调用:
UML图:
对象适配器的UML类图及通用写法
对象适配器的原理就是通过组合来实现适配器功能。具体做法是,首先让Adapter实现ITarget接口,然后内部持有Adaptee实例,最后在ITarget接口规定的方法内转换Adaptee。代码只需更改Adapter实现,其它与类适配器一致
UML图
接口适配器的UML类图及通用写法
接口适配器的关注点与类适配器、对象适配器的关注点不太一样,类适配器和对象适配器着重于将系统存在的一个角色(Adaptee)转化成目标接口(ITarget)所需的内容,而接口适配器的使用场景是当接口的方法过多时,如果直接实现接口,则类会多出许多空实现的方法,显得很臃肿。此时,使用接口适配器就能只实现我们需要的接口方法,使目标更清晰。
接口适配器的主要原理就是使用抽象类实现接口,并且空实现众多。我们来看接口适配器的源码实现,首先创建ITarget角色DC类。
然后创建Adaptee角色AC220类。
最后创建Adapter角色PowerAdapter类。
客户端调用:
解决实际问题
稍微工作几年的小伙伴一定有这样的经历,很早以前开发的老系统都有登录接口,但是随着业务的发展和社会的进步,单纯地依赖用户名和密码登录显然不能满足用户需求。现在,大部分系统都已经支持多种登录方式,如QQ登录、微信登录、手机登录、微博登录等,同时保留用户名和密码登录的方式。虽然登录形式丰富,但是登录后的处理逻辑可以不必改,都是将登录状态保存到Session,遵循开闭原则。首先创建统一的返回结果ResultMsg类。
假设在老系统中,处理登录逻辑的代码在PassportService类中。
为了遵循开闭原则,不修改老系统的代码。下面开启代码重构之路,创建Member类。
我们也不改动运行非常稳定的代码,创建ITarget角色IPassportForThird接口。
创建Adapter角色实现兼容,创建一个新的类PassportForThirdAdapter,继承原来的逻辑。
客户端调用:
使用接口适配器优化代码
通过这么一个简单的适配动作,我们完成了代码兼容。当然,代码还可以更加优雅,根据不同的登录方式,创建不同的Adapter。首先创建LoginAdapter接口。
public interface ILoginAdapter {
boolean support(Object obj);
ResultMsg login(String id, Object adapter);
}
然后创建一个抽象类AbstractAdapter继承PassportService原有的功能,同时实现ILoginAdapter接口,再分别实现不同的登录适配。QQ登录LoginForQQAdapter如下。
public abstract class AbstractAdapter extends PassportService implements ILoginAdapter {
public abstract boolean support(Object obj);
public abstract ResultMsg login(String id, Object adapter);
public ResultMsg loginForRegist(String username, String password){
if(null == password){
password = "EMPTY";
}
super.regist(username, password);
return super.login(username, password);
}
}
public class LoginForQQAdapter extends AbstractAdapter {
@Override
public boolean support(Object adapter) {
return adapter instanceof LoginForQQAdapter;
}
@Override
public ResultMsg login(String id, Object adapter) {
if(!support(adapter)){
return null;
}
return super.loginForRegist(id, null);
}
}
Token自动登录LoginForTokenAdapter如下。
public class LoginForTokenAdapter extends AbstractAdapter {
@Override
public boolean support(Object adapter) {
return adapter instanceof LoginForTokenAdapter;
}
@Override
public ResultMsg login(String id, Object adapter) {
if(!support(adapter)){
return null;
}
return super.loginForRegist(id, null);
}
}
微信登录LoginForWechatAdapter如下。
public class LoginForWechatAdapter extends AbstractAdapter {
@Override
public boolean support(Object adapter) {
return adapter instanceof LoginForWechatAdapter;
}
@Override
public ResultMsg login(String id, Object adapter) {
if(!support(adapter)){
return null;
}
return super.loginForRegist(id, null);
}
}
接着创建适配器PassportForThirdAdapter类,实现目标接口IPassportForThird完成兼容。
public class PassportForThirdAdapter implements IPassportForThird {
@Override
public ResultMsg loginForQQ(String openId) {
return processLogin(openId, LoginForQQAdapter.class);
}
@Override
public ResultMsg loginForWechat(String openId) {
return processLogin(openId, LoginForWechatAdapter.class);
}
@Override
public ResultMsg loginForToken(String token) {
return processLogin(token, LoginForTokenAdapter.class);
}
@Override
public ResultMsg loginForTelphone(String phone, String code) {
return processLogin(phone, LoginForTelAdapter.class);
}
private ResultMsg processLogin(String id, Class<? extends ILoginAdapter> clazz){
try{
ILoginAdapter adapter = clazz.newInstance();
if(adapter.support(adapter)){
return adapter.login(id, adapter);
}
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
return null;
}
}
客户端测试代码如下。
public class Main {
public static void main(String[] args) {
IPassportForThird adapter = new PassportForThirdAdapter();
adapter.loginForQQ("asdfdsafsf");
}
}
UML图
至此,在遵循开闭原则的前提下,我们完整地实现了一个兼容多平台登录的业务场景。
学习到这里,小伙伴们可能会有一个疑问:适配器模式与策略模式好像区别不大?笔者要强调一下,适配器模式主要解决的是功能兼容问题,单场景适配大家可能不会和策略模式对比,但复杂场景适配大家就很容易混淆。其实,大家有没有发现一个细节,笔者给每个适配器类都加上了一个support()方法,用来判断是否兼容,support()方法的参数类型也是Object,而support()来自接口。适配器类的实现逻辑并不依赖接口,完全可以将ILoginAdapter接口去掉。而加上接口,只是为了代码规范。上面的代码可以说是策略模式、简单工厂模式和适配器模式的综合运用。
适配器模式的优点
- 能提高类的透明性和复用,但现有的类复用不需要改变。
- 适配器类和原角色类解耦,提高程序的扩展性。
- 在很多业务场景中符合开闭原则。
适配器模式的缺点
- 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
- 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。