设计模式学习之路-策略模式「附详细案例」

我列一些我写过的设计模式文章,若有兴趣可以点链接看看。

深入 GO 选项模式 - 掘金

深入设计模式之适配器模式「附详细案例」 - 掘金

深入设计模式之工厂模式「附详细案例」 - 掘金

学习策略模式强烈建议看看工厂模式,策略模式这篇解决了工厂模式遗留问题。

名词解释

策略模式(Strategy Pattern)是一种行为设计模式。它定义了一系列算法,将每种算法封装在独立的策略实现子类中,并使它们可以互相替换,使得算法变化可以独立于使用算法的使用方。

算法理解成业务逻辑或业务规则;使用方理解成使用代码的业务方。

SOP

本文以"SOP"为案例,"SOP"也叫"营销自动化"。所谓 SOP,是 Standard Operating Procedure 三个单词中首字母的大写 ,即标准作业程序,指将某一事件的标准操作步骤和要求以统一的格式描述出来,用于指导和规范日常的工作标准作业程序_百度百科。比如你去餐厅,服务人员先给你拿菜单、点菜、上菜、买单这一套操作就是标准的流程。

"SOP"旨在帮助企业将复杂场景的用户运营策略自动化执行,并提供运营效果量化追踪的平台。比如:自动发短信、发消息等。

比如你要在情人节给朋友圈客户发送满减活动。你会经历下面这几步

1、 谁来发「员工」

2、 发给谁「客户」

3、 发什么「内容」

4、 何时发「时间」

5、 怎么发「通道」

编码案例围绕「通道」触达用户,通道可能是"短信通道"、"公众号群发通道"、"邮件通道"、"抖音私信通道"、"企业微信群发通道"等。。。假设你做一套解决方案整合通道能力给客户提供更加便利触达用户工具。

工厂设计模式章节,我提到了工厂模式关注的是对象的创建过程,创建过程封装在工厂类中,使用方不需要关心对象的具体创建过程,只需通过工厂方法或抽象工厂获取所需对象即可。

策略模式关注运行时选择不同的算法;将算法的实现与使用算法的业务方代码分离开来,以提高代码的灵活性、可维护性和可复用性时;

算法(策略)定义

策略定义抽象策略接口,若干个实现抽象策略接口的实现类。策略接口和实类不一定要用 Strategy 结尾。代码片段如下

go 复制代码
// Sender 定义策略
type Sender interface {
	// SendMsg 定义算法(规则和逻辑)
	SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error)
}

// SMS 短信通道实现类
type SMS struct {
}

func NewSMS() Sender {
	return &SMS{}
}

// SendMsg 实现具体算法/规则
func (c *SMS) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过短信通道触达客户逻辑省略
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

// Tiktok 抖音通道实现类
type Tiktok struct {
}

func NewTiktok() Sender {
	return &Tiktok{}
}

// SendMsg 实现具体算法/规则
func (c *Tiktok) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过抖音通道触达客户逻辑省略
	return nil, nil
}

策略创建和使用

仅仅有策略定义,还算不上是完整的策略模式,虽然这些类也能直接用创建对象方式使用,比如创建子类对象就可以使用。代码如下

scss 复制代码
func TestStrategy(t *testing.T) {
	NewSMS().SendMsg(context.TODO(), "xxx", "您好")
}

如果使用方都各自调用子类,没有发挥策略模式优势,只能说使用了多态的特性。

上篇文章我讲了简单工厂我在给大家回顾下。代码如下:

go 复制代码
type SenderType string

const (
	SMSSenderType    SenderType = "sms"
	TiktokSenderType SenderType = "tiktok"
)

// Mgr 工厂类
type Mgr struct {
}

func NewMgr() *Mgr {
	return &Mgr{}
}

func (m *Mgr) GetSender(tp SenderType) (Sender, bool) {
	if tp == SMSSenderType {
		return NewSMS(), true
	} else if tp == TiktokSenderType {
		return NewTiktok(), true
	}
	
	return nil, false
}

上段代码定义了工厂类,工厂类提供方法 GetSender() 获取 Sender。接下来我把上段代码优化下,用 Map 来缓存策略实现类,策略随取随用不用每次 new 新对象,改造代码如下

go 复制代码
type SenderType string

const (
	SMSSenderType    SenderType = "sms"
	TiktokSenderType SenderType = "tiktok"
)

var (
	senderMapping = make(map[SenderType]Sender)
)

// 提前初始化并且把策略缓存在 Map 中,即用即去,不用每次都创建对象
func init() {
	senderMapping[SMSSenderType] = NewSMS()
	senderMapping[TiktokSenderType] = NewTiktok()
}

// Mgr 工厂类
type Mgr struct {
}

func NewMgr() *Mgr {
	return &Mgr{}
}

func (m *Mgr) GetSender(tp SenderType) (Sender, bool) {
	v, ok := senderMapping[tp]
	return v, ok
}

定义全局变量 senderMapping 用来缓存策略,业务场景按需组册到 senderMapping 即可。我解释下 init() 函数,在 go 中包加载时候会自动调用,不需要手动调用。在工厂类 Mgr 中,根据 SenderType 获取对应的策略避免了 if else 判断逻辑。不用 Map 用其他数据结构可以吗?其实数组、切片都是可以的,本来策略实现类就不多,效率也没啥区别。

策略使用方比较简单了

go 复制代码
func TestStrategy(t *testing.T) {
	strategy, exists := NewMgr().GetSender(SMSSenderType)
	if !exists {
		panic("策略器不存在")
	}
	strategy.SendMsg(context.TODO(), "xxx", "您好")
}

结果输出如下

diff 复制代码
=== RUN   TestStrategy
推送信息,targetID=xxx,data=您好
--- PASS: TestStrategy (0.00s)
PASS

看到这里有是不是有疑问?策略模式和工厂模式太像了。是的你想的没错,设计模式都是基于接口编程非实现编程,你学习过更多设计模式你会发现都是这样的,另外在开发中设计模式基本都是组合使用。

项目中优化过程

好的代码并不是一开始就是最优的,都是逐步的演变而来。一般都会经历下面几步。

第一步:最简单原则

第一步是最基础的,也是大多数研发同学刚开始选择使用的,对于刚开始使用该模式的人来说是一个很好的步骤。另外也可能因为项目紧急选择的一种方案。

go 复制代码
func SMSSendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

func TiktokSendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

func WeComSendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

func TestBasicStrategy(t *testing.T) {
	SendMsg(SMSSenderType, context.TODO(), "xxx", "您好")
}

func SendMsg(tp SenderType, ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	if tp == SMSSenderType {
		return SMSSendMsg(ctx, targetID, data)
	} else if tp == TiktokSenderType {
		return TiktokSendMsg(ctx, targetID, data)
	} else if tp == WeComSenderType {
		return WeComSendMsg(ctx, targetID, data)
	}

	return nil, errors.New("未找到通道实现类")
}
第二步:优化 if-else

如果对自己写的代码有要求,时刻回顾加上需求积累,应该是很容易发现问题。下一步清理 if-else ,它太啰嗦了,并且扩展性差,后期 SendMsg() 方法会非常臃肿,if-else 用 map 优化掉。

go 复制代码
var (
	m = make(map[SenderType]func(ctx context.Context, targetID string, data interface{}) (interface{}, error))
)

func init() {
	m[SMSSenderType] = SMSSendMsg
	m[TiktokSenderType] = TiktokSendMsg
	m[WeComSenderType] = WeComSendMsg
}

func SMSSendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

func TiktokSendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

func WeComSendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

func TestBasicStrategy(t *testing.T) {
	SendMsg(SMSSenderType, context.TODO(), "xxx", "您好")
}

func SendMsg(tp SenderType, ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	fn, exists := m[tp]
	if !exists {
		return nil, errors.New("未找到通道实现类")
	}

	return fn(ctx, targetID, data)
}

用 map 替换了 if-else,看上去是扩展性更强了,但并没有定义策略接口,还不算标准策略模式,哪些场景会使用呢?我总结了几点:

1、 你有一些 if 或 switch 语句,希望它们更简洁。

2、 你有一小部分可供选择的算法。

3、 开发代码的童鞋比较偷懒。

第三步:定义抽象策略接口

我比较倾向用这种方式,扩展更强,低耦合。

go 复制代码
func init() {
	if err := Register(SMSSenderType, NewSMS()); err != nil {
		panic(err)
	}
}

// SMS 短信通道实现类
type SMS struct {
}

func NewSMS() Sender {
	return &SMS{}
}

// SendMsg 实现具体算法/规则
func (c *SMS) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过短信通道触达客户逻辑省略
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

// Tiktok 抖音通道实现类
type Tiktok struct {
}

func init() {
	if err := Register(TiktokSenderType, NewTiktok()); err != nil {
		panic(err)
	}
}

func NewTiktok() Sender {
	return &Tiktok{}
}

// SendMsg 实现具体算法/规则
func (c *Tiktok) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过抖音通道触达客户逻辑省略
	return nil, nil
}

type SenderType string

const (
	SMSSenderType    SenderType = "sms"
	TiktokSenderType SenderType = "tiktok"
	WeComSenderType  SenderType = "wecom"
)

var (
	senderMapping = make(map[SenderType]Sender)
)

// Register 注册策略实现类
func Register(tp SenderType, sender Sender) error {
	_, exists := senderMapping[tp]
	if exists {
		return errors.New("重复注册")
	}

	senderMapping[tp] = sender
	return nil
}

// Mgr 工厂类
type Mgr struct {
}

func NewMgr() *Mgr {
	return &Mgr{}
}

func (m *Mgr) GetSender(tp SenderType) (Sender, bool) {
	v, ok := senderMapping[tp]
	return v, ok
}

第三步相比第一步和第二步增加了策略接口,策略类实现了策略接口。工厂类获取策略对象的方式不变,唯一的区别是策略的创建,提供了 Register() 函数,该方法是把策略实现类注册到 senderMapping 中,另外注册逻辑调整到子类 init() 函数,优点是如果增加策略实现类,只需要在实现类中调用 Register() 函数即可。

总结

策略类定义:策略类包含一个抽象策略接口和若干实现该接口的具体实现类。策略接口定义了算法(逻辑)的标准,具体策略类则实现了标准,并提供了特定的算法(逻辑)实现。

策略创建:策略创建由工厂类来完成,工厂类封装了策略创建细节。工厂类负责根据使用方需求创建具体的策略对象,并返回给使用方。

策略使用:使用方用策略对象来执行特定算法(逻辑)。使用方可以选择不同的策略对象,从而实现算法(逻辑)的动态切换。

思考题

1、你们项目中有用策略模式吗?是怎么用的呢?

2、用 map 缓存策略子类,因为对象是可以复用的,所以是单例。如果你的业务场景每次都需要 new 新对象(非单例),应该如何实现?我提供 2 种方案

方案一:高阶函数+闭包

go 复制代码
func init() {
	if err := Register(SMSSenderType, func() Sender {
		return NewSMS()
	}); err != nil {
		panic(err)
	}
}

// SMS 短信通道实现类
type SMS struct {
	tag string
}

func NewSMS() Sender {
	return &SMS{}
}

// SendMsg 实现具体算法/规则
func (c *SMS) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过短信通道触达客户逻辑省略
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

// Tiktok 抖音通道实现类
type Tiktok struct {
}

func init() {
	if err := Register(TiktokSenderType, func() Sender {
		return NewTiktok()
	}); err != nil {
		panic(err)
	}
}

func NewTiktok() Sender {
	return &Tiktok{}
}

// SendMsg 实现具体算法/规则
func (c *Tiktok) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过抖音通道触达客户逻辑省略
	return nil, nil
}

// 工厂类
var (
	dynamicSenderMapping = make(map[SenderType]func() Sender)
)

// Register 注册策略实现类
func Register(tp SenderType, fn func() Sender) error {
	_, exists := senderMapping[tp]
	if exists {
		return errors.New("重复注册")
	}

	dynamicSenderMapping[tp] = fn
	return nil
}

func GetDynamicSender(tp SenderType) (Sender, bool) {
	sender, exists := dynamicSenderMapping[tp]
	if !exists {
		return nil, false
	}

	return sender(), true
}

dynamicSenderMapping 映射一个函数,调用 GetDynamicSender() 函数后通过 fn() 函数来创建新的对象。还可以传参,根据自己的业务场景传参即可。

业务方使用方式如下:

go 复制代码
func TestGetDynamicSender(t *testing.T) {
	sender, exists := GetDynamicSender(SMSSenderType)
	if !exists {
		panic("未找到信息")
	}

	fmt.Printf("sender打印地址:%p\n", sender)
	sender1, exists := GetDynamicSender(SMSSenderType)
	if !exists {
		panic("未找到策略实现类")
	}
	fmt.Printf("sender1打印地址:%p\n", sender1)
}

结果输出如下

diff 复制代码
=== RUN   TestGetDynamicSender
sender打印地址:0xc00005a2a0
sender1打印地址:0xc00005a2b0
--- PASS: TestGetDynamicSender (0.00s)
PASS

方案二:用反射

go 复制代码
func init() {
	if err := Register(SMSSenderType, NewSMS()); err != nil {
		panic(err)
	}
}

// SMS 短信通道实现类
type SMS struct {
	tag string
}

func NewSMS() Sender {
	return &SMS{}
}

// SendMsg 实现具体算法/规则
func (c *SMS) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过短信通道触达客户逻辑省略
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

// Tiktok 抖音通道实现类
type Tiktok struct {
}

func init() {
	if err := Register(TiktokSenderType, NewTiktok()); err != nil {
		panic(err)
	}
}

func NewTiktok() Sender {
	return &Tiktok{}
}

// SendMsg 实现具体算法/规则
func (c *Tiktok) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过抖音通道触达客户逻辑省略
	return nil, nil
}

var (
	dynamicSenderMapping = make(map[SenderType]Sender)
)

// Register 注册策略实现类
func Register(tp SenderType, sender Sender) error {
	_, exists := dynamicSenderMapping[tp]
	if exists {
		return errors.New("重复注册")
	}

	dynamicSenderMapping[tp] = sender
	return nil
}

func GetDynamicSender(tp SenderType) (Sender, bool) {
	sender, exists := dynamicSenderMapping[tp]
	if !exists {
		return nil, false
	}

	t := reflect.TypeOf(sender)
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	val := reflect.New(t)
	out, ok := val.Interface().(Sender)
	if !ok {
		return nil, false
	}

	return out, true
}

函数通过反射获取 sender 的类型,并检查是否是一个指针类型。如果是指针类型,通过 Elem() 方法获取指针指向的值的类型。最后,使用 reflect.New 创建一个新值,类型与 sender 相同。

结果输出如下

diff 复制代码
=== RUN   TestGetDynamicSender
打印地址:0xc000096270
打印地址:0xc000096280
--- PASS: TestGetDynamicSender (0.00s)
PASS

参考文献

zh.wikipedia.org/wiki/%E7%AD...

相关推荐
吾与谁归in3 分钟前
【C#设计模式(4)——构建者模式(Builder Pattern)】
设计模式·c#·建造者模式
shinelord明5 分钟前
【再谈设计模式】建造者模式~对象构建的指挥家
开发语言·数据结构·设计模式
不会编程的懒洋洋29 分钟前
Spring Cloud Eureka 服务注册与发现
java·笔记·后端·学习·spring·spring cloud·eureka
NiNg_1_2341 小时前
SpringSecurity入门
后端·spring·springboot·springsecurity
Lucifer三思而后行2 小时前
YashanDB YAC 入门指南与技术详解
数据库·后端
王二端茶倒水3 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
夜色呦4 小时前
现代电商解决方案:Spring Boot框架实践
数据库·spring boot·后端
爱敲代码的小冰4 小时前
spring boot 请求
java·spring boot·后端
java小吕布5 小时前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Goboy5 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员