注:参考视频Easy搞定Golang设计模式 本文为对此视频讲解内容的总结,加入了部分自己的理解和思考
1. 概述
1.1 设计模式的定义
有一个四人组(gang of four,简称GoF)将模式的概念引入了软件设计领域。软件模式与具体的应用领域无关,也就是说无论你从事的是移动应用开发、桌面应用开发、Web应用开发还是嵌入式软件的开发,都可以使用软件模式。无论你是使用Java、C#、Objective-C、VB.net、Smalltalk等纯面向对象编程语言,还是使用C++、PHP、Delphi、JavaScript等可支持面向对象编程的语言,你都需要了解软件设计模式!
GoF给软件设计模式提供如下定义:
软件设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。
一句大白话可以总结:“在一定环境下,用固定套路解决问题“
1.2 软件设计模式的种类
GoF提出的设计模式有23个,可分为以下三种:
- 创建型(Creational)模式:如何创建对象;
- 结构型(Structural )模式:如何实现类或对象的组合;
- 行为型(Behavioral)模式:类或对象怎样交互以及怎样分配职责。
有一个“简单工厂模式”不属于GoF 23种设计模式,但大部分的设计模式书籍都会对它进行专门的介绍。所以目前设计模式种类为GoF的23种 + “简单工厂模式” = 24种
1.3 软件设计模式的作用
- 如何将代码分散在几个不同的类中?
- 为什么要有“接口”?
- 何谓针对抽象编程?
- 何时不应该使用继承?
- 如果不修改源代码增加新功能?
- 更好地阅读和理解现有类库与其他系统中的源代码。
1.4 设计模式总览表
设计模式 | 模式名称 | 作用 |
---|---|---|
创建型模式 Creational Pattern | 单例模式 | 保证一个类只有一个实例,并提供一个全局访问点 |
简单工厂模式 | 通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类 | |
工厂方法模式 | 定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类中 | |
抽象工厂模式 | 提供一个接口,用于创建相关或依赖对象的家族,而无需指定具体类 | |
原型模式 | 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象 | |
建造者模式 | 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 | |
结构型模式 Structural Pattern | 适配器模式 | 将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的两个类可以一起工作 |
桥接模式 | 将抽象部分与实际部分分离,使它们都可以独立的变化 | |
组合模式 | 将对象组合成树形结构以表示“部分-整体”的层次结构 | |
装饰器模式 | 动态地给对象添加额外的职责,即增加功能的方式,不改变对象自身 | |
外观模式 | 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用 | |
享元模式 | 运用共享技术有效地支持大量细粒度的对象 | |
代理模式 | 为其他对象提供一种代理以控制对这个对象的访问 | |
行为型模式 Behavioral Pattern | 职责链模式 | 很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任 |
命令模式 | 将一个请求封装为一个对象,使你可用不同的请求对客户端进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作 | |
解释器模式 | 给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子 | |
迭代器模式 | 提供一种方法顺序访问一个聚合对象中各个元素,而不暴露其内部的表示 | |
中介者模式 | 用一个中介对象来封装一系列对象之间的交互,使得各对象之间松耦合,也可简化对象之间的通信 | |
备忘录模式 | 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样就可以恢复到原先保存的状态 | |
观察者模式 | 定义对象间的一对多依赖,当一个对象改变状态时,所有依赖于它的对象都得到通知并自动更新 | |
状态模式 | 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类 | |
策略模式 | 定义一系列算法,并将每个算法封装起来,使得它们可以相互替换,且客户端可以选择使用哪一个算法 | |
模板方法模式 | 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变算法结构即可重定义该算法的某些特定步骤 | |
访问者模式 | 封装一些作用于某对象结构中的各元素的操作,它使得元素的访问和操作分离开来 |
2. 面向对象设计原则
对于面向对象软件系统的设计而言,如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。
面向对象设计原则就是为支持可维护性和可复用性而诞生的,
原则的目的可以用简单的6个字来概括:高内聚,低耦合。
而高内聚和低耦合在本质上是冲突的,如果内聚很高,那就意味着模块的职责很单一,模块的数量会很多,那么自然而然他们之间的关联将变得复杂,耦合度也会很高。
反之,如果内聚很低,那么模块的职责将很广泛,那么模块的数量将会很少,那么自然而然他们之间的关联将变得简单,耦合度也会很低。
因此,我们需要使用面向对象的设计原则,在二者中trade-off。
2.1 单一职责原则(Single Responsibility Principle,SRP)
在面向对象编程的过程中,设计一个类,建议对外提供的功能单一,接口单一,影响一个类的范围就只限定在这一个接口上,一个类的一个接口具备这个类的功能含义,职责单一不复杂。
2.2 开闭原则(Open-Closed Principle,OCP)
2.2.1 定义
Software entities like classes,modules and functions should be open for extension but closed for modifications
一个软件实体, 如类, 模块, 函数等应该对扩展开放, 对修改封闭
- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对已有代码进行任何修改
2.2.2 如何实现开闭原则
实现开放封闭的核心思想就是面对抽象编程,而不是面对具体编程,因为抽象相对稳定。
让类依赖于固定的抽象,所以对修改是封闭的;而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。这是实施开放封闭原则的基本思路。
例:
一个银行业务员,他可能拥有很多的业务,比如Save()存款、Transfer()转账、Pay()支付等。
//一个类,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()
}
这样的设计会导致,当我们去给Banker添加新的业务的时候,会直接修改原有的Banker代码,那么Banker模块的功能会越来越多,出现问题的几率也就越来越大,假如此时Banker已经有99个业务了,现在我们要添加第100个业务,可能由于一次的不小心,导致之前99个业务也一起崩溃,因为所有的业务都在一个Banker类里,他们的耦合度太高,Banker的职责也不够单一,代码的维护成本随着业务的复杂正比成倍增大
我们可以使用接口interface抽象一层出来,制作一个抽象的Banker模块,然后提供一个抽象的方法。 分别根据这个抽象模块,去实现支付Banker(实现支付方法),转账Banker(实现转账方法)等业务
//抽象的银行业务员
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("进行了支付")
}
//实现架构层(基于抽象层进行业务封装-针对interface接口进行封装)
func BankerBusiness(banker AbstractBanker) {
//通过接口来向下调用,(多态现象)
banker.DoBusi()
}
func main() {
//进行存款
BankerBusiness(&SaveBanker{})
//进行存款
BankerBusiness(&TransferBanker{})
//进行存款
BankerBusiness(&PayBanker{})
}
这样的设计,我们只需要实现AbstractBanker接口,就可以实现新的业务,而不需要修改原有的Banker类,也不会影响到其他的业务。
2.3 依赖倒转原则(Dependency Inversion Principle,DIP)
在设计一个系统的时候,将模块分为3个层次,抽象层、实现层、业务逻辑层。那么,我们首先将抽象层的模块和接口定义出来,这里就需要了interface接口的设计,然后我们依照抽象层,依次实现每个实现层的模块,在我们写实现层代码的时候,实际上我们只需要参考对应的抽象层实现就好了,实现每个模块,也和其他的实现的模块没有关系,这样也符合了上面介绍的开闭原则。这样实现起来每个模块只依赖对象的接口,而和其他模块没关系,依赖关系单一。系统容易扩展和维护。
我们在指定业务逻辑也是一样,只需要参考抽象层的接口来写业务就好了,抽象层暴露出来的接口就是我们业务层可以使用的方法,然后可以通过多态的线下,接口指针指向哪个实现模块,调用了就是具体的实现方法,这样我们业务逻辑层也是依赖抽象成编程
例:实现不同人开不同汽车
// ===== > 抽象层 < ========
type Car interface {
Run()
}
type Driver interface {
Drive(car Car)
}
// ===== > 实现层 < ========
type Taxi struct {
//...
}
func (t *Taxi) Run() {
fmt.Println("taxi is running...")
}
type Truck struct {
//...
}
func (t *Truck) Run() {
fmt.Println("truck is running...")
}
type Student struct {
// ...
}
func (s *Student) Drive(car Car) {
fmt.Println("student is driving...", car)
}
type Teacher struct {
// ...
}
func (t *Teacher) Drive(car Car) {
fmt.Println("teacher is driving...", car)
}
// ===== > 业务层 < ========
func main() {
taxi := &Taxi{}
truck := &Truck{}
student := &Student{}
teacher := &Teacher{}
student.Drive(taxi)
student.Drive(truck)
teacher.Drive(taxi)
teacher.Drive(truck)
}
2.4 合成复用原则(Composite Reuse Principle,CRP)
对于继承和组合,优先使用组合。
因为如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。
由于golang只支持组合,没有继承,所以天然强迫用户满足了合成复用原则
2.5 迪米特法则(Law of Demeter,LOD)
一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。
例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)
2.6 小结
面向对象设计原则的中心思想本质上就是六个字:高内聚,低耦合。
接下来要介绍的设计模式,都是通过上面这些原则而演化出来一些模板,亦或者是固定的流程,让我们开发者能够在一些场景中套用进去,提高开发效率。
3. 创建型模式
3.1 简单工厂模式(Simple Factory Pattern)
简单工厂模式并不属于GoF的23种设计模式。他是开发者自发认为的一种非常简易的设计模式,其角色和职责如下:
- 工厂(Factory)角色:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。
- 抽象产品(AbstractProduct)角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
- 具体产品(Concrete Product)角色:简单工厂模式所创建的具体实例对象。
3.1.1 简单工厂模式代码实现
// ======= 抽象层 =========
//水果类(抽象接口)
type Fruit interface {
Show() //接口的某方法
}
// ======= 基础类模块 =========
type Apple struct {}
func (apple *Apple) Show() {
fmt.Println("我是苹果")
}
type Banana struct {}
func (banana *Banana) Show() {
fmt.Println("我是香蕉")
}
type Pear struct {}
func (pear *Pear) Show() {
fmt.Println("我是梨")
}
// ========= 工厂模块 =========
//一个工厂, 有一个生产水果的机器,返回一个抽象水果的指针
type Factory struct {}
func (fac *Factory) CreateFruit(kind string) Fruit {
var fruit Fruit
switch kind {
case "apple":
fruit = new(Apple)
case "banana":
fruit = new(Banana)
case "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()
}
3.1.2 简单工厂模式的优缺点
优点:
- 实现了对象创建和使用的分离。
- 不需要记住具体类名,记住参数即可,减少使用者记忆量。
缺点:
- 对工厂类职责过重,一旦不能工作,系统受到影响。
- 增加系统中类的个数,复杂度和理解度增加。
- 违反“开闭原则”,添加新产品需要修改工厂逻辑,工厂越来越复杂。
适用场景:
- 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
- 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。
3.2 工厂方法模式(Factory Method Pattern)
工厂方法模式 = 简单工厂模式 + 开闭原则
工厂方法模式是简单工厂模式的进一步抽象,其角色和职责如下:
- 抽象工厂(Abstract Factory):工厂方法模式的核心,任何工厂类都必须实现这个接口。
- 工厂(Concrete Factory):具体工厂类是抽象工厂的一个实现,负责实例化产品对象。
- 抽象产品(Abstract Product):工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
- 具体产品(Concrete Product):工厂方法模式所创建的具体实例对象
相较于简单工厂模式,工厂方法模式在简单工厂模式的基础上增加了抽象工厂角色,抽象工厂角色负责定义创建产品对象的接口,而具体工厂角色负责实现这个接口。
3.2.1 工厂方法模式代码实现
代码实现与简单工厂相似,只需要在新增某个具体产品时,也实现一个该产品的具体工厂类即可。
3.2.2 工厂方法模式的优缺点
优点:
- 业务层不需要记住具体类名,甚至连具体参数都不用记忆。
- 实现了对象创建和使用的分离。
- 系统的可扩展性也就变得非常好,无需修改接口和原类。
- 对于新产品的创建,符合开闭原则。
缺点:
- 增加系统中类的个数,复杂度和理解度增加。
- 增加了系统的抽象性和理解难度。
适用场景:
- 客户端不知道它所需要的对象的类。
- 抽象工厂类通过其子类来指定创建哪个对象。
3.3 抽象工厂模式(Abstract Factory Pattern)
3.3.1 产品族与产品等级结构
- 产品族:具有同一个地区、同一个厂商、同一个开发包、同一个组织模块等,但是具备不同特点或功能的产品集合,称之为是一个产品族。
- 产品等级结构:具有相同特点或功能,但是来自不同的地区、不同的厂商、不同的开发包、不同的组织模块等的产品集合,称之为是一个产品等级结构。
当程序中的对象可以被划分为产品族和产品等级结构之后,“抽象工厂方法模式”才可以被适用。
“抽象工厂方法模式”是针对“产品族”进行生产产品。
3.3.2 抽象工厂模式的角色和职责
- 抽象工厂(Abstract Factory):声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
- 具体工厂(Concrete Factory):实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
- 抽象产品(Abstract Product):为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
- 具体产品(Concrete Product):定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。
3.3.3 抽象工厂模式代码实现
例:电脑包括(GPU,RAM,CPU)3个固定的插口,GPU具有显示功能(display),RAM具有存储功能(storage),cpu具有计算功能(calculate)。现有Intel厂商,nvidia厂商,均会生产以上三种硬件。
// ======= 抽象层 =========
type Factory interface {
CreateCPU() CPU
CreateGPU() GPU
CreateRAM() RAM
}
type CPU interface {
Calculate()
}
type GPU interface {
Display()
}
type RAM interface {
Storage()
}
// ======== 实现层 =========
/* Intel 产品族 */
type IntelFactory struct {
}
func (if *IntelFactory) CreateCPU() CPU {
return new(IntelCPU)
}
func (if *IntelFactory) CreateGPU() GPU {
return new(IntelGPU)
}
func (if *IntelFactory) CreateRAM() RAM {
return new(IntelRAM)
}
type IntelCPU struct {
}
func (c *IntelCPU) Calculate() {
fmt.Println("Intel CPU is calculating.")
}
type IntelGPU struct {
}
func (g *IntelGPU) Display() {
fmt.Println("Intel GPU is displaying.")
}
type IntelRAM struct {
}
func (r *IntelRAM) Storage() {
fmt.Println("Intel RAM is storing data.")
}
/* Nvidia 产品族 */
type NvidiaFactory struct {
}
func (nf *NvidiaFactory) CreateCPU() CPU {
return new(NvidiaCPU)
}
func (nf *NvidiaFactory) CreateGPU() GPU {
return new(NvidiaGPU)
}
func (nf *NvidiaFactory) CreateRAM() RAM {
return new(NvidiaRAM)
}
type NvidiaCPU struct {
}
func (c *NvidiaCPU) Calculate() {
fmt.Println("Nvidia CPU is calculating.")
}
type NvidiaGPU struct {
}
func (g *NvidiaGPU) Display() {
fmt.Println("Nvidia GPU is displaying.")
}
type NvidiaRAM struct {
}
func (r *NvidiaRAM) Storage() {
fmt.Println("Nvidia RAM is storing data.")
}
// ======== 业务逻辑层 =======
func main() {
// 使用intel的CPU GPU,使用nvidia的RAM
var factory Factory
factory = new(IntelFactory)
cpu := factory.CreateCPU()
gpu := factory.CreateGPU()
factory = new(NvidiaFactory)
ram := factory.CreateRAM()
cpu.Calculate()
gpu.Display()
ram.Storage()
}
3.3.4 抽象工厂模式的优缺点
优点:
- 拥有工厂方法模式的优点
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
- 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。
缺点:
- 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,带来较大的不便,违背了“开闭原则”。
适用场景
- 系统中有多于一个的产品族。而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
- 产品等级结构稳定。设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。
3.4 单例模式(Singleton Pattern)
保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
个人感觉单例模式在开发中是非常常用的模式,尤其是在一些全局配置类、工具类、数据库连接池等场景。
3.4.1 单例模式代码实现
var once sync.Once
type singleton struct {}
// 唯一实例
var instance *singleton
// 暴露给外界的全局访问方法
func GetInstance() *singleton {
// 保证线程安全 实例只被创建一次
once.Do(func(){
instance = new(singleton)
})
return instance
}
func (s *singleton) SomeThing() {
fmt.Println("单例对象的某方法")
}
3.4.2 单例模式的优缺点
优点:
- 单例模式提供了对唯一实例的受控访问。
- 节约系统资源。由于在系统内存中只存在一个对象。
缺点:
- 扩展略难。单例模式中没有抽象层。
- 单例类的职责过重。
适用场景:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
4. 结构型模式
4.1 代理模式
Proxy模式又叫做代理模式,是构造型的设计模式之一,它可以为其他对象提供一种代理(Proxy)以控制对这个对象的访问。
所谓代理,是指具有与代理元(被代理的对象)具有相同的接口的类,客户端必须通过代理与被代理的目标类交互,而代理一般在交互的过程中(交互前后),进行某些特别的处理。
4.1.1 代理模式中的角色和职责
- subject(抽象主题角色):真实主题与代理主题的共同接口。
- RealSubject(真实主题角色):定义了代理角色所代表的真实对象。
- Proxy(代理主题角色):含有对真实主题角色的引用,代理角色通常在将客户端调用传递给真是主题对象之前或者之后执行某些操作,而不是单纯返回真实的对象。
4.1.2 代理模式的优缺点
优点:
- 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。
缺点:
- 代理实现较为复杂。
适用场景:
- 为其他对象提供一种代理以控制对这个对象的访问。
4.2 装饰器模式(Decorator Pattern)
4.2.1 装饰模式中的角色和职责
- Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
- ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
4.2.2 装饰模式的优缺点
优点:
- 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
- 可以通过一种动态的方式来扩展一个对象的功能,从而实现不同的行为。
- 可以对一个对象进行多次装饰。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。
缺点: - 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,影响程序的性能。
- 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
适用场景:
- 动态、透明的方式给单个对象添加职责。
- 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。
装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。
4.3 适配器模式(Adapter Pattern)
将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
4.3.1 适配器模式中的角色和职责
- Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
- Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
- Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
根据对象适配器模式结构图,在对象适配器中,客户端需要调用request()方法,而适配者类Adaptee没有该方法,但是它所提供的specificRequest()方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的request()方法中调用适配者的specificRequest()方法。因为适配器类与适配者类是关联关系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。
4.3.2 适配器模式的代码实现
//适配的目标
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()
}
4.3.3 适配器模式的优缺点
优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
- 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
- 灵活性和扩展性都非常好,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
缺点:
- 适配器中置换适配者类的某些方法比较麻烦。
适应场景
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
- 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
4.4 外观模式(Facade Pattern)
为多个复杂的子系统提供一个简单接口。
4.4.1 外观模式中的角色和职责
- Facade(外观角色):外观类是客户端所需的接口,它包含了子系统中的所有类,客户可以通过它访问子系统中的所有对象。
- SubSystem(子系统角色):子系统可以是一组类,它们共同完成一个特定的功能,客户可以通过它访问这些类的一些方法。
4.4.2 外观模式的优缺点
优点:
- 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
- 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。
- 一个子系统的修改对其他子系统没有任何影响。
缺点:
- 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活 性。
- 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。
适用场景
- 复杂系统需要简单入口使用。
- 客户端程序与多个子系统之间存在很大的依赖性。
- 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
5. 行为型模式
5.1 模板方法模式(Template Method Pattern)
5.1.1 模板方法模式中的角色和职责
- AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。
- ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。
5.1.2 模板方法的优缺点
优点:
- 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
- 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。
- 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
- 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
缺点:
- 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象。
适用场景
- 具有统一的操作步骤或操作过程;
- 具有不同的操作细节;
- 存在多个具有同样操作步骤的应用场景,但某些具体的操作细节却各不相同;
- 在抽象类中统一操作步骤,并规定好接口;让子类实现接口。这样可以把各个具体的子类和操作步骤解耦合。
5.2 命令模式(Command Pattern)
将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
5.2.1 命令模式中的角色和职责
- Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
- ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
- Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。
- Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
5.2.2 命令模式的优缺点
优点:
- 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
- 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
- 可以比较容易地设计一个命令队列或宏命令(组合命令)。
缺点:
- 可能导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类。
适用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
- 系统需要将一组操作组合在一起形成宏命令。
5.3 策略模式(Strategy Pattern)
5.3.1 策略模式中的角色和职责
- Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
- Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
- ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。
5.3.2 示例代码
//武器策略(抽象的策略)
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()
//更换策略2
hero.SetWeaponStrategy(new(Knife))
hero.Fight()
}
5.3.3 策略模式的优缺点
优点:
- 提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
- 可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。
- 提供了一种算法的复用机制。由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
- 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。
适用场景
- 准备一组算法,并将每一个算法封装起来,使得它们可以互换。
5.4 观察者模式(Observer Pattern)
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
5.4.1 观察者模式中的角色和职责
- Subject(被观察者或目标,抽象主题):被观察的对象。当需要被观察的状态发生变化时,需要通知队列中所有观察者对象。Subject需要维持(添加,删除,通知)一个观察者对象的队列列表。
- ConcreteSubject(具体被观察者或目标,具体主题):被观察者的具体实现。包含一些基本的属性状态及其他操作。
- Observer(观察者):接口或抽象类。当Subject的状态发生变化时,Observer对象将通过一个callback函数得到通知。
- ConcreteObserver(具体观察者):观察者的具体实现。得到通知后将完成一些具体的业务逻辑处理。
5.4.2 示例代码
//--------- 抽象层 --------
// 抽象的观察者
type Listener interface {
OnTeacherComming() //观察者得到通知后要触发的动作
}
// 抽象的被观察者(通知者)
type Notifier interface {
AddListener(listener Listener)
RemoveListener(listener Listener)
Notify()
}
//--------- 实现层 --------
// 观察者学生
type StuZhang3 struct {
Badthing string
}
func (s *StuZhang3) OnTeacherComming() {
fmt.Println("张3 停止 ", s.Badthing)
}
func (s *StuZhang3) DoBadthing() {
fmt.Println("张3 正在", s.Badthing)
}
type StuZhao4 struct {
Badthing string
}
func (s *StuZhao4) OnTeacherComming() {
fmt.Println("赵4 停止 ", s.Badthing)
}
func (s *StuZhao4) DoBadthing() {
fmt.Println("赵4 正在", s.Badthing)
}
// 具体的通知者 班长
type ClassMonitor struct {
listenerList []Listener //需要通知的全部观察者集合
}
func (m *ClassMonitor) AddListener(listener Listener) {
// 添加观察者
}
func (m *ClassMonitor) RemoveListener(listener Listener) {
// 删除指定观察者
}
func (m* ClassMonitor) Notify() {
for _, listener := range m.listenerList {
//依次调用全部观察的具体动作
listener.OnTeacherComming()
}
}
func main() {
s1 := &StuZhang3{
Badthing: "抄作业",
}
s2 := &StuZhao4{
Badthing: "玩王者荣耀",
}
classMonitor := new(ClassMonitor)
fmt.Println("上课了,但是老师没有来,学生们都在忙自己的事...")
s1.DoBadthing()
s2.DoBadthing()
classMonitor.AddListener(s1)
classMonitor.AddListener(s2)
fmt.Println("这时候老师来了,班长给学什么使了一个眼神...")
classMonitor.Notify()
}
5.4.3 观察者模式的优缺点
优点:
- 可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。
- 在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
- 支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。
- 满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。
缺点:
- 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
适用场景
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。
- 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。
- 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。