适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
适配器模式的一些其他名称:变压器模式、转换器模式、包装(Wrapper)模式。适配器模式可以用于增加新的方法,但是,其主要意图是转换接口。
1. 现实生活中的适配器模式
连接口径不一致的两根水管问题。假设两根水管,一根粗一根细,为了将这两根水管连接到一起,就需要用一个连接器将两个管子连接起来,如下:
2.适配器模式与概念
适配器模式有类的适配器和对象的适配器模式。如下图:
2.1 类的适配器模式
类的适配器模式把适配的类的API转换成为目标类的API,其静态结构如下:
从图中看出,Adaptee没有option()方法,而客户端期待这个方法。因此采用一个适配器Adapter,继承Adaptee并且实现Target接口,在Adapter中实现option方法,并且重用Adaptee的option2方法。由于Adapter继承Adaptee,所以为类适配器模式。
角色有:
目标(Target)角色:期待的接口,由于是类适配器模式,因此目标不可以是类。
源(Adaptee)角色:现有需要适配的接口。
适配器(Adapter)角色:适配器是本模式的核心。把原接口转换成目标接口。这一角色必须是类,不可以是接口。
代码如下:
package cn.qlq.adapter; public interface Target { void option(); void option2(); }
package cn.qlq.adapter; public class Adaptee { public void option2() { System.out.println("option2"); } }
package cn.qlq.adapter; public class Adapter extends Adaptee implements Target { @Override public void option() { System.out.println("option"); } }
效果:使用一个具体类(Adapter)把源接口适配到目标(Target)中。这一依赖,如果源以及源的子类都使用此类适配就行不通了。
由于适配器类是源的子类,因此可以在适配器类覆盖源的一些方法。
2.2 对象的适配器模式
与类适配器不同的是,此模式不使用继承,而是使用委派关系连接到Adaptee类。其结构图如下:
从图中看出,Adaptee没有option()方法,而客户端期待这个方法。因此采用一个适配器Adapter,实现Target接口。Adapter内部将option2任务委派给Adaptee,自己实现option方法。
角色有:
目标(Target)角色:期待的接口,目标可以是具体或抽象的类。
源(Adaptee)角色:现有需要适配的接口。
适配器(Adapter)角色:适配器是本模式的核心。把原接口转换成目标接口。这一角色必须是类,不可以是接口。
代码如下:
package cn.qlq.adapter; public interface Target { void option(); void option2(); }
package cn.qlq.adapter; public class Adaptee { public void option2() { System.out.println("option2"); } }
package cn.qlq.adapter; public class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { super(); this.adaptee = adaptee; } @Override public void option() { System.out.println("option"); } @Override public void option2() { adaptee.option2(); } }
效果:
(1)一个适配器可以把不同的源适配到同一个Target,换言之,同一个适配器可以把源类以及它的子类都适配到目标接口。
(2)与类的适配器模式相比,想要更换源类的方法就不容易。增加一些新的方法很容易。
2.3 类适配器模式与对象适配器模式区别
对象适配器模式可以把多种不同的源适配到同一个Target,而类的适配器模式很难实现。
如果一个被适配源类有大量的方法,使用类适配器模式比价容易(只需要Adapter类继承Adaptee即可)。
2.4 适配器模式适用场景:
1. 系统需要使用现有的类,而此类的接口不符合系统的需要。
2. 想要建立一些可以重复使用的类,使得本来接口不相容并且无关的类结合在一起工作。
3. 在设计中需要改变多个子类接口,在作用相同但名称不同的类或方法之间进行适配时。
3. 缺省适配器模式
缺省适配(Default Adapter)模式为一个接口提供缺省实现,这样子类型可以从这个缺省实现进行扩展,而不必从原有接口进行扩展。作为适配器模式的一个特例,缺省适配模式在JAVA语言中有着特殊的应用。
其实主要就是为Target提供一个默认的实现方法(该实现一般为abstract类,因为其实例化是没有意义的,用于在子类中扩展所需方法)。(JDK8之前不允许接口有实现方法,JDK8可以用default关键字声明方法)。
鲁智深的故事
和尚要做什么呢?吃斋、念经、打坐、撞钟、习武等。如果设计一个和尚接口,给出所有的和尚都需要实现的方法,那么这个接口应当如下:
public interface 和尚 { public void 吃斋(); public void 念经(); public void 打坐(); public void 撞钟(); public void 习武(); public String getName(); }
显然,所有的和尚类都应当实现接口所定义的全部方法,不然就根本通不过JAVA语言编辑器。像下面的鲁智深类就不行。
public class 鲁智深 implements 和尚{ public void 习武(){ } public String getName(){ return "鲁智深"; } }
由于鲁智深只实现了getName()和习武()方法,而没有实现任何其他的方法。因此,它根本就通不过Java语言编译器。鲁智深类只有实现和尚接口的所有的方法才可以通过Java语言编译器,但是这样一来鲁智深就不再是鲁智深了。以史为鉴,可以知天下。研究一下几百年前鲁智深是怎么剃度成和尚的,会对Java编程有很大的启发。不错,当初鲁达剃度,众僧说:“此人形容丑恶、相貌凶顽,不可剃度他",但是长老却说:”此人上应天星、心地刚直。虽然时下凶顽,命中驳杂,久后却得清净。证果非凡,汝等皆不及他。”
原来如此!看来只要这里也应上一个天星的话,问题就解决了!使用面向对象的语言来说,“应”者,实现也;“天星”者,抽象类也。
public abstract class 天星 implements 和尚 { @Override public void 吃斋() { } @Override public void 念经() { } @Override public void 打坐() { } @Override public void 撞钟() { } @Override public void 习武() { } @Override public String getName() { return null; } }
鲁智深类继承抽象类“天星”
public class 鲁智深 extends 天星 { public void 习武() { System.out.println("少林金刚圈"); } public String getName() { return "鲁智深"; } }
这个抽象的天星类便是一个适配器类,鲁智深实际上借助于适配器模式达到了剃度的目的。此适配器类实现了和尚接口所要求的所有方法。但是与通常的适配器模式不同的是,此适配器类给出的所有的方法的实现都是“平庸”的(也就是都是空方法,没有任何业务相关代码)。这种“平庸化”的适配器模式称作缺省适配模式。
其类图如下:
在很多情况下,必须让一个具体类实现某一个接口,但是这个类又用不到接口所规定的所有的方法。通常的处理方法是,这个具体类要实现所有的方法,那些有用的方法要有实现,那些没有用的方法也要有空的、平庸的实现。
这些空的方法是一种浪费,有时也是一种混乱。除非看过这些空方法的代码,程序员可能会以为这些方法不是空的。即便他知道其中有一些方法是空的,也不一定知道哪些方法是空的,哪些方法不是空的,除非看过这些方法的源代码或是文档。
缺省适配模式可以很好的处理这一情况。可以设计一个抽象的适配器类实现接口,此抽象类要给接口所要求的每一种方法都提供一个空的方法。就像帮助了鲁智深的“上应天星”一样,此抽象类可以使它的具体子类免于被迫实现空的方法。
缺省适配器模式的核心是一个缺省适配器类,这个类应该是抽象类,因为这个类不应该被实例化,其实例化也是没意义的。但是抽象类的方法应该是具体的方法,而不是抽象方法,这些方法的存在就是为了提供默认的实现,以便缺省适配器的具体子类可以按需扩展相应方法。
在任何时候,如果不准备实现一个接口的所有方法时,就可以使用“缺省适配模式”制造一个抽象类,给出所有方法的平庸(也就是空方法)的具体实现。这样,从这个抽象类再继承下去的子类就不必实现所有的方法了。