4.3 模 式 讲 解
4.3.1 认识适配器模式
1.模式的功能
适配器模式的主要功能是进行转换匹配,目的是复用已有的功能,而不是来实现新的接口。也就是说,客户端需要的功能应该是已经实现好了的,不需要适配器模式来实现,适配器模式主要负责把不兼容的接口转换成客户端期望的样子就可以了。
但这并不是说,在适配器里面就不能实现功能。适配器里面可以实现功能,称这种适配器为智能适配器。再说了,在接口匹配和转换的过程中,也有可能需要额外实现一定的功能,才能够转换过来,比如需要调整参数以进行匹配等。
2.Adaptee和Target的关系
适配器模式中被适配的接口Adaptee和适配成为的接口Target是没有关联的,也就是说,Adaptee和Target中的方法既可以相同,也可以不同。极端情况下两个接口里面的方法可能是完全不同的,当然这种情况下也可以完全相同。
这里所说的相同和不同,是指方法定义的名称、参数列表、返回值,以及方法本身的功能都可以相同或不同。
3.对象组合
根据前面的实现,你会发现,适配器的实现方式其实是依靠对象组合的方式。通过给适配器对象组合被适配的对象,然后当客户端调用Target的时候,适配器会把相应的功能委托给被适配的对象去完成。
4.适配器模式的调用顺序示意图
适配器模式的调用顺序如图4.11所示。
4.3.2 适配器模式的实现
1.适配器的常见实现
在实现适配器的时候,适配器通常是一个类,一般会让适配器类去实现Target接口,然后在适配器的具体实现里面调用Adaptee。也就是说适配器通常是一个Target类型,而不是Adaptee类型。如同前面的例子演示的那样。
2.智能适配器
在实际开发中,适配器也可以实现一些Adaptee没有实现,但是在Target中定义的功能。这种情况就需要在适配器的实现里面,加入新功能的实现。这种适配器被称为智能适配器。
如果要使用智能适配器,一般新加入功能的实现会用到很多Adaptee的功能,相当于利用Adaptee的功能来实现更高层的功能。当然也可以完全实现新加入的功能,和已有的功能都不相关,变相地扩展了功能。
3.适配多个Adaptee
适配器在适配的时候,可以适配多个Adaptee,也就是说实现某个新的Target的功能的时候,需要调用多个模块的功能,适配多个模块的功能才能满足新接口的要求。
4.适配器Adapter实现的复杂程度
适配器Adapter实现的复杂程度取决于Target和Adaptee的相似程度。
如果相似程度很高,比如只有方法名称不一样,那么Adapter只需要简单地转调一下接口就可以了。
如果相似程度低,比如两边接口的方法所定义的功能完全不一样,在Target中定义的一个方法,可能在Adaptee中定义了三个更小的方法,那么这个时候在实现Adapter的时候,就需要组合调用了。
5.缺省适配
缺省适配的意思是,为一个接口提供缺省实现。有了它,就不用直接去实现接口,而是采用继承这个缺省适配对象,从而让子类可以有选择地去覆盖实现需要的方法,对于不需要的方法,使用缺省适配的方法就可以了。
4.3.3 双向适配器
适配器也可以实现双向的适配,前面我们讲的都是把Adaptee适配成为Target,其实也可以把Target适配成为Adaptee。也就是说这个适配器可以同时当作Target和Adaptee来使用。
继续前面讲述的例子。如果说由于某些原因,第一版和第二版会同时共存一段时间,比如第二版的应用还在不断调整中,也就是第二版还不够稳定。客户提出,希望在两版共存期间,主要还是使用第一版,同时希望第一版的日志也能记录到数据库中,也就是客户虽然操作的接口是第一版的日志接口,界面也是第一版的界面,但是可以使用第二版的将日志记录到数据库的功能。
也就是说希望两版能实现双向的适配,结构如图4.12所示。
图4.12 双向适配器示意图
下面用简单的代码示意一下,以利于大家理解。
这里只加了几个新的东西,一个是DB存储日志的实现,前面的例子中没有,因为直接被适配成使用文件存储日志的实现了;另外一个就是双向适配器,其实与把文件存储的方式适配成为DB实现的接口是一样的,只需要新加上把DB实现的功能适配成为文件实现的接口就可以了。
(1)先看看DB存储日志的实现。为了简单,这里不再真正地实现和数据库交互了,示意一下就可以了。示例代码如下:
(2)然后看看新的适配器的实现。
由于是双向的适配器,一个方向是:把新的DB实现的接口适配成为旧的文件操作需要的接口;另外一个方向是把旧的文件操作的接口适配成为新的DB实现需要的接口。示例代码如下:
(3)下面看看如何使用这个双向适配器。示例代码如下:
运行一下,看看结果,体会一下双向适配器。
注意
事实上,使用适配器有一个潜在的问题,就是被适配的对象不再兼容Adaptee的接口,因为适配器只是实现了Target的接口。这导致并不是所有Adaptee对象可以被使用的地方都能使用适配器。
而双向适配器就解决了这样的问题,双向适配器同时实现了Target和Adaptee的接口,使得双向适配器可以在Target或Adaptee被使用的地方使用,以提供对所有客户的透明性。尤其在两个不同的客户需要用不同的方式查看同一个对象时,适合使用双向适配器。
4.3.4 对象适配器和类适配器
在标准的适配器模式里面,根据适配器的实现方式,把适配器分成了两种,一种是对象适配器,另一种是类适配器。
对象适配器的实现:依赖于对象组合。就如同前面的实现示例,都是采用对象组合的方式,也就是对象适配器实现的方式。
类适配器的实现:采用多重继承对一个接口与另一个接口进行匹配。由于Java不支持多重继承,所以到目前为止还没有涉及。
1.类适配器
前面已经学习过对象适配器了,下面简单地介绍一下类适配器。首先来看看类适配器的结构,如图4.13所示。
图4.13 类适配器的结构示意图
从结构图上可以看出,类适配器是通过继承来实现接口适配的,标准的设计模式中,类适配器是同时继承Target和Adaptee的,也就是一个多重继承,这在Java里面是不被支持的,也就是说Java中是不能实现标准的类适配器的。
但是Java中有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现Target的接口,然后继承Adaptee的实现,虽然不是十分标准,但是意思差不多。下面就来看个小示例。
2.Java中类似实现类适配器的例子
还是来实现前面的那个示例,就是让文件存储日志的实现,能够经过适配,满足第二版日志操作接口的要求。
基本的实现方式是:写一个适配器类,让适配器类去继承文件存储日志的实现,然后让适配器类去实现第二版日志操作接口的要求。
这样实现的示例整体结构如图4.14所示。
图4.14 类似类适配器示例的结构示意图
在实现中,主要是适配器的实现与以前不一样,与对象适配器实现同样的功能相比,类适配器在实现上有如下改变。
■ 需要继承LogFileOperate的实现,然后再实现LogDbOperateApi接口。
■ 需要按照继承LogFileOperate的要求,提供传入文件路径和名称的构造方法。
■ 不再需要持有LogFileOperate的对象了,因为适配器本身就是LogFileOperate对象的子类了。
■ 以前调用被适配对象的方法的地方,全部修改成调用自己的方法。
真正功能的实现,类适配器和对象适配器两种方式都差不多。示例代码如下:
自己写个客户端去测试看看,体会一下。
3.类适配器和对象适配器的权衡
■ 从实现上:类适配器使用对象继承的方式,是静态的定义方式;而对象适配器使用对象组合的方式,是动态组合的方式
■ 对于类适配器,由于适配器直接继承了Adaptee,使得适配器不能和Adaptee的子类一起工作,因为继承是静态的关系,当适配器继承了Adaptee后,就不可能再去处理Adaptee的子类了。
对于对象适配器,允许一个Adapter和多个Adaptee,包括Adaptee和它所有的子类一起工作。因为对象适配器采用的是对象组合的关系,只要对象类型正确,是不是子类都无所谓。
■ 对于类适配器,适配器可以重定义Adaptee的部分行为,相当于子类覆盖父类的部分实现方法。
对于对象适配器,要重定义Adaptee的行为比较困难,这种情况下,需要定义Adaptee的子类来实现重定义,然后让适配器组合子类。
■ 对于类适配器,仅仅引入了一个对象,并不需要额外的引用来间接得到Adaptee。对于对象适配器,需要额外的引用来间接得到Adaptee。
在Java开发中,建议大家尽量使用对象适配器的实现方式。当然,具体问题具体分析,根据需要来选用实现方式,最合适的才是最好的。
4.3.5 适配器模式的优缺点
适配器模式有如下优点。
■ 更好的复用性
如果功能是已经有了的,只是接口不兼容,那么通过适配器模式就可以让这些功能得到更好的复用。
■ 更好的可扩展性
在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
适配器模式有如下缺点。
■ 过多地使用适配器,会让系统非常零乱,不容易整体进行把握
比如,明明看到调用的是A接口,其实内部被适配成了B接口来实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
4.3.6 思考适配器模式
1.适配器模式的本质
适配器模式的本质是:转换匹配,复用功能
适配器通过转换调用已有的实现,从而能把已有的实现匹配成需要的接口,使之能满足客户端的需要。也就是说转换匹配是手段,而复用已有的功能才是目的。
在进行转换匹配的过程中,适配器还可以在转换调用的前后实现一些功能处理,也就是实现智能的适配。
2.何时选用适配器模式
建议在以下情况中选用适配器模式。
■ 如果你想要使用一个已经存在的类,但是它的接口不符合你的需求,这种情况可以使用适配器模式,来把已有的实现转换成你需要的接口。
■ 如果你想创建一个可以复用的类,这个类可能和一些不兼容的类一起工作,这种情况可以使用适配器模式,到时候需要什么就适配什么。
■ 如果你想使用一些已经存在的子类,但是不可能对每一个子类都进行适配,这种情况可以选用对象适配器,直接适配这些子类的父类就可以了。
4.3.7 相关模式
■ 适配器模式与桥接模式
其实这两个模式除了结构略为相似外,功能上完全不同。
适配器模式是把两个或者多个接口的功能进行转换匹配;而桥接模式是让接口和实现部分相分离,以便它们可以相对独立地变化。
■ 适配器模式与装饰模式
从某种意义上讲,适配器模式能模拟实现简单的装饰模式的功能,也就是为已有功能增添功能。比如我们在适配器里面这么写:
如上的写法,就相当于在调用Adaptee的被适配方法前后添加了新的功能,这样适配过后,客户端得到的功能就不单纯是Adaptee的被适配方法的功能了。看看是不是类似装饰模式的功能呢?
注意
注意,仅仅是类似,造成这种类似的原因是:两种设计模式在实现上都是使用的对象组合,都可以在转调组合对象的功能前后进行一些附加的处理,因此有这么一个相似性。它们的目的和本质都是不一样的。
两个模式有一个很大的不同:一般适配器适配过后是需要改变接口的,如果不改接口就没有必要适配了;而装饰模式是不改变接口的,无论多少层装饰都是一个接口。因此装饰模式可以很容易地支持递归组合,而适配器就做不到,每次的接口不同,无法递归。
■ 适配器模式和代理模式
适配器模式可以和代理模式组合使用。在实现适配器的时候,可以通过代理来调用Adaptee,这样可以获得更大的灵活性。
■ 适配器模式和抽象工厂模式
在适配器实现的时候,通常需要得到被适配的对象。如果被适配的是一个接口,那么就可以结合一些可以创造对象实例的设计模式,来得到被适配的对象示例,比如抽象工厂模式、单例模式、工厂方法模式等。