【设计模式】啊?没听过命令模式,能用在哪?

291 阅读8分钟

命令模式是行为设计模式

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

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

场景

实现商品到货通知、商品降价通知。

商品到货通知
当商品缺货时候,用户可以订阅到货通知,当商品有库存时,发送消息通知用户。

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

分析

用户订阅商品到货通知、用户订阅商品降价通知,这两个流程也可以优雅的实现,请点击查桥接模式
这里我们从用户已经订阅消息之后,商品数据已经变化,消费商品数据变动消息开始。
实现这个两个功能并不难,大致的技术实现流程如下

商品到货通知

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

商品降价通知

降价通知订阅表。记录用户的订阅记录。
发送记录表,记录消息的发送记录。 订阅时,降价通知订阅表插入数据。消息发送时,发送记录表插入数据。
关注商品的价格状态,一般是使用订阅价格变动消息,当商品价格变化时,遍历订阅表,判断价格是否达到用户的预期,价格达到预期,发送消息,并且在数据库中记录发送消息的记录。
备注:单体应用,可以使用观察者模式订阅商品数据变动

小结

调用者:负责扫描订阅表,把符合记录的订阅信息推送给接收者;依赖订阅记录的抽象接口。
接收者:负责自己的业务职责,组装数据,发送消息;需要接收订阅记录传递的数据(需要发送的消息)。
到货通知命令:业务逻辑,组装触达给用户的消息记录。
降价通知命令:业务逻辑,组装触达给用户的消息记录。

实现

const (
    Sms                  = "sms"
    AppPush              = "app_push"
    MiniProgramSubscribe = "mini_program_subscribe"
)

// SubscribeRecord 订阅记录
type SubscribeRecord struct {
    ProductName       string // 商品名称
    UserId            uint64 // 用户ID
    Telephone         string // 手机号
    MiniProgramOpenId string // 微信小程序 open id
    SendWay           string // 发送方式
    Type              string
}

// Command 命令接口
type Command interface {
    Execute()
}

调用者

统一进行日志记录,错误监控(发送错误告警)
统一判断发送消息的时间,若当前时间超出发送的时间段,则不进行消息发送,插入数据库记录

// Invoker 调用者
type Invoker struct {
    commands []Command
}

func NewInvoker() *Invoker {
    return &Invoker{}
}

func (i *Invoker) AddCommand(command Command) {
    i.commands = append(i.commands, command)
}

func (i *Invoker) ExecuteCommands() {
    // 统一进行日志记录,错误监控(发送错误告警)
    // 统一判断发送消息的时间,若当前时间超出发送的时间段,则不进行消息发送,插入数据库记录
    for _, command := range i.commands {
       command.Execute()
    }
}

接收者

// Receiver 接收者
type Notify struct {
    sendWay string
}

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

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

小结:调用者、接收者通过命令可以很好的进行解耦

命令

持有接收者的引用,把命令传递给接收者

// Command 命令接口
type Command interface {
    Execute()
}

到货通知命令

// AlertStockCommand 到货通知命令
type AlertStockCommand struct {
    notify     *Notify
    record     *SubscribeRecord
    productMsg string
}

func NewAlertStockCommand(notify *Notify, record *SubscribeRecord) Command {
    productMsg := record.ProductName + "到货了"
    return &AlertStockCommand{
       notify:     notify,
       record:     record,
       productMsg: productMsg,
    }
}

func (a *AlertStockCommand) Execute() {
    a.notify.Send(a.record, a.productMsg)
}

降价通知命令

// ReducePriceCommand 降价通知命令
type ReducePriceCommand struct {
    notify     *Notify
    record     *SubscribeRecord
    productMsg string
}

func NewReducePriceCommand(notify *Notify, record *SubscribeRecord) Command {
    productMsg := record.ProductName + "降价了"
    return &ReducePriceCommand{
       notify:     notify,
       record:     record,
       productMsg: productMsg,
    }
}

func (r *ReducePriceCommand) Execute() {
    r.notify.Send(r.record, r.productMsg)
}

完整代码

package main

import (
    "fmt"
    "testing"
)

const (
    Sms                  = "sms"
    AppPush              = "app_push"
    MiniProgramSubscribe = "mini_program_subscribe"
)

// SubscribeRecord 订阅记录
type SubscribeRecord struct {
    ProductName       string // 商品名称
    UserId            uint64 // 用户ID
    Telephone         string // 手机号
    MiniProgramOpenId string // 微信小程序 open id
    SendWay           string // 发送方式
    Type              string
}

// Receiver 接收者
type Notify struct {
    sendWay string
}

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

func (n *Notify) Send(record *SubscribeRecord, msg string) {
    // 可以对时间进一步处理。记录一条发送日志
    fmt.Printf("%v, 发送方式: %v, 命令执行了!\n", msg, n.sendWay)
}

// Command 命令接口
type Command interface {
    Execute()
}

// ReducePriceCommand 降价通知命令
type ReducePriceCommand struct {
    notify     *Notify
    record     *SubscribeRecord
    productMsg string
}

func NewReducePriceCommand(notify *Notify, record *SubscribeRecord) Command {
    productMsg := record.ProductName + "降价了"
    return &ReducePriceCommand{
       notify:     notify,
       record:     record,
       productMsg: productMsg,
    }
}

func (r *ReducePriceCommand) Execute() {
    r.notify.Send(r.record, r.productMsg)
}

// AlertStockCommand 到货通知命令
type AlertStockCommand struct {
    notify     *Notify
    record     *SubscribeRecord
    productMsg string
}

func NewAlertStockCommand(notify *Notify, record *SubscribeRecord) Command {
    productMsg := record.ProductName + "到货了"
    return &AlertStockCommand{
       notify:     notify,
       record:     record,
       productMsg: productMsg,
    }
}

func (a *AlertStockCommand) Execute() {
    a.notify.Send(a.record, a.productMsg)
}

// Invoker 调用者
type Invoker struct {
    commands []Command
}

func NewInvoker() *Invoker {
    return &Invoker{}
}

func (i *Invoker) AddCommand(command Command) {
    i.commands = append(i.commands, command)
}

func (i *Invoker) ExecuteCommands() {
    // 统一进行日志记录,错误监控(发送错误告警)
    // 统一判断发送消息的时间,若当前时间超出发送的时间段,则不进行消息发送,插入数据库记录
    for _, command := range i.commands {
       command.Execute()
    }
}

func getProductName(productId uint64) string {
    return "【设计模式:可复用面向对象软件的基础】"
}

func filterReducePriceSubscribeRecord(productId uint64) []*SubscribeRecord {
    productName := getProductName(productId)
    // 从数据库中读取用的订阅数据,筛选出符合条件的记录
    // 这里省略了,模拟返回数据
    return []*SubscribeRecord{
       {ProductName: productName, Telephone: "13999999999", MiniProgramOpenId: "xxx", SendWay: Sms, UserId: 1, Type: "reduce_price"},
       {ProductName: productName, Telephone: "13999999998", MiniProgramOpenId: "xxx", SendWay: AppPush, UserId: 2, Type: "reduce_price"},
       {ProductName: productName, Telephone: "13999999997", MiniProgramOpenId: "xxx", SendWay: MiniProgramSubscribe, UserId: 3, Type: "reduce_price"},
    }
}

func filterAlertStockSubscribeRecord(productId uint64) []*SubscribeRecord {
    productName := getProductName(productId)
    // 从数据库中读取用的订阅数据,筛选出符合条件的记录
    // 这里省略了,模拟返回数据
    return []*SubscribeRecord{
       {ProductName: productName, Telephone: "13999999999", MiniProgramOpenId: "xxx", SendWay: Sms, UserId: 1, Type: "alert_stock"},
       {ProductName: productName, Telephone: "13999999998", MiniProgramOpenId: "xxx", SendWay: AppPush, UserId: 2, Type: "alert_stock"},
       {ProductName: productName, Telephone: "13999999997", MiniProgramOpenId: "xxx", SendWay: MiniProgramSubscribe, UserId: 3, Type: "alert_stock"},
    }
}

func main() {
    // 消费kafka,商品数据变化消息
    // 这里不是重点,代码省略...
    productId := uint64(100)

    // 降价通知示例

    // 创建调用者
    reduceInvoker := NewInvoker()

    // 找出符合条件的记录
    reduceProductRecords := filterReducePriceSubscribeRecord(productId)
    // 消息发送,真实情况应该是并发执行通知,这里只是演示,修改为串行发送了
    for _, record := range reduceProductRecords {
       // 创建接收者
       notify := NewNotify(record.SendWay)

       // 创建命令
       reducePriceCommand := NewReducePriceCommand(notify, record)
       reduceInvoker.AddCommand(reducePriceCommand)
    }

    // 执行命令
    reduceInvoker.ExecuteCommands()

    // 到货通知示例

    // 创建调用者
    alertInvoker := NewInvoker()

    // 找出符合条件的记录
    alertStockRecords := filterAlertStockSubscribeRecord(productId)
    // 消息发送,真实情况应该是并发执行通知,这里只是演示,修改为串行发送了
    for _, record := range alertStockRecords {
       // 创建接收者
       notify := NewNotify(record.SendWay)

       // 创建命令
       alertStockCommand := NewAlertStockCommand(notify, record)
       alertInvoker.AddCommand(alertStockCommand)
    }

    // 执行命令
    alertInvoker.ExecuteCommands()
}

消息发送,真实情况应该是并发执行通知。

并发执行有很多的方案可以实现。比如:
1.协程,如 ants(阿里的协程池)
2.redis,如 asynq(基于redis实现的分布式任务队列)
3.消息中间件,如 kafka
不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高

总结

怎么样?看了上面的代码有什么感觉。是不是觉得可以非常方便的扩展新的业务。
比如来了一个新的需求:
小程序端,需要订阅促销活动即将开始的通知、订阅促销活动即将结束的通知。
这两个微信小程序订阅消息也可以仿照上述的逻辑,快速的实现。
不用调整大量的代码,体现了“对扩展开放,对修改关闭”的设计原则。

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

再举个例子,很多教材会用饭馆来举例,这里沿用这个示例。
1.作为顾客的我们是命令的下达者。
2.服务员是这个命令的接收者。
3.菜单是这个实际的命令。
4.厨师是这个命令的执行者。
业务场景:
当你要修改菜单的时候,只需要和服务员说就好了,她会转达给厨师,也就是说,我们实现了顾客和厨师的解耦。也就是调用者与实现者的解耦。
命令模式能够做到的是让一个命令接收者实现多个命令(服务员下单、拿酒水、上菜),或者把一条命令转达给多个实现者(热菜厨师、凉菜厨师、主食师傅)。