结合Golang聊聊23种设计模式—行为型(2)

策略模式

策略模式,英文全称是 Strategy Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

我们知道,工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,不过,它解耦的是策略的定义、创建、使用这三部分。

为什么要使用策略模式

策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。

我们可以利用策略模式来移除 if-else 或者 switch-case 分支判断逻辑。将每个策略对应的复杂逻辑抽取出来和主体程序解藕。

代码示例

思考一下构建内存缓存的情形。 由于处在内存中, 故其大小会存在限制。 在达到其上限后, 一些条目就必须被移除以留出空间。 此类操作可通过多种算法进行实现。 一些流行的算法有:

  • 最少最近使用 (LRU): 移除最近使用最少的一条条目。
  • 先进先出 (FIFO): 移除最早创建的条目。
  • 最少使用 (LFU): 移除使用频率最低一条条目。

问题在于如何将我们的缓存类与这些算法解耦, 以便在运行时更改算法。 此外, 在添加新算法时, 缓存类不应改变。

这就是策略模式发挥作用的场景。 可创建一系列的算法, 每个算法都有自己的类。 这些类中的每一个都遵循相同的接口, 这使得系列算法之间可以互换。 假设通用接口名称为 eviction­Algo移除算法 。

现在, 我们的主要缓存类将嵌入至 eviction­Algo接口中。 缓存类会将全部类型的移除算法委派给 eviction­Algo接口, 而不是自行实现。 鉴于 eviction­Algo是一个接口, 我们可在运行时将算法更改为 LRU、 FIFO 或者 LFU, 而不需要对缓存类做出任何更改。

evictionAlgo.go: 策略接口

1
2
3
4
5
package main

type EvictionAlgo interface {
evict(c *Cache)
}

fifo.go: 具体策略

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type Fifo struct {
}

func (l *Fifo) evict(c *Cache) {
fmt.Println("Evicting by fifo strtegy")
}

lru.go: 具体策略

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type Lru struct {
}

func (l *Lru) evict(c *Cache) {
fmt.Println("Evicting by lru strtegy")
}

lfu.go: 具体策略

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type Lfu struct {
}

func (l *Lfu) evict(c *Cache) {
fmt.Println("Evicting by lfu strtegy")
}

cache.go: 背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

type Cache struct {
storage map[string]string
evictionAlgo EvictionAlgo
capacity int
maxCapacity int
}

func initCache(e EvictionAlgo) *Cache {
storage := make(map[string]string)
return &Cache{
storage: storage,
evictionAlgo: e,
capacity: 0,
maxCapacity: 2,
}
}

func (c *Cache) setEvictionAlgo(e EvictionAlgo) {
c.evictionAlgo = e
}

func (c *Cache) add(key, value string) {
if c.capacity == c.maxCapacity {
c.evict()
}
c.capacity++
c.storage[key] = value
}

func (c *Cache) get(key string) {
delete(c.storage, key)
}

func (c *Cache) evict() {
c.evictionAlgo.evict(c)
c.capacity--
}

main.go: 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

func main() {
lfu := &Lfu{}
cache := initCache(lfu)

cache.add("a", "1")
cache.add("b", "2")

cache.add("c", "3")

lru := &Lru{}
cache.setEvictionAlgo(lru)

cache.add("d", "4")

fifo := &Fifo{}
cache.setEvictionAlgo(fifo)

cache.add("e", "5")

}

output.txt: 执行结果

1
2
3
Evicting by lfu strtegy
Evicting by lru strtegy
Evicting by fifo strtegy

场景

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

策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。

当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。

策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。

如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。

策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。

当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。

策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。

优点

  • 你可以在运行时切换对象内的算法。
  • 你可以将算法的实现和使用算法的代码隔离开来。
  • 你可以使用组合来代替继承。
  • 开闭原则。 你无需对上下文进行修改就能够引入新的策略。

缺点

  • 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。
  • 客户端必须知晓策略间的不同——它需要选择合适的策略。
  • 许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。 这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁。

值得说的是:

桥接模式状态模式策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

状态模式

状态模式(有限状态机),英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

其主要思想是程序在任意时刻仅可处于几种有限状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移

你还可将该方法应用在对象上。 假如你有一个 文档Document类。 文档可能会处于 草稿Draft 、 审阅中Moderation和 已发布Published三种状态中的一种。 文档的 publish发布方法在不同状态下的行为略有不同:

  • 处于 草稿状态时, 它会将文档转移到审阅中状态。
  • 处于 审阅中状态时, 如果当前用户是管理员, 它会公开发布文档。
  • 处于 已发布状态时, 它不会进行任何操作。

为什么要使用状态模式

接着上面的文档状态,当我们逐步在 文档类中添加更多状态和依赖于状态的行为后, 基于条件语句的状态机就会暴露其最大的弱点。 为了能根据当前状态选择完成相应行为的方法, 绝大部分方法中会包含复杂的条件语句。 修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句, 导致代码的维护工作非常艰难。

这个问题会随着项目进行变得越发严重。 我们很难在设计阶段预测到所有可能的状态和转换。 随着时间推移, 最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。

状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。

原始对象被称为上下文 (context), 它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象。

这个结构可能看上去与策略模式相似, 但有一个关键性的不同——在状态模式中, 特定状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。

代码示例

让我们在一台自动售货机上使用状态设计模式。 为简单起见, 让我们假设自动售货机仅会销售一种类型的商品。 同时, 依然为了简单起见, 我们假设自动售货机可处于 4 种不同的状态中:

  • 有商品 (has­Item)
  • 无商品 (no­Item)
  • 商品已请求 (item­Requested)
  • 收到纸币 (has­Money)

同时, 自动售货机也会有不同的操作。 再一次的, 为了简单起见, 我们假设其只会执行 4 种操作:

  • 选择商品
  • 添加商品
  • 插入纸币
  • 提供商品

当对象可以处于许多不同的状态中时应使用状态设计模式, 同时根据传入请求的不同, 对象需要变更其当前状态。

在我们的例子中, 自动售货机可以有多种不同的状态, 同时会在这些状态之间持续不断地互相转换。 我们假设自动售货机处于 商品已请求状态中。 在 “插入纸币” 的操作发生后, 机器将自动转换至 收到纸币状态。

根据其当前状态, 机器可就相同请求采取不同的行为。 例如, 如果用户想要购买一件商品, 机器将在 有商品状态时继续操作, 而在 无商品状态时拒绝操作。

自动售货机的代码不会被这一逻辑污染; 所有依赖于状态的代码都存在于各自的状态实现中。

vendingMachine.go: 背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import "fmt"

type VendingMachine struct {
hasItem State
itemRequested State
hasMoney State
noItem State

currentState State

itemCount int
itemPrice int
}

func newVendingMachine(itemCount, itemPrice int) *VendingMachine {
v := &VendingMachine{
itemCount: itemCount,
itemPrice: itemPrice,
}
hasItemState := &HasItemState{
vendingMachine: v,
}
itemRequestedState := &ItemRequestedState{
vendingMachine: v,
}
hasMoneyState := &HasMoneyState{
vendingMachine: v,
}
noItemState := &NoItemState{
vendingMachine: v,
}

v.setState(hasItemState)
v.hasItem = hasItemState
v.itemRequested = itemRequestedState
v.hasMoney = hasMoneyState
v.noItem = noItemState
return v
}

func (v *VendingMachine) requestItem() error {
return v.currentState.requestItem()
}

func (v *VendingMachine) addItem(count int) error {
return v.currentState.addItem(count)
}

func (v *VendingMachine) insertMoney(money int) error {
return v.currentState.insertMoney(money)
}

func (v *VendingMachine) dispenseItem() error {
return v.currentState.dispenseItem()
}

func (v *VendingMachine) setState(s State) {
v.currentState = s
}

func (v *VendingMachine) incrementItemCount(count int) {
fmt.Printf("Adding %d items\n", count)
v.itemCount = v.itemCount + count
}

state.go: 状态接口

1
2
3
4
5
6
7
8
package main

type State interface {
addItem(int) error
requestItem() error
insertMoney(money int) error
dispenseItem() error
}

noItemState.go: 具体状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type NoItemState struct {
vendingMachine *VendingMachine
}

func (i *NoItemState) requestItem() error {
return fmt.Errorf("Item out of stock")
}

func (i *NoItemState) addItem(count int) error {
i.vendingMachine.incrementItemCount(count)
i.vendingMachine.setState(i.vendingMachine.hasItem)
return nil
}

func (i *NoItemState) insertMoney(money int) error {
return fmt.Errorf("Item out of stock")
}
func (i *NoItemState) dispenseItem() error {
return fmt.Errorf("Item out of stock")
}

hasItemState.go: 具体状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

type HasItemState struct {
vendingMachine *VendingMachine
}

func (i *HasItemState) requestItem() error {
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItem)
return fmt.Errorf("No item present")
}
fmt.Printf("Item requestd\n")
i.vendingMachine.setState(i.vendingMachine.itemRequested)
return nil
}

func (i *HasItemState) addItem(count int) error {
fmt.Printf("%d items added\n", count)
i.vendingMachine.incrementItemCount(count)
return nil
}

func (i *HasItemState) insertMoney(money int) error {
return fmt.Errorf("Please select item first")
}
func (i *HasItemState) dispenseItem() error {
return fmt.Errorf("Please select item first")
}

itemRequestedState.go: 具体状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type ItemRequestedState struct {
vendingMachine *VendingMachine
}

func (i *ItemRequestedState) requestItem() error {
return fmt.Errorf("Item already requested")
}

func (i *ItemRequestedState) addItem(count int) error {
return fmt.Errorf("Item Dispense in progress")
}

func (i *ItemRequestedState) insertMoney(money int) error {
if money < i.vendingMachine.itemPrice {
return fmt.Errorf("Inserted money is less. Please insert %d", i.vendingMachine.itemPrice)
}
fmt.Println("Money entered is ok")
i.vendingMachine.setState(i.vendingMachine.hasMoney)
return nil
}
func (i *ItemRequestedState) dispenseItem() error {
return fmt.Errorf("Please insert money first")
}

hasMoneyState.go: 具体状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

type HasMoneyState struct {
vendingMachine *VendingMachine
}

func (i *HasMoneyState) requestItem() error {
return fmt.Errorf("Item dispense in progress")
}

func (i *HasMoneyState) addItem(count int) error {
return fmt.Errorf("Item dispense in progress")
}

func (i *HasMoneyState) insertMoney(money int) error {
return fmt.Errorf("Item out of stock")
}
func (i *HasMoneyState) dispenseItem() error {
fmt.Println("Dispensing Item")
i.vendingMachine.itemCount = i.vendingMachine.itemCount - 1
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItem)
} else {
i.vendingMachine.setState(i.vendingMachine.hasItem)
}
return nil
}

main.go: 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"fmt"
"log"
)

func main() {
vendingMachine := newVendingMachine(1, 10)

err := vendingMachine.requestItem()
if err != nil {
log.Fatalf(err.Error())
}

err = vendingMachine.insertMoney(10)
if err != nil {
log.Fatalf(err.Error())
}

err = vendingMachine.dispenseItem()
if err != nil {
log.Fatalf(err.Error())
}

fmt.Println()

err = vendingMachine.addItem(2)
if err != nil {
log.Fatalf(err.Error())
}

fmt.Println()

err = vendingMachine.requestItem()
if err != nil {
log.Fatalf(err.Error())
}

err = vendingMachine.insertMoney(10)
if err != nil {
log.Fatalf(err.Error())
}

err = vendingMachine.dispenseItem()
if err != nil {
log.Fatalf(err.Error())
}
}

output.txt: 执行结果

1
2
3
4
5
6
7
8
9
Item requestd
Money entered is ok
Dispensing Item

Adding 2 items

Item requestd
Money entered is ok
Dispensing Item

场景

如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。

模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。

如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。

状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。

当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。

状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。

优点

  • 单一职责原则。 将与特定状态相关的代码放在单独的类中。
  • 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
  • 通过消除臃肿的状态机条件语句简化上下文代码。

缺点

  • 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。

命令模式

命令模式的英文翻译是 Command Design Pattern。

在 GoF 的《设计模式》一书中,它是这么定义的:The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations.

翻译成中文就是下面这样。为了帮助你理解,我对这个翻译稍微做了补充和解释,也一起放在了下面的括号中。

命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。

对于 GoF 给出的定义,我这里再进一步解读一下。落实到编码实现,命令模式用的最核心的实现手段,是将函数封装成对象。我们知道,go 语言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函数没法儿作为参数传递给其他函数,也没法儿赋值给变量。借助命令模式,我们可以将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。从实现的角度来说,它类似我们之前讲过的回调。当我们把函数封装成对象之后,对象就可以存储下来,方便控制执行。所以,命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

为什么要使用命令模式

命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

代码示例

下面我们通过电视机的例子来了解命令模式。 你可通过以下方式打开电视机:

  • 按下遥控器上的 ON 开关;
  • 按下电视机上的 ON 开关。

我们可以从实现 ON 命令对象并以电视机作为接收者入手。 当在此命令上调用 execute执行方法时, 方法会调用 TV.on打开电视函数。 最后的工作是定义请求者: 这里实际上有两个请求者: 遥控器和电视机。 两者都将嵌入 ON 命令对象。

注意我们是如何将相同请求封装进多个请求者的。 我们也可以采用相同的方式来处理其他命令。 创建独立命令对象的优势在于可将 UI 逻辑与底层业务逻辑解耦。 这样就无需为每个请求者开发不同的处理者了。 命令对象中包含执行所需的全部信息, 所以也可用于延迟执行。

button.go: 请求者

1
2
3
4
5
6
7
8
9
package main

type Button struct {
command Command
}

func (b *Button) press() {
b.command.execute()
}

command.go: 命令接口

1
2
3
4
5
package main

type Command interface {
execute()
}

onCommand.go: 具体接口

1
2
3
4
5
6
7
8
9
package main

type OnCommand struct {
device Device
}

func (c *OnCommand) execute() {
c.device.on()
}

offCommand.go: 具体接口

1
2
3
4
5
6
7
8
9
package main

type OffCommand struct {
device Device
}

func (c *OffCommand) execute() {
c.device.off()
}

device.go: 接收者接口

1
2
3
4
5
6
package main

type Device interface {
on()
off()
}

tv.go: 具体接收者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type Tv struct {
isRunning bool
}

func (t *Tv) on() {
t.isRunning = true
fmt.Println("Turning tv on")
}

func (t *Tv) off() {
t.isRunning = false
fmt.Println("Turning tv off")
}

main.go: 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

func main() {
tv := &Tv{}

onCommand := &OnCommand{
device: tv,
}

offCommand := &OffCommand{
device: tv,
}

onButton := &Button{
command: onCommand,
}
onButton.press()

offButton := &Button{
command: offCommand,
}
offButton.press()
}

output.txt: 执行结果

1
2
Turning tv on
Turning tv off

场景

如果你需要通过操作来参数化对象, 可使用命令模式。

命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。

举个例子: 你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。

如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。

同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。

如果你想要实现操作回滚功能, 可使用命令模式。

尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。

为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。

这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。

其次, 备份状态可能会占用大量内存。 因此, 有时你需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。

优点

  • 单一职责原则。 你可以解耦触发和执行操作的类。
  • 开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
  • 你可以实现撤销和恢复功能。
  • 你可以实现操作的延迟执行。
  • 你可以将一组简单命令组合成一个复杂命令。

缺点

  • 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。