文章目录
简介
设计模式是面向对象软件的设计经验,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。每一种设计模式系统的命名、解释和评价了面向对象中一个重要的和重复出现的设计。
结构模式主要关注类和对象的组合,具体有如下几种:
- 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 装饰模式(Decorator Pattern)
- 组合模式(Composite Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
适配器模式
通俗解释
在朋友聚会上碰到了一个美女 Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友 kent 了,他作为我和 Sarah 之间的 Adapter,让我和 Sarah 可以相互交谈了 (也不知道他会不会耍我)
适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。
概念
适配器模式 (Adapter pattern) 是一种结构型设计模式,帮助我们实现两个不兼容接口之间 的兼容。比如,去香港或欧洲旅游想给手机充电,则需要一个插座适配器,来转换不同的插头及电压等标准。再比如在代码升级过程中,一个老接口很复杂,想对其进行复用,但接口参数/返回值格式与当前不同,这样的情况下我们使用适配器是一种很好的方式,在不该动源代码的情况下适配当前代码。
如上图所示,Adapter和Adaptee是关联关系,但Adapter和Adaptee也可以是继承关系,这种情况一般用于Adaptee大部分成员函数已经和Target一致,只有少部分需要修改,使用继承能够减少代码改动。如果Adaptee大部分成员函数和Target不一致,最好还是用组合,毕竟组合优于继承。当然对Go而言就无所谓了,反正只有组合没有继承,而且匿名组合能够直接复用组合对象的功能。
适配器模式的使用也比较简单,核心就是用Adapter重新封装一下Adaptee,使其符合Target的要求。
应用场景
- 封装有缺陷的接口设计:例如如果引入的外部系统接口设计方面有缺陷,会影响我们自身代码的可测性等,就可以考虑使用适配器模式,将引入的系统向我们自身系统设计上靠拢
- 统一多个类的接口设计:如果一个功能依赖多个外部系统,且这些外部系统的能力是相似的但接口不统一,可以使用适配器模式,依赖于继承、多态的特性,使调用方可以以聚合方式使用外部系统,提升代码扩展性
- 替换依赖的外部系统:如果一个功能有多个外部系统可供选择,我们可以定义一个Target接口,将外部系统适配为Target,这样就能利用多态性,实现外部系统的替换
- 兼容老版本接口:老版本中功能A在新版本中被废弃,A将由B替代,为了不影响使用者,新版本中仍然会有A,但是其内部实现委托B执行
- 适配不同格式的数据:有时数据来源不同,数据格式也不同,需要对数据做适配,改为统一格式后再处理,也可使用适配器模式
优点
提高类的透明性和复用,现有的类复用但不需要改变 目标类和和适配器类解耦,提高程序扩展性 符合开闭原则
缺点
适配器在编写过程中需要全面考虑,可能会增加系统的复杂性 增加系统代码可读的难度
实例演示
实例1
对账,是指从第三方支付公司拉取指定时间内的支付单信息,与系统内部支付单信息做对比,主要用来发现支付异常
-
支付网关有数据,第三方没有数据
- 可能被黑客攻击了,用户没有真正支付,但是我们发货了
- 代码有问题,用户没有完成支付,但是系统认为支付成功了
- 第三方提供数据不全
-
支付网关没有数据,第三方有数据
- 用户支付成功,但是同步或者异步通知都失败了
-
金额不一致
-
代码有问题,电商发起支付金额和真正调用第三方金额不一致
-
第三方提供数据有问题
-
做对比的逻辑是一致的,但是第三方支付账单数据格式不一致,所以需要先将这些数据转化为标准格式。
代码实现:
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 与系统支付单进行对账
对账完成
实例2
现在有一个运维系统,需要分别调用阿里云和 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)
}
桥接模式
通俗解释
早上碰到 MM,要说早上好,晚上碰到 MM,要说晚上好;碰到 MM 穿了件新衣服,要说你的衣服好漂亮哦,碰到MM新做的发型,要说你的头发好漂亮哦。不要问我 “早上碰到 MM 新做了个发型怎么说” 这种问题,自己用 BRIDGE 组合一下不就行了。
桥接模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合 / 聚合关系而不是继承关系,从而使两者可以独立的变化。
概念
桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的