前面三篇文章讲了代理模式、桥接模式、装饰器模式,这次讲一下适配器模式。
适配器模式比较简单,也比较容易理解。适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。
UML类图位置:https://www.processon.com/diagraming/609b375407912943913a4c13
本文代码链接为:https://github.com/shidawuhen/asap/blob/master/controller/design/14adapter.go
1.定义
1.1适配器模式
适配器模式:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
UML:
1.2分析
通过UML图看到Adapter和Adaptee是关联关系,但Adapter和Adaptee也可以是继承关系,这种情况一般用于Adaptee大部分成员函数已经和Target一致,只有少部分需要修改,使用继承能够减少代码改动。如果Adaptee大部分成员函数和Target不一致,最好还是用组合,毕竟组合优于继承。当然对Go而言就无所谓了,反正只有组合没有继承,而且匿名组合能够直接复用组合对象的功能。
适配器模式的使用也比较简单,核心就是用Adapter重新封装一下Adaptee,使其符合Target的要求。
2.使用场景
遇到如下几种场景的时候可以考虑使用适配器模式:
- 封装有缺陷的接口设计:例如如果引入的外部系统接口设计方面有缺陷,会影响我们自身代码的可测性等,就可以考虑使用适配器模式,将引入的系统向我们自身系统设计上靠拢
- 统一多个类的接口设计:如果一个功能依赖多个外部系统,且这些外部系统的能力是相似的但接口不统一,可以使用适配器模式,依赖于继承、多态的特性,使调用方可以以聚合方式使用外部系统,提升代码扩展性
- 替换依赖的外部系统:如果一个功能有多个外部系统可供选择,我们可以定义一个Target接口,将外部系统适配为Target,这样就能利用多态性,实现外部系统的替换
- 兼容老版本接口:老版本中功能A在新版本中被废弃,A将由B替代,为了不影响使用者,新版本中仍然会有A,但是其内部实现委托B执行
- 适配不同格式的数据:有时数据来源不同,数据格式也不同,需要对数据做适配,改为统一格式后再处理,也可使用适配器模式
我在项目中用适配器模式,主要处理数据适配。以前做支付网关的时候,每个第三方返回的数据都不一致,为了符合支付系统定义的格式,必须进行适配。这种地方特别多,就选对账功能这块讲一下,其它关于支付的内容可以参考这篇文章如何高效对接第三方支付。
3.代码实现
所谓对账,是指从第三方支付公司拉取指定时间内的支付单信息,与系统内部支付单信息做对比,主要用来发现支付异常
1.支付网关有数据,第三方没有数据
- 可能被黑客攻击了,用户没有真正支付,但是我们发货了
- 代码有问题,用户没有完成支付,但是系统认为支付成功了
- 第三方提供数据不全
2.支付网关没有数据,第三方有数据
- 用户支付成功,但是同步或者异步通知都失败了
3.金额不一致
- 代码有问题,电商发起支付金额和真正调用第三方金额不一致
- 第三方提供数据有问题
做对比的逻辑是一致的,但是第三方支付账单数据格式不一致,所以需要先将这些数据转化为标准格式。
1 | package main |
输出:
➜ myproject go run main.go
从WX获取到的对账数据,支付时间需要格式化为时间戳
开始对账
从自身系统中获取指定时间内的支付单
WX订单222 与系统支付单进行对账
对账完成
从ZFB获取到的对账数据,金额需要从元转化为分
开始对账
从自身系统中获取指定时间内的支付单
ZFB订单111 与系统支付单进行对账
对账完成
PS:代码中定义了对账单的结构,今后对接新的支付方式,只要实现了StatementData接口,就能参与对账,扩展性极好。
总结
适配器模式简单好用,用对了场景能够极大提高扩展性和优雅性。
适配器模式和代理、装饰器、桥接模式有一定相似性,我们在此处也总结一下:
代理模式: 代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
装饰器模式: 装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式: 适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
桥接模式: 桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。