golang设计模式——结构模式

  • Post author:
  • Post category:golang





简介

设计模式是面向对象软件的设计经验,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。每一种设计模式系统的命名、解释和评价了面向对象中一个重要的和重复出现的设计。


结构模式主要关注类和对象的组合

,具体有如下几种:

  1. 适配器模式(Adapter Pattern)
  2. 桥接模式(Bridge Pattern)
  3. 装饰模式(Decorator Pattern)
  4. 组合模式(Composite Pattern)
  5. 外观模式(Facade Pattern)
  6. 享元模式(Flyweight Pattern)
  7. 代理模式(Proxy Pattern)



适配器模式



通俗解释

在朋友聚会上碰到了一个美女 Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友 kent 了,他作为我和 Sarah 之间的 Adapter,让我和 Sarah 可以相互交谈了 (也不知道他会不会耍我)

适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。



概念

适配器模式 (Adapter pattern) 是一种结构型设计模式,帮助我们实现两个不兼容接口之间 的兼容。比如,去香港或欧洲旅游想给手机充电,则需要一个插座适配器,来转换不同的插头及电压等标准。再比如在代码升级过程中,一个老接口很复杂,想对其进行复用,但接口参数/返回值格式与当前不同,这样的情况下我们使用适配器是一种很好的方式,在不该动源代码的情况下适配当前代码。

在这里插入图片描述

如上图所示,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

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

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

运行结果:

➜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)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。

这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。

桥接模式并不常用,而且桥接模式的概念比较抽象。桥接模式一般用于有多种分类的情况,

如果实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让他们独立变化,减少他们之间的耦合。

在这里插入图片描述

单看桥接模式的定义和上图,比较难理解这个模式,如果放到指定场景下,就容易理解的多。这里借用一下《大话设计模式》里的例子:

Abstraction是手机类,RefinedAbstractionA指小米手机,RefinedAbstractionB指华为手机。

我是一手机软件提供商,Implementor是手机软件,ConcreteImplementorA是游戏软件,理论上ConcreteImplementorA应该有两个子类,分别是小米手机的的游戏和华为手机的游戏,ConcretelmplementorB是通讯录软件,也有两个子类,分别是小米手机的通讯录和华为手机的通讯录。

在这里插入图片描述

看这个设计的话,大家可能觉得平平无奇,但真正设计的时候,很多同学可能不会拆开两类,有可能按照品牌来设计,如手机品牌、手机品牌下包含对应的应用,或者按照手机软件来设计,如手机软件、软件下包含对应的手机。类似于这种:

在这里插入图片描述

第二种设计肯定没有第一种设计好,但好在哪里呢?

  1. 分类更加合理。第一种手机是手机,软件是软件,分的很清晰,但是第二种手机和软件却杂糅在一起,显得很乱。
  2. 组合优于继承。第一种使用组合,使得手机和软件之间的关系很弱,使得两者可以独立变化。第二种方式使用继承,软件继承自手机,不合适,而且会使继承链路变长。
  3. 修改影响小。无论是修改哪一类或者增加哪一类,第一种方案对系统的改动都要小。

还有点需要指出,两个分类使用的是聚合,使得抽象类可以方便的关联多个实现类。



应用场景

  • 实现系统可能有多个角度分类,每一种角度都可能变化。
  • 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  • 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
  • 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。



优点

  • 抽象和实现的分离。
  • 优秀的扩展能力。
  • 实现细节对客户透明。



缺点

桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。



实例演示

触达系统业务场景:已经定义好触达的紧急情况,触达需要的数据来源不同,当运营使用的时候,根据触达紧急情况,配置好数据(文案、收件人等)即可。可以看出:一个分类是触达方式、一个分类是触达紧急情况。

代码实现:

package main

import "fmt"

/**
 * @Description: 消息发送接口
 */
type MessageSend interface {
   send(msg string)
}

/**
 * @Description: 短信消息
 */
type SMS struct {
}

func (s *SMS) send(msg string) {
   fmt.Println("sms 发送的消息内容为: " + msg)
}

/**
 * @Description: 邮件消息
 */
type Email struct {
}

func (e *Email) send(msg string) {
   fmt.Println("email 发送的消息内容为: " + msg)
}

/**
 * @Description: AppPush消息
 */
type AppPush struct {
}

func (a *AppPush) send(msg string) {
   fmt.Println("appPush 发送的消息内容为: " + msg)
}

/**
 * @Description: 站内信消息
 */
type Letter struct {
}

func (l *Letter) send(msg string) {
   fmt.Println("站内信 发送的消息内容为: " + msg)
}

/**
 * @Description: 用户触达父类,包含触达方式数组messageSends
 */
type Touch struct {
   messageSends []MessageSend
}

/**
 * @Description: 触达方法,调用每一种方式进行触达
 * @receiver t
 * @param msg
 */
func (t *Touch) do(msg string) {
   for _, s := range t.messageSends {
      s.send(msg)
   }
}

/**
 * @Description: 紧急消息做用户触达
 */
type TouchUrgent struct {
   base Touch
}

/**
 * @Description: 紧急消息,先从db中获取各种信息,然后使用各种触达方式通知用户
 * @receiver t
 * @param msg
 */
func (t *TouchUrgent) do(msg string) {
   fmt.Println("touch urgent 从db获取接收人等信息")
   t.base.do(msg)
}

/**
 * @Description: 普通消息做用户触达
 */
type TouchNormal struct {
   base Touch
}

/**
 * @Description: 普通消息,先从文件中获取各种信息,然后使用各种触达方式通知用户
 * @receiver t
 * @param msg
 */
func (t *TouchNormal) do(msg string) {
   fmt.Println("touch normal 从文件获取接收人等信息")
   t.base.do(msg)
}

func main() {
   //触达方式
   sms := &SMS{}
   appPush := &AppPush{}
   letter := &Letter{}
   email := &Email{}
   //根据触达类型选择触达方式
   fmt.Println("-------------------touch urgent")
   touchUrgent := TouchUrgent{
      base: Touch{
         messageSends: []MessageSend{sms, appPush, letter, email},
      },
   }
   touchUrgent.do("urgent情况")
   fmt.Println("-------------------touch normal")
   touchNormal := TouchNormal{ //
      base: Touch{
         messageSends: []MessageSend{sms, appPush, letter, email},
      },
   }
   touchNormal.do("normal情况")
}

运行结果:

➜ go run main.go

——————-touch urgent

touch urgent 从db获取接收人等信息

sms 发送的消息内容为: urgent情况

appPush 发送的消息内容为: urgent情况

站内信 发送的消息内容为: urgent情况

email 发送的消息内容为: urgent情况

——————-touch normal

touch normal 从文件获取接收人等信息

sms 发送的消息内容为: normal情况

appPush 发送的消息内容为: normal情况

站内信 发送的消息内容为: normal情况

email 发送的消息内容为: normal情况



总结

桥接模式符合了开放-封闭原则、里氏替换原则、依赖倒转原则。

使用桥接模式,一定要看一下场景中是否有多种分类、且分类之间有一定关联。如果符合的话,建议用桥接模式,这样不同分类可以独立变化,相互之间不影响。



装饰模式



通俗解释

Mary 过完轮到 Sarly 过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,拿出我去年在华山顶上照的照片,在背面写上 “最好的的礼物,就是爱你的 Fita”,再到街上礼品店买了个像框(卖礼品的 MM 也很漂亮哦),再找隔壁搞美术设计的 Mike 设计了一个漂亮的盒子装起来……,我们都是 Decorator,最终都在修饰我这个人呀,怎么样,看懂了吗?

装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。



概念

装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

在这里插入图片描述



应用场景

扩展一个类的功能或给一个类添加职责 动态的给一个对象添加功能,这些功能可以再动态地撤销



优点

继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能。 通过使用不同的装饰类,以及这些装饰类的排列组合,可以实现不同的效果。 符合开闭原则



缺点

会出现更多的代码,更多类,增加程序复杂性 动态装饰时,多层装饰时会更复杂



实例演示

尼古拉斯凯奇主演的《战争之王》不知道大家看过没有。记得里面有个场景,凯奇买了一架武装直升机,这时FBI带人抓捕,凯奇将直升机和导弹分开就合法了。直升机就是那个封装特别好的类,能够长距离飞行。想用武装直升机,就在上面加导弹。想用救援直升机就在上面加医生。想用武装救援直升机,就在上面即加导弹又加医生。

代码实现:

package main

import "fmt"

/**
 * @Description: 飞行器接口,有fly函数
 */
type Aircraft interface {
   fly()
   landing()
}

/**
 * @Description: 直升机类,拥有正常飞行、降落功能
 */
type Helicopter struct {
}

func (h *Helicopter) fly() {
   fmt.Println("我是普通直升机")
}

func (h *Helicopter) landing() {
   fmt.Println("我有降落功能")
}

/**
 * @Description: 武装直升机
 */
type WeaponAircraft struct {
   Aircraft
}

/**
 * @Description: 给直升机增加武装功能
 * @receiver a
 */
func (a *WeaponAircraft) fly() {
   a.Aircraft.fly()
   fmt.Println("增加武装功能")
}

/**
 * @Description: 救援直升机
 */
type RescueAircraft struct {
   Aircraft
}

/**
 * @Description: 给直升机增加救援功能
 * @receiver r
 */
func (r *RescueAircraft) fly() {
   r.Aircraft.fly()
   fmt.Println("增加救援功能")
}

func main() {
   //普通直升机
   fmt.Println("------------普通直升机")
   helicopter := &Helicopter{}
   helicopter.fly()
   helicopter.landing()

   //武装直升机
   fmt.Println("------------武装直升机")
   weaponAircraft := &WeaponAircraft{
      Aircraft: helicopter,
   }
   weaponAircraft.fly()

   //救援直升机
   fmt.Println("------------救援直升机")
   rescueAircraft := &RescueAircraft{
      Aircraft: helicopter,
   }
   rescueAircraft.fly()

   //武装救援直升机
   fmt.Println("------------武装救援直升机")
   weaponRescueAircraft := &RescueAircraft{
      Aircraft: weaponAircraft,
   }
   weaponRescueAircraft.fly()
}

运行结果:

➜ go run main.go

————普通直升机

我是普通直升机

我有降落功能

————武装直升机

我是普通直升机

增加武装功能

————救援直升机

我是普通直升机

增加救援功能

————武装救援直升机

我是普通直升机

增加武装功能

增加救援功能

代码实现中没有Decorator类,主要是因为Go组合的特性。之所以有Decorator,是因为Decorator中有component成员变量,Decorator中函数实现是调用component的函数,所以对于component中的每一个函数,Decorator都需要封装一下,否则无法使用。但是Go组合方式会自动完成这项任务,无需封装,自然也就不需要Decorator了。



总结

装饰器模式理解和使用都比较简单,主要通过组合方式实现复用能力,如果组合的变量为接口或者基类,便可实现串联功能。

在使用上,首先需要确定复用的功能抽象的比较好,以免使用的时候,发现很多增强功能可以收敛其中。其次判断是否有增强的功能需要串联的情况,如果有的话,使用装饰器模式是十分合适的。

装饰器模式体现了开闭原则、里氏替换原则、依赖倒转原则。



代理模式



通俗解释

跟 MM 在网上聊天,一开头总是 “hi, 你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?” 这些话,真烦人,写个程序做为我的 Proxy 吧,凡是接收到这些话都设置好了自己的回答,接收到其他的话时再通知我回答,怎么样,酷吧。

代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。

客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。



概念

代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做 Proxy 或 Surrogate,它是一种对象结构型模式。例如,web框架中的鉴权中间件的设计就数据代理模式。

在这里插入图片描述


静态代理

  1. 代理类实现和目标类相同的接口,每个类都单独编辑一个代理类。
  2. 我们需要在代理类中,将目标类中的所有方法都要重新实现,并且为每个方法都附加相似的代码逻辑。
  3. 如果要添加方法增强的类不止一个,我们需要对每个类都创建一个代理类。


动态代理

  1. 不需要为每个目标类编辑代理类。
  2. 在程序运行时,系统会动态地创建代理类,然后用代理类替换掉原始类。
  3. 一般采用反射实现。



应用场景

虚代理 COW代理 远程代理 保护代理 Cache 代理 防火墙代理 同步代理 智能指引 等等



优点

  • 代理模式能够协调调用者和被调用者,在一定程度上降低了系 统的耦合度。
  • 远程代理使得客户端可以访问在远程机器上的对象,远程机器 可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  • 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系 统资源的消耗,对系统进行优化并提高运行速度。
  • 保护代理可以控制对真实对象的使用权限。



缺点

  • 由于在客户端和真实主题之间增加了代理对象,因此 有些类型的代理模式可能会造成请求的处理速度变慢。
  • 实现代理模式需要额外的工作,有些代理模式的实现 非常复杂。



实例演示

接下来会通过 golang 实现静态代理,有 Golang 和 java 的差异性,我们无法比较方便的利用反射实现动态代理,但是我们可以利用go generate实现类似的效果,并且这样实现有两个比较大的好处,一个是有静态代码检查,我们在编译期间就可以及早发现问题,第二个是性能会更好。



静态代理

代码实现:

package proxy

import (
	"log"
	"time"
)

// IUser IUser
type IUser interface {
	Login(username, password string) error
}

// User 用户
type User struct {
}

// Login 用户登录
func (u *User) Login(username, password string) error {
	// 不实现细节
	return nil
}

// UserProxy 代理类
type UserProxy struct {
	user *User
}

// NewUserProxy NewUserProxy
func NewUserProxy(user *User) *UserProxy {
	return &UserProxy{
		user: user,
	}
}

// Login 登录,和 user 实现相同的接口
func (p *UserProxy) Login(username, password string) error {
	// before 这里可能会有一些统计的逻辑
	start := time.Now()

	// 这里是原有的业务逻辑
	if err := p.user.Login(username, password); err != nil {
		return err
	}

	// after 这里可能也有一些监控统计的逻辑
	log.Printf("user login cost time: %s", time.Now().Sub(start))

	return nil
}

单元测试:

package proxy

import (
	"testing"

	"github.com/stretchr/testify/require"
)

func TestUserProxy_Login(t *testing.T) {
	proxy := NewUserProxy(&User{})

	err := proxy.Login("test", "password")

	require.Nil(t, err)
}



Go Generate 实现 “动态代理”


注意: 在真实的项目中并不推荐这么做,因为有点得不偿失,本文只是在探讨一种可能性,并且可以复习一下 go 语法树先关的知识点


需求

:动态代理相比静态代理主要就是为了解决生产力,将我们从繁杂的重复劳动中解放出来,正好,在 Go 中 Generate 也是干这个活的

如下面的代码所示,我们的 generate 会读取 struct 上的注释,如果出现 @proxy 接口名 的注释,我们就会为这个 struct 生成一个 proxy 类,同时实现相同的接口,这个接口就是在注释中指定的接口

// User 用户
// @proxy IUser
type User struct {
}


代码实现

接来下我们会简单的实现这个需求,由于篇幅和时间的关系,我们会略过一些检查之类的代码,例如

User

是否真正实现了

IUser

这种情况。

主要思路:

  1. 读取文件, 获取文件的 ast 语法树
  2. 通过 NewCommentMap 构建 node 和 comment 的关系
  3. 通过 comment 是否包含 @proxy 接口名 的接口,判断该节点是否需要生成代理类
  4. 通过 Lookup 方法找到接口
  5. 循环获取接口的每个方法的,方法名、参数、返回值信息
  6. 将方法信息,包名、需要代理类名传递给构建好的模板文件,生成代理类
  7. 最后用 format 包的方法格式化源代码
package proxy

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"strings"
	"text/template"
)

func generate(file string) (string, error) {
	fset := token.NewFileSet() // positions are relative to fset
	f, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
	if err != nil {
		return "", err
	}

	// 获取代理需要的数据
	data := proxyData{
		Package: f.Name.Name,
	}

	// 构建注释和 node 的关系
	cmap := ast.NewCommentMap(fset, f, f.Comments)
	for node, group := range cmap {
		// 从注释 @proxy 接口名,获取接口名称
		name := getProxyInterfaceName(group)
		if name == "" {
			continue
		}

		// 获取代理的类名
		data.ProxyStructName = node.(*ast.GenDecl).Specs[0].(*ast.TypeSpec).Name.Name

		// 从文件中查找接口
		obj := f.Scope.Lookup(name)

		// 类型转换,注意: 这里没有对断言进行判断,可能会导致 panic
		t := obj.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType)

		for _, field := range t.Methods.List {
			fc := field.Type.(*ast.FuncType)

			// 代理的方法
			method := &proxyMethod{
				Name: field.Names[0].Name,
			}

			// 获取方法的参数和返回值
			method.Params, method.ParamNames = getParamsOrResults(fc.Params)
			method.Results, method.ResultNames = getParamsOrResults(fc.Results)

			data.Methods = append(data.Methods, method)
		}
	}

	// 生成文件
	tpl, err := template.New("").Parse(proxyTpl)
	if err != nil {
		return "", err
	}

	buf := &bytes.Buffer{}
	if err := tpl.Execute(buf, data); err != nil {
		return "", err
	}

	// 使用 go fmt 对生成的代码进行格式化
	src, err := format.Source(buf.Bytes())
	if err != nil {
		return "", err
	}

	return string(src), nil
}

// getParamsOrResults 获取参数或者是返回值
// 返回带类型的参数,以及不带类型的参数,以逗号间隔
func getParamsOrResults(fields *ast.FieldList) (string, string) {
	var (
		params     []string
		paramNames []string
	)

	for i, param := range fields.List {
		// 循环获取所有的参数名
		var names []string
		for _, name := range param.Names {
			names = append(names, name.Name)
		}

		if len(names) == 0 {
			names = append(names, fmt.Sprintf("r%d", i))
		}

		paramNames = append(paramNames, names...)

		// 参数名加参数类型组成完整的参数
		param := fmt.Sprintf("%s %s",
			strings.Join(names, ","),
			param.Type.(*ast.Ident).Name,
		)
		params = append(params, strings.TrimSpace(param))
	}

	return strings.Join(params, ","), strings.Join(paramNames, ",")
}

func getProxyInterfaceName(groups []*ast.CommentGroup) string {
	for _, commentGroup := range groups {
		for _, comment := range commentGroup.List {
			if strings.Contains(comment.Text, "@proxy") {
				interfaceName := strings.TrimLeft(comment.Text, "// @proxy ")
				return strings.TrimSpace(interfaceName)
			}
		}
	}
	return ""
}

// 生成代理类的文件模板
const proxyTpl = `
package {{.Package}}

type {{ .ProxyStructName }}Proxy struct {
	child *{{ .ProxyStructName }}
}

func New{{ .ProxyStructName }}Proxy(child *{{ .ProxyStructName }}) *{{ .ProxyStructName }}Proxy {
	return &{{ .ProxyStructName }}Proxy{child: child}
}

{{ range .Methods }}
func (p *{{$.ProxyStructName}}Proxy) {{ .Name }} ({{ .Params }}) ({{ .Results }}) {
	// before 这里可能会有一些统计的逻辑
	start := time.Now()

	{{ .ResultNames }} = p.child.{{ .Name }}({{ .ParamNames }})

	// after 这里可能也有一些监控统计的逻辑
	log.Printf("user login cost time: %s", time.Now().Sub(start))

	return {{ .ResultNames }}
}
{{ end }}
`

type proxyData struct {
	// 包名
	Package string
	// 需要代理的类名
	ProxyStructName string
	// 需要代理的方法
	Methods []*proxyMethod
}

// proxyMethod 代理的方法
type proxyMethod struct {
	// 方法名
	Name string
	// 参数,含参数类型
	Params string
	// 参数名
	ParamNames string
	// 返回值
	Results string
	// 返回值名
	ResultNames string
}


单元测试

package proxy

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func Test_generate(t *testing.T) {
	want := `package proxy

type UserProxy struct {
	child *User
}

func NewUserProxy(child *User) *UserProxy {
	return &UserProxy{child: child}
}

func (p *UserProxy) Login(username, password string) (r0 error) {
	// before 这里可能会有一些统计的逻辑
	start := time.Now()

	r0 = p.child.Login(username, password)

	// after 这里可能也有一些监控统计的逻辑
	log.Printf("user login cost time: %s", time.Now().Sub(start))

	return r0
}
`
	got, err := generate("./static_proxy.go")
	require.Nil(t, err)
	assert.Equal(t, want, got)
}



总结

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


代理模式

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


装饰器模式

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


适配器模式

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


桥接模式

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



外观模式



通俗解释

我有一个专业的 Nikon 相机,我就喜欢自己手动调光圈、快门,这样照出来的照片才专业,但 MM 可不懂这些,教了半天也不会。幸好相机有 Facade 设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样 MM 也可以用这个相机给我拍张照片了。

外观模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。



概念

外观模式(Facade Pattern,也叫门面模式)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问的接口.为子系统中的一组接口提供一个一致的界面,这个接口使得这一子系统更加容易使用。

在这里插入图片描述

分析:

我方系统中包含多个子系统,完成一项任务需要多个子系统通力合作。

我们可以选择将子系统所有接口暴露给Client,让Client自行调用。但这会导致一些问题,一是后期沟通成本会很高,加入完成一个功能需要调用多个接口,Client联调时出问题率会飙升,系统提供者需要不断答疑。二是如果有多个Client,相同代码Client需要重复开发,而且后期代码有变更,各方都会很烦费力。三是影响响应时间和性能,多个接口往返,白白增加了很多通信时间和请求量。

另一种方式是,对于指定功能,系统端做好封装,只提供一个接口。好处有很多,沟通成本低、Client不需要重复开发、功能更改影响范围小、提高响应时间和性能。一般这些接口会有对应的OpenAPI,实现了功能对外开放的效果。



应用场景

降低子系统访问的复杂性,简化客户端与子系统之间的接口。



优点

  • 减少系统间的相互依赖;
  • 提高灵活性;
  • 提高安全性,符合迪米特法则,即最少知道原则;



缺点

新需求可能修改接口代码,不符合开闭原则。



实例演示

电商系统一般包含商品、库存、营销、商家、交易、支付、售后、履约、物流、仓储等子系统。拿商品详情页来说,商详页接口一般会涉及商品、库存、营销、商家等系统。电商系统的客户端有PC、Mobile、Android、IOS等,如果让这些客户端调用接口拼凑出商详页的数据,感觉客户端的同学能拿着大砍刀和服务端同学谈心。为了避免这种情况,一般商品组同学会提供商详页接口,该接口获取商详页的所有信息,返回给客户端。

当然,如果流量特别大,需要优化接口性能,可以根据具体情况将接口做拆分,客户端需要请求多个接口,但即使这样,相关的接口也是封装好的。如果真实场景中遇到这种拆分的情况,那恭喜你,说明公司在发展,流量在增加,能够推动大家更快的成长。

代码实现:

package main

import "fmt"

type ProductSystem struct {
}

func (p *ProductSystem) GetProductInfo() {
   fmt.Println("获取到商品信息")
}

type StockSystem struct {
}

func (s *StockSystem) GetStockInfo() {
   fmt.Println("获取到库存信息")
}

type PromotionSystem struct {
}

func (p *PromotionSystem) GetPromotionInfo() {
   fmt.Println("获取营销信息")
}

func ProductDetail() {
   product := &ProductSystem{}
   stock := &StockSystem{}
   promotion := &PromotionSystem{}
   product.GetProductInfo()
   stock.GetStockInfo()
   promotion.GetPromotionInfo()
   fmt.Println("整理完成商品详情页所有数据")
}
func main() {
   ProductDetail()
}

运行结果:

➜go run main.go

获取到商品信息

获取到库存信息

获取营销信息

整理完成商品详情页所有数据



享元模式



通俗解释

每天跟 MM 发短信,手指都累死了,最近买了个新手机,可以把一些常用的句子存在手机里,要用的时候,直接拿出来,在前面加上 MM 的名字就可以发送了,再不用一个字一个字敲了。共享的句子就是 Flyweight,MM 的名字就是提取出来的外部特征,根据上下文情况使用。享元模式:FLYWEIGHT 在拳击比赛中指最轻量级。

享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。

将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。



概念

说到享元模式,第一个想到的应该就是池技术了,数据库连接池、缓冲池、

GRPC连接池

等等都是享元模式的应用,所以说享元模式是池技术的重要实现方式。

比如我们每次创建字符串对象时,都需要创建一个新的字符串对象的话,内存开销会很大,所以如果第一次创建了字符串对象“adam“,下次再创建相同的字符串”adam“时,只是把它的引用指向”adam“,这样就实现了”adam“字符串再内存中的共享。

举个最简单的例子,网络联机下棋的时候,一台服务器连接了多个客户端(玩家),如果我们每个棋子都要创建对象,那一盘棋可能就有上百个对象产生,玩家多点的话,因为内存空间有限,一台服务器就难以支持了,所以这里要使用享元模式,将棋子对象减少到几个实例。下面给出享元模式的定义。

享元模式主要是为了复用对象,节省内存。使用享元模式需要有

两个前提

  1. 享元对象不可变:当享元模式创建出来后,它的变量和属性不会被修改
  2. 系统中存在大量重复对象:这些重复对象可以使用同一个享元,内存中只存在一份,这样会节省大量空间。当然这也是为什么享元对象不可变的原因,因为有很多引用,变更的话会引起很多问题。

在这里插入图片描述

分析:

享元模式主要是把系统中共同的、不变的对象抽象出来,达到共用一份的效果。

抽象出的对象接口为Flyweight,ConcreteFlyweight为实际被共享的对象。UnsharedConcreteFlyweight是否存在,主要看是否有对象是无需共享的。

享元模式里有工厂FlyweightFactory,主要是因为系统中需要的享元结构虽然确定了,但是享元的属性不同,所以需要管理多个对象,此处使用了工厂模式。



应用场景

尝尝应用于系统的底层开发,以便解决系统的性能问题。例如数据库的连接池。 系统有大量的相似对象,需要缓冲池的场景。



优点

减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率 减少内存之外的其他资源



缺点

关注内/外部状态、关注线程安全问题 使系统、程序逻辑复杂化



实例演示

写一下象棋游戏中对于象棋的管理

代码实现:

package main

import "fmt"

/**
 * @Description: 棋子类,有文案、颜色、规则,这三种不变属性
 */
type Piece struct {
   text  string
   color string
   rule  string
}

/**
 * @Description: 棋子信息说明
 * @receiver p
 * @return string
 */
func (p *Piece) String() string {
   return fmt.Sprintf("%s,颜色为%s,规则为%s", p.text, p.color, p.rule)
}

/**
 * @Description: 棋子在棋盘位置
 */
type Pos struct {
   x int64
   y int64
}

/**
 * @Description: 游戏中的棋子
 */
type GamePiece struct {
   piece   *Piece //棋子指针
   pos     Pos    //棋子位置
   ownerId int64  //玩家ID
   roomId  int64  //房间ID
}

/**
 * @Description: 游戏中的棋子说明
 * @receiver g
 * @return string
 */
func (g *GamePiece) String() string {
   return fmt.Sprintf("%s位置为(%d,%d)", g.piece, g.pos.x, g.pos.y)
}

/**
 * @Description: 棋子工厂,包含32颗棋子信息
 */
type PieceFactory struct {
   pieces []*Piece
}

/**
 * @Description: 创建棋子。棋子的信息都是不变的
 * @receiver f
 */
func (f *PieceFactory) CreatePieces() {
   f.pieces = make([]*Piece, 32)
   f.pieces[0] = &Piece{
      text:  "兵",
      color: "红",
      rule:  "过河前只能一步一步前进,过河后只能一步一步前进或者左右移",
   }
   f.pieces[1] = &Piece{
      text:  "兵",
      color: "黑",
      rule:  "过河前只能一步一步前进,过河后只能一步一步前进或者左右移",
   }
   //todo 创建其它棋子。此处可以使用配置文件创建,能方便一些。系统中可以设置一个规则引擎,控制棋子运动。
}

/**
 * @Description: 获取棋子信息
 * @receiver f
 * @param id
 * @return *Piece
 */
func (f *PieceFactory) GetPiece(id int64) *Piece {
   return f.pieces[id]
}

/**
 * @Description: 初始化棋盘
 * @param roomId
 * @param u1
 * @param u2
 */
func InitBoard(roomId int64, u1 int64, u2 int64, factory *PieceFactory) {
   fmt.Printf("创建房间%d,玩家为%d和%d \n", roomId, u1, u2)
   fmt.Println("初始化棋盘")

   fmt.Printf("玩家%d的棋子为 \n", u1)
   piece := &GamePiece{
      piece:   factory.GetPiece(0),
      pos:     Pos{1, 1},
      roomId:  roomId,
      ownerId: u1,
   }
   fmt.Println(piece)

   fmt.Printf("玩家%d的棋子为 \n", u2)
   piece2 := &GamePiece{
      piece:   factory.GetPiece(1),
      pos:     Pos{16, 1},
      roomId:  roomId,
      ownerId: u2,
   }
   fmt.Println(piece2)
}
func main() {
   factory := &PieceFactory{}
   factory.CreatePieces()
   InitBoard(1, 66, 88, factory)
}

运行结果:

➜go run main.go

创建房间1,玩家为66和88

初始化棋盘

玩家66的棋子为

兵,颜色为红,规则为过河前只能一步一步前进,过河后只能一步一步前进或者左右移位置为(1,1)

玩家88的棋子为

兵,颜色为黑,规则为过河前只能一步一步前进,过河后只能一步一步前进或者左右移位置为(16,1)



组合模式



通俗解释

Mary 今天过生日。“我过生日,你要送我一件礼物。”“嗯,好吧,去商店,你自己挑。”“这件 T 恤挺漂亮,买,这条裙子好看,买,这个包也不错,买。”“喂,买了三件了呀,我只答应送一件礼物的哦。”“什么呀,T 恤加裙子加包包,正好配成一套呀,小姐,麻烦你包起来。”“……”,MM 都会用 Composite 模式了,你会了没有?

组合模式:合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。



概念

组合模式(Composite Pattern):组合多个对象形成树形结构(如文件夹)以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。

角色:

  • Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
  • Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
  • Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。

在这里插入图片描述

分析:

Composite是目录,Leaf是目录下的文件,目录和文件都继承自Component。目录能够增加、删除文件,可以展示目录所在位置,文件只能展示文件所在位置。

对于目录这种需求,有两种实现方式:

  1. 第一种不使用组合模式,只用一个类,有2个核心变量:

    • 一个成员变量表明对象是文件还是目录
    • 一个成员变量存放目录下文件列表,如果对象为文件,则该变量为空
type FileSystemNode struct {
   isFile   bool             //表明是文件还是目录
   subNodes []FileSystemNode //目录下包含的内容
}
  1. 第二种方案使用组合模式。虽然第一种方案能够实现文件管理的功能,但并不优雅。因为文件和目录是不同的,各自有各自的特性,将特有的内容放到一个类里,

    不满足单一职责原则

所以我们可以将其拆分为两个类:文件类和目录类。两个类必须继承自同一个父类,除了重复的功能可以复用外,更重要的一点是消除了两个类调用上的区别,subNodes不需要做任何区分。而且这两个类可以独立进化,相互不影响,何乐而不为呢。



应用场景

  • 希望客户端可以忽略组合对象与单个对象的差异时
  • 处理一个树形结构



优点

  • 清楚地定义分层次的复杂对象,表示对象的全部或部分层次
  • 让客户端忽略了层次的差异,方便对整个层次结构进行控制
  • 简化客户端代码
  • 符合开闭原则



缺点

  • 限制类型时比较复杂
  • 使设计变得更加抽象



实例演示

公司的人员组织就是一个典型的树状的结构,现在假设我们现在有部分,和员工,两种角色,一个部门下面可以存在子部门和员工,员工下面不能再包含其他节点。

我们现在要实现一个统计一个部门下员工数量的功能

package composite

// IOrganization 组织接口,都实现统计人数的功能
type IOrganization interface {
	Count() int
}

// Employee 员工
type Employee struct {
	Name string
}

// Count 人数统计
func (Employee) Count() int {
	return 1
}

// Department 部门
type Department struct {
	Name string

	SubOrganizations []IOrganization
}

// Count 人数统计
func (d Department) Count() int {
	c := 0
	for _, org := range d.SubOrganizations {
		c += org.Count()
	}
	return c
}

// AddSub 添加子节点
func (d *Department) AddSub(org IOrganization) {
	d.SubOrganizations = append(d.SubOrganizations, org)
}

// NewOrganization 构建组织架构 demo
func NewOrganization() IOrganization {
	root := &Department{Name: "root"}
	for i := 0; i < 10; i++ {
		root.AddSub(&Employee{})
		root.AddSub(&Department{Name: "sub", SubOrganizations: []IOrganization{&Employee{}}})
	}
	return root
}

单元测试:

package composite

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNewOrganization(t *testing.T) {
	got := NewOrganization().Count()
	assert.Equal(t, 20, got)
}



总结

组合模式是对指定场景有用,所以大家能不能用到,完全看运气。这个设计模式满足单一职责原则、开闭原则、里氏替换原则。



版权声明:本文为qq_39280718原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。