重修设计模式-结构型-适配器模式
重修设计模式-结构型-适配器模式
将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作
适配器模式(Adapter Pattern)允许将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。简单来说,适配器解决的就是接口不兼容问题,引入适配器充当中间桥梁,从而让不兼容接口协同工作。
适配器模式的三个角色:
- 目标接口(Target):客户端所期望的接口,可以是抽象类、接口或具体的类。
- 需要适配的类(Adaptee):需要适配的类或接口,它通常是一个已经存在的、与客户端期望的接口不兼容的类。
- 适配器(Adapter):适配器类的核心,负责将 Adaptee 的接口转换成 Target 的接口。适配器类通常是具体类,通过继承或组合的方式与 Adaptee 发生关联。
适配器模式的两种实现:
-
类适配器:使用继承来实现适配。
通过继承将一个接口与另一个接口进行匹配,比较适合
Adaptee
和Target
定义相似的场景,这种场景使用继承可以让代码少一些,代码可读性会高一些。 -
对象适配器:使用组合来实现适配。
通过组合的方式,使用适配器类将需要适配的类(Adaptee)的实例包装起来,适配器类通过调用
Adaptee
的实例方法来实现目标接口。
举个例子,程序中存在这样的逻辑,从数据源A(DataSourceA) 处获取用户数据,并调用 saveUser 进行用户数据的处理,saveUser 只接收 IUser 接口类型的数据。这时需求变更,需要扩展出另一个数据源B(DataSourceB),但该数据源返回的数据类型为 UserB,saveUser 并不能接收 UserB 类型数据,代码表示如下:
//Target
interface IUser {
fun getName(): String
fun getAge(): Int
fun getSign(): String?
}
//数据源A
object DataSourceA {
//模拟返回数据
fun getData(): IUser {
return object: IUser {
override fun getName(): String = "白泽"
override fun getAge(): Int = 18
override fun getSign(): String? = "人生得意须尽欢"
}
}
}
//Adaptee
class UserB(val nick: String, val birthday: String)
//新增的数据源B
object DataSourceB {
//模拟返回数据
fun getData(): UserB {
return UserB("白泽", "1996")
}
}
//处理User数据
fun saveUser(user: IUser) {
//处理user相关逻辑
println("处理用户信息 name:${user.getName()} age:${user.getAge()} sign:${user.getSign()}")
}
//调用处:
fun main() {
val user1 = DataSourceA.getData()
saveUser(user1) //正常调用
val user2 = DataSourceB.getData()
//saveUser(user2) //类型不兼容
}
1.使用继承类方式实现适配器,创建适配器(ExtendUser),让其继承需要适配的类(UserB),并实现目标接口(IUser ),并覆写相关逻辑:
//适配器-继承
class ExtendUser(nick: String, birthday: String): UserB(nick, birthday), IUser {
override fun getName(): String {
return super.nick
}
override fun getAge(): Int {
return Date().year - (super.birthday.toIntOrNull() ?: 0)
}
override fun getSign(): String? {
return null
}
}
object DataSourceB {
...
//新增返回ExtendUser类型方法
fun getDataExtend(): ExtendUser {
return ExtendUser("白泽", "1996")
}
}
//调用处:
fun main() {
val user2 = DataSourceB.getDataExtend()
saveUser(user2)
}
其实更优方式是让 UserB 直接实现 IUser 接口,这里为了明确三个适配器角色还是额外抽出一个适配器类,但切记设计模式不能死记硬背,因地制宜才是代码设计的核心。
2.通过组合方式实现适配器,创建适配器(AdapterUser),其内持有需要适配的类的实例(UserB),并实现目标接口(IUser):
//适配器-组合
class AdapterUser(val user: UserB): IUser {
override fun getName(): String {
return user.nick
}
override fun getAge(): Int {
return Date().year - (user.birthday.toIntOrNull() ?: 0)
}
override fun getSign(): String? {
return null
}
}
fun main() {
val user2 = DataSourceB.getData()
val adapterUser = AdapterUser(user2)
saveUser(adapterUser)
}
这两种方式都可以实现适配器模式,区别在于以下几点:
- 继承方式会有语言层单继承的限制,如果 Adaptee 和 Target 都不是接口,继承方式就无能为力了。
- 继承适合 Adaptee 和 Target 接口定义大部分相同场景,用代码语义相似性来让可读性更高;组合侧重部分可以在整体内自由变化,适合更加灵活的场景。
代理、桥接、装饰器、适配器 4 种设计模式的区别:
代理、桥接、装饰器、适配器,这 4 种模式的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。
尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。
代理模式
:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,独立思考非相关功能,这是它跟装饰器模式最大的不同。
桥接模式
:桥接模式的目的是将抽象部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式
:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式
:适配器模式既可以视作补救策略,也可以视作统一规范。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
适配器模式使用场景
一方面适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷,协调规避接口不兼容的问题。另一方面,适配器也可以在设计之初特意为变化预留接口,定制适配规范。
比如 Android 中列表控件 RecyclerView 就是适配器模式的经典运用,RecyclerView.Adapter 是抽象出来的适配规范,RecyclerView 内部只需要关注 View 的缓存,绘制和展示即可,而无需关注具体 View 是哪个,如何绑定数据等操作,这些逻辑都由调用方自己实现适配器并处理。 JVM 虚拟机也可以用适配器的思想去看,JVM 就相当于一个适配器,它向下屏蔽了各种系统的差异,任何语言只需要遵循 JVM 的相关规范,都可以运行在 JVM 虚拟机上,比如 Java、Kotlin、Groovy、JRuby、Jython、Scala等语言都是基于 JVM 规范而开发出来的。