【设计模式】啊?没听过命令模式,有什么优点?

命令模式是行为设计模式

GoF定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作

优点:调用者与实现者的解耦

场景

会员系统实现,订阅到商品到货通知、订阅商品降价通知。

订阅到商品到货通知: 大致的流程是,当商品缺货时候,用户可以订阅到货通知,当商品有库存时,发送消息通知用户。

订阅商品降价通知:大致的流程是,当用户觉得商品太贵时,可以订阅降价通知,当商品的价格达到用户期望价格时,发送消息通知用户。

流程实现

实现这个两个功能并不难。

大致的技术实现流程如下,具体细节不做展开,用这两个例子是为了介绍引入命令模式。

订阅到商品到货通知: 到货通知订阅表,记录用户的订阅记录。

发送记录表,记录消息的发送记录。

订阅时,到货通知订阅表插入数据。消息发送时,发送记录表插入数据。

关注商品的库存状态,一般是使用订阅库存消息,当商品有库存时,遍历订阅表,发送消息,并且在数据库中记录下发送消息的记录。

订阅商品降价通知:

降价通知订阅表。记录用户的订阅记录。

发送记录表,记录消息的发送记录。 订阅时,降价通知订阅表插入数据。消息发送时,发送记录表插入数据。

关注商品的价格状态,一般是使用订阅价格变动消息,当商品价格变化时,遍历订阅表,判断价格是否达到用户的预期,价格达到预期,发送消息,并且在数据库中记录下发送消息的记录。

分析上面的两个业务

调用者:负责扫描订阅表,把符合记录的订阅信息推送给执行者;依赖订阅记录的抽象接口。

执行者:负责自己的业务职责,组装数据,发送消息;需要接收订阅记录传递的数据(需要发送的消息)。

订阅记录:需要保存执行的触达方式。

具体实现:

调用者

go 复制代码
// ISubscribeRecord 抽象出来的订阅记录
type ISubscribeRecord interface {  
	// 发送消息
    ExecuteSend()  
}


// Invoke 调用者  
type Invoke struct {  
    // 满足条件的订阅消息
    subscribeRecords []ISubscribeRecord  
}  
  
func NewInvoke() *Invoke {  
    return &Invoke{}  
}  

// SetSubscribeRecord 添加满足条件的订阅消息
func (i *Invoke) SetSubscribeRecord(subscribeRecord ISubscribeRecord) {  
    i.subscribeRecords = append(i.subscribeRecords, subscribeRecord)  
}  

// Exec 执行数据的发送
func (i *Invoke) Exec() {  
    if len(i.subscribeRecords) == 0 {  
       return  
    }  
  
    for _, subscribeRecord := range i.subscribeRecords {  
       subscribeRecord.ExecuteSend()  
    }  
}

到货通知、降价通知的调用者都可以共用这段代码。

这里的订阅记录是个接口,这样可以很方便的进行数据的发送

实现者

go 复制代码
// Notify 发送方式
type Notify struct {
	sendWay string
}

func NewNotify(sendWay string) *Notify {
	return &Notify{sendWay: sendWay}
}

func (n *Notify) Send(productName string) {
	fmt.Printf("%v, 发送方式: %v, 命令执行了!\n", productName, n.sendWay)
}

这部分只关心消息的发送,按照指定的发送方式,触达用户

订阅记录

到货通知

go 复制代码
// AlertSockProductSubscribeRecord 到货通知订阅记录
type AlertSockProductSubscribeRecord struct {
	ProductName string
	Notify      *Notify
}

func NewAlertSockProductSubscribeRecord(notify *Notify, productName string) ISubscribeRecord {
	return &AlertSockProductSubscribeRecord{
		Notify:      notify,
		ProductName: productName + "到货了",
	}
}

func (r *AlertSockProductSubscribeRecord) ExecuteSend() {
	r.Notify.Send(r.ProductName)
}

降价提醒

go 复制代码
// ReducePriceProductSubscribeRecord 降价通知订阅记录  
type ReducePriceProductSubscribeRecord struct {  
    ProductName string  
    Notify      *Notify  
}  
  
func NewReducePriceProductSubscribeRecord(notify *Notify, productName string) ISubscribeRecord {  
    return &ReducePriceProductSubscribeRecord{  
       Notify:      notify,  
       ProductName: productName + "降价了",  
    }  
}  
  
func (r *ReducePriceProductSubscribeRecord) ExecuteSend() {  
    // 可以加入一些其他的判断。晚上时段,不打扰用户,可以把发送记录标记上暂时不发送。  
    // 判断当前时间是否符合发送时间  
    // 不符合,记录到数据库  
    // 符合,立即执行发送  
    r.Notify.Send(r.ProductName)  
}

到货通知订阅记录、降价提醒订阅记录 持有触达方式(执行者)的一个引用。

这样可以知道该订阅记录应该用什么方式触达。

完整代码:

go 复制代码
// 命令模式

import (
	"fmt"
)

type ISubscribeRecord interface {
	ExecuteSend()
}

// Invoke 调用者
type Invoke struct {
	subscribeRecords []ISubscribeRecord
}

func NewInvoke() *Invoke {
	return &Invoke{}
}

// SetSubscribeRecord 添加满足条件的订阅消息
func (i *Invoke) SetSubscribeRecord(subscribeRecord ISubscribeRecord) {
	i.subscribeRecords = append(i.subscribeRecords, subscribeRecord)
}

func (i *Invoke) Exec() {
	if len(i.subscribeRecords) == 0 {
		return
	}

	for _, subscribeRecord := range i.subscribeRecords {
		subscribeRecord.ExecuteSend()
	}
}

// Notify 发送消息 实现者
type Notify struct {
	sendWay string
}

func NewNotify(sendWay string) *Notify {
	return &Notify{sendWay: sendWay}
}

func (n *Notify) Send(productName string) {
	fmt.Printf("%v, 发送方式: %v, 命令执行了!\n", productName, n.sendWay)
}

// ------------------降价通知业务逻辑----------------------

// ReducePriceProductSubscribeRecord 降价通知订阅记录
type ReducePriceProductSubscribeRecord struct {
	ProductName string
	Notify      *Notify
}

func NewReducePriceProductSubscribeRecord(notify *Notify, productName string) ISubscribeRecord {
	return &ReducePriceProductSubscribeRecord{
		Notify:      notify,
		ProductName: productName + "降价了",
	}
}

func (r *ReducePriceProductSubscribeRecord) ExecuteSend() {
	// 可以加入一些其他的判断。晚上时段,不打扰用户,可以把发送记录标记上暂时不发送。
	// 判断当前时间是否符合发送时间
	// 不符合,记录到数据库
	// 符合,立即执行发送
	r.Notify.Send(r.ProductName)
}

// ------------------到货通知业务逻辑----------------------

// AlertSockProductSubscribeRecord 到货通知订阅记录
type AlertSockProductSubscribeRecord struct {
	ProductName string
	Notify      *Notify
}

func NewAlertSockProductSubscribeRecord(notify *Notify, productName string) ISubscribeRecord {
	return &AlertSockProductSubscribeRecord{
		Notify:      notify,
		ProductName: productName + "到货了",
	}
}

func (r *AlertSockProductSubscribeRecord) ExecuteSend() {
	r.Notify.Send(r.ProductName)
}

// 调用者 和 实现者解耦

func main() {
	// 执行者
	notifySms := NewNotify("短信")
	notifyApp := NewNotify("APP推送")
	notifyMniProgramSubscribe := NewNotify("微信小程序订阅消息")

	// 商品名称
	productName := "【设计模式:可复用面向对象软件的基础】"

	// 订阅降价通知
	reducePriceProduct1 := NewReducePriceProductSubscribeRecord(notifySms, productName)
	reducePriceProduct2 := NewReducePriceProductSubscribeRecord(notifyApp, productName)
	reducePriceProduct3 := NewReducePriceProductSubscribeRecord(notifyMniProgramSubscribe, productName)

	// 下面三个步骤省略,不是这次的介绍的重点
	// 1.记录数据库操作...
	// 2.遍历数据库操作...
	// 3.找出符合条件的记录...

	// 降价通知调用者
	reduceInvoke := NewInvoke()
	reduceInvoke.SetSubscribeRecord(reducePriceProduct1)
	reduceInvoke.SetSubscribeRecord(reducePriceProduct2)
	reduceInvoke.SetSubscribeRecord(reducePriceProduct3)

	reduceInvoke.Exec()

	// 订阅到货通知
	alertStockPriceProduct1 := NewAlertSockProductSubscribeRecord(notifySms, productName)
	alertStockPriceProduct2 := NewAlertSockProductSubscribeRecord(notifyApp, productName)

	// 下面三个步骤省略,不是这次的介绍的重点
	// 1.记录数据库操作...
	// 2.遍历数据库操作...
	// 3.找出符合条件的记录...

	// 到货通知调用者
	alertInvoke := NewInvoke()
	alertInvoke.SetSubscribeRecord(alertStockPriceProduct1)
	alertInvoke.SetSubscribeRecord(alertStockPriceProduct2)

	alertInvoke.Exec()
}

总结

怎么样?看了上面的代码有什么感觉。是不是觉得可以非常方便的扩展新的业务。

比如来了一个新的需求:

小程序端,需要订阅促销活动即将开始的通知、订阅促销活动即将结束的通知。

这两个微信小程序订阅消息也可以仿照上述的逻辑,快速的实现。

不用调整大量的代码,体现了"对扩展开放,对修改关闭 "的设计原则。

消息发送的执行者,消息的触达方式也有很多,比如:短信、邮件、APP推送、微信小程序订阅消息、微信公众号模板消息。这些触达方式的实现也有很多设计模式可以实现解耦,比如上次介绍的抽象工厂

再举个例子,很多教材会用饭馆来举例,这里沿用这个示例。

1.作为顾客的我们是命令的下达者。

2.服务员是这个命令的接收者。

3.菜单是这个实际的命令。

4.厨师是这个命令的执行者。

业务场景:

当你要修改菜单的时候,只需要和服务员说就好了,她会转达给厨师,也就是说,我们实现了顾客和厨师的解耦。也就是调用者与实现者的解耦。

命令模式能够做到的是让一个命令接收者实现多个命令(服务员下单、拿酒水、上菜),或者把一条命令转达给多个实现者(热菜厨师、凉菜厨师、主食师傅)。

相关推荐
Asthenia04121 小时前
为什么说MVCC无法彻底解决幻读的问题?
后端
Asthenia04121 小时前
面试官问我:三级缓存可以解决循环依赖的问题,那两级缓存可以解决Spring的循环依赖问题么?是不是无法解决代理对象的问题?
后端
Asthenia04121 小时前
面试复盘:使用 perf top 和火焰图分析程序 CPU 占用率过高
后端
Asthenia04121 小时前
面试复盘:varchar vs char 以及 InnoDB 表大小的性能分析
后端
Asthenia04121 小时前
面试问题解析:InnoDB中NULL值是如何记录和存储的?
后端
Asthenia04121 小时前
面试官问我:TCP发送到IP存在但端口不存在的报文会发生什么?
后端
Asthenia04121 小时前
HTTP 相比 TCP 的好处是什么?
后端
Asthenia04121 小时前
MySQL count(*) 哪个存储引擎更快?为什么 MyISAM 更快?
后端
Asthenia04121 小时前
面试官问我:UDP发送到IP存在但端口不存在的报文会发生什么?
后端
Asthenia04121 小时前
深入理解 TCP backlog 参数:意义、应用场景与 Netty 中的应用
后端