在生活中,想用苹果充电线给安卓的手机充电时,因为两者的接口不一样,会导致充电口无法进行匹配, 这时候,就需要适配器,将安卓的充电口转化为苹果的接口,这样就可以充电啦.已有的类与新的接口不兼容问题是很普遍的, 人们在日常的生产中,为找到了一个解决方案,以上面的例子引出今天要讲的设计模式--适配器模式.
定义与结构
1.1 定义
适配器模式将一个类的接口变成客户端所期待的另种接口, 从而使原本的接口不匹配无法在一起工作的两个类能够一起工作.
在适配器模式,存在两种不同的模式结构: 类的适配器模式和对象的适配器模式.
1.2 对象的适配器模式
1.2.1 适配器模式设计三个角色
- Target(目标抽象类): 目标抽象类定义为客户所需接口, 是个抽象类或者接口, 也可以是具体类. 例如对于上面的例子,客户端的目标是给只接受安卓充电口的安卓手机充电, 所以目标抽象类就是安卓充电线的接口.
- Adaptee(适配者类): 适配者类是被适配的角色, 定义了一个已经存在的接口, 接口需要适配, 适配者类一般是一个具体类, 包含了客户希望使用的业务方法. 例如苹果充电线就是适配者类.
- Adapter(适配器类): 通过需要包装一个需要适配的对象,将原接口转化成目标接口. 例如为了充电, 需要一个适配器, 使之一边可以连接安卓充电的接口, 一边可以连接苹果充电线的接口.
1.2.2 UML图
1.2.3 代码示例
(1) Target类
public class Android { public void isAndroid(){ System.out.println("这是一个只接受安卓充电线的插口"); } }
(2)Adaptee类
public class Iphone { public void isIphone(){ System.out.println("这是一个适配苹果充电线的插口"); } }
(3)Adapter类: 把两者进行适配
/** * 适配器,作为中间件,把他们进行适配 */ public class Adapter extends Android{ private Iphone iphone; public Adapter(Iphone iphone){ this.iphone = iphone; } @Override public void isAndroid() { iphone.isIphone(); } }
(4) 测试
public class Demo { public static void main(String[] args){ Android android = new Adapter(new Iphone()); //调用的是安卓的接口,但实际上 //确实一个可以接受苹果充电器的接口 android.isAndroid(); } }
对于这种对象的适配器模式, 实际上就是通过一个适配器类,把目标类和需要被适配的类进行组合。所以适配器类Adapter一般需要继承或实现Targert,并且还得持有Adaptee的实例引用。
1.3 类的适配器模式
1.3.1 定义
除了上面的对象适配器模式之外, 还有另外一种类适配器模式. 在这种模式中, Adapter不持有Adaptee的实例引用,而是直接继承了Adaptee类, 然后再实现Target接口. 或者直接继承Adaptee类和Target类, 但由于Java不支持多重继承, 所以只能实现Target的方式.
1.3.2 代码
(1) Target接口类
interface Android { void isAndroid(); }
(2) Adaptee类
public class Iphone { public void isIphone(){ System.out.println("这是一个适配苹果充电线的接口"); } }
(3) Adapter类:继承Adaptee,实现Target
/** * 适配器,把安卓手机的插口转化为可以用苹果充电线充电的接口 */ public class Adapter extends Iphone implements Android{ @Override public void isAndroid() { //直接调用 isIphone(); } }
(4) 测试类
public class Demo { public static void main(String[] args){ Android android = new Adapter(); android.isAndroid(); } }
这两种最重要的区别是:
对象适配器模式通过组合来实现适配器功能;
类适配器模式通过多继承来实现Target来实现适配器功能.
实例
2.1 适用型
- 你想使用一个已经存在的类,而它的接口不符合你的需求,即已有类的接口与需求不匹配
- 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作
- (仅适用于对象Adapter)你想使用一些已存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口,对象适配器可以适配它的父类接口
2.2 委托
适配器模式是把类的接口变换为客户端要求的另一种接口,这里的客户端它们是什么?它们是Cocoa Touch框架中的类,那么此处什么是Target呢?是一个委托协议,实现协议的具体类会是个适配器,那么什么是与框架不匹配而需要适配的类呢?应用程序中的其他类,所以为什么委托模式其实是适配器模式。我们在Cocoa Touch框架中见过的许多框架类,是用协议中定义的某种形式的委托来实现的,我们可以把自己的委托实现为适配器。
2.3 简单实用
//协议 角色中的Target protocol Target{ func userExpectInterface() } //类 角色中的Adaptee class Adaptee: NSObject { func doSomething(){ print("adaptee doing something!") } } //类适配器 经常父类,遵守协议 class ClassAdapter: Adaptee,Target { func userExpectInterface() { super.doSomething() } } //对象适配器 遵守协议,拥有其它类对象,间接引用 class ObjectAdapter:Target{ let adaptee = Adaptee() func userExpectInterface() { adaptee.doSomething() }
}
2.4 Demo讲解
我们经常需要使用第三方的服务,可以使用适配器模式对第三方的接口进行适配,比如:使用谷歌登录
1. 声明协议
// target public protocol AuthenticationService { func login(email: String, password: String, success: @escaping (User, Token) -> Void, failure: @escaping (Error?) -> Void) }
2. 原有的登录方法
// adaptee public class GoogleAuthenticator { public func login(email: String, password: String, completion: @escaping (GoogleUser?, Error?) -> Void) { let token = "special-token-value" let user = GoogleUser(email: email, password: password, token: token) completion(user, nil) } }
3、相关数据结构
public struct GoogleUser { public var email: String public var password: String public var token: String } public struct User { public let email: String public let password: String } public struct Token { public let value: String }
4、适配器类
// adapter public class GoogleAuthenticatorAdapter: AuthenticationService { private var authenticator = GoogleAuthenticator() public func login(email: String, password: String, success: @escaping (User, Token) -> Void, failure: @escaping (Error?) -> Void) { authenticator.login(email: email, password: password) { (googleUser, error) in guard let googleUser = googleUser else { failure(error) return } let user = User(email: email, password: password) let token = Token(value: googleUser.token) success(user, token) } } }
5、简单实用
var authService: AuthenticationService = GoogleAuthenticatorAdapter() authService.login(email: "user@example.com", password: "password", success: { (user, token) in print("auth succeeded: (user.email), (token.value)") }) { (error) in print("auth failed") }
优缺点
3.1 优点
- 更好的复用性:系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。
- 更好的扩展性:在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
- 灵活性非常好:不想要适配器时,删掉这个适配器就好了,其他代码不用改。
3.2 缺点
- 过多的使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
总结
适配器模式有点如上,其缺点也存在,很多东西本来可以直接了当,用了适配器后就多了一大坨代码。当然如果把上面的实现在拆分下文件,那么显然如果不知道适配器原理的人 就很难理解代码why了。但是对于项目的长远来看,如果可以写出可变性好的代码,偶尔降低代码的可读性 也是可以接受的,毕竟这些模式都很金典,看不懂只能说明学得还不够多。