设计模式-适配器模式

2,491 阅读7分钟

# 七大软件设计原则
# 设计模式-工厂模式
# 设计模式-单例模式
# 设计模式-原型模式
# 设计模式-策略模式
# 设计模式-责任链模式
# 设计模式-建造者模式
# 设计模式-深度分析代理模式
# 设计模式-门面模式
# 设计模式-装饰器模式
# 设计模式-享元模式
# 设计模式-组合模式

适配器模式(Adapter Pattern)又叫做变压器模式,它的功能是将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而导致无法在一起工作的两个类能够一起工作,属于结构计模式。

在软件开发中,基本上任何问题都可以通过增加一个中间层进行解决。适配器模式 其实就是一个中间层。综上,适配器模式其实起着转化/委托的作用,将一种接口转化为另一种符合需求的接口。

模式的动机

通常情况下,接口的方法是满足客户端的需求的,但是可能随着版本的迭代需要新增一些新的功能,功能的实现和以前的又类似,但是现有的接口又不支持(比如少参数等),修改以前老的接口及代码破坏了开闭原则,也容易出现bug,此时就可以使用适配器模式

通用UML类图

适配器主要分为三种:类适配器、接口适配器、对象适配器

类适配器

类适配器的原理就是通过继承来实现适配器功能。具体做法:让 Adapter 实现 Target 接口,并且 继承 Adaptee,这样 Adapter 就具备 TargetAdaptee可以将两者进行转化。 image.png 具体代码如下:

public class Adaptee {
    public void specialRequest(){
        System.out.println("处理特殊请求");
    }
}
public interface Target {
    void request();
}
public class Adapter extends Adaptee implements Target{
    @Override
    public void request() {
        System.out.println("处理普通请求");
        super.specialRequest();
    }
}
public class Client {
    public static void main(String[] args) {
        Target target = new Adapter();
        target.request();
    }
}

这样做有个弊端就是Adapter也就是适配器也能调用最原始的功能接口,应为是继承过来的,这违反了最少知道原则。(当然平时开发中如果这个模式适合你的项目也是可以的)

对象适配器

对象适配器的原理就是通过组合来实现适配器功能。具体做法:让Adapter 实现 Target 接口,然 后内部持有 Adaptee 实例,然后再 Target 接口规定的方法内转换 Adaptee

具体代码实现很简单就是将Adapter代码修改一下,原来是继承 Adaptee,现在是持有 Adaptee示例 具体代码如下:

public class Adapter implements Target{

    private Adaptee adaptee;

    public Adapter(Adaptee adaptee){
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        System.out.println("处理普通请求");
        this.adaptee.specialRequest();
    }
}
public class Client {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee();
        Target target = new Adapter(adaptee);
        target.request();
    }
}

类图如下: image.png

接口适配器

接口适配器的关注点与类适配器和对象适配器的关注点不太一样,类适配器和对象适配器着重于将系统存在的一个角色(Adaptee)转化成目标接口(Target)所需内容,而接口适配器的使用场景是解决接口方法过多,如果直接实现接口,那么类会多出许多空实现的方法,类显得很臃肿。此时,使用接口适配器就能让我们只实现我们需要的接口方法,目标更清晰。

其实接口适配器和对象适配器就很像了无非是对象适配器关注点是一个对象的转换,接口可能是多个他的关注点不再是对象转换而是接口转换,这些接口可能需要用到很多个源对象,也就是说这样设计之后转换器中持有的对象就可能是多个了

具体类图如下: image.png

代码示例

比如我们项目中常做的登录,第一个版本的需求目标肯定是快速上线,能满足基本功能,所以一般第一个版本就只做了密码登录,但是后期维护的过程中发现用户对第三方登录的需求增大,所以又想着添加第三方登录。(第三方和密码登录的区别就是第三方只有openId没有密码,所以这里将用户的第三方openId作为密码存到数据库,用户名就是冲第三方获取的用户账户比如QQ号、微信号等)

老代码是这样的:

public interface ILoginService {
    void login(String username, String password);
}
public class LoginService implements ILoginService{
    @Override
    public void login(String username, String password) {
        System.out.println("登录成功:");
        System.out.println("用户:" + username);
        System.out.println("密码:" + password);
    }
}

现在我们需要在不动老代码的基础上添加上第三方登录:

public interface ILoginAdapter {
    void qqLogin(String userName,String openId);
    void weChatLogin(String userName,String openId);
}
public class LoginAdapter implements ILoginAdapter{

    private final ILoginService loginService;

    public LoginAdapter(ILoginService loginService) {

        this.loginService = loginService;
    }

    @Override
    public void qqLogin(String userName,String openId) {

        System.out.println("qq登录");
        this.loginService.login(userName,openId);
    }

    @Override
    public void weChatLogin(String userName,String openId) {
        System.out.println("微信登录");
        this.loginService.login(userName,openId);
    }
}
public class Client {
    public static void main(String[] args) {
        ILoginService loginService = new LoginService();
        loginService.login("土豆","123");

        ILoginAdapter adapter = new LoginAdapter(loginService);
        adapter.qqLogin("qwe","123qwe234234");
    }
}

至此在不动原来代码的基础下就完成了新代码的新增。

但是这个代码还可以在优化,比如现在又多一种三方登录方式比如是微博,我们就需要修改原来接口已经添加对应实现,这样增加了类出现bug的风险,不满足开闭原则,这里我们可以再优化一下,分别创建对应适配器,具体代码如下:
先创建一个第三方登录的接口

public interface IThirdLoginAdapter {
    boolean support(IThirdLoginAdapter adapter);
    void login(String userName,String openId);
}

提取抽象类(这里抽象类仅仅是有一个公共的loginService,也可以不写这个抽象类)

public abstract class ThirdLoginAdapter implements IThirdLoginAdapter{
    public final ILoginService loginService;

    public ThirdLoginAdapter(ILoginService loginService) {
        this.loginService = loginService;
    }

}

分别实现适配器:

public class QQLoginAdapter extends ThirdLoginAdapter{
    public QQLoginAdapter(ILoginService loginService) {
        super(loginService);
    }

    @Override
    public boolean support(IThirdLoginAdapter adapter) {
        return adapter instanceof QQLoginAdapter;
    }

    @Override
    public void login(String userName, String openId) {

    }
}
public class WeChatLoginAdapter extends ThirdLoginAdapter{
    public WeChatLoginAdapter(ILoginService loginService) {
        super(loginService);
    }

    @Override
    public boolean support(IThirdLoginAdapter adapter) {
        return adapter instanceof WeChatLoginAdapter;
    }

    @Override
    public void login(String userName, String openId) {
        System.out.println("微信登录:");
        super.loginService.login(userName, openId);
    }
}

最后要变的是登录的适配器

public class LoginAdapter implements ILoginAdapter{

    private final ILoginService loginService;

    public LoginAdapter(ILoginService loginService) {
        this.loginService = loginService;
    }

    @Override
    public void qqLogin(String userName,String openId) {
        login(userName,openId,WeChatLoginAdapter.class);
    }

    @Override
    public void weChatLogin(String userName,String openId) {
        login(userName,openId,WeChatLoginAdapter.class);
    }

    private void login(String userName, String openId, Class<? extends IThirdLoginAdapter> clazz) {
        try {
            IThirdLoginAdapter instance = clazz.getDeclaredConstructor(ILoginService.class).newInstance(this.loginService);
            if(instance.support(instance)){
                instance.login(userName,openId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里不是是对应登录的代码而是使用对应的适配器实现具体逻辑

public class Client {
    public static void main(String[] args) {
        ILoginService loginService = new LoginService();
        loginService.login("土豆","123");

        ILoginAdapter adapter = new LoginAdapter(loginService);
        adapter.qqLogin("qwe","123qwe234234");
    }
}

客户端调用依旧不变

这样的优化虽然说是新增第三方登录还是需要修改原接口也就是ILoginAdapter也还是要增加对应实现,但是新增的逻辑代码是在新增加的类中,代码上更加优雅类内容也不会变的臃肿。

具体类图如下: image.png

其实这里还可以在优化就是适配器的优化如下: image.png image.png 这样如果新增支付方式就不需要再修改接口以及实现

适配器模式优缺点

优点:

  1. 能提高类的透明性和复用,现有的类复用但不需要改变。
  2. 目标类和适配器类解耦,提高程序的扩展性。
  3. 在很多业务场景中符合开闭原则。

缺点:

  1. 适配器编写过程需要全面考虑,可能会增加系统的复杂性。
  2. 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。

适配器模式和装饰器模式之间的区别

部分人可能看了上文的描述发现适配器模式和装饰器模式很像,的确有一点点像,但还是有本质的区别。

装饰器模式是装饰器和被装饰类都实现同一个接口或者抽象类,装饰器模式是可以层层装饰的。
适配器模式则不然,它仅仅是拥有被适配的类,不能装饰器模式一样层层装饰