适配器模式比较简单,也比较容易理解。适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。
适配器模式:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
UML:
通过UML图看到Adapter和Adaptee是关联关系,但Adapter和Adaptee也可以是继承关系,这种情况一般用于Adaptee大部分成员函数已经和Target一致,只有少部分需要修改,使用继承能够减少代码改动。如果Adaptee大部分成员函数和Target不一致,最好还是用组合,毕竟组合优于继承。当然对Go而言就无所谓了,反正只有组合没有继承,而且匿名组合能够直接复用组合对象的功能。
适配器模式的使用也比较简单,核心就是用Adapter重新封装一下Adaptee,使其符合Target的要求。
遇到如下几种场景的时候可以考虑使用适配器模式:
所谓对账,是指从第三方支付公司拉取指定时间内的支付单信息,与系统内部支付单信息做对比,主要用来发现支付异常
1.支付网关有数据,第三方没有数据
2.支付网关没有数据,第三方有数据
3.金额不一致
做对比的逻辑是一致的,但是第三方支付账单数据格式不一致,所以需要先将这些数据转化为标准格式。
package main
import (
"fmt"
"time"
)
/**
* @Author: Jason Pang
* @Description: 对账单数据
*/
type StatementItem struct {
OrderId string //系统单号
TransactionId string //第三方交易号
Amount int64 //支付金额,单位:分
PaymentTime int64 //订单支付时间
}
/**
* @Author: Jason Pang
* @Description: 从第三方获取对账数据
*/
type StatementData interface {
GetStatementData(startTime int64, endTime int64) []*StatementItem
}
/**
* @Author: Jason Pang
* @Description: WX支付
*/
type WXStatementData struct {
}
func (w *WXStatementData) GetStatementData(startTime int64, endTime int64) []*StatementItem {
fmt.Println("从WX获取到的对账数据,支付时间需要格式化为时间戳")
return []*StatementItem{
{
OrderId: "WX订单222",
TransactionId: "WX支付单号",
Amount: 999,
PaymentTime: time.Date(2014, 1, 7, 5, 50, 4, 0, time.Local).Unix(),
},
}
}
/**
* @Author: Jason Pang
* @Description: ZFB支付
*/
type ZFBStatementData struct {
}
func (z *ZFBStatementData) GetStatementData(startTime int64, endTime int64) []*StatementItem {
fmt.Println("从ZFB获取到的对账数据,金额需要从元转化为分")
return []*StatementItem{
{
OrderId: "ZFB订单111",
TransactionId: "ZFB支付单号",
Amount: 99.9 * 100,
PaymentTime: 1389058332,
},
}
}
/**
* @Author: Jason Pang
* @Description: 对账函数
* @param list 从第三方获取的对账单
* @return bool
*/
func DoStatement(list []*StatementItem) bool {
fmt.Println("开始对账")
fmt.Println("从自身系统中获取指定时间内的支付单")
for _, item := range list {
fmt.Println(item.OrderId + " 与系统支付单进行对账")
}
fmt.Println("对账完成")
return true
}
func main() {
wx := &WXStatementData{}
zfb := &ZFBStatementData{}
stattementData := []StatementData{
wx,
zfb,
}
for _, s := range stattementData {
DoStatement(s.GetStatementData(1389058332, 1389098332))
}
}
➜ myproject go run main.go
从WX获取到的对账数据,支付时间需要格式化为时间戳
开始对账
从自身系统中获取指定时间内的支付单
WX订单222 与系统支付单进行对账
对账完成
从ZFB获取到的对账数据,金额需要从元转化为分
开始对账
从自身系统中获取指定时间内的支付单
ZFB订单111 与系统支付单进行对账
对账完成
PS:代码中定义了对账单的结构,今后对接新的支付方式,只要实现了StatementData接口,就能参与对账,扩展性极好。
假设我现在有一个运维系统,需要分别调用阿里云和 AWS 的 SDK 创建主机,两个 SDK 提供的创建主机的接口不一致,此时就可以通过适配器模式,将两个接口统一。
PS:AWS 和 阿里云的接口纯属虚构,没有直接用原始的 SDK,只是举个例子
package adapter
import "fmt"
// ICreateServer 创建云主机
type ICreateServer interface {
CreateServer(cpu, mem float64) error
}
// AWSClient aws sdk
type AWSClient struct{}
// RunInstance 启动实例
func (c *AWSClient) RunInstance(cpu, mem float64) error {
fmt.Printf("aws client run success, cpu: %f, mem: %f", cpu, mem)
return nil
}
// AwsClientAdapter 适配器
type AwsClientAdapter struct {
Client AWSClient
}
// CreateServer 启动实例
func (a *AwsClientAdapter) CreateServer(cpu, mem float64) error {
a.Client.RunInstance(cpu, mem)
return nil
}
// AliyunClient aliyun sdk
type AliyunClient struct{}
// CreateServer 启动实例
func (c *AliyunClient) CreateServer(cpu, mem int) error {
fmt.Printf("aws client run success, cpu: %d, mem: %d", cpu, mem)
return nil
}
// AliyunClientAdapter 适配器
type AliyunClientAdapter struct {
Client AliyunClient
}
// CreateServer 启动实例
func (a *AliyunClientAdapter) CreateServer(cpu, mem float64) error {
a.Client.CreateServer(int(cpu), int(mem))
return nil
}
package adapter
import (
"testing"
)
func TestAliyunClientAdapter_CreateServer(t *testing.T) {
// 确保 adapter 实现了目标接口
var a ICreateServer = &AliyunClientAdapter{
Client: AliyunClient{},
}
a.CreateServer(1.0, 2.0)
}
func TestAwsClientAdapter_CreateServer(t *testing.T) {
// 确保 adapter 实现了目标接口
var a ICreateServer = &AwsClientAdapter{
Client: AWSClient{},
}
a.CreateServer(1.0, 2.0)
}
适配器模式简单好用,用对了场景能够极大提高扩展性和优雅性。
适配器模式和代理、装饰器、桥接模式有一定相似性,我们在此处也总结一下:
代理模式: 代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
装饰器模式: 装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式: 适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
桥接模式: 桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。