设计模式学习笔记


前言

模式具有普适价值,在软件实现上可多思考,多套用模式。学习模式不经历,不看,不写,不想是学不好的。
就像处在一个陌生的环境,也是要掌握生活的,社交的模式,如果不会,只能慢慢摸索,直到熟练。软件设计的设计模式也是一样的。

设计模式的目的是让代码工程:可复用,可扩展,可维护,灵活性好。

基于这一目的,需要遵循以下设计原则:
单一职责原则;
开放-封闭原则;
依赖倒置原则;
LoD 迪米特法则;

原则是方针,设计模式是手段,语言是工具,它们是为目的服务。

常用经典设计模式有 23 个,分三大类:
创建型设计模式;
行为型设计模式;
结构型设计模式;

创建型设计模式

简单工厂模式

示例代码如下:

package cal

type Operator interface {
	GetResult()
}

type Add struct {
	p1     int
	p2     int
	result int
}

func (a *Add) GetResult() {
	a.result = a.p1 + a.p2
	fmt.Println(a.result)
}

// ......

type NewOperatorStructFactory struct{}

func (f *NewOperatorStructFactory) NewOperator(parameter1, parameter2 int, symbol byte) (Operator, error) {
	var o Operator
	switch symbol {
	case '+':
		o = &Add{
			p1: parameter1,
			p2: parameter2,
		}
	case '-':
		o = &Sub{
			p1: parameter1,
			p2: parameter2,
		}
	case '*':
		o = &Mul{
			p1: parameter1,
			p2: parameter2,
		}
	case '/':
		o = &Div{
			p1: parameter1,
			p2: parameter2,
		}
	default:
		return o, fmt.Errorf("wrong operator: %c, support operator: '+', '-', '*', '/'", symbol)
	}
	return o, nil
}

// 单看这一工厂实现,函数和结构体都可以实现,貌似函数的实现更加方便,为何这里要用结构体呢?
// Go 中的结构体能等同于传统面向对象语言中的类吗?
// 结构体是结构化的,函数是偏过程的。这里用结构体做为工厂还是有一定可取之处的。譬如,后续对工厂进行抽象,那么面向接口编程的结构体
// 就是自然要用的,因为它实现了 Go 的多态。
//
// 这里同理,用接口实现了多态,达到了和传统面向对象编程一样的效果。
// 不同于基类,子类。 Go 中没有这种表示,使用结构体嵌套实现不了基类,子类就能实现的多态效果。结构体嵌套可实现子结构体继承父
// 结构体的方法和变量,但是实现不了多态。所以,Go 要面向接口编程。
//
// 这里的简单工厂用来决定该实例化哪个结构体“类”。
// main.go

package main

import (
	"cal/cal"
	"fmt"
)

func main() {
	var parameter1 int
	var parameter2 int
	var operator byte
	fmt.Println("please input the parameter1, parameter2, operator:")
	fmt.Scanf("%d %d %c", &parameter1, &parameter2, &operator)


	c := cal.NewOperatorStructFactory{}
	o, err := c.NewOperator(parameter1, parameter2, operator)
	if err != nil {
		fmt.Println(err)
	}
	o.GetResult()
}
 

需要注意的是,简单工厂实现违反了开放-封闭原则和依赖倒置原则:
开放-封闭原则:简单工厂类中若要新加实例化类需要修改 switch 匹配,不能扩展。违反开放-封闭原则。
依赖倒置原则:创建工厂类直接基于实现而不是接口,导致强耦合。

简单工厂违反上述原则,可使用工厂方法模式进行改进。

工厂方法模式

工厂方法模式对工厂类进行抽象,实现面向接口编程。克服了简单工厂违反开放-封闭原则和依赖倒置原则的缺点。

实现代码如下:

type Operator interface {
	GetResult()
}

type Add struct {
	p1     int
	p2     int
	result int
}

func (a *Add) GetResult() {
	a.result = a.p1 + a.p2
	fmt.Println(a.result)
}

type Sub struct {
	p1     int
	p2     int
	result int
}

func (a *Sub) GetResult() {
	a.result = a.p1 - a.p2
	fmt.Println(a.result)
}

type Mul struct {
	p1     int
	p2     int
	result int
}

func (a *Mul) GetResult() {
	a.result = a.p1 * a.p2
	fmt.Println(a.result)
}

type Div struct {
	p1     int
	p2     int
	result int
}

func (a *Div) GetResult() {
	a.result = a.p1 / a.p2
	fmt.Println(a.result)
}

// factory method
type operatorFactory interface {
	createOperator(int, int) Operator
}

type addOperatorFactory struct{}

func (aof *addOperatorFactory) createOperator(parameter1, parameter2 int) Operator {
	return &Add{
		p1: parameter1,
		p2: parameter2,
	}
}

type subOperatorFactory struct{}

func (sof *subOperatorFactory) createOperator(parameter1, parameter2 int) Operator {
	return &Sub{
		p1: parameter1,
		p2: parameter2,
	}
}

type mulOperatorFactory struct{}

func (mof *mulOperatorFactory) createOperator(parameter1, parameter2 int) Operator {
	return &Mul{
		p1: parameter1,
		p2: parameter2,
	}
}

type divOperatorFactory struct{}

func (dof *divOperatorFactory) createOperator(parameter1, parameter2 int) Operator {
	return &Div{
		p1: parameter1,
		p2: parameter2,
	}
}

func main() {
	var parameter1 int
	var parameter2 int
	var operator byte
	fmt.Println("please input the parameter1, parameter2, operator:")
	fmt.Scanf("%d %d %c", &parameter1, &parameter2, &operator)

	var of operatorFactory
	var o Operator
	switch operator {
	case '+':
		of = &addOperatorFactory{}
		o = of.createOperator(parameter1, parameter2)
	case '-':
		of = &subOperatorFactory{}
		o = of.createOperator(parameter1, parameter2)
	case '*':
		of = &mulOperatorFactory{}
		o = of.createOperator(parameter1, parameter2)
	case '/':
		of = &divOperatorFactory{}
		o = of.createOperator(parameter1, parameter2)
	default:
		fmt.Printf("operator %c is not supported!!!", o)
	}

	o.GetResult()
}

// 相比于简单工厂,工厂方法符合开闭原则。但是,判断依然存在且提前到 “main” 了
// 相比于策略,工厂方法在客户端定义两个类,不如策略简洁

工厂方法模式创建的对象是单一的,如果需要创建一系列对象的话就需要使用抽象工厂模式。

抽象工厂模式

不同于工厂方法模式,工厂接口具有多个方法,可以创建一系列对象。

示例代码如下:

type sportFactory interface {
	createShoes() shoes
	createShort() short
}

type adidas struct{}

func (ad *adidas) createShoes() shoes {
	return &adidasShoes{
		logo: "adidas",
		name: "shoes",
	}
}

func (ad *adidas) createShort() short {
	return &adidasShort{
		logo: "adidas",
		name: "short",
	}
}

type nike struct{}

func (ad *nike) createShoes() shoes {
	return &adidasShoes{
		logo: "nike",
		name: "shoes",
	}
}

func (ad *nike) createShort() short {
	return &adidasShort{
		logo: "nike",
		name: "short",
	}
}

type shoes interface {
	getModel()
}

type adidasShoes struct {
	logo string
	name string
}

func (s *adidasShoes) getModel() {
	fmt.Printf("logo: %s, name: %s\n", s.logo, s.name)
}

type short interface {
	getModel()
}
type adidasShort struct {
	logo string
	name string
}

func (s *adidasShort) getModel() {
	fmt.Printf("logo: %s, name: %s\n", s.logo, s.name)
}

func main() {
	var sf sportFactory
	var ss shoes
	var st short

	sf = &adidas{}

	ss = sf.createShoes()
	ss.getModel()

	st = sf.createShort()
	st.getModel()

	sf = &nike{}
	ss = sf.createShoes()
	ss.getModel()

	st = sf.createShort()
	st.getModel()
}

纵观工厂模式有一点不得不提的是,工厂需要根据给定元素创建对象,对于给定元素的判断多用到 switch, if 等判断决定创建哪个对象。
不同的是,判断放在哪里处理了客户端 main 还是 main 后。对于这种需要 switch, if 判断的可结合反射去除判断,Go 中反射结合工厂
模式部分留作后续研究。

单例模式

单例模式可以说是使用最广的模式了。顾名思义,单例模式是在软件工程里只能创建一次的模式。具体的应用有:

  • log 实例,只允许有一个实例记录,存储 log 日志,多个实例会带来混乱,且不必要。
  • DB 实例,只允许有个数据库实例,记录,存储数据。

单例模式要考虑多 goroutine 对象创建冲突及减少性能损耗两个方向。这里介绍 Double-Check Locking 和 sync.Once 两种创建单例方式。

Double-Check Locking

func GetLoggerInstance() *logger {
    // Double-Check Locking
	if log == nil {
		mu.Lock()
		defer mu.Unlock()
		if log == nil {
			log = &logger{
				time: time,
			}
			fmt.Println("log created")
			return log
		} else {
			fmt.Println("log already created: 1")
		}
	} else {
		fmt.Println("log already created: 2")
	}
	return log
}

sync.Once

Go 语言的 sync 包提供了只针对执行一次场景的解决方案:sync.Once

示例代码:

func GetLoggerInstance() *logger {
	if log == nil {
		fmt.Println("entry...")
		once.Do(func() {
			fmt.Println("create log...")
			log = &logger{}
		})
	} else {
		fmt.Println("log alrady created!!!")
	}

	return log
}

注意,有多个 goroutine 进到 log == nil 判断,而 once.Do 只执行一次:

entry...
create log...
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
log alrady created!!!
entry...

进阶单例模式看这里

行为型设计模式

策略模式

和工厂模式结构类型极为相似。策略模式偏重于行为,策略,算法。而不是对象创建。

其定义了一系列策略,每个策略面向接口。策略类中内嵌策略接口实现通过策略类调用不同的策略,也即通过策略类封装相应策略。

策略类由于内嵌了策略接口,使得在客户端程序中只需开放一个策略类即可,无需开放策略接口及策略的具体实现。这点和工厂模式不一样,
工厂模式客户端即要开放工厂类,也要开放接口类。

工厂模式和策略模式异同:https://www.cnblogs.com/me115/p/3790615.html

策略模式示例代码:

type evictionAlgo interface {
	evict()
}

type fifo struct{}
type lru struct{}
type lfu struct{}

func (fifo *fifo) evict() {
	fmt.Println("Eviction by FIFO")
}

func (lru *lru) evict() {
	fmt.Println("Eviction by LRU")
}

func (lfu *lfu) evict() {
	fmt.Println("Eviction by LFU")
}

type cache struct {
	evictionAlgo evictionAlgo
}

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

func (c *cache) runEvict() {
	c.evictionAlgo.evict()
}

type evictionAlgoType int

const (
	FIFO evictionAlgoType = 0
	LRU  evictionAlgoType = 1
	LFU  evictionAlgoType = 2
)

func (c *cache) createEvictionAlgo(et evictionAlgoType) {
	switch et {
	case FIFO:
		fifo := &fifo{}
		c.setEvictionAlgo(fifo)
	case LRU:
		lru := &lru{}
		c.setEvictionAlgo(lru)
	case LFU:
		lfu := &lfu{}
		c.setEvictionAlgo(lfu)
	default:
		fmt.Printf("The eviction algo %d is not supported!!!", et)
	}
}

func main() {

	cache := &cache{}

	var FIFOAlgo evictionAlgoType = 0
	var LRUAlgo evictionAlgoType = 1
	var LFUAlgo evictionAlgoType = 2

	cache.createEvictionAlgo(FIFOAlgo)
	cache.runEvict()

	cache.createEvictionAlgo(LRUAlgo)
	cache.runEvict()

	cache.createEvictionAlgo(LFUAlgo)
	cache.runEvict()
}

// 工厂模式侧重对象创建方式,策略模式侧重策略执行方式;
// 工厂根据相应信息创建对象,再由用户调用,运行时更改对象需要更改元素较多
// 策略模式将对象更改放在 context 中,在“运行时”可动态调整
// 策略可于工厂结合,实现根据相应信息动态更改对象

模板方法模式

模板方法模式实现对于一种策略或算法的抽象。其定义子类公共的模板方法实现策略或算法,具体的实现在子类中定义。

不同于策略模式,模板方法模式是对一种策略的抽象,而不是多种策略或算法。重在深度而不是广度。
Go 中实现这种抽象类是通过接口结合模板方法实现的,具体的引用参考:https://golangbyexample.com/go-abstract-class/

如果按传统面向对象语言解析 Go 抽象类进化过程。第一,要实现子类调用抽象类默认方法,那抽象类就应是结构体,子类嵌套该结构体实现,
子类调用嵌套类(抽象类)的方法。实现:

type exams struct{}

func (e *exams) paper1() {
	fmt.Println("1 + 1 = ?")
}

func (e *exams) paper2() {
	fmt.Println("2 + 2 = ?")
}

type exams1 struct {
	exams
	name string
}

func (e *exams1) answer1() {
	fmt.Println("2")
}

func (e *exams1) answer2() {
	fmt.Println("4")
}

type exams2 struct {
	exams
	name string
}

func (e *exams2) answer1() {
	fmt.Println("4")
}

func (e *exams2) answer2() {
	fmt.Println("4")
}

func main() {
	hxia := &exams1{
		name: "hxia",
	}

	luban := &exams2{
		name: "luban",
	}

	hxia.paper1()
	hxia.answer1()
	hxia.paper2()
	hxia.answer2()

	luban.paper1()
	luban.answer1()
	luban.paper2()
	luban.answer2()
}

但是,结构体嵌套最大的问题是实现不了多态,即意味着高耦合。

通过抽象类实现接口,子类嵌套抽象类间接实现接口可以实现多态吗?可以是可以,问题在于抽象类就能实例化了,抽象类即是抽象就不应该实例化。

那么,子类要实现多态就要用到接口。Go 接口又没有默认方法,想调用默认方法需要实现结构体嵌套,能否实现结构体组合实现多态呢?可以的。实现:

//Abstract Interface
type iAlpha interface {
	work()
	common()
}

//Abstract Concrete Type
type alpha struct {
	name string
}

func (a *alpha) common() {
	fmt.Println("common called")
}

//Implementing Type
type beta struct {
	alpha
}

func (b *beta) work() {
	fmt.Println("work called")
	fmt.Printf("name is %s\n", b.name)
	b.common()
}

func main() {
	a := alpha{
		name: "test",
	}
	b := &beta{
		alpha: a,
	}
	b.work()

	var beta iAlpha = &beta{}
	beta.work()
}

具体代码就不分析了,个人感觉是这种实现挺搓的,有具体应用在细看。

最后,结合模板方法和接口的模板方法模式实现如下:

type exami interface {
	sendPaper()
	writeAnswer()
}

type student struct {
	name string
}

func (s *student) sendPaper() {
	fmt.Println("1 + 1 = ?")
}

func (s *student) writeAnswer() {
	fmt.Println("11")
}

type teacher struct {
	name string
}

func (t *teacher) sendPaper() {
	fmt.Println("1 + 1 = ?")
}

func (t *teacher) writeAnswer() {
	fmt.Println("2")
}

// template method
type invigilate struct {
	exami
	name string
}

func (i *invigilate) invigilate() {
	i.exami.sendPaper()
	i.exami.writeAnswer()
}

func main() {
	hxia := &student{
		name: "hxia",
	}
	luban := &teacher{
		name: "luban",
	}

	Dora := &invigilate{
		exami: hxia,
		name:  "Dora",
	}
	Dora.invigilate()

	Dora = &invigilate{
		exami: luban,
		name:  "Dora",
	}
	Dora.invigilate()
}

策略模式和模板方法模式的异同:https://www.jianshu.com/p/ef6c822e1c35

结构型设计模式

装饰模式

装饰模式是添加装饰类对主类进行装饰的模式。其分离了主类的核心功能和装饰功能,降低了主类的复杂度。

从结构图可以看出 Decorator 装饰类和主类是聚合关系。回忆上文,包含聚合关系的模式有行为型设计模式 - 策略模式。
那么策略模式可以完成装饰模式的功能吗?
不能。举例如下:

type person struct {
	dress dress
}

type dress interface {
	show()
}

type TShirt struct{}

func (ts *TShirt) show() {
	fmt.Println("wear T-Shirt")
}

type tie struct{}

func (t *tie) show() {
	fmt.Println("wear tie")
}

type shoes struct{}

func (s *shoes) show() {
	fmt.Println("wear shoes")
}

type pants struct{}

func (p *pants) show() {
	fmt.Println("wear pants")
}

func (p *person) wear(dress string) {
	switch dress {
	case "tie":
		p.dress = &tie{}
	case "Tshirt":
		p.dress = &TShirt{}
	case "shoes":
		p.dress = &shoes{}
	case "pants":
		p.dress = &pants{}
	default:
		fmt.Println("The dress is not supported!!!")
		os.Exit(1)
	}
	p.dress.show()
}

func main() {
	p := person{}
	p.wear("shoes")
	p.wear("pants")
	p.wear("Tshirt")
}

使用策略模式给主类穿衣服是一件一件穿,因为选择的策略不同,每种策略都能实现穿衣服这种行为。但实际上,穿衣服不是一种策略就能解决的,而是好几种策略的组合。
这也就导致策略模式不能实现装饰模式的功能。使用装饰模式实现给人穿衣服的行为如下:

type person interface {
	wearClothes()
}

type male struct{}

func (m *male) wearClothes() {
	fmt.Println("male is wearing clothes:")
}

type tie struct {
	person person
}

func (t *tie) wearClothes() {
	t.person.wearClothes()
	fmt.Println("wearing tie")
}

type pants struct {
	person person
}

func (p *pants) wearClothes() {
	p.person.wearClothes()
	fmt.Println("wearing pants")
}

type shoes struct {
	person person
}

func (s *shoes) wearClothes() {
	s.person.wearClothes()
	fmt.Println("wearing shoes")
}

type tshirt struct {
	person person
}

func (t *tshirt) wearClothes() {
	t.person.wearClothes()
	fmt.Println("wearing tshirt")
}

func main() {
	var p person = &male{}
	personWithShoes := &shoes{
		person: p,
	}

	personWithShoesAndPants := &pants{
		person: personWithShoes,
	}

	personWithShoesAndPantsThirt := &tshirt{
		person: personWithShoesAndPants,
	}

	personWithShoesAndPantsThirt.wearClothes()
}

装饰模式实现了穿好衣服,而不是一件一件穿衣服。这里穿好衣服有点类似做了个递归,有意思。
有一点要强调的是,装饰模式可以随意组合顺序,如果装饰的顺序有依赖,需要注意(或者对有依赖的顺序有没有更好的设计模式呢?)

代理模式

顾名思义, 代理模式是动作,行为,功能的代理者,不是实际行为的执行者。但代理模式是要做实际行为做的动作,所以代理模式需要
实现实际行为执行者的行为,理所应当的需要抽象出该行为为接口。

实现代理模式:

type person interface {
	sendGift()
}

type pursuit struct {
	name string
}

type proxy struct {
	pursuit *pursuit
	name    string
}

func (p *pursuit) sendGift() {
	fmt.Println("send gift")
}

func (p *proxy) sendGift() {
	p.pursuit = &pursuit{}
	p.pursuit.sendGift()
}

func main() {
	p := &proxy{}
	p.sendGift()
}

什么时候用代理模式呢?抓一个点,行为需要隐藏在代理之后,或者行为需要被代理接管则用代理模式。
常用的场合有:
远程代理;
虚拟代理;
安全代理;

同样有一点要注意的是,代理模式是动作的接受者,实际类是动作的真正执行者。反过来适用吗?代理类可以做成双向代理吗?
如果实现双方共同接口,应该是可以做成双向代理的。

原型模式

原型模式是在给定原型基础上 clone 对象的模式。通过原型模式省去了重复 new 对象带来的性能损耗,同时也能简化代码。

原型模式的 clone 方法返回原型。代码如下:

type icloneable interface {
	clone() icloneable
}

type resume struct{}

func (r *resume) clone() icloneable {
	newr := r
	return newr
}

type letter struct{}

func (l *letter) clone() icloneable {
	newl := l
	return newl
}

func main() {
	var i icloneable = &resume{}
	newi := i.clone()
	fmt.Println(newi)
}

原型模式需要注意的点是浅拷贝还是深拷贝,即拷贝值还是拷贝引用的问题。


posted @ 2022-04-02 14:19  hxia043  阅读(66)  评论(0编辑  收藏  举报