Golang设计模式
一、设计模式概述
1.1 概述
如果把修习软件开发当做武功修炼的话,那么可以分为招式和内功。
招式:
-
Java、C#、C++、Golang、Rust等编程语言;
-
Eclipse、Visual Studio、Goland、Vim等开发工具;
-
Struts、Hibernate、JBPM、Gin、Istio、gRPC等框架技术;
内功:
-
数据结构
-
算法
-
设计模式
-
架构设计
-
软件工程
1.2 软件设计模式又从何而来
GoF将模式的概念引入软件工程领域,这标志着软件模式的诞生。软件模式(Software Patterns)是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。
软件模式与具体的应用领域无关,也就是说无论你从事的是移动应用开发、桌面应用开发、Web应用开发还是嵌入式软件的开发,都可以使用软件模式。无论你是使用Java、C#、Objective-C、VB.net、Smalltalk等纯面向对象编程语言,还是使用C++、PHP、Delphi、JavaScript等可支持面向对象编程的语言,你都需要了解软件设计模式! GoF给软件设计模式提供了定义,如下:
“软件设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。”
一句大白话可以总结:“在一定环境下,用固定套路解决问题。”
1.3 软件设计模式的种类
GoF提出的设计模式有23个,包括:
-
创建型(Creational)模式:如何创建对象。
-
结构型(Structural)模式:如何实现类或对象的组合。
-
行为型(Behavioral)模式:类或对象怎样交互以及怎样分配职责。
有一个 简单工厂模式 不属于GoF 23种涉及模式,但大部分的设计模式书籍都会对它进行专门的介绍。
设计模式目前种类:GoF 的 23 种 + “简单工厂模式” = 24 种。
1.4 软件设计模式的作用
那么对于初学者来说,学习设计模式将有助于更加深入地理解面向对象思想,让你知道:
如何将代码分散在几个不同的类中?
为什么要有“接口”?
何谓针对抽象编程?
何时不应该使用继承?
如果不修改源代码增加新功能?
更好地阅读和理解现有类库与其他系统中的源代码。
学习设计模式会让你早点脱离面向对象编程的 “菜鸟期”。
1.5 如何学号设计模式。
设计模式的基础是:多态。
初学者:积累案例,不要盲目的背类图。
初级开发人员:多思考,多梳理,归纳总结,尊重事物的认知规律,注意临界点的突破,不要浮躁。
中级开发人员:合适的开发环境,寻找合适的设计模式来解决问题。
多应用,对经典啧组合设计模式的大量,自由的运用。要不断的追求。
1.6 设计模式总览表
二、面向对象设计原则
对于面向对象软件系统的设计而言,在支持可维护性的同时,提高系统的可复用性是一个至关重要的问题,如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。
面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。面向对象设计原则也是我们用于评价一个设计模式的使用效果的重要指标之一。
原则的目的: 高内聚,低耦合
2.1 面向对象设计原则表
名称 | 定义 |
---|---|
单一职责原则 (Single Responsibility Principle SRP) | 类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个 |
开闭原则 (Open-Closed Principle,OCP) | 类的改动是通过增加代码进行的,而不是修改源代码。 |
里氏替换原则 (Liskov Substitution Principle, LSP) | 任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。 |
依赖倒转原则 (DependenceInversion Principle, DIP) | 依赖于抽象(接口),不要依赖具体的实现(类),也局势针对接口编程。 |
接口隔离原则 (Interface Segregation Principle, ISP) | 不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。 |
合成复用原则 (Composite Reuse Principle ,CRP) | 如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。 |
迪米特法则 (Law of Demeter, LoD) | 一个对象应当对其他对象经可能的少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个从程序中,各个模块之间互相调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理) |
2.2 单一职责原则
类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个
package main
import "fmt"
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.3 开闭原则
2.3.1 平铺式设计
那么作为 interface 数据类型,它存在的意义在哪里呢?实际上是为了满足一些面向对象的编程思想。我们知道,软件设计的最高目标就是 高内聚,低耦合
。那么其中有一个设计原则叫 开闭原则
。什么是开闭原则呢,接下来我们看一个例子:
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()
支付等。那么如果这个业务员模块只有这几个方法还好,但是随着我们的程序写的越来越复杂,银行业务员可能就要增加方法,会导致业务员模块越来越臃肿。
这样的设计会导致,当我们去给 Banker 添加新的业务的时候,会直接修改原有的Banker代码,那么 Banker 模块的功能会越来越多,出现问题的几率也就越来越大,假如此时 Banker 已经有 99 个业务了,现在我们要添加第 100 个业务,可能由于一次的不小心,导致之前 99 个业务也一起崩溃,因为所有的业务都在一个 Banker 类里,他们的耦合度太高,Banker 的职责也不够单一,代码的维护成本随着业务的复杂正比成倍增大。
2.3.2 开闭原则设计
那么,如果我们拥有接口,interface 这个东西,那么我们就可以抽象一层出来,制作一个抽象的 Banker 模块,然后提供一个抽象的方法。分别根据这个抽象的模块,去实现 支付Banker(实现支付方法),转账Banker(实现转账方法)
那么依然可以搞定程序的需求。然后,当我们想要给Banker太耐额外功能的时候,之前我们是直接修改Banker的内容,现在我们可以单独定义一个股票 Banker(实现股票方法),到这个系统中。而且股票 Banker 的实现成功或者失败都不会影响之前的稳定系统,他很单一,而且独立。
所以以上,当我们给一个系统天极爱一个功能的时候,不是通过修改代码,而是通过添加代码来完成,那么就开闭原则的核心思想了。所以要想满足上面的要求,是一定需要 interface 来提供一层抽象的接口的。
golang代码实现如下:
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
设计一个小框架
//实现架构层(基于抽象层进行业务封装-针对interface接口进行封装)
func BankerBusiness(banker AbstractBanker) {
//通过接口来向下调用,(多态现象)
banker.DoBusi()
}
那么main中可以如下实现业务调用:
func main() {
//进行存款
BankerBusiness(&SaveBanker{})
//进行存款
BankerBusiness(&TransferBanker{})
//进行存款
BankerBusiness(&PayBanker{})
}
再看开闭原则定义: 开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 简单的说就是在修改需求的时候,应该尽量通过扩展来实现变化,而不是通过修改已有代码来实现变化。
2.3.3 接口的意义
好了,现在interface已经基本了解,那么接口的意义最终在哪里呢,想必现在你已经有了一个初步的认知,实际上接口的最大的意义就是实现多态的思想,就是我们可以根据interface类型来设计API接口,那么这种API接口的适应能力不仅能适应当下所实现的全部模块,也适应未来实现的模块来进行调用。调用未来可能就是接口的最爱意义所在,这也是为什么架构师那么值钱,因为架构师是可以针对 interface 设计一套框架,在未来许多年依然适用。
2.4 依赖倒转原则
2.4.1 耦合度极高的模块关系涉及
package main
import "fmt"
// === > 奔驰汽车 <===
type Benz struct {
}
func (this *Benz) Run() {
fmt.Println("Benz is running...")
}
// === > 宝马汽车 <===
type BMW struct {
}
func (this *BMW) Run() {
fmt.Println("BMW is running ...")
}
//===> 司机张三 <===
type Zhang3 struct {
//...
}
func (zhang3 *Zhang3) DriveBenZ(benz *Benz) {
fmt.Println("zhang3 Drive Benz")
benz.Run()
}
func (zhang3 *Zhang3) DriveBMW(bmw *BMW) {
fmt.Println("zhang3 drive BMW")
bmw.Run()
}
//===> 司机李四 <===
type Li4 struct {
//...
}
func (li4 *Li4) DriveBenZ(benz *Benz) {
fmt.Println("li4 Drive Benz")
benz.Run()
}
func (li4 *Li4) DriveBMW(bmw *BMW) {
fmt.Println("li4 drive BMW")
bmw.Run()
}
func main() {
//业务1 张3开奔驰
benz := &Benz{}
zhang3 := &Zhang3{}
zhang3.DriveBenZ(benz)
//业务2 李四开宝马
bmw := &BMW{}
li4 := &Li4{}
li4.DriveBMW(bmw)
}
我们来看上面的代码和图中每个模块之间的依赖关系,实际上并没有用到任何的interface
接口层的代码,显然最后我们的两个业务 张三开奔驰
, 李四开宝马
,程序中也都实现了。但是这种设计的问题就在于,小规模没什么问题,但是一旦程序需要扩展,比如我现在要增加一个丰田汽车
或者 司机王五
, 那么模块和模块的依赖关系将成指数级递增,想蜘蛛网一样越来越难维护和捋顺。
2.4.2 面向抽象层依赖倒转
如上图所示,如果我们在设计一个系统的时候,将模块分为3个层次,抽象层、实现层、业务逻辑层。那么,我们首先将抽象层的模块和接口定义出来,这里就需要了interface
接口的设计,然后我们依照抽象层,依次实现每个实现层的模块,在我们写实现层代码的时候,实际上我们只需要参考对应的抽象层实现就好了,实现每个模块,也和其他的实现的模块没有关系,这样也符合了上面介绍的开闭原则。这样实现起来每个模块只依赖对象的接口,而和其他模块没关系,依赖关系单一。系统容易扩展和维护。
我们在指定业务逻辑也是一样,只需要参考抽象层的接口来业务就好了,抽象层暴露出来的接口就是我们业务层可以使用的方法,然后可以通过多态的线下,接口指针指向哪个实现模块,调用了就是具体的实现方法,这样我们业务逻辑层也是依赖抽象成编程。
我们就将这种的设计原则叫做依赖倒转原则
。
来一起看一下修改的代码:
package main
import "fmt"
// ===== > 抽象层 < ========
type Car interface {
Run()
}
type Driver interface {
Drive(car Car)
}
// ===== > 实现层 < ========
type BenZ struct {
//...
}
func (benz * BenZ) Run() {
fmt.Println("Benz is running...")
}
type Bmw struct {
//...
}
func (bmw * Bmw) Run() {
fmt.Println("Bmw is running...")
}
type Zhang_3 struct {
//...
}
func (zhang3 *Zhang_3) Drive(car Car) {
fmt.Println("Zhang3 drive car")
car.Run()
}
type Li_4 struct {
//...
}
func (li4 *Li_4) Drive(car Car) {
fmt.Println("li4 drive car")
car.Run()
}
// ===== > 业务逻辑层 < ========
func main() {
//张3 开 宝马
var bmw Car
bmw = &Bmw{}
var zhang3 Driver
zhang3 = &Zhang_3{}
zhang3.Drive(bmw)
//李4 开 奔驰
var benz Car
benz = &BenZ{}
var li4 Driver
li4 = &Li_4{}
li4.Drive(benz)
}
2.5 合成复用原则
如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。
package main
import "fmt"
type Cat struct {}
func (c *Cat) Eat() {
fmt.Println("小猫吃饭")
}
//给小猫添加一个 可以睡觉的方法 (使用继承来实现)
type CatB struct {
Cat
}
func (cb *CatB) Sleep() {
fmt.Println("小猫睡觉")
}
//给小猫添加一个 可以睡觉的方法 (使用组合的方式)
type CatC struct {
C *Cat
}
func (cc *CatC) Sleep() {
fmt.Println("小猫睡觉")
}
func main() {
//通过继承增加的功能,可以正常使用
cb := new(CatB)
cb.Eat()
cb.Sleep()
//通过组合增加的功能,可以正常使用
cc := new(CatC)
cc.C = new(Cat)
cc.C.Eat()
cc.Sleep()
}
2.6 迪米特法则
-
和陌生人说话
-
不和陌生人说话
-
与依赖倒转原则结合 某人和 抽象陌生人说话 让某人和陌生人进行解耦合
三、创建型模式
3.1 概述
目前标准的创建型设计模式共有6种(注:设计模式种类并非仅仅局限于此,设计模式实则是一种编程思想,开发者可以根据自身经验来总结出很多种设计模式思想,这6中创建型设计模式为早期官方认可的标准模式)
本章节主要介绍常用的“单例模式”、“简单工程模式”、“工厂方法模式”、“抽象工厂模式”等。“原型模式”、“建造者模式”思想类似,作为读者选修篇幅,本章暂时先不介绍。
3.2 简单工厂模式
3.2.1 为什么需要工厂模式
首先来看,如果没有工厂模式,在开发者创建一个类的对象时,如果有很多不同种类的对象将会如何实现,代码如下:
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模块做改动,影响业务层的开发效率和稳定性。整体的依赖关系为。
业务逻辑层 ---> 基础类模块
那么如何将业务层创建对象与基础类模块做解耦呢,这里即可以在中间加一层工厂模块层,来降低业务逻辑层对基础模块层的直接依赖和耦合关联。
业务逻辑层 ---> 工厂模块 ---> 基础类模块
这样就引出了需要对工厂模块的一些设计和加工生成基础模块对象的模式。
3.2.2 简单工厂模式角色和职责
简单工厂模式并不属于GoF的23种设计模式。它是开发者自发认为的一种非常简易的设计模式,其角色和职责如下:
工厂角色:简单工厂模式的核心,它负责实现创建所有市里的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。
抽象产品角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
具体产品角色:简单工厂模式所创建的具体事例对象。
3.2.3 简单工厂模式实现
根据本章节的案例可以将标准的 ”简单工厂模式“ 类图改进如下:
简单工厂方法模式的实现代码如下:
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 简单工厂方法模式的优缺点
优点:
-
实现了对象创建和使用的分离。
-
不需要记住具体类名,记住参数即可,减少使用者记忆量。
缺点:
-
对工厂类职责过重,一旦不能工作,系统受到影响。
-
增加系统中类的个数,复杂度和理解度增加。
-
违反“开闭原则”,添加新产品需要修改工厂逻辑,工厂越来越复杂。
适用场景:
-
工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
-
客户端只知道传入工厂类的参数,对于如何创建对象并不关心。
3.2 工厂方法模式
3.2.1 工厂方法模式中的角色和职责
抽象工厂(Abstract Factory)角色:工厂方法模式的核心,任何工厂类都必须实现这个接口。
工厂(Concrete Factory)角色:具体工厂类是抽象工厂的一个实现,负责实例化产品对象。
抽象产品(Abstract Product)角色:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
具体产品(Concrete Product)角色:工厂方法模式所创建的具体实例对象。
简单工厂模式 + “开闭原则” = 工厂方法模式
工厂方法模式的标准类图如下:
其标准的设计模式类图如下:
3.2.2 工厂方法模式的视线
根据本章节的案例可以将标准的“工厂方法模式”类图改进如下:
实现代码如下:
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 =