GO设计模式

Olivia的小跟班 Lv3

题记

本文参考了刘丹冰的Easy搞定Golang设计模式

本文有关设计模式的实现请访问

一、设计模式概述

修习软件开发比作武功修炼,可以分成招式和内功两部分。

招式:

  • 编程语言
  • 开发工具
  • 框架技术

内功:

  • 数据结构
  • 算法
  • 设计模式
  • 架构设计
  • 软件工程

注意:招式可以很快学会,但是内功的修炼需要更长的时间。

1.1设计模式从何而来

克里斯托弗.亚历山大在作品《建筑的永恒之道》中对“模式”的描述是:“每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心,通过这种方式,我们可以无数次地重用那些已有的成功的解决方案,无须再重复相同的工作。”他给出了设计模式的定义。我们也可以用下面这句话来理解“设计模式”的定义:“设计模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。

1.2 软件设计模式又从何而来

GoF —(Ralph Johnson,Richard Helm,Erich Gamma,John Vlissides)

​ GoF将模式的概念引入软件工程领域,这标志着软件模式的诞生。软件模式(Software Patterns)是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。软件模式与具体的应用领域无关,也就是说无论你从事的是移动应用开发、桌面应用开发、Web应用开发还是嵌入式软件的开发,都可以使用软件模式。无论你是使用Java、C#、Objective-C、VB.net、Smalltalk等纯面向对象编程语言,还是使用C++、PHP、Delphi、JavaScript等可支持面向对象编程的语言,你都需要了解软件设计模式!
​ GoF给软件设计模式提供了定义,如下:“软件设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。”

一句大白话可以总结:“在一定环境下,用固定套路解决问题。”

1.3 软件设计模式的种类

GoF提出的设计模式有23个,包括:

(1)创建型(Creational)模式:如何创建对象;

(2)结构型(Structural )模式:如何实现类或对象的组合;

(3)行为型(Behavioral)模式:类或对象怎样交互以及怎样分配职责。

有一个“简单工厂模式”不属于GoF 23种设计模式,但大部分的设计模式书籍都会对它进行专门的介绍。设计模式目前种类: GoF的23种 + “简单工厂模式” = 24种。

1.4 软件设计模式的作用

那么对于初学者来说,学习设计模式将有助于更加深入地理解面向对象思想, 让你知道:

1
2
3
4
5
6
1. 如何将代码分散在几个不同的类中?
2. 为什么要有“接口”?
3. 何谓针对抽象编程?
4. 何时不应该使用继承?
5. 如何不修改源代码增加新功能?
6. 更好地阅读和理解现有类库与其他系统中的源代码。

学习设计模式会让你早点脱离面向对象编程的“菜鸟期”。

1.5 如何学好设计模式

设计模式的基础是:多态。

初学者:积累案例,不要盲目的背类图。

初级开发人员:多思考,多梳理,归纳总结,尊重事物的认知规律,注意临界点的突破,不要浮躁。

中级开发人员:合适的开发环境,寻找合适的设计模式来解决问题。

多应用,对经典则组合设计模式的大量,自由的运用。要不断的追求。

二、面向对象设计原则

对于面向对象软件系统的设计而言,在支持可维护性的同时,提高系统的可复用性是一个至关重要的问题,如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。

面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。面向对象设计原则也是我们用于评价一个设计模式的使用效果的重要指标之一。

原则的目的: 高内聚,低耦合

1.单一职责原则

类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type ClothesShop struct {}

func (cs *ClothesShop) OnShop() {
fmt.Println("休闲的装扮")
}

type ClothesWork struct {}

func (cw *ClothesWork) OnWork() {
fmt.Println("工作的装扮")
}

func main() {
//工作的时候
cw := new(ClothesWork)
cw.OnWork()

//shopping的时候
cs := new(ClothesShop)
cs.OnShop()
}

在面向对象编程的过程中,设计一个类,建议对外提供的功能单一,接口单一,影响一个类的范围就只限定在这一个接口上,一个类的一个接口具备这个类的功能含义,职责单一不复杂。

2.开闭原则设计

image.png

2.1 平铺式设计

那么作为interface数据类型,他存在的意义在哪呢? 实际上是为了满足一些面向对象的编程思想。我们知道,软件设计的最高目标就是高内聚,低耦合。那么其中有一个设计原则叫开闭原则。什么是开闭原则呢,接下来我们看一个例子:

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"

//我们要写一个类,Banker银行业务员
type Banker struct {
}

//存款业务
func (this *Banker) Save() {
fmt.Println( "进行了 存款业务...")
}

//转账业务
func (this *Banker) Transfer() {
fmt.Println( "进行了 转账业务...")
}

//支付业务
func (this *Banker) Pay() {
fmt.Println( "进行了 支付业务...")
}

func main() {
banker := &Banker{}

banker.Save()
banker.Transfer()
banker.Pay()
}

代码很简单,就是一个银行业务员,他可能拥有很多的业务,比如Save()存款、Transfer()转账、Pay()支付等。那么如果这个业务员模块只有这几个方法还好,但是随着我们的程序写的越来越复杂,银行业务员可能就要增加方法,会导致业务员模块越来越臃肿。

img

这样的设计会导致,当我们去给Banker添加新的业务的时候,会直接修改原有的Banker代码,那么Banker模块的功能会越来越多,出现问题的几率也就越来越大,假如此时Banker已经有99个业务了,现在我们要添加第100个业务,可能由于一次的不小心,导致之前99个业务也一起崩溃,因为所有的业务都在一个Banker类里,他们的耦合度太高,Banker的职责也不够单一,代码的维护成本随着业务的复杂正比成倍增大。

2.2 开闭原则设计

那么,如果我们拥有接口, interface这个东西,那么我们就可以抽象一层出来,制作一个抽象的Banker模块,然后提供一个抽象的方法。 分别根据这个抽象模块,去实现支付Banker(实现支付方法),转账Banker(实现转账方法)
如下:
img

那么依然可以搞定程序的需求。 然后,当我们想要给Banker添加额外功能的时候,之前我们是直接修改Banker的内容,现在我们可以单独定义一个股票Banker(实现股票方法),到这个系统中。 而且股票Banker的实现成功或者失败都不会影响之前的稳定系统,他很单一,而且独立。

所以以上,当我们给一个系统添加一个功能的时候,不是通过修改代码,而是通过增添代码来完成,那么就是开闭原则的核心思想了。所以要想满足上面的要求,是一定需要interface来提供一层抽象的接口的。

golang代码实现如下:

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
package main

import "fmt"

//抽象的银行业务员
type AbstractBanker interface{
DoBusi() //抽象的处理业务接口
}

//存款的业务员
type SaveBanker struct {
//AbstractBanker
}

func (sb *SaveBanker) DoBusi() {
fmt.Println("进行了存款")
}

//转账的业务员
type TransferBanker struct {
//AbstractBanker
}

func (tb *TransferBanker) DoBusi() {
fmt.Println("进行了转账")
}

//支付的业务员
type PayBanker struct {
//AbstractBanker
}

func (pb *PayBanker) DoBusi() {
fmt.Println("进行了支付")
}


func main() {
//进行存款
sb := &SaveBanker{}
sb.DoBusi()

//进行转账
tb := &TransferBanker{}
tb.DoBusi()

//进行支付
pb := &PayBanker{}
pb.DoBusi()

}

当然我们也可以根据AbstractBanker设计一个小框架

1
2
3
4
5
//实现架构层(基于抽象层进行业务封装-针对interface接口进行封装)
func BankerBusiness(banker AbstractBanker) {
//通过接口来向下调用,(多态现象)
banker.DoBusi()
}

那么main中可以如下实现业务调用:

1
2
3
4
5
6
7
8
9
10
func main() {
//进行存款
BankerBusiness(&SaveBanker{})

//进行存款
BankerBusiness(&TransferBanker{})

//进行存款
BankerBusiness(&PayBanker{})
}

再看开闭原则定义:
开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
简单的说就是在修改需求的时候,应该尽量通过扩展来实现变化,而不是通过修改已有代码来实现变化。

2.3 接口的意义

好了,现在interface已经基本了解,那么接口的意义最终在哪里呢,想必现在你已经有了一个初步的认知,实际上接口的最大的意义就是实现多态的思想,就是我们可以根据interface类型来设计API接口,那么这种API接口的适应能力不仅能适应当下所实现的全部模块,也适应未来实现的模块来进行调用。 调用未来可能就是接口的最大意义所在吧,这也是为什么架构师那么值钱,因为良好的架构师是可以针对interface设计一套框架,在未来许多年却依然适用。

3.里氏替换原则

任何基类可以出现的地方,子类一定可以出现。

里氏替换原则是对开闭原则的补充,实现开闭原则的关键步骤就是抽象化,基类与子类的关系就是要尽可能的抽象化。

4.依赖倒置原则

面向接口编程,抽象不应该依赖于具体类,具体类应当依赖于抽象。

这是为了减少类间的耦合,使系统更适宜于扩展,也更便于维护。

5.最少知道原则(迪米特法则)

一个实体应当尽量少地与其他实体之间发生相互作用。

还是为了降低耦合,一个类与其他类的关联越少,越易于扩展。

6.接口分离原则

使用多个专门的接口,而不使用高耦合的单一接口。

避免同一个接口占用过多的职责,更明确的划分,可以降低耦合。高耦合会导致程序不易扩展,提高出错的风险。

三、创建型模式

3.1简单工厂模式

3.1.1 为什么需要工厂模式

首先来看,如果没有工厂模式,在开发者创建一个类的对象时,如果有很多不同种类的对象将会如何实现,代码如下:

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
package main

import "fmt"

//水果类
type Fruit struct {
//...
//...
//...
}

func (f *Fruit) Show(name string) {
if name == "apple" {
fmt.Println("我是苹果")
} else if name == "banana" {
fmt.Println("我是香蕉")
} else if name == "pear" {
fmt.Println("我是梨")
}
}

//创建一个Fruit对象
func NewFruit(name string) *Fruit {
fruit := new(Fruit)

if name == "apple" {
//创建apple逻辑
} else if name == "banana" {
//创建banana逻辑
} else if name == "pear" {
//创建pear逻辑
}

return fruit
}

func main() {
apple := NewFruit("apple")
apple.Show("apple")

banana := NewFruit("banana")
banana.Show("banana")

pear := NewFruit("pear")
pear.Show("pear")
}

不难看出,Fruit类是一个“巨大的”类,在该类的设计中存在如下几个问题:

​ (1) 在Fruit类中包含很多“if…else…”代码块,整个类的代码相当冗长,代码越长,阅读难度、维护难度和测试难度也越大;而且大量条件语句的存在还将影响系统的性能,程序在执行过程中需要做大量的条件判断。

​ (2) Fruit类的职责过重,它负责初始化和显示所有的水果对象,将各种水果对象的初始化代码和显示代码集中在一个类中实现,违反了“单一职责原则”,不利于类的重用和维护;

(3) 当需要增加新类型的水果时,必须修改Fruit类的构造函数NewFruit()和其他相关方法源代码,违反了“开闭原则”。

关键是来观察main()函数,main()函数与Fruit类是两个模块。当业务层希望创建一个对象的时候,将直接依赖Fruit类型的构造方法NewFruit(),这样随着Fruit的越来越复杂,那么业务层的开发逻辑也需要依赖Fruit模块的更新,且随之改变,这样将导致业务层开发需要观察Fruit模块做改动,影响业务层的开发效率和稳定性。整体的依赖关系为。

1
业务逻辑层 ---> 基础类模块

那么如何将业务层创建对象与基础类模块做解耦呢,这里即可以在中间加一层工厂模块层,来降低业务逻辑层对基础模块层的直接依赖和耦合关联。

1
业务逻辑层 ---> 工厂模块 ---> 基础类模块

​ 这样就引出了需要对工厂模块的一些设计和加工生成基础模块对象的模式。

3.1.2 简单工厂模式角色和职责

​ 简单工厂模式并不属于GoF的23种设计模式。他是开发者自发认为的一种非常简易的设计模式,其角色和职责如下:

工厂(Factory)角色:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。

抽象产品(AbstractProduct)角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。

具体产品(Concrete Product)角色:简单工厂模式所创建的具体实例对象。

其标准的设计模式类图如下:

img

3.1.3 简单工厂模式实现

​ 根据本章节的案例可以将标准的“简单工厂模式”类图改进如下:

img

简单工厂方法模式的实现代码如下:

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
67
68
package main

import "fmt"

// ======= 抽象层 =========

//水果类(抽象接口)
type Fruit interface {
Show() //接口的某方法
}
// ======= 基础类模块 =========

type Apple struct {
Fruit //为了易于理解显示继承(此行可以省略)
}

func (apple *Apple) Show() {
fmt.Println("我是苹果")
}

type Banana struct {
Fruit
}

func (banana *Banana) Show() {
fmt.Println("我是香蕉")
}

type Pear struct {
Fruit
}

func (pear *Pear) Show() {
fmt.Println("我是梨")
}

// ========= 工厂模块 =========
//一个工厂, 有一个生产水果的机器,返回一个抽象水果的指针
type Factory struct {}

func (fac *Factory) CreateFruit(kind string) Fruit {
var fruit Fruit

if kind == "apple" {
fruit = new(Apple)
} else if kind == "banana" {
fruit = new(Banana)
} else if kind == "pear" {
fruit = new(Pear)
}

return fruit
}


// ==========业务逻辑层==============
func main() {
factory := new(Factory)

apple := factory.CreateFruit("apple")
apple.Show()

banana := factory.CreateFruit("banana")
banana.Show()

pear := factory.CreateFruit("pear")
pear.Show()
}

上述代码可以看出,业务逻辑层只会和工厂模块进行依赖,这样业务逻辑层将不再关心Fruit类是具体怎么创建基础对象的。

3.1.4 简单工厂方法模式的优缺点

优点:

  1. 实现了对象创建和使用的分离。

  2. 不需要记住具体类名,记住参数即可,减少使用者记忆量。

缺点:

  1. 对工厂类职责过重,一旦不能工作,系统受到影响。

  2. 增加系统中类的个数,复杂度和理解度增加。

  3. 违反“开闭原则”,添加新产品需要修改工厂逻辑,工厂越来越复杂。

适用场景:

  1. 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。

  2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。

3.2 工厂方法模式

3.2.1 工厂方法模式中的角色和职责

抽象工厂(Abstract Factory)角色:工厂方法模式的核心,任何工厂类都必须实现这个接口。

工厂(Concrete Factory)角色:具体工厂类是抽象工厂的一个实现,负责实例化产品对象。

抽象产品(Abstract Product)角色:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。

具体产品(Concrete Product)角色:工厂方法模式所创建的具体实例对象。

1
简单工厂模式  + “开闭原则” =    工厂方法模式

工厂方法模式的标准类图如下:

其标准的设计模式类图如下:

img

3.2.2 工厂方法模式的实现

​ 根据本章节的案例可以将标准的“工厂方法模式”类图改进如下:

img

实现的代码如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package main

import "fmt"

// ======= 抽象层 =========

//水果类(抽象接口)
type Fruit interface {
Show() //接口的某方法
}

//工厂类(抽象接口)
type AbstractFactory interface {
CreateFruit() Fruit //生产水果类(抽象)的生产器方法
}

// ======= 基础类模块 =========
type Apple struct {
Fruit //为了易于理解显示继承(此行可以省略)
}

func (apple *Apple) Show() {
fmt.Println("我是苹果")
}

type Banana struct {
Fruit
}

func (banana *Banana) Show() {
fmt.Println("我是香蕉")
}

type Pear struct {
Fruit
}

func (pear *Pear) Show() {
fmt.Println("我是梨")
}

// ========= 工厂模块 =========
//具体的苹果工厂
type AppleFactory struct {
AbstractFactory
}

func (fac *AppleFactory) CreateFruit() Fruit {
var fruit Fruit

//生产一个具体的苹果
fruit = new(Apple)

return fruit
}

//具体的香蕉工厂
type BananaFactory struct {
AbstractFactory
}

func (fac *BananaFactory) CreateFruit() Fruit {
var fruit Fruit

//生产一个具体的香蕉
fruit = new(Banana)

return fruit
}


//具体的梨工厂
type PearFactory struct {
AbstractFactory
}

func (fac *PearFactory) CreateFruit() Fruit {
var fruit Fruit

//生产一个具体的梨
fruit = new(Pear)

return fruit
}

//======= 业务逻辑层 =======
func main() {
/*
本案例为了突出根据依赖倒转原则与面向接口编程特性。
一些变量的定义将使用显示类型声明方式
*/

//需求1:需要一个具体的苹果对象
//1-先要一个具体的苹果工厂
var appleFac AbstractFactory
appleFac = new(AppleFactory)
//2-生产相对应的具体水果
var apple Fruit
apple = appleFac.CreateFruit()

apple.Show()


//需求2:需要一个具体的香蕉对象
//1-先要一个具体的香蕉工厂
var bananaFac AbstractFactory
bananaFac = new(BananaFactory)
//2-生产相对应的具体水果
var banana Fruit
banana = bananaFac.CreateFruit()

banana.Show()

//需求3:需要一个具体的梨对象
//1-先要一个具体的梨工厂
var pearFac AbstractFactory
pearFac = new(PearFactory)
//2-生产相对应的具体水果
var pear Fruit
pear = pearFac.CreateFruit()

pear.Show()

//需求4:需要一个日本的苹果?
}

上述代码是通过面向抽象层开发,业务逻辑层的main()函数逻辑,依然是只与工厂耦合,且只与抽象的工厂和抽象的水果类耦合,这样就遵循了面向抽象层接口编程的原则。

那么抽象的工厂方法模式如何体现“开闭原则”的。接下来可以尝试在原有的代码上添加一种新产品的生产,如“日本苹果”,具体的代码如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package main

import "fmt"

// ======= 抽象层 =========

//水果类(抽象接口)
type Fruit interface {
Show() //接口的某方法
}

//工厂类(抽象接口)
type AbstractFactory interface {
CreateFruit() Fruit //生产水果类(抽象)的生产器方法
}

// ======= 基础类模块 =========
type Apple struct {
Fruit //为了易于理解显示继承(此行可以省略)
}

func (apple *Apple) Show() {
fmt.Println("我是苹果")
}

type Banana struct {
Fruit
}

func (banana *Banana) Show() {
fmt.Println("我是香蕉")
}

type Pear struct {
Fruit
}

func (pear *Pear) Show() {
fmt.Println("我是梨")
}

//(+) 新增一个"日本苹果"
type JapanApple struct {
Fruit
}

func (jp *JapanApple) Show() {
fmt.Println("我是日本苹果")
}

// ========= 工厂模块 =========
//具体的苹果工厂
type AppleFactory struct {
AbstractFactory
}

func (fac *AppleFactory) CreateFruit() Fruit {
var fruit Fruit

//生产一个具体的苹果
fruit = new(Apple)

return fruit
}

//具体的香蕉工厂
type BananaFactory struct {
AbstractFactory
}

func (fac *BananaFactory) CreateFruit() Fruit {
var fruit Fruit

//生产一个具体的香蕉
fruit = new(Banana)

return fruit
}


//具体的梨工厂
type PearFactory struct {
AbstractFactory
}

func (fac *PearFactory) CreateFruit() Fruit {
var fruit Fruit

//生产一个具体的梨
fruit = new(Pear)

return fruit
}

//具体的日本工厂
type JapanAppleFactory struct {
AbstractFactory
}

func (fac *JapanAppleFactory) CreateFruit() Fruit {
var fruit Fruit

//生产一个具体的日本苹果
fruit = new(JapanApple)

return fruit
}

// ========= 业务逻辑层 =========
func main() {
/*
本案例为了突出根据依赖倒转原则与面向接口编程特性。
一些变量的定义将使用显示类型声明方式
*/

//需求1:需要一个具体的苹果对象
//1-先要一个具体的苹果工厂
var appleFac AbstractFactory
appleFac = new(AppleFactory)
//2-生产相对应的具体水果
var apple Fruit
apple = appleFac.CreateFruit()

apple.Show()


//需求2:需要一个具体的香蕉对象
//1-先要一个具体的香蕉工厂
var bananaFac AbstractFactory
bananaFac = new(BananaFactory)
//2-生产相对应的具体水果
var banana Fruit
banana = bananaFac.CreateFruit()

banana.Show()

//需求3:需要一个具体的梨对象
//1-先要一个具体的梨工厂
var pearFac AbstractFactory
pearFac = new(PearFactory)
//2-生产相对应的具体水果
var pear Fruit
pear = pearFac.CreateFruit()

pear.Show()

//需求4:需要一个日本的苹果?
//1-先要一个具体的日本评估工厂
var japanAppleFac AbstractFactory
japanAppleFac = new(JapanAppleFactory)
//2-生产相对应的具体水果
var japanApple Fruit
japanApple = japanAppleFac.CreateFruit()

japanApple.Show()
}

可以看见,新增的基本类“日本苹果”,和“具体的工厂” 均没有改动之前的任何代码。完全符合开闭原则思想。新增的功能不会影响到之前的已有的系统稳定性。

3.2.3 工厂方法模式的优缺点

优点:

  • 不需要记住具体类名,甚至连具体参数都不用记忆。
  • 实现了对象创建和使用的分离。
  • 系统的可扩展性也就变得非常好,无需修改接口和原类。
  • 对于新产品的创建,符合开闭原则。

缺点:

  • 增加系统中类的个数,复杂度和理解度增加。
  • 增加了系统的抽象性和理解难度。

适用场景:

  • 客户端不知道它所需要的对象的类。
  • 抽象工厂类通过其子类来指定创建哪个对象。

3.3抽象工厂方法

工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。因此,可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是本文将要学习的抽象工厂模式的基本思想。

img

从工厂方法模式可以看出来:

(1)当添加一个新产品的时候,比如葡萄,虽然不用修改代码,但是需要添加大量的类,而且还需要添加相对的工厂。(系统开销,维护成本)

(2)如果使用同一地域的水果(日本苹果,日本香蕉,日本梨),那么需要分别创建具体的工厂,如果选择出现失误,将会造成混乱,虽然可以加一些约束,但是代码实现变得复杂。

所以“抽象工厂方法模式”引出了“产品族”和“产品等级结构”概念,其目的是为了更加高效的生产同一个产品组产品。

3.3.1 产品族与产品等级结构

img

上图表示“产品族”和“产品登记结构”的关系。

产品族:具有同一个地区、同一个厂商、同一个开发包、同一个组织模块等,但是具备不同特点或功能的产品集合,称之为是一个产品族。

产品等级结构:具有相同特点或功能,但是来自不同的地区、不同的厂商、不同的开发包、不同的组织模块等的产品集合,称之为是一个产品等级结构。

当程序中的对象可以被划分为产品族和产品等级结构之后,那么“抽象工厂方法模式”才可以被适用。

“抽象工厂方法模式”是针对“产品族”进行生产产品,具体如下图所示。

img

3.3.2 抽象工厂模式的角色和职责

抽象工厂(Abstract Factory)角色:它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。

具体工厂(Concrete Factory)角色:它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。

抽象产品(Abstract Product)角色:它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。

具体产品(Concrete Product)角色:它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。

img

可以看出来具体的工厂1,只负责生成具体的产品A1和B1,具体的工厂2,只负责生成具体的产品A2和B2。

“工厂1、A1、B1”为一组,是一个产品族, “工厂2、A2、B2”为一组,也是一个产品族。

3.3.3 抽象工厂方法模式的实现

抽象工厂方法模式按照本章节的案例,可以得到类图如下:

img

代码如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package main

import "fmt"

// ======= 抽象层 =========
type AbstractApple interface {
ShowApple()
}

type AbstractBanana interface {
ShowBanana()
}

type AbstractPear interface {
ShowPear()
}

//抽象工厂
type AbstractFactory interface {
CreateApple() AbstractApple
CreateBanana() AbstractBanana
CreatePear() AbstractPear
}

// ======== 实现层 =========
/* 中国产品族 */
type ChinaApple struct {}

func (ca *ChinaApple) ShowApple() {
fmt.Println("中国苹果")
}

type ChinaBanana struct {}

func (cb *ChinaBanana) ShowBanana() {
fmt.Println("中国香蕉")
}

type ChinaPear struct {}

func (cp *ChinaPear) ShowPear() {
fmt.Println("中国梨")
}

type ChinaFactory struct {}

func (cf *ChinaFactory) CreateApple() AbstractApple {
var apple AbstractApple

apple = new(ChinaApple)

return apple
}

func (cf *ChinaFactory) CreateBanana() AbstractBanana {
var banana AbstractBanana

banana = new(ChinaBanana)

return banana
}

func (cf *ChinaFactory) CreatePear() AbstractPear {
var pear AbstractPear

pear = new(ChinaPear)

return pear
}

/* 日本产品族 */
type JapanApple struct {}

func (ja *JapanApple) ShowApple() {
fmt.Println("日本苹果")
}

type JapanBanana struct {}

func (jb *JapanBanana) ShowBanana() {
fmt.Println("日本香蕉")
}

type JapanPear struct {}

func (cp *JapanPear) ShowPear() {
fmt.Println("日本梨")
}

type JapanFactory struct {}

func (jf *JapanFactory) CreateApple() AbstractApple {
var apple AbstractApple

apple = new(JapanApple)

return apple
}

func (jf *JapanFactory) CreateBanana() AbstractBanana {
var banana AbstractBanana

banana = new(JapanBanana)

return banana
}

func (cf *JapanFactory) CreatePear() AbstractPear {
var pear AbstractPear

pear = new(JapanPear)

return pear
}

/* 美国产品族 */
type AmericanApple struct {}

func (aa *AmericanApple) ShowApple() {
fmt.Println("美国苹果")
}

type AmericanBanana struct {}

func (ab *AmericanBanana) ShowBanana() {
fmt.Println("美国香蕉")
}

type AmericanPear struct {}

func (ap *AmericanPear) ShowPear() {
fmt.Println("美国梨")
}

type AmericanFactory struct {}

func (af *AmericanFactory) CreateApple() AbstractApple {
var apple AbstractApple

apple = new(AmericanApple)

return apple
}

func (af *AmericanFactory) CreateBanana() AbstractBanana {
var banana AbstractBanana

banana = new(AmericanBanana)

return banana
}

func (af *AmericanFactory) CreatePear() AbstractPear {
var pear AbstractPear

pear = new(AmericanPear)

return pear
}

// ======== 业务逻辑层 =======
func main() {
//需求1: 需要美国的苹果、香蕉、梨 等对象
//1-创建一个美国工厂
var aFac AbstractFactory
aFac = new(AmericanFactory)

//2-生产美国苹果
var aApple AbstractApple
aApple = aFac.CreateApple()
aApple.ShowApple()

//3-生产美国香蕉
var aBanana AbstractBanana
aBanana = aFac.CreateBanana()
aBanana.ShowBanana()

//4-生产美国梨
var aPear AbstractPear
aPear = aFac.CreatePear()
aPear.ShowPear()

//需求2: 需要中国的苹果、香蕉
//1-创建一个中国工厂
cFac := new(ChinaFactory)

//2-生产中国苹果
cApple := cFac.CreateApple()
cApple.ShowApple()

//3-生产中国香蕉
cBanana := cFac.CreateBanana()
cBanana.ShowBanana()
}

思考:抽象工厂模式是否符合“开闭原则”?

3.3.4 抽象工厂模式的优缺点

优点:

  1. 拥有工厂方法模式的优点

  2. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。

  3. 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。

缺点:

  1. 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。

3.3.5 适用场景

(1) 系统中有多于一个的产品族。而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。

(2) 产品等级结构稳定。设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。

3.4单例模式

保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。

是GoF给出单例模式的定义。

3.4.1 单例模式中的角色和职责

单例模式的标准类图如下:

img

Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。

单例模式要解决的问题是:

保证一个类永远只能有一个对象,且该对象的功能依然能被其他模块使用。

3.4.2 单例模式逻辑推演实现

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
package main

import "fmt"

/*
三个要点:
一是某个类只能有一个实例;
二是它必须自行创建这个实例;
三是它必须自行向整个系统提供这个实例。
*/

/*
保证一个类永远只能有一个对象
*/


//1、保证这个类非公有化,外界不能通过这个类直接创建一个对象
// 那么这个类就应该变得非公有访问 类名称首字母要小写
type singelton struct {}

//2、但是还要有一个指针可以指向这个唯一对象,但是这个指针永远不能改变方向
// Golang中没有常指针概念,所以只能通过将这个指针私有化不让外部模块访问
var instance *singelton = new(singelton)

//3、如果全部为私有化,那么外部模块将永远无法访问到这个类和对象,
// 所以需要对外提供一个方法来获取这个唯一实例对象
// 注意:这个方法是否可以定义为singelton的一个成员方法呢?
// 答案是不能,因为如果为成员方法就必须要先访问对象、再访问函数
// 但是类和对象目前都已经私有化,外界无法访问,所以这个方法一定是一个全局普通函数
func GetInstance() *singelton {
return instance
}

func (s *singelton) SomeThing() {
fmt.Println("单例对象的某方法")
}

func main() {
s := GetInstance()
s.SomeThing()
}

上面代码推演了一个单例的创建和逻辑过程,上述是单例模式中的一种,属于“饿汉式”。含义是,在初始化单例唯一指针的时候,就已经提前开辟好了一个对象,申请了内存。饿汉式的好处是,不会出现线程并发创建,导致多个单例的出现,但是缺点是如果这个单例对象在业务逻辑没有被使用,也会客观的创建一块内存对象。那么与之对应的模式叫“懒汉式”,代码如下:

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 singelton struct {}

var instance *singelton

func GetInstance() *singelton {
//只有首次GetInstance()方法被调用,才会生成这个单例的实例
if instance == nil {
instance = new(singelton)
return instance
}

//接下来的GetInstance直接返回已经申请的实例即可
return instance
}

func (s *singelton) SomeThing() {
fmt.Println("单例对象的某方法")
}

func main() {
s := GetInstance()
s.SomeThing()
}

3.4.3 线程安全的单例模式实现

上面的“懒汉式”实现是非线程安全的设计方式,也就是如果多个线程或者协程同时首次调用GetInstance()方法有概率导致多个实例被创建,则违背了单例的设计初衷。那么在上面的基础上进行修改,可以利用Sync.Mutex进行加锁,保证线程安全。这种线程安全的写法,有个最大的缺点就是每次调用该方法时都需要进行锁操作,在性能上相对不高效,具体的实现改进如下:

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
package main

import (
"fmt"
"sync"
)

//定义锁
var lock sync.Mutex

type singelton struct {}

var instance *singelton

func GetInstance() *singelton {
//为了线程安全,增加互斥
lock.Lock()
defer lock.Unlock()

if instance == nil {
return new(singelton)
} else {
return instance
}
}

func (s *singelton) SomeThing() {
fmt.Println("单例对象的某方法")
}


func main() {
s := GetInstance()
s.SomeThing()
}

上面代码虽然解决了线程安全,但是每次调用GetInstance()都要加锁会极大影响性能。所以接下来可以借助”sync/atomic”来进行内存的状态存留来做互斥。atomic就可以自动加载和设置标记,代码如下:

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
package main

import (
"fmt"
"sync"
"sync/atomic"
)

//标记
var initialized uint32
var lock sync.Mutex

type singelton struct {}

var instance *singelton

func GetInstance() *singelton {
//如果标记为被设置,直接返回,不加锁
if atomic.LoadUint32(&initialized) == 1 {
return instance
}

//如果没有,则加锁申请
lock.Lock()
defer lock.Unlock()

if initialized == 0 {
instance = new(singelton)
//设置标记位
atomic.StoreUint32(&initialized, 1)
}

return instance
}

func (s *singelton) SomeThing() {
fmt.Println("单例对象的某方法")
}

func main() {
s := GetInstance()
s.SomeThing()
}

上述的实现其实Golang有个方法已经帮助开发者实现完成,就是Once模块,来看下Once.Do()方法的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
func (o *Once) Do(f func()) {   //判断是否执行过该方法,如果执行过则不执行
if atomic.LoadUint32(&o.done) == 1 {
return
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()  
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

所以完全可以借助Once来实现单例模式的实现,优化的代码如下:

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"
"sync"
)

var once sync.Once

type singelton struct {}

var instance *singelton

func GetInstance() *singelton {

once.Do(func(){
instance = new(singelton)
})

return instance
}

func (s *singelton) SomeThing() {
fmt.Println("单例对象的某方法")
}

func main() {
s := GetInstance()
s.SomeThing()
}

3.4.4 单例模式的优缺点

优点:

(1) 单例模式提供了对唯一实例的受控访问。

(2) 节约系统资源。由于在系统内存中只存在一个对象。

缺点:

(1) 扩展略难。单例模式中没有抽象层。

(2) 单例类的职责过重。

3.4.5 适用场景

(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。

(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

四、结构型模式

4.1代理模式

Proxy模式又叫做代理模式,是构造型的设计模式之一,它可以为其他对象提供一种代理(Proxy)以控制对这个对象的访问。

​ 所谓代理,是指具有与代理元(被代理的对象)具有相同的接口的类,客户端必须通过代理与被代理的目标类交互,而代理一般在交互的过程中(交互前后),进行某些特别的处理。

用一个日常可见的案例来理解“代理”的概念,如下图:

img

这里假设有一个“自己”的角色,正在玩一款网络游戏。称这个网络游戏就是代理模式的“Subject”,表示要做一件事的目标或者对象事件主题。

(1)“自己”有一个给游戏角色升级的需求或者任务,当然“自己”可以独自完成游戏任务的升级。

(2)或者“自己”也可以邀请以为更加擅长游戏的“游戏代练”来完成升级这件事,这个代练就是“Proxy”代理。

(3)“游戏代练”不仅能够完成升级的任务需求,还可以额外做一些附加的能力。比如打到一些好的游戏装备、加入公会等等周边收益。

所以代理的出现实则是为了能够覆盖“自己”的原本的需求,且可以额外做其他功能,这种额外创建的类是不影响已有的“自己”和“网络游戏”的的关系。是额外添加,在设计模式原则上,是符合“开闭原则”思想。那么当需要给“自己”增加额外功能的时候,又不想改变自己,那么就选择邀请一位”代理”来完成吧。

4.1.1 代理模式中的角色和职责

代理模式的标准类图如下:

img

subject(抽象主题角色):真实主题与代理主题的共同接口。

RealSubject(真实主题角色):定义了代理角色所代表的真实对象。

Proxy(代理主题角色):含有对真实主题角色的引用,代理角色通常在将客户端调用传递给真是主题对象之前或者之后执行某些操作,而不是单纯返回真实的对象。

4.1.2 代理模式案例实现

讲述标准类图改成一个案例来理解。

img

这里以一个购物作为一个主题任务,这是一个抽象的任务。具体的购物主题分别包括“韩国购物”、“美国购物”、“非洲购物”等。可以这些都是“自己”去完成主题,那么如果希望不仅完成购物,还要做真假辨别、海关安检等,同样依然能够完成自己本身的具体购物主题,那么则可以创建一个新的代理来完成这件事。代理需要将被代理的主题关联到本类中,去重新实现Buy()方法,在Buy()方法中,调用被调用的Buy(),在额外完成辨别真伪和海关安检两个任务动作,具体的代码实现如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package main

import "fmt"

type Goods struct {
Kind string //商品种类
Fact bool //商品真伪
}

// =========== 抽象层 ===========
//抽象的购物主题Subject
type Shopping interface {
Buy(goods *Goods) //某任务
}


// =========== 实现层 ===========
//具体的购物主题, 实现了shopping, 去韩国购物
type KoreaShopping struct {}

func (ks *KoreaShopping) Buy(goods *Goods) {
fmt.Println("去韩国进行了购物, 买了 ", goods.Kind)
}


//具体的购物主题, 实现了shopping, 去美国购物
type AmericanShopping struct {}

func (as *AmericanShopping) Buy(goods *Goods) {
fmt.Println("去美国进行了购物, 买了 ", goods.Kind)
}

//具体的购物主题, 实现了shopping, 去非洲购物
type AfrikaShopping struct {}

func (as *AfrikaShopping) Buy(goods *Goods) {
fmt.Println("去非洲进行了购物, 买了 ", goods.Kind)
}


//海外的代理
type OverseasProxy struct {
shopping Shopping //代理某个主题,这里是抽象类型
}

func (op *OverseasProxy) Buy(goods *Goods) {
// 1. 先验货
if (op.distinguish(goods) == true) {
//2. 进行购买
op.shopping.Buy(goods) //调用原被代理的具体主题任务
//3 海关安检
op.check(goods)
}
}

//创建一个代理,并且配置关联被代理的主题
func NewProxy(shopping Shopping) Shopping {
return &OverseasProxy{shopping}
}

//验货流程
func (op *OverseasProxy) distinguish(goods *Goods) bool {
fmt.Println("对[", goods.Kind,"]进行了辨别真伪.")
if (goods.Fact == false) {
fmt.Println("发现假货",goods.Kind,", 不应该购买。")
}
return goods.Fact
}

//安检流程
func (op *OverseasProxy) check(goods *Goods) {
fmt.Println("对[",goods.Kind,"] 进行了海关检查, 成功的带回祖国")
}


func main() {
g1 := Goods{
Kind: "韩国面膜",
Fact: true,
}

g2 := Goods{
Kind: "CET4证书",
Fact: false,
}

//如果不使用代理来完成从韩国购买任务
var shopping Shopping
shopping = new(KoreaShopping) //具体的购买主题

//1-先验货
if g1.Fact == true {
fmt.Println("对[", g1.Kind,"]进行了辨别真伪.")
//2-去韩国购买
shopping.Buy(&g1)
//3-海关安检
fmt.Println("对[",g1.Kind,"] 进行了海关检查, 成功的带回祖国")
}

fmt.Println("---------------以下是 使用 代理模式-------")
var overseasProxy Shopping
overseasProxy = NewProxy(shopping)
overseasProxy.Buy(&g1)
overseasProxy.Buy(&g2)
}

运行结果如下:

1
2
3
4
5
6
7
8
9
对[ 韩国面膜 ]进行了辨别真伪.
去韩国进行了购物, 买了 韩国面膜
对[ 韩国面膜 ] 进行了海关检查, 成功的带回祖国
---------------以下是 使用 代理模式-------
对[ 韩国面膜 ]进行了辨别真伪.
去韩国进行了购物, 买了 韩国面膜
对[ 韩国面膜 ] 进行了海关检查, 成功的带回祖国
对[ CET4证书 ]进行了辨别真伪.
发现假货 CET4证书 , 不应该购买。

4.1.3 代理模式纯理解案例

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
package main

import "fmt"

//抽象主题
type BeautyWoman interface {
//对男人抛媚眼
MakeEyesWithMan()
//和男人浪漫的约会
HappyWithMan()
}


//具体主题
type PanJinLian struct {}

//对男人抛媚眼
func (p *PanJinLian) MakeEyesWithMan() {
fmt.Println("潘金莲对本官抛了个媚眼")
}

//和男人浪漫的约会
func (p *PanJinLian) HappyWithMan() {
fmt.Println("潘金莲和本官共度了浪漫的约会。")
}

//代理中介人, 王婆
type WangPo struct {
woman BeautyWoman
}

func NewProxy(woman BeautyWoman) BeautyWoman {
return &WangPo{woman}
}

//对男人抛媚眼
func (p *WangPo) MakeEyesWithMan() {
p.woman.MakeEyesWithMan()
}

//和男人浪漫的约会
func (p *WangPo) HappyWithMan() {
p.woman.HappyWithMan()
}


//西门大官人
func main() {
//大官人想找金莲,让王婆来安排
wangpo := NewProxy(new(PanJinLian))
//王婆命令潘金莲抛媚眼
wangpo.MakeEyesWithMan()
//王婆命令潘金莲和西门庆约会
wangpo.HappyWithMan()
}

情节自己脑补,结果如下:

1
2
潘金莲对本官抛了个媚眼
潘金莲和本官共度了浪漫的约会。

4.1.4 代理模式的优缺点

优点:

(1) 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。

(2) 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。

缺点:

(1) 代理实现较为复杂。

4.1.5 适用场景

​ 为其他对象提供一种代理以控制对这个对象的访问。

4.2 装饰模式

装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。

img

以上图为例,一开始有个手机(裸机Phone类),如果需要不断的为这个Phone增添某个功能从而变成一个新功能的Phone,就需要一个装饰器的类,来动态的给一个类额外添加一个指定的功能,而生成另一个类,但原先的类又没有改变,不影响原有系统的稳定。

在装饰器模式中,“裸机”、“有贴膜的手机”、“有手机壳的手机”、“有手机壳&贴膜的手机”都是一个构件。

“贴膜装饰器”、“手机壳装饰器”是装饰器也是一个构件。

4.2.1 装饰模式中的角色和职责

Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。

ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。

其标准的类图如下所示:

img

4.2.2 装饰模式中的代码实现

接下来按照上述手机案例,结合装饰器的设计模式特点,得到对应案例的类图,如下:

img

实现的代码如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package main

import "fmt"

// ---------- 抽象层 ----------
//抽象的构件
type Phone interface {
Show() //构件的功能
}

//装饰器基础类(该类本应该为interface,但是Golang interface语法不可以有成员属性)
type Decorator struct {
phone Phone
}

func (d *Decorator) Show() {}


// ----------- 实现层 -----------
// 具体的构件
type HuaWei struct {}

func (hw *HuaWei) Show() {
fmt.Println("秀出了HuaWei手机")
}

type XiaoMi struct{}

func (xm *XiaoMi) Show() {
fmt.Println("秀出了XiaoMi手机")
}

// 具体的装饰器类
type MoDecorator struct {
Decorator //继承基础装饰器类(主要继承Phone成员属性)
}

func (md *MoDecorator) Show() {
md.phone.Show() //调用被装饰构件的原方法
fmt.Println("贴膜的手机") //装饰额外的方法
}

func NewMoDecorator(phone Phone) Phone {
return &MoDecorator{Decorator{phone}}
}

type KeDecorator struct {
Decorator //继承基础装饰器类(主要继承Phone成员属性)
}

func (kd *KeDecorator) Show() {
kd.phone.Show()
fmt.Println("手机壳的手机") //装饰额外的方法
}

func NewKeDecorator(phone Phone) Phone {
return &KeDecorator{Decorator{phone}}
}


// ------------ 业务逻辑层 ---------
func main() {
var huawei Phone
huawei = new(HuaWei)
huawei.Show() //调用原构件方法

fmt.Println("---------")
//用贴膜装饰器装饰,得到新功能构件
var moHuawei Phone
moHuawei = NewMoDecorator(huawei) //通过HueWei ---> MoHuaWei
moHuawei.Show() //调用装饰后新构件的方法

fmt.Println("---------")
var keHuawei Phone
keHuawei = NewKeDecorator(huawei) //通过HueWei ---> KeHuaWei
keHuawei.Show()

fmt.Println("---------")
var keMoHuaWei Phone
keMoHuaWei = NewMoDecorator(keHuawei) //通过KeHuaWei ---> KeMoHuaWei
keMoHuaWei.Show()
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
秀出了HuaWei手机
---------
秀出了HuaWei手机
贴膜的手机
---------
秀出了HuaWei手机
手机壳的手机
---------
秀出了HuaWei手机
手机壳的手机
贴膜的手机

4.2.3 装饰模式的优缺点

优点:

(1) 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。

(2) 可以通过一种动态的方式来扩展一个对象的功能,从而实现不同的行为。

(3) 可以对一个对象进行多次装饰。

(4) 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。

缺点:

(1) 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,影响程序的性能。

(2) 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。

4.2.3 适用场景

(1) 动态、透明的方式给单个对象添加职责。

(2) 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。

装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。

4.3 适配器模式

image.png

5V <————> 适配器 <————> 220V

将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

4.3.1 适配器模式中的角色和职责

适配器模式的标准类图如下:

img

Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。

Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。

Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

根据对象适配器模式结构图,在对象适配器中,客户端需要调用request()方法,而适配者类Adaptee没有该方法,但是它所提供的specificRequest()方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的request()方法中调用适配者的specificRequest()方法。因为适配器类与适配者类是关联关系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。

4.3.2 适配器模式的代码实现

按照上述的例子,来重新设计类图如下:

img

实现的代码如下:

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
package main

import "fmt"

//适配的目标
type V5 interface {
Use5V()
}

//业务类,依赖V5接口
type Phone struct {
v V5
}

func NewPhone(v V5) *Phone {
return &Phone{v}
}

func (p *Phone) Charge() {
fmt.Println("Phone进行充电...")
p.v.Use5V()
}


//被适配的角色,适配者
type V220 struct {}

func (v *V220) Use220V() {
fmt.Println("使用220V的电压")
}

//电源适配器
type Adapter struct {
v220 *V220
}

func (a *Adapter) Use5V() {
fmt.Println("使用适配器进行充电")

//调用适配者的方法
a.v220.Use220V()
}

func NewAdapter(v220 *V220) *Adapter {
return &Adapter{v220}
}



// ------- 业务逻辑层 -------
func main() {
iphone := NewPhone(NewAdapter(new(V220)))

iphone.Charge()
}

运行结果如下:

1
2
3
Phone进行充电...
使用适配器进行充电
使用220V的电压

4.3.3 适配器模式优缺点

优点:

(1) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。

(2) 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。

(3) 灵活性和扩展性都非常好,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

缺点:

适配器中置换适配者类的某些方法比较麻烦。

4.3.4 适应场景

(1) 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。

(2) 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

根据迪米特法则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。

​ Facade模式也叫外观模式,是由GoF提出的23种设计模式中的一种。Facade模式为一组具有类似功能的类群,比如类库,子系统等等,提供一个一致的简单的界面。这个一致的简单的界面被称作facade。

4.4外观模式

4.4.1 外观模式中角色和职责

外观模式的标准类图如下:

img

Façade(外观角色):为调用方, 定义简单的调用接口。

SubSystem(子系统角色):功能提供者。指提供功能的类群(模块或子系统)。

4.4.2 外观模式的代码实现

外观模式相对简单一些,代码实现如下:

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 SubSystemA struct {}

func (sa *SubSystemA) MethodA() {
fmt.Println("子系统方法A")
}

type SubSystemB struct {}

func (sb *SubSystemB) MethodB() {
fmt.Println("子系统方法B")
}

type SubSystemC struct {}

func (sc *SubSystemC) MethodC() {
fmt.Println("子系统方法C")
}

type SubSystemD struct {}

func (sd *SubSystemD) MethodD() {
fmt.Println("子系统方法D")
}

//外观模式,提供了一个外观类, 简化成一个简单的接口供使用
type Facade struct {
a *SubSystemA
b *SubSystemB
c *SubSystemC
d *SubSystemD
}

func (f *Facade) MethodOne() {
f.a.MethodA()
f.b.MethodB()
}


func (f *Facade) MethodTwo() {
f.c.MethodC()
f.d.MethodD()
}

func main() {
//如果不用外观模式实现MethodA() 和 MethodB()
sa := new(SubSystemA)
sa.MethodA()
sb := new(SubSystemB)
sb.MethodB()

fmt.Println("-----------")
//使用外观模式
f := Facade{
a: new(SubSystemA),
b: new(SubSystemB),
c: new(SubSystemC),
d: new(SubSystemD),
}

//调用外观包裹方法
f.MethodOne()
}

结果如下:

1
2
3
4
5
子系统方法A
子系统方法B
-----------
子系统方法A
子系统方法B

4.4.3 外观模式的案例

img

代码实现如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package main

import "fmt"

//电视机
type TV struct {}

func (t *TV) On() {
fmt.Println("打开 电视机")
}

func (t *TV) Off() {
fmt.Println("关闭 电视机")
}


//电视机
type VoiceBox struct {}

func (v *VoiceBox) On() {
fmt.Println("打开 音箱")
}

func (v *VoiceBox) Off() {
fmt.Println("关闭 音箱")
}

//灯光
type Light struct {}

func (l *Light) On() {
fmt.Println("打开 灯光")
}

func (l *Light) Off() {
fmt.Println("关闭 灯光")
}


//游戏机
type Xbox struct {}

func (x *Xbox) On() {
fmt.Println("打开 游戏机")
}

func (x *Xbox) Off() {
fmt.Println("关闭 游戏机")
}


//麦克风
type MicroPhone struct {}

func (m *MicroPhone) On() {
fmt.Println("打开 麦克风")
}

func (m *MicroPhone) Off() {
fmt.Println("关闭 麦克风")
}

//投影仪
type Projector struct {}

func (p *Projector) On() {
fmt.Println("打开 投影仪")
}

func (p *Projector) Off() {
fmt.Println("关闭 投影仪")
}


//家庭影院(外观)
type HomePlayerFacade struct {
tv TV
vb VoiceBox
light Light
xbox Xbox
mp MicroPhone
pro Projector
}


//KTV模式
func (hp *HomePlayerFacade) DoKTV() {
fmt.Println("家庭影院进入KTV模式")
hp.tv.On()
hp.pro.On()
hp.mp.On()
hp.light.Off()
hp.vb.On()
}

//游戏模式
func (hp *HomePlayerFacade) DoGame() {
fmt.Println("家庭影院进入Game模式")
hp.tv.On()
hp.light.On()
hp.xbox.On()
}

func main() {
homePlayer := new(HomePlayerFacade)

homePlayer.DoKTV()

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

homePlayer.DoGame()
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
家庭影院进入KTV模式
打开 电视机
打开 投影仪
打开 麦克风
关闭 灯光
打开 音箱
------------
家庭影院进入Game模式
打开 电视机
打开 灯光
打开 游戏机

4.4.4 外观模式的优缺点

优点:

(1) 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。

(2) 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。

(3) 一个子系统的修改对其他子系统没有任何影响。

缺点:

(1) 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活 性。

(2) 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。

4.4.5 适用场景

(1) 复杂系统需要简单入口使用。

(2) 客户端程序与多个子系统之间存在很大的依赖性。

(3) 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度

五、行为型模式

5.1模板方法模式

5.1.1 模板方法模式中的角色和职责

AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。

ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

img

5.1.2 模板方法模式案例

img

上图的案例类图如下:

img

实现代码如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package main

import "fmt"

//抽象类,制作饮料,包裹一个模板的全部实现步骤
type Beverage interface {
BoilWater() //煮开水
Brew() //冲泡
PourInCup() //倒入杯中
AddThings() //添加酌料

WantAddThings() bool //是否加入酌料Hook
}

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

//封装的固定模板
func (t *template) MakeBeverage() {
if t == nil {
return
}

t.b.BoilWater()
t.b.Brew()
t.b.PourInCup()

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


//具体的模板子类 制作咖啡
type MakeCaffee struct {
template //继承模板
}

func NewMakeCaffee() *MakeCaffee {
makeCaffe := new(MakeCaffee)
//b 为Beverage,是MakeCaffee的接口,这里需要给接口赋值,指向具体的子类对象
//来触发b全部接口方法的多态特性。
makeCaffe.b = makeCaffe
return makeCaffe
}

func (mc *MakeCaffee) BoilWater() {
fmt.Println("将水煮到100摄氏度")
}

func (mc *MakeCaffee) Brew() {
fmt.Println("用水冲咖啡豆")
}

func (mc *MakeCaffee) PourInCup() {
fmt.Println("将充好的咖啡倒入陶瓷杯中")
}

func (mc *MakeCaffee) AddThings() {
fmt.Println("添加牛奶和糖")
}

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

//具体的模板子类 制作茶
type MakeTea struct {
template //继承模板
}

func NewMakeTea() *MakeTea {
makeTea := new(MakeTea)
//b 为Beverage,是MakeTea,这里需要给接口赋值,指向具体的子类对象
//来触发b全部接口方法的多态特性。
makeTea.b = makeTea
return makeTea
}

func (mt *MakeTea) BoilWater() {
fmt.Println("将水煮到80摄氏度")
}

func (mt *MakeTea) Brew() {
fmt.Println("用水冲茶叶")
}

func (mt *MakeTea) PourInCup() {
fmt.Println("将充好的咖啡倒入茶壶中")
}

func (mt *MakeTea) AddThings() {
fmt.Println("添加柠檬")
}

func (mt *MakeTea) WantAddThings() bool {
return false //关闭Hook条件
}

func main() {
//1. 制作一杯咖啡
makeCoffee := NewMakeCaffee()
makeCoffee.MakeBeverage() //调用固定模板方法

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

//2. 制作茶
makeTea := NewMakeTea()
makeTea.MakeBeverage()
}

运行结果如下:

1
2
3
4
5
6
7
8
将水煮到100摄氏度
用水冲咖啡豆
将充好的咖啡倒入陶瓷杯中
添加牛奶和糖
------------
将水煮到80摄氏度
用水冲茶叶
将充好的咖啡倒入茶壶中

5.1.3 模板方法的优缺点

优点:

(1) 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。

(2) 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。

(3) 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。

(4) 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。

缺点:

需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象。

5.1.4 适用场景

(1)具有统一的操作步骤或操作过程;

(2) 具有不同的操作细节;

(3) 存在多个具有同样操作步骤的应用场景,但某些具体的操作细节却各不相同;

在抽象类中统一操作步骤,并规定好接口;让子类实现接口。这样可以把各个具体的子类和操作步骤解耦合。

5.2命令模式

假设你实现了开启和关闭电视机的功能,随着业务迭代,还需要实现开启和关闭冰箱的功能,开启和关闭电灯的功能,开启和关闭微波炉的功能……这些功能都基于你的基类,开启和关闭。如果你之后对基类进行修改,很可能会影响到其他功能,这使项目变得不稳定了。

一个优秀的设计往往会关注于软件的分层与解耦,命令模式试图做到这样的结果:让命令和对应功能解耦,并能根据不同的请求将其方法参数化。

解决

还是用开启和关闭家用电器的例子来举例吧。请看以下代码:

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
package command

import "fmt"

// 请求者

type button struct {
command command
}

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

// 具体命令接口

type command interface {
execute()
}

type onCommand struct {
device device
}

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

type offCommand struct {
device device
}

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

// 接收者

type device interface {
on()
off()
}

type tv struct{}

func (t *tv) on() {
fmt.Println("Turning tv on")
}

func (t *tv) off() {
fmt.Println("Turning tv off")
}

type airConditioner struct{}

func (t *airConditioner) on() {
fmt.Println("Turning air conditioner on")
}

func (t *airConditioner) off() {
fmt.Println("Turning air conditioner off")
}

我们分别实现了请求者button,命令接口command,接收者device。请求者button就像是那个可以执行开启或关闭的遥控器,命令接口command则是一个中间层,它使我们的请求者和接收者解藕。

测试代码:

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
package command

func ExampleCommand() {
Tv()
AirConditioner()

// Output:
// Turning tv on
// Turning tv off
// Turning air conditioner on
// Turning air conditioner off
}

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

onTvCommand := &onCommand{
device: tv,
}

offTvCommand := &offCommand{
device: tv,
}

onTvButton := &button{
command: onTvCommand,
}
onTvButton.press()

offTvButton := &button{
command: offTvCommand,
}
offTvButton.press()
}

func AirConditioner() {
airConditioner := &airConditioner{}

onAirConditionerCommand := &onCommand{
device: airConditioner,
}

offAirConditionerCommand := &offCommand{
device: airConditioner,
}

onAirConditionerButton := &button{
command: onAirConditionerCommand,
}
onAirConditionerButton.press()

offAirConditionerButton := &button{
command: offAirConditionerCommand,
}
offAirConditionerButton.press()
}

命令模式的优缺点

优点:

(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。

(2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。

(3) 可以比较容易地设计一个命令队列或宏命令(组合命令)。

缺点:

使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

适用场景

(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。

(2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。

(3) 系统需要将一组操作组合在一起形成宏命令。

5.3策略模式

5.3.1 策略模式中的角色和职责

策略模式的标准类图如下:

img

Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。

Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。

ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

5.3.2 策略模式案例

img

代码实现如下:

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
package main

import "fmt"

//武器策略(抽象的策略)
type WeaponStrategy interface {
UseWeapon() //使用武器
}


//具体的策略
type Ak47 struct {}

func (ak *Ak47) UseWeapon() {
fmt.Println("使用Ak47 去战斗")
}

//具体的策略
type Knife struct {}

func (k *Knife) UseWeapon() {
fmt.Println("使用匕首 去战斗")
}

//环境类
type Hero struct {
strategy WeaponStrategy //拥有一个抽象的策略
}

//设置一个策略
func (h *Hero) SetWeaponStrategy(s WeaponStrategy) {
h.strategy = s
}

func (h *Hero) Fight() {
h.strategy.UseWeapon() //调用策略
}

func main() {
hero := Hero{}
//更换策略1
hero.SetWeaponStrategy(new(Ak47))
hero.Fight()

hero.SetWeaponStrategy(new(Knife))
hero.Fight()
}

运行结果如下:

1
2
使用Ak47 去战斗
使用匕首 去战斗

练习:

​ 商场促销有策略A(0.8折)策略B(消费满200,返现100),用策略模式模拟场景

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
package main

import "fmt"

/*
练习:
商场促销有策略A(0.8折)策略B(消费满200,返现100),用策略模式模拟场景
*/

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

type StrategyA struct {}

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

type StrategyB struct {}

func (sb *StrategyB) GetPrice(price float64) float64 {
fmt.Println("执行策略B, 所有商品满200 减100")

if price >= 200 {
price -= 100
}

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 main() {
nike := Goods{
Price: 200.0,
}
//上午 ,商场执行策略A
nike.SetStrategy(new(StrategyA))
fmt.Println("上午nike鞋卖", nike.SellPrice())

//下午, 商场执行策略B
nike.SetStrategy(new(StrategyB))
fmt.Println("下午nike鞋卖", nike.SellPrice())
}

结果:

1
2
3
4
5
6
原价值  200  .
执行策略A, 所有商品打八折
上午nike鞋卖 160
原价值 200 .
执行策略B, 所有商品满200 减100
下午nike鞋卖 100

5.3.3 策略模式的优缺点

优点:

(1) 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。

(2) 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。

(3) 策略模式提供了一种算法的复用机制。由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。

缺点:

(1) 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。

(2) 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。

5.3.4 适用场景

​ 准备一组算法,并将每一个算法封装起来,使得它们可以互换

5.4 观察者模式

如果你需要在一个对象的状态被改变时,其他对象能作为其“观察者”而被通知,就可以使用观察者模式。

我们将自身的状态改变就会通知给其他对象的对象称为“发布者”,关注发布者状态变化的对象则称为“订阅者”。

解决

请看以下代码:

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
package observer

import "fmt"

// 发布者

type Subject struct {
observers []Observer
content string
}

func NewSubject() *Subject {
return &Subject{
observers: make([]Observer, 0),
}
}

// 添加订阅者

func (s *Subject) AddObserver(o Observer) {
s.observers = append(s.observers, o)
}

// 改变发布者的状态

func (s *Subject) UpdateContext(content string) {
s.content = content
s.notify()
}

// 通知订阅者接口

type Observer interface {
Do(*Subject)
}

func (s *Subject) notify() {
for _, o := range s.observers {
o.Do(s)
}
}

// 订阅者

type Reader struct {
name string
}

func NewReader(name string) *Reader {
return &Reader{
name: name,
}
}

func (r *Reader) Do(s *Subject) {
fmt.Println(r.name + " get " + s.content)
}

很简单,我们只要实现一个通知notify方法,在发布者的状态改变时执行即可。

测试代码:

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

func ExampleObserver() {
subject := NewSubject()

boy := NewReader("小明")
girl := NewReader("小美")

subject.AddObserver(boy)
subject.AddObserver(girl)

subject.UpdateContext("hi~")

// Output:
// 小明 get hi~
// 小美 get hi~
}

观察者模式的优缺点

优点:

(1) 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。

(2) 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。

(3) 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。

(4) 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。

缺点:

(1) 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。

(2) 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

(3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

适用场景

​ (1) 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。

​ (2) 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。

​ (3) 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

  • 标题: GO设计模式
  • 作者: Olivia的小跟班
  • 创建于 : 2023-03-25 11:43:58
  • 更新于 : 2023-05-27 03:46:58
  • 链接: https://www.youandgentleness.cn/2023/03/25/GO设计模式/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
GO设计模式