头图

设计模式那些事(5)——常见行为型模式的Go实现

前两篇介绍了设计模式那些事(2)——创建型模式在Go中的应用设计模式那些事(4)——常见结构型模式在Go中的应用,接下来我们看一下最后一种类型——行为型的设计模式。

创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。

行为型模式比较多,有 11 种,它们分别是:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式,今天重点介绍一下前四种最常见的模式。

一、观察者模式

定义

观察者模式允许你定义一种订阅机制,可在对象事件发生时通知多个 “观察” 该对象的其他对象。

使用场景

小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。

实现

同步阻塞是最经典的实现方式,主要是为了代码解耦;
异步非阻塞除了能实现代码解耦之外,还能提高代码的执行效率;
进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互。

下面实现一个同步阻塞的发布者、订阅者模型

package main
import "fmt"


type ISubject interface {
    register(observer Observer)
    notifyAll()
}

type Blogger struct {
    observerList []Observer
    name         string
}

func newBlogger(name string) * Blogger {
    return &Blogger{
        name: name,
    }
}

func (i * Blogger) Publish() {
    fmt.Printf("Blogger %s  has been updated\n", i.name)
    i.notifyAll()
}

func (i * Blogger) register(o Observer) {
    i.observerList = append(i.observerList, o)
}

func (i * Blogger) notifyAll() {
    for _, observer := range i.observerList {
        observer.update(i.name)
    }
}

type Observer interface {
    update(string)
    getID() string
}


type Reader struct {
    id string
}

func (c *Reader) update(bloggerName string) {
    fmt.Printf("Sending notice to reader %s for Blogger %s\n", c.id, bloggerName)
}

func (c *Reader) getID() string {
    return c.id
}

func main() {
    blogger := newBlogger("韩寒")

    observerFirst := &Reader{id: "abc@gmail.com"}
    observerSecond := &Reader{id: "xyz@gmail.com"}

    blogger.register(observerFirst)
    blogger.register(observerSecond)

    blogger.Publish()
}

输出

Blogger 韩寒  has been updated
Sending notice to reader abc@gmail.com for Blogger 韩寒
Sending notice to reader xyz@gmail.com for Blogger 韩寒

二、模板模式

定义

模板模式在一个方法中定义一个通用的算法(业务逻辑)骨架,并将某些步骤推迟到子类中实现。

模板模式有两大作用:复用和扩展。其中复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

使用场景

当多个类的算法具有通用步骤,除一些细微不同之外几乎完全一样时,你可使用该模式。

实现

线上购物和线下购物有着相同的步骤,这里定义一个购物类的接口、抽出一套通用的模版,线下购物和线上购物类都继承这套模版,分别实现这些步骤,这样不用分别写两套业务逻辑,也不用在同一套流程中做很多的 if else 判断,实现了代码的复用和扩展性。

package main

import "fmt"

//抽象类,购物,包裹一个模板的全部实现步骤
type IShopping interface {
    goToMall()            //开始购物
    browseProducts()      //浏览商品
    addShoppingCart()     //加入购物车
    pay()                 //结算
    finish()              //购物结束
    haveDinner()          //餐厅吃饭
    WantHaveDinner() bool //是否在餐厅吃饭
}

//封装一套流程模板,让具体的制作流程继承且实现
type template struct {
    s IShopping
}

//封装的固定模板
func (t *template) GoShoping() {
    t.s.goToMall()
    t.s.browseProducts()
    t.s.addShoppingCart()
    t.s.pay()
    t.s.finish()

    //子类可以重写该方法来决定是否执行下面动作
    if t.s.WantHaveDinner() == true {
        t.s.haveDinner()
    }
}


//具体的模板子类 线下购物
type OfflineShopping struct {
    template  //继承模板
}

func NewOfflineShopping() *OfflineShopping {
    offlineShopping := new(OfflineShopping)
    offlineShopping.s = offlineShopping
    return offlineShopping
}

func (mc *OfflineShopping) goToMall() {
    fmt.Println("开车前往商场")
}

func (mc *OfflineShopping) browseProducts() {
    fmt.Println("在商场浏览商品")
}

func (mc *OfflineShopping) addShoppingCart() {
    fmt.Println("将中意的商品加入手推车中")
}

func (mc *OfflineShopping) pay() {
    fmt.Println("前往收银台付款")
}

func (mc *OfflineShopping) finish() {
    fmt.Println("购物结束,将商品带回家")
}


func (mc *OfflineShopping) haveDinner() {
    fmt.Println("在商场吃个西餐")
}

func (mc *OfflineShopping) WantHaveDinner() bool {
    return true //启动Hook条件
}

//具体的模板子类 线上购物
type OnlineShopping struct {
    template  //继承模板
}

func NewOnlineShopping() *OnlineShopping {
    onlineShopping := new(OnlineShopping)
    onlineShopping.s = onlineShopping
    return onlineShopping
}

func (mt *OnlineShopping) goToMall() {
    fmt.Println("打开购物App")
}

func (mt *OnlineShopping) browseProducts() {
    fmt.Println("浏览商品页")
}

func (mt *OnlineShopping) addShoppingCart() {
    fmt.Println("将中意的商品加入购物车中")
}

func (mt *OnlineShopping) pay() {
    fmt.Println("线上付款")
}

func (mc *OnlineShopping) finish() {
    fmt.Println("购物结束,等待商品送货上门")
}

func (mc *OnlineShopping) haveDinner() {
    fmt.Println("点个外卖吃吃")
}

func (mc *OnlineShopping) WantHaveDinner() bool {
    return true //启动Hook条件
}

func main() {
    //1. 线下购物
    offline := NewOfflineShopping()
    offline.GoShoping() 

    fmt.Println("------------")

    //2. 线上购物
    online := NewOnlineShopping()
    online.GoShoping()
}

输出

开车前往商场
在商场浏览商品
将中意的商品加入手推车中
前往收银台付款
购物结束,将商品带回家
在商场吃个西餐
------------
打开购物App
浏览商品页
将中意的商品加入购物车中
线上付款
购物结束,等待商品送货上门
点个外卖吃吃

三、策略模式

定义

策略模式用来解耦策略的定义、创建、使用,实际上,一个完整的策略模式就是由这三个部分组成的。

策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。
策略的创建由工厂类来完成,封装策略创建的细节。

使用场景

当你想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可使用策略模式。

我们还可以通过策略模式来移除 if-else 分支判断。实际上,这得益于策略工厂 类,更本质上点讲,是借助“查表法”,根据 type 查表(map)替代根据 type 分支判断(进行实例化)。

实现

商家有两种不同的活动策略,一种是打折,一种是满减,在后台配置以后就可以用于各种营销活动之中:

package main

import  (
    "fmt"
    "math"
)

//销售策略
type SellStrategy interface {
    //根据原价得到售卖价
    GetPrice(price float64)     float64
}

type StrategyA struct {}

func (sa *StrategyA) GetPrice(price float64) float64 {
    fmt.Println("执行策略A, 打8折")
    return price * 0.8;
}

type StrategyB struct {}

func (sb *StrategyB) GetPrice(price float64) float64 {
    fmt.Println("执行策略B, 每满200减50")
    n:=int(math.Floor(price)) / 200
    if price >= 200 {
        price -= float64(n)*50
    }

    return price;
}

//环境类
type Goods struct {
    Price float64
    Strategy SellStrategy
}

func (g *Goods) SetStrategy(s SellStrategy) {
    g.Strategy = s
}

func (g *Goods) SellPrice() float64 {
    fmt.Println("原价", g.Price)
    return g.Strategy.GetPrice(g.Price)
}

func NewStrategy(t string) SellStrategy {
    switch t {
    case "A":
        return &StrategyA{}
    case "B":
        return &StrategyB{}
    }
    return nil
}

func main() {
    nike := Goods{
        Price: 1199.0,
    }
    //淘宝执行策略A
    strategyA:=NewStrategy("A")
    nike.SetStrategy(strategyA)
    fmt.Println("淘宝nike鞋卖", nike.SellPrice())

    //京东执行策略B
    strategyB:=NewStrategy("B")
    nike.SetStrategy(strategyB)
    fmt.Println("京东nike鞋卖", nike.SellPrice())
}

输出

原价 1199
执行策略A, 打8折
淘宝nike鞋卖 959.2
原价 1199
执行策略B, 每满200减50
京东nike鞋卖 949

四、职责链模式

定义

在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。

当然,在实际的开发中,也存在对这个模式的变体,那就是请求不会中途终止传递, 而是会被所有的处理器都处理一遍。

使用场景

职责链模式最常用来开发框架的过滤器和拦截器,让框架的使用者在不需要修改框架源码的情况下,添加新的过滤拦截功能。
这也体现了对扩展开放、对修改关闭的设计原则。

实现

这里实现一个敏感词过滤器

package main

import "fmt"

// SensitiveWordFilter 敏感词过滤器,判定是否是敏感词
type SensitiveWordFilter interface {
    Filter(content string) bool
}

// SensitiveWordFilterChain 职责链
type SensitiveWordFilterChain struct {
    filters []SensitiveWordFilter
}

// AddFilter 添加一个过滤器
func (c *SensitiveWordFilterChain) AddFilter(filter SensitiveWordFilter) {
    c.filters = append(c.filters, filter)
}

// Filter 执行过滤
func (c *SensitiveWordFilterChain) Filter(content string) bool {
    for _, filter := range c.filters {
        // 如果发现敏感直接返回结果
        if filter.Filter(content) {
            fmt.Println("发现敏感词,请修改后重新发布")
            return true
        }
    }
    fmt.Println("内容正常,可以发布")
    return false
}

// AdSensitiveWordFilter 广告
type AdSensitiveWordFilter struct{}

// Filter 实现过滤算法
func (f *AdSensitiveWordFilter) Filter(content string) bool {
    // TODO: 实现算法
    fmt.Println("发现广告敏感词")
    return true
}

// PoliticalWordFilter 政治敏感
type PoliticalWordFilter struct{}

// Filter 实现过滤算法
func (f *PoliticalWordFilter) Filter(content string) bool {
    // TODO: 实现算法
    fmt.Println("没有发现政治敏感词")
    return false
}

func main(){
    chain := &SensitiveWordFilterChain{}
    chain.AddFilter(&PoliticalWordFilter{})

    chain.AddFilter(&AdSensitiveWordFilter{})

    // chain.AddFilter(&PoliticalWordFilter{})

    chain.Filter("xxx")
}

输出

没有发现政治敏感词
发现广告敏感词
发现敏感词,请修改后重新发布

五、总结

设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。

借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性。

参考资料:
1、《设计模式之美》
2、https://refactoringguru.cn/de...
3、Easy搞掂Golang设计模式
4、https://lailin.xyz/post/go-de...

一篇文章讲明白一个知识点,每月更新,欢迎关注与交流。

20 声望
10 粉丝
0 条评论
推荐阅读
从0到1安装启动Kratos框架
{代码...} 1、环境准备1.1、安装Go方法一:pkg包安装安装 - Go官方网站安装go1.18以上的版本即可,注意选择合适自己电脑芯片的安装包,点击下一步即可该安装包会将Go发行版安装到 /usr/local/go 中方法二:homebr...

爆裂Gopher阅读 220

封面图
又一款眼前一亮的Linux终端工具!
今天给大家介绍一款最近发现的功能十分强大,颜值非常高的一款终端工具。这个神器我是在其他公众号文章上看到的,但他们都没把它的强大之处介绍明白,所以我自己体验一波后,再向大家分享自己的体验。

良许6阅读 1.9k

「刷起来」Go必看的进阶面试题详解
逃逸分析是Go语言中的一项重要优化技术,可以帮助程序减少内存分配和垃圾回收的开销,从而提高程序的性能。下面是一道涉及逃逸分析的面试题及其详解。

王中阳Go4阅读 2k评论 1

封面图
初学后端,如何做好表结构设计?
这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。

王中阳Go4阅读 1.8k评论 2

封面图
一分钟搞明白!快速掌握 Go WebAssembly
最近因为各种奇怪的原因,更多的接触到了 WebAssembly。虽然之前很多博客也翻过写过各种文章,但总感觉欠些味道。于是今天梳理了一版,和大家一起展开学习。

煎鱼4阅读 2.3k

go 协程操作map导致的数据竞争及解决方法
有个查询结果集的操作,无可避免的需要在循环获取数据,然后将结果集放到 map 中,这个操作在压测的时候,没出现问题,发布到生产环境之后,开始偶现 fatal error: concurrent map read and map write 错误,导致...

hxd_5阅读 890评论 4

Linux终端居然也可以做文件浏览器?
大家好,我是良许。在抖音上做直播已经整整 5 个月了,我很自豪我一路坚持到了现在【笑脸】最近我在做直播的时候,也开始学习鱼皮大佬,直播写代码。当然我不懂 Java 后端,因此就写写自己擅长的 Shell 脚本。但...

良许1阅读 2.1k

一篇文章讲明白一个知识点,每月更新,欢迎关注与交流。

20 声望
10 粉丝
宣传栏