06、适配器模式.md

适配器模式

目录

  • 适配器模式
      • 分析
  • 使用场景
  • 代码实现
  • 实例
    • 代码
    • 单元测试
  • 总结

*

适配器模式比较简单,也比较容易理解。适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。

适配器模式:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

UML

*

分析

通过UML图看到Adapter和Adaptee是关联关系,但Adapter和Adaptee也可以是继承关系,这种情况一般用于Adaptee大部分成员函数已经和Target一致,只有少部分需要修改,使用继承能够减少代码改动。如果Adaptee大部分成员函数和Target不一致,最好还是用组合,毕竟组合优于继承。当然对Go而言就无所谓了,反正只有组合没有继承,而且匿名组合能够直接复用组合对象的功能。

适配器模式的使用也比较简单,核心就是用Adapter重新封装一下Adaptee,使其符合Target的要求。

使用场景

遇到如下几种场景的时候可以考虑使用适配器模式:

1、 封装有缺陷的接口设计:例如如果引入的外部系统接口设计方面有缺陷,会影响我们自身代码的可测性等,就可以考虑使用适配器模式,将引入的系统向我们自身系统设计上靠拢
2、 统一多个类的接口设计:如果一个功能依赖多个外部系统,且这些外部系统的能力是相似的但接口不统一,可以使用适配器模式,依赖于继承、多态的特性,使调用方可以以聚合方式使用外部系统,提升代码扩展性
3、 替换依赖的外部系统:如果一个功能有多个外部系统可供选择,我们可以定义一个Target接口,将外部系统适配为Target,这样就能利用多态性,实现外部系统的替换
4、 兼容老版本接口:老版本中功能A在新版本中被废弃,A将由B替代,为了不影响使用者,新版本中仍然会有A,但是其内部实现委托B执行
5、 适配不同格式的数据:有时数据来源不同,数据格式也不同,需要对数据做适配,改为统一格式后再处理,也可使用适配器模式;

代码实现

所谓对账,是指从第三方支付公司拉取指定时间内的支付单信息,与系统内部支付单信息做对比,主要用来发现支付异常

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)
}

总结

适配器模式简单好用,用对了场景能够极大提高扩展性和优雅性。

适配器模式和代理、装饰器、桥接模式有一定相似性,我们在此处也总结一下:

代理模式: 代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

装饰器模式: 装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式: 适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

桥接模式: 桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: