概要
概念
适配器模式是一种结构型设计模式,用于将一个类的接口转换成客户端所期望的另一个接口。适配器模式通过创建一个适配器类,将原始接口转换为目标接口,使得两个不兼容的类可以协同工作。
组成
适配器模式由以下几个主要组件构成:
- 目标接口(Target ):客户端期望的接口,适配器将原始接口转换为目标接口。
- 原始接口(Adaptee ):需要被适配的类的接口。
- 适配器(Adapter):实现目标接口,同时持有一个原始接口的引用,在目标接口方法中调用原始接口方法来完成适配。
类图
工作原理
- 适配器模式:将一个类的接口转换成另一种接口.让原本接口不兼容的类可以兼容
- 从用户的角度看不到被适配者,是解耦的
- 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法
- 用户收到反馈结果,感觉只是和目标接口交互,如图(关键词:src,adapter,dst )
应用场景
当需要使用一个已存在的类,但其接口与其他类不兼容时,可以使用适配器模式进行接口转换。
当想要创建一个可复用的类,该类能与多个不相关的类或类层次结构协同工作时,可以使用适配器模式。
优点
1、可以让已存在的类与其他类协同工作,无需重写已有代码。
2、可以将实现细节封装在适配器中,对客户端隐藏具体的实现细节。
3、可以提高代码的可复用性和灵活性,适配器可以用于多种不同的上下文。
类型
类适配器模式
在类适配器模式中,适配器实现目标接口并继承原始类,通过重写目标方法,调用原始类的方法来完成适配。这种方式需要多重继承支持,方便但局限性较大。
对象适配器模式
在对象适配器模式中,适配器持有一个原始类的引用,并实现目标接口,目标方法调用原始类的方法来完成适配。这种方式不需要多重继承支持,更加灵活。
两者区别
类适配器和对象适配器的区别在于它们如何与被适配类(Adaptee)进行连接。
类适配器使用多重继承,同时继承目标接口(Target)和被适配类(Adaptee)。通过继承的方式,适配器类可以通过调用被适配类的方法来实现目标接口的方法。
对象适配器使用关联(组合),在适配器类内部持有一个被适配类的实例。适配器类通过调用被适配类实例的方法来实现目标接口的方法。
总结起来是:类适配器会继承被适配类的接口以及实现,然后将其转换为目标接口;而对象适配器则是利用组合的方式将被适配类的实例嵌入到适配器中,进而实现目标接口
二维表展示:
类适配器 | 对象适配器 | |
实现方式 | 使用类的继承关系实现适配 | 使用对象的组合关系实现适配 |
关联关系 | 适配器类同时继承目标接口和被适配者类 | 适配器持有被适配者对象的引用 |
可扩展性 | 不支持适配多个被适配者类 | 可以适配多个被适配者对象 |
适配方式 | 通过继承实现适配,可以重写被适配者的方法 | 通过组合实现适配,可以调用被适配者的方法 |
依赖关系 | 类适配器依赖于被适配者类 | 对象适配器依赖于被适配者对象 |
注意:在C++等支持多重继承的语言中,适配器类可以同时继承目标接口和被适配类,从而实现对两者的继承。而在Java等不支持多重继承的语言中,可以通过实现目标接口和关联被适配类的方式来实现相同的逻辑。
但是,由于多重继承可能会带来一些问题,如命名冲突、菱形继承等,因此在使用类适配器模式时需要注意继承关系的设计和维护,避免潜在的问题。
还有一种说法是适配器分成三类,多了一个接口适配器,这三种方式,是根据adeptee在Adapter里的形式来分类,也是命名的由来。
- 类适配器:在Adapter里,将adeptee当做类,来继承;
- 对象适配器:在Adapter里,将adeptee作为一个对象,来持有
- 接口适配器:在Adapter里,将adeptee作为一个接口,来实现
示例代码
类适配器模式示例(java 单继承,用接口和类模仿继承自两个类的效果)
// 目标接口 interface Target { void request(); } // 原始类 class Adaptee { void specificRequest() { System.out.println("执行特殊请求"); } } // 类适配器 class ClassAdapter extends Adaptee implements Target { public void request() { specificRequest(); } } // 客户端代码 public class Client { public static void main(String[] args) { Target target = new ClassAdapter(); target.request(); } }
对象适配器模式示例
// 目标接口 interface Target { void request(); } // 原始类 class Adaptee { void specificRequest() { System.out.println("执行特殊请求"); } } // 对象适配器 class ObjectAdapter implements Target { private Adaptee adaptee;//持有一个被适配类的实例 ObjectAdapter(Adaptee adaptee) { this.adaptee = adaptee; } public void request() { adaptee.specificRequest(); } } // 客户端代码 public class Client { public static void main(String[] args) { Adaptee adaptee = new Adaptee(); Target target = new ObjectAdapter(adaptee); target.request(); } }
实现(对象适配器详解)
以姚明在NBA打球为例讲解一下适配器模式
业务背景
在姚明(外籍中锋)加入NBA之处,英文不熟悉,这时候就需要一位翻译在他和教练之间进行沟通。
代码
//抽象球员,包含attack和defense的方法 abstract class Player { protected String name; public Player(String name){ this.name=name; } public Player(){};//空构造为的是表现翻译者的隐身 public abstract void attack();//进攻 public abstract void defense();//防守 } //外籍中锋 public class ForeignCenter { private String name; public String getName(){ return this.name; } public void setName(String name){ this.name=name; } public void 进攻(){ System.out.println("外籍中锋"+name+"进攻"); } public void 防守(){ System.out.println("外籍中锋"+name+"防守"); } } //翻译(适配器) public class Translator extends Player { private ForeignCenter foreignCenter = new ForeignCenter(); public Translator(String name) { //super(name); foreignCenter.setName(name);//给姚明配的翻译,翻译一出生就转配给了姚明 } @Override public void attack() { foreignCenter.进攻(); } @Override public void defense() { foreignCenter.防守(); } } //客户端 public class Client { public static void main(String[] args) { Player center=new Translator("姚明");//翻译隐身 System.out.println(center.name);//这里会显示null center.attack(); center.defense();//姚明其实听不懂这两句,需要翻译 } }
这里有两个不容易觉察又很重要的地方:
1、在翻译者类当中,attach和defense两个方法,实际分别调用的都是外籍中锋的进攻和防守方法,这里是用关联的方式实现的逻辑继承,关于这点在后面会细说。
2、这里做了个小埋伏,让我们看到翻译者的“隐身”
客户端给翻译者传的名字是姚明,结合业务就是教练叫姚明进攻,姚明听不懂,但是翻译听懂了,翻译再翻译给姚明,“姚明”通过翻译者的构造函数传给谁了呢?看下面的代码
可以看到这个名字是赋给了外籍中锋,这里我特意在球员类里面添加了一个空的构造函数
现在来看,客户端中的Translator表面看是Player类型,通过构造函数传进去的名字,也给了ForeignCenter,所以,Center.name就是null,这也是适配器的魅力所在,在无形中解决了接口不相容的问题。
常见问题
为什么有适配器模式
适配器模式的主要目的是解决两个不兼容的接口之间的兼容性问题。在软件开发过程中,经常会遇到以下情况导致接口不兼容:
系统需要使用已存在的类,但其接口与系统要求的接口不一致:当我们需要使用某个类的功能,但其接口与我们现有的系统接口不同,无法直接对接,这时候适配器模式可以通过创建适配器来将已存在的类的接口转换为系统要求的接口,从而使这个类能够被系统使用。
需要复用一些功能类,但这些类的接口与系统接口不兼容:在系统设计过程中可能会存在一些功能优秀的类,我们希望能够将它们复用于系统中,但是由于这些类的接口与系统接口不一致,无法直接复用,这时候适配器模式可以通过创建适配器来将这些类的接口转换为系统接口,从而让它们能够被复用。
适配器模式的出现可以降低代码的耦合性,使得不兼容的接口能够协调工作。它能够提高代码的可复用性和灵活性,并且将适配过程封装在适配器中,对客户端隐藏了具体的实现细节,符合面向对象设计原则中的封装和抽象原则。
适配器模式告诉我们什么
在软件设计中尽量避免使用适配器模式,而是在接口设计阶段就考虑兼容性问题并设计好接口。会的目的是不出现这样的问题。
在软件设计中,应该尽可能提前考虑这些因素,并遵循良好的接口设计原则,以避免后期出现兼容性问题。这包括:
- 接口设计要明确、简洁、易于理解和使用,遵循单一职责原则和接口隔离原则。
- 合理规划系统的接口,预见可能的变化和需求,避免频繁修改接口。
- 使用设计模式和设计原则来约束和指导接口设计,例如依赖倒置原则、开闭原则等。
总之,尽管适配器模式在某些情况下是非常有用的,但在软件设计过程中,应该尽量避免使用适配器模式,而是通过良好的接口设计来解决兼容性问题。这样可以提高代码的可读性、可维护性和可扩展性,减少后期修改和重构的工作量。
适配器模式体现了哪些设计原则
单一职责原则(Single Responsibility Principle):每个类都只有一个职责,Adaptee 是适配器模式中的已有类,Adapter 是适配器类,Target 是目标接口,它们各自承担不同的职责。适配器类的职责就是进行接口转换,只有一个职责。
开闭原则(Open-Closed Principle):适配器模式通过引入适配器类,可以在不修改原有代码的情况下,实现与新接口的兼容,符合开闭原则的要求。
接口隔离原则(Interface Segregation Principle):抽象目标接口 Target 只包含客户端所需的方法,避免了客户端直接依赖于不需要的方法。
关联方式实现了逻辑继承
Adapter 类通过关联一个 Adaptee 对象来实现适配器功能。当 Adapter 的 request() 方法被调用时,它实际上会调用被适配类 Adaptee 的 specificRequest() 方法。(请配合上面的类图看)。适配器类(Adapter)包含一个被适配的类(Adaptee)的实例,并在目标接口的方法中调用被适配类的相应方法来实现适配。作为一个中间层,适配器类(Adapter)不仅提供了目标接口的实现,还继承了被适配类(Adaptee)的功能,实现了逻辑上的继承关系。当客户端调用适配器类的方法时,实际上是通过适配器类来调用被适配类的方法,这就体现了适配器类对被适配类的逻辑继承。
适配器模式在SpringMVC框架应用
SpringMvc 中的 HandlerAdapter , 就使用了适配器模式
使用 HandlerAdapter 的原因:处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果需要直接调用Controller方法,需要调用的时候就得不断是使用if else来进行判断是哪一种子类然后执行。那么如果后面要扩展Controller,就得修改原来的代码,这样违背了开闭原则。
Spring 定义了一个适配接口,使得每一种 Controller 有一种对应的适配器实现类,适配器代替controller执行相应的方法,扩展 Controller 时,只需要增加一个适配器类就完成了SpringMVC的扩展了。
总结
适配器模式是一种非常实用的设计模式,通过将不兼容的接口转换为目标接口,使得原本无法协同工作的类能够协作。它能够提高代码的复用性和灵活性,并且将实现细节封装在适配器中,对客户端隐藏具体的实现细节。但是,重点是学习适配器模式的目的是让我们尽量避免使用它,在软件设计中,应该尽可能提前考虑这些因素,并遵循良好的接口设计原则,以避免后期出现兼容性问题。