设计模式是面向对象软件的设计经验,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。每一种设计模式系统的命名、解释和评价了面向对象中一个重要的和重复出现的设计。
结构模式主要关注类和对象的组合,具体有如下几种:
在朋友聚会上碰到了一个美女 Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友 kent 了,他作为我和 Sarah 之间的 Adapter,让我和 Sarah 可以相互交谈了 (也不知道他会不会耍我)
适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。
适配器模式 (Adapter pattern) 是一种结构型设计模式,帮助我们实现两个不兼容接口之间 的兼容。比如,去香港或欧洲旅游想给手机充电,则需要一个插座适配器,来转换不同的插头及电压等标准。再比如在代码升级过程中,一个老接口很复杂,想对其进行复用,但接口参数/返回值格式与当前不同,这样的情况下我们使用适配器是一种很好的方式,在不该动源代码的情况下适配当前代码。
如上图所示,Adapter和Adaptee是关联关系,但Adapter和Adaptee也可以是继承关系,这种情况一般用于Adaptee大部分成员函数已经和Target一致,只有少部分需要修改,使用继承能够减少代码改动。如果Adaptee大部分成员函数和Target不一致,最好还是用组合,毕竟组合优于继承。当然对Go而言就无所谓了,反正只有组合没有继承,而且匿名组合能够直接复用组合对象的功能。
适配器模式的使用也比较简单,核心就是用Adapter重新封装一下Adaptee,使其符合Target的要求。
提高类的透明性和复用,现有的类复用但不需要改变 目标类和和适配器类解耦,提高程序扩展性 符合开闭原则
适配器在编写过程中需要全面考虑,可能会增加系统的复杂性 增加系统代码可读的难度
对账,是指从第三方支付公司拉取指定时间内的支付单信息,与系统内部支付单信息做对比,主要用来发现支付异常
支付网关有数据,第三方没有数据
支付网关没有数据,第三方有数据
金额不一致
代码有问题,电商发起支付金额和真正调用第三方金额不一致
第三方提供数据有问题
做对比的逻辑是一致的,但是第三方支付账单数据格式不一致,所以需要先将这些数据转化为标准格式。
代码实现:
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))
}
}
运行结果:
➜go run main.go
从WX获取到的对账数据,支付时间需要格式化为时间戳
开始对账
从自身系统中获取指定时间内的支付单
WX订单222 与系统支付单进行对账
对账完成
从ZFB获取到的对账数据,金额需要从元转化为分
开始对账
从自身系统中获取指定时间内的支付单
ZFB订单111 与系统支付单进行对账
对账完成
现在有一个运维系统,需要分别调用阿里云和 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 = &am