设计模式
软件设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。
设计模式目前包括: GoF提出的23种设计模式 + “简单工厂模式” ,可以分为以下几类
- 创建型(Creational)模式:如何创建对象;
- 结构型(Structural)模式:如何实现类或对象的组合;
- 行为型(Behavioral)模式:类或对象怎样交互以及怎样分配职责。
面向对象的设计原则(Object-Oriented Design Principles)是指导开发人员如何设计和编写高质量、可维护、可扩展的软件的基本准则。这些原则帮助开发人员创建灵活、可重用的代码,并减少代码的复杂性和耦合度。以下是一些重要的面向对象设计原则
- 单一职责原则(Single Responsibility Principle, SRP):类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。
- 开闭原则(Open/Closed Principle, OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。(类的改动是通过增加代码进行的,而不是修改源代码。)
- 里氏替换原则(Liskov Substitution Principle, LSP):子类对象必须能够替换父类对象,并且程序的行为不会发生变化。任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。
- 依赖倒转原则(Dependence Inversion Principle, DIP):高层模块不应该依赖于低层模块,二者都应该依赖于抽象(接口);抽象不应该依赖于细节,细节应该依赖于抽象。换句话说,是针对接口编程。
- 接口隔离原则(Interface Segregation Principle, ISP):不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。
- 合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的。如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。
- 迪米特法则(Law of Demeter, LoD):一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。
因为Golang不提供继承机制,需要使用匿名组合模拟实现继承。此处需要注意:因为父类需要调用子类方法,所以子类需要匿名组合父类的同时,父类需要持有子类的引用。
创建型模式
单例模式(Singleton)
确保一个类仅有一个实例,并提供一个访问它的全局访问。
实现单例模式的步骤为:
- 类私有化:外部不能通过这个类直接创建一个对象
- 提供一个全局访问点:对外提供一个方法来获取这个唯一实例对象
- 确保线程安全:在并发环境下,确保实例的创建是线程安全的
单例模式分为饿汉式和懒汉式,前者在初始化单例唯一指针的时候,就已经提前开辟好了一个对象,申请了内存;后者在第一次使用时才创建实例。饿汉式的好处是,不会出现线程并发创建,导致多个单例的出现,但是缺点是如果这个单例对象在业务逻辑没有被使用,也会客观的创建一块内存对象。懒汉式的好处是延迟实例化,节省资源,但需要处理多线程环境下的同步问题。
- 懒汉式
1、非线程安全实现方式
// 私有类,首字母名称小写
type singleton struct{}
// 指针指向这个唯一对象,golang没有长指针概念,则将指针私有化不让外部模块访问
var instance *singleton
// 对外提供方法或者这个唯一实例对象
func GetInstanceLazyUnsafe() *singleton {
//首次调用才生成单例的实例,非线程安全
if instance == nil {
instance = new(singleton)
return instance
}
return instance
}
2、使用互斥锁
// 私有类,首字母名称小写
type singleton struct{}
// 指针指向这个唯一对象,golang没有长指针概念,则将指针私有化不让外部模块访问
var instance *singleton
// 锁
var lock sync.Mutex
func GetInstanceLazyMutex() *singleton {
//为了线程安全,增加互斥锁,性能较低
lock.Lock()
defer lock.Unlock()
if instance == nil {
return new(singleton)
}
return instance
}
3、使用原子操作检查状态
// 私有类,首字母名称小写
type singleton struct{}
// 指针指向这个唯一对象,golang没有长指针概念,则将指针私有化不让外部模块访问
var instance *singleton
// 标记
var initialized uint32
// 锁
var mulock sync.Mutex
func GetInstanceLazyAtomic() *singleton{
// 借助原子操作,若已标记,即已实例化,直接返回
if atomic.LoadUint32(&initialized) == 1{
return instance
}
// 否则加锁申请
mulock.Lock()
defer mulock.Unlock()
if initialized == 0{
instance = new(singleton)
// 设置标记位
atomic.StoreUint32(&initialized,1)
}
return instance
}
4、使用sync.once包
// 私有类,首字母名称小写
type singleton struct{}
// 指针指向这个唯一对象,golang没有长指针概念,则将指针私有化不让外部模块访问
var instance *singleton
// 使用golang once
var once sync.Once
func GetInstanceLazyOnce() *singleton{
once.Do(func () {
instance = new(singleton)
})
return instance
}
- 饿汉式
// 私有类,首字母名称小写
type singleton struct{}
// 包加载时创建
var instanceEager *singleton = new(singleton)
func GetInstanceEager()*singleton{
return instance
}
单例模式优点
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 提供了对唯一实例的受控访问
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能
- 允许可变数目的实例
- 避免对共享资源的多重占用
单例模式的缺点
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
单例模式的使用场景
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
简单工厂模式(Simple Factory)
通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
在没有简单工厂模式,创建不同种类的对象的代码如下:
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类中包含多个if…else模块,代码冗长维护难度大,而且大量条件判断的存在也会影响系统性能
- Fruit类的职责过重,它负责初始化和显示所有的水果对象,违反了“单一职责原则”,不利于类的重用和维护
- 当需要增加新类型的水果时,必须修改Fruit类的构造函数和其他相关方法源代码,违反了“开闭原则”。
也就是说,当业务层希望创建一个对象的时候,将直接依赖类的构造方法,这样随着类的越来越复杂,那么业务层的开发逻辑也需要依赖类的更新,且随之改变,影响开发效率和稳定性。
简单工厂模式则考虑在业务层和基础类模块中添加一个工厂模块层,降低耦合关联性。
简单工厂模式的角色和产品如下:
- 工厂(Factory)角色:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。
- 抽象产品(AbstractProduct)角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
- 具体产品(Concrete Product)角色:简单工厂模式所创建的具体实例对象。
简单工厂模式实现Fruit类不同对象的代码如下:
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()
}
简单工厂模式的优点
- 封装对象创建:将对象的创建逻辑集中在一个工厂类中,客户端不需要知道具体的创建细节,简化了客户端代码。
- 减少代码重复:避免了在多个地方重复创建对象的代码,提高了代码的复用性。
简单工厂模式的缺点
- 不符合开闭原则:简单工厂模式在添加新产品时需要修改工厂类的代码,违反了开闭原则(对扩展开放,对修改关闭)。
- 单一职责问题:工厂类承担了过多的职责,不仅负责对象的创建,还可能涉及到对象的初始化和配置,导致类的职责过重。
- 难以扩展:当产品种类较多时,工厂类的代码会变得复杂,难以维护和扩展。
- 缺乏多态性:简单工厂模式通常返回具体的产品对象,客户端代码可能依赖于具体的产品类,缺乏多态性和灵活性。
简单工厂模式的使用场景
- 创建对象的逻辑较为简单,不需要复杂的初始化和配置。
- 系统中只有少量的产品类,且产品类的变化不频繁时。
- 客户端不关心对象的创建过程,只需要使用工厂提供的对象。
工厂方法模式(Factory Method)
定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类。
工厂方法模式的角色和职责
- 抽象工厂(Abstract Factory)角色:工厂方法模式的核心,任何工厂类都必须实现这个接口。
- 工厂(Concrete Factory)角色:具体工厂类是抽象工厂的一个实现,负责实例化产品对象。
- 抽象产品(Abstract Product)角色:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
- 具体产品(Concrete Product)角色:工厂方法模式所创建的具体实例对象。
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()
}
工厂方法模式的优点
- 遵循开闭原则:工厂方法模式通过引入抽象工厂和具体工厂子类,使得系统在扩展新产品类型时不需要修改已有的代码,只需添加新的工厂子类和产品类即可。
- 遵循单一职责原则:每个具体工厂类只负责创建一种特定类型的产品,职责单一,代码清晰,易于维护。
- 提高代码的灵活性和可维护性:通过引入抽象产品类或接口,工厂方法模式降低了工厂类和产品类之间的耦合度,使得代码更加灵活和可维护。
- 延迟对象的创建:工厂方法模式将对象的创建延迟到子类中,使得父类不需要知道具体的产品类,符合依赖倒置原则。
工厂方法模式的缺点
- 增加系统的复杂性:引入了更多的类和接口,增加了系统的复杂性,特别是在产品种类较多时,可能会导致类的数量急剧增加。
- 学习成本较高:对于初学者来说,理解和实现工厂方法模式可能需要一定的学习成本。
- 不适合创建简单对象:对于创建逻辑简单且变化不大的对象,使用工厂方法模式可能显得过于复杂,简单工厂模式可能更合适。
工厂方法模式的适用场景
- 系统需要独立于其产品创建和表示时:当一个类不知道它所需要的对象的具体类型时,可以使用工厂方法模式将对象的创建延迟到子类中。
- 系统需要灵活地扩展和更改产品类型时:当系统需要在不修改现有代码的情况下引入新的产品类型时,工厂方法模式是一个很好的选择。
- 产品的创建逻辑复杂或变化频繁时:当产品的创建逻辑较为复杂或变化频繁时,可以使用工厂方法模式将创建逻辑封装在具体工厂类中,简化客户端代码。
- 需要对产品的创建过程进行控制时:当需要对产品的创建过程进行精细控制,如初始化、配置等,可以使用工厂方法模式将这些逻辑封装在具体工厂类中。
抽象工厂模式(Abstract Factory)
工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。因此,可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产。抽象工厂模式提供一个创建一系列相关或相互依赖的接口,而无需指定它们具体的类。
抽象工厂模式的角色和职责为
- 抽象工厂(Abstract Factory)角色:它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
- 具体工厂(Concrete Factory)角色:它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
- 抽象产品(Abstract Product)角色:它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
- 具体产品(Concrete Product)角色:它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。
抽象工厂模式提到的产品族和产品模式如下
- 产品族:具有同一个地区、同一个厂商、同一个开发包、同一个组织模块等,但是具备不同特点或功能的产品集合,称之为是一个产品族。
- 产品等级结构:具有相同特点或功能,但是来自不同的地区、不同的厂商、不同的开发包、不同的组织模块等的产品集合,称之为是一个产品等级结构。
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()
}
抽象工厂模式的优点
- 分离接口和实现:抽象工厂模式通过提供一个创建对象的接口,使得客户端代码与具体的产品类解耦,符合依赖倒置原则。
- 提高可扩展性:当需要添加新的产品族时,只需添加新的具体工厂类和产品类,而不需要修改已有的代码,符合开闭原则。
- 一致的产品族创建:抽象工厂模式确保了同一产品族中的对象是一致的,避免了客户端代码中出现不兼容的产品对象。
抽象工厂模式的缺点
- 增加系统的复杂性:引入了更多的类和接口,增加了系统的复杂性,特别是在产品族较多时,可能会导致类的数量急剧增加。
- 难以支持新种类的产品:如果需要向产品族中添加新的产品种类,可能需要修改抽象工厂接口及其所有的具体工厂类,违反了开闭原则。
抽象工厂模式的适用场景
- 系统需要多个产品族中的产品对象时:当一个系统需要使用多个产品族中的产品对象,并且这些产品族中的对象是相互关联或依赖的,可以使用抽象工厂模式。
- 系统需要确保产品族中的对象的一致性时:当一个系统需要确保同一产品族中的对象是一致的,避免出现不兼容的产品对象时,可以使用抽象工厂模式。
- 产品等级结构稳定。设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。
原型模式(Prototype)
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。原型模式使对象能复制自身,并且暴露到接口中,使客户端面向接口编程时,不知道接口实际对象的情况下生成新的对象。原型模式配合原型管理器使用,使得客户端在不知道具体类的情况下,通过接口管理器得到新的实例,并且包含部分预设定配置。
package prototype
// Cloneable 是原型对象需要实现的接口
type Cloneable interface {
Clone() Cloneable
}
type PrototypeManager struct {
prototypes map[string]Cloneable
}
func NewPrototypeManager() *PrototypeManager {
return &PrototypeManager{
prototypes: make(map[string]Cloneable),
}
}
func (p *PrototypeManager) Get(name string) Cloneable {
return p.prototypes[name].Clone()
}
func (p *PrototypeManager) Set(name string, prototype Cloneable) {
p.prototypes[name] = prototype
}
原型模式的优点
- 减少子类数量:通过克隆对象来创建新对象,可以减少子类的数量。
- 动态配置对象:可以在运行时动态地创建和配置对象。
- 性能优化:通过克隆对象来创建新对象,可能比通过实例化类来创建对象更高效。
原型模式的缺点
- 深拷贝和浅拷贝:实现深拷贝可能比较复杂,特别是当对象包含其他对象时。
- 对象状态不一致:如果原型对象的状态发生变化,可能会影响到克隆对象。
原型模式的适用场景
- 对象的创建成本较高:通过克隆现有对象来创建新对象,可以降低创建成本。
- 系统需要动态地创建对象:可以在运行时动态地创建和配置对象。
建造者模式(Builder)
将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。建造者模式通过使用多个简单的对象一步一步构建一个复杂的对象。
package main
import "fmt"
// Product 是要构建的复杂对象
type Product struct {
partA string
partB string
partC string
}
// Builder 是定义了构建步骤的接口
type Builder interface {
BuildPartA()
BuildPartB()
BuildPartC()
GetResult() *Product
}
// ConcreteBuilder 是具体的建造者类
type ConcreteBuilder struct {
product *Product
}
func (b *ConcreteBuilder) BuildPartA() {
b.product.partA = "PartA"
}
func (b *ConcreteBuilder) BuildPartB() {
b.product.partB = "PartB"
}
func (b *ConcreteBuilder) BuildPartC() {
b.product.partC = "PartC"
}
func (b *ConcreteBuilder) GetResult() *Product {
return b.product
}
// Director 是指导建造过程的类
type Director struct {
builder Builder
}
func (d *Director) Construct() {
d.builder.BuildPartA()
d.builder.BuildPartB()
d.builder.BuildPartC()
}
func main() {
builder := &ConcreteBuilder{product: &Product{}}
director := &Director{builder: builder}
director.Construct()
product := builder.GetResult()
fmt.Println("Product:", product.partA, product.partB, product.partC)
}
建造者模式的优点
- 分离构建过程和表示:将对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
- 更好的控制:可以更好地控制对象的构建过程。
- 代码复用:可以复用构建过程中的代码。
建造模式的缺点
- 增加复杂性:引入建造者模式会增加系统的复杂性,因为需要额外的建造者类。
- 不适合简单对象:对于简单对象,使用建造者模式可能会显得过于复杂。
建造模式的适用场景
- 复杂对象的创建:需要创建一个由多个部分组成的复杂对象。
- 不同表示的对象:需要通过同样的构建过程创建不同表示的对象。
结构型模式
代理模式(Proxy)
在不改变原始对象的情况下,通过代理对象来控制对原始对象的访问。
package main
import "fmt"
// Subject 是定义了 RealSubject 和 Proxy 的共同接口
type Subject interface {
Request() string
}
// RealSubject 是我们要访问的真实对象
type RealSubject struct{}
func (r *RealSubject) Request() string {
return "RealSubject: Handling request."
}
// Proxy 是代理类,控制对 RealSubject 的访问
type Proxy struct {
realSubject *RealSubject
}
func (p *Proxy) Request() string {
if p.realSubject == nil {
p.realSubject = &RealSubject{}
}
// 在调用真实对象的方法之前,可以添加额外的功能
fmt.Println("Proxy: Logging the request.")
return p.realSubject.Request()
}
func main() {
var subject Subject = &Proxy{}
fmt.Println(subject.Request())
}
代理模式的优点
- 控制访问:代理模式可以控制对原始对象的访问。例如,保护代理可以在访问原始对象之前进行权限检查。
代理模式的缺点
- 增加复杂性:引入代理模式会增加系统的复杂性,因为需要额外的代理类。
- 性能开销:代理模式可能会增加系统的性能开销,特别是在代理中添加了额外的处理逻辑时。
- 潜在的过度设计
代理模式的适用场景
- 远程代理:为一个位于不同地址空间的对象提供局部代表。典型的例子是RMI(远程方法调用)。
- 虚拟代理:根据需要创建开销很大的对象。典型的例子是按需加载的图像。
- 保护代理:控制对原始对象的访问。典型的例子是权限控制。
- 智能引用:在访问对象时执行一些附加操作。典型的例子是引用计数、日志记录等。
装饰模式(Decorator)
动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活,它允许向一个现有的对象添加新的功能,同时又不改变其结构,其通过创建一个装饰类来包装原始类,从而在保持类接口不变的情况下增强类的功能。装饰模式是一种对象结构型模式。
装饰模式的角色和职责
- Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
- ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)
package main
import "fmt"
// Component 是定义了基本操作的接口
type Component interface {
Operation() string
}
// ConcreteComponent 是具体的组件类,实现了 Component 接口
type ConcreteComponent struct{}
func (c *ConcreteComponent) Operation() string {
return "ConcreteComponent"
}
// Decorator 是装饰器类,实现了 Component 接口,并包含一个 Component 类型的字段
type Decorator struct {
component Component
}
func (d *Decorator) Operation() string {
return d.component.Operation()
}
// ConcreteDecoratorA 是具体的装饰器类,扩展了 Decorator 类
type ConcreteDecoratorA struct {
Decorator
}
func (d *ConcreteDecoratorA) Operation() string {
return fmt.Sprintf("ConcreteDecoratorA(%s)", d.component.Operation())
}
// ConcreteDecoratorB 是另一个具体的装饰器类,扩展了 Decorator 类
type ConcreteDecoratorB struct {
Decorator
}
func (d *ConcreteDecoratorB) Operation() string {
return fmt.Sprintf("ConcreteDecoratorB(%s)", d.component.Operation())
}
func main() {
// 创建具体的组件对象
component := &ConcreteComponent{}
// 使用装饰器A装饰组件
decoratorA := &ConcreteDecoratorA{Decorator{component: component}}
fmt.Println(decoratorA.Operation())
// 使用装饰器B装饰组件
decoratorB := &ConcreteDecoratorB{Decorator{component: component}}
fmt.Println(decoratorB.Operation())
// 使用装饰器A和装饰器B组合装饰组件
decoratorAB := &ConcreteDecoratorA{Decorator{component: decoratorB}}
fmt.Println(decoratorAB.Operation())
}
装饰模式的优点
- 灵活性:可以在运行时动态地添加或删除对象的功能。
- 遵循开闭原则:可以在不修改现有类的情况下扩展对象的功能。
- 组合功能:可以通过多个装饰器类的组合来实现复杂的功能。
装饰模式的缺点
- 增加复杂性:引入装饰模式会增加系统的复杂性,因为需要额外的装饰类。
- 多层装饰:如果装饰器类过多,会导致系统变得复杂且难以调试。
装饰模式的适用场景
- 需要扩展类的功能:在不修改现有类的情况下,动态地添加或删除类的功能。
- 需要组合功能:通过多个装饰器类的组合来实现复杂的功能。
适配器模式(Adapter)
将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。适配器模式可以分为类适配器和对象适配器两种。
适配器模式的角色和职责
- Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
- Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
- Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
package main
import "fmt"
// Target 是客户期望的接口
type Target interface {
Request() string
}
// Adaptee 是需要适配的类
type Adaptee struct{}
func (a *Adaptee) SpecificRequest() string {
return "Adaptee's specific request"
}
// Adapter 是将 Adaptee 转换为 Target 接口的适配器
type Adapter struct {
adaptee *Adaptee
}
func (a *Adapter) Request() string {
return a.adaptee.SpecificRequest()
}
func main() {
adaptee := &Adaptee{}
adapter := &Adapter{adaptee: adaptee}
fmt.Println(adapter.Request())
}
适配器模式的优点
- 提高类的复用性:通过适配器模式,可以将现有的类复用到新的环境中。
- 提高类的灵活性:通过适配器模式,可以在不修改现有代码的情况下,使用新的接口。
- 解耦:适配器模式可以将客户端与具体实现解耦,使得代码更加灵活和可维护。
适配器模式的缺点
- 增加系统复杂性:引入适配器模式会增加系统的复杂性,因为需要额外的适配器类。
- 性能开销:适配器模式可能会增加系统的性能开销,特别是在适配器中添加了额外的处理逻辑时。
适配器模式的应用场景
- 接口不兼容:想使用一个已经存在的类,但它的接口不符合需求时,可以使用适配器模式。
- 复用现有类:想复用一些现有的类,但这些类的接口与目标接口不兼容时,可以使用适配器模式。
外观模式(Facade)
为子系统中的一组接口提供一个一致的界面。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式的角色和职责
- Façade(外观角色):为调用方, 定义简单的调用接口。
- SubSystem(子系统角色):功能提供者。指提供功能的类群(模块或子系统)。
package main
import "fmt"
// 子系统A
type SubsystemA struct{}
func (s *SubsystemA) OperationA() {
fmt.Println("SubsystemA: OperationA")
}
// 子系统B
type SubsystemB struct{}
func (s *SubsystemB) OperationB() {
fmt.Println("SubsystemB: OperationB")
}
// 子系统C
type SubsystemC struct{}
func (s *SubsystemC) OperationC() {
fmt.Println("SubsystemC: OperationC")
}
// Facade 是外观类,提供了一个简单的接口来访问子系统
type Facade struct {
subsystemA *SubsystemA
subsystemB *SubsystemB
subsystemC *SubsystemC
}
func NewFacade() *Facade {
return &Facade{
subsystemA: &SubsystemA{},
subsystemB: &SubsystemB{},
subsystemC: &SubsystemC{},
}
}
func (f *Facade) Operation() {
fmt.Println("Facade: Operation")
f.subsystemA.OperationA()
f.subsystemB.OperationB()
f.subsystemC.OperationC()
}
func main() {
facade := NewFacade()
facade.Operation()
}
外观模式的优点
- 简化接口:外观模式提供了一个简单的接口,简化了客户端与复杂系统之间的交互。
- 松散耦合:外观模式将客户端与子系统解耦,客户端不需要了解子系统的内部细节。
- 更好的分层:外观模式可以帮助分层系统中的各个子系统之间的交互。
外观模式的缺点
- 不完全封装:外观模式并不能完全封装子系统,客户端仍然可以直接访问子系统的类。
外观模式的适用场景
- 简化复杂系统的使用:外观模式可以为复杂系统提供一个简单的接口,使得客户端更容易使用。
- 分层系统:在分层系统中,外观模式可以帮助分离各个子系统,使得它们之间的交互更加清晰。
桥接模式(Bridge)
将抽象部分与它的实现部分分离,使它们都可以独立地变化。桥接模式的核心思想是将继承关系转化为组合关系,从而减少类之间的耦合度。
package main
import "fmt"
// Implementor 是实现部分的接口
type Implementor interface {
OperationImpl() string
}
// ConcreteImplementorA 是实现部分的具体实现A
type ConcreteImplementorA struct{}
func (c *ConcreteImplementorA) OperationImpl() string {
return "ConcreteImplementorA: OperationImpl"
}
// ConcreteImplementorB 是实现部分的具体实现B
type ConcreteImplementorB struct{}
func (c *ConcreteImplementorB) OperationImpl() string {
return "ConcreteImplementorB: OperationImpl"
}
// Abstraction 是抽象部分的接口
type Abstraction interface {
Operation() string
}
// RefinedAbstraction 是抽象部分的具体实现
type RefinedAbstraction struct {
implementor Implementor
}
func (r *RefinedAbstraction) Operation() string {
return r.implementor.OperationImpl()
}
func main() {
implementorA := &ConcreteImplementorA{}
implementorB := &ConcreteImplementorB{}
abstractionA := &RefinedAbstraction{implementor: implementorA}
abstractionB := &RefinedAbstraction{implementor: implementorB}
fmt.Println(abstractionA.Operation())
fmt.Println(abstractionB.Operation())
}
桥接模式的优点
- 分离抽象和实现:桥接模式将抽象部分与实现部分分离,使它们可以独立变化。
- 提高扩展性:由于抽象和实现是独立的,扩展其中一个不会影响另一个。
- 减少类的数量:通过组合而不是继承来扩展功能,可以减少子类的数量。
桥接模式的缺点
- 增加复杂性:引入桥接模式会增加系统的复杂性,因为需要额外的抽象层。
桥接模式的适用场景
- 希望抽象和实现可以独立变化:当一个类存在多个维度的变化时,可以使用桥接模式将这些维度分离开来。
- 避免类爆炸:当系统中存在多个继承层次时,使用桥接模式可以减少子类的数量。
组合模式(Composite)
将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端可以统一地处理单个对象和组合对象。
package main
import "fmt"
// Component 是组合模式中的组件接口
type Component interface {
Operation()
Add(Component)
Remove(Component)
GetChild(int) Component
}
// Leaf 是组合模式中的叶子节点
type Leaf struct {
name string
}
func (l *Leaf) Operation() {
fmt.Println("Leaf", l.name, "operation")
}
func (l *Leaf) Add(c Component) {
fmt.Println("Cannot add to a leaf")
}
func (l *Leaf) Remove(c Component) {
fmt.Println("Cannot remove from a leaf")
}
func (l *Leaf) GetChild(i int) Component {
fmt.Println("Cannot get child from a leaf")
return nil
}
// Composite 是组合模式中的组合节点
type Composite struct {
children []Component
name string
}
func (c *Composite) Operation() {
fmt.Println("Composite", c.name, "operation")
for _, child := range c.children {
child.Operation()
}
}
func (c *Composite) Add(component Component) {
c.children = append(c.children, component)
}
func (c *Composite) Remove(component Component) {
for i, child := range c.children {
if child == component {
c.children = append(c.children[:i], c.children[i+1:]...)
break
}
}
}
func (c *Composite) GetChild(i int) Component {
if i >= 0 && i < len(c.children) {
return c.children[i]
}
return nil
}
func main() {
leaf1 := &Leaf{name: "Leaf1"}
leaf2 := &Leaf{name: "Leaf2"}
leaf3 := &Leaf{name: "Leaf3"}
composite1 := &Composite{name: "Composite1"}
composite2 := &Composite{name: "Composite2"}
composite1.Add(leaf1)
composite1.Add(leaf2)
composite2.Add(leaf3)
composite2.Add(composite1)
composite2.Operation()
}
组合模式的优点
- 简化客户端代码:客户端可以一致地使用组合结构和单个对象,无需关心它们的区别。
- 更容易扩展:可以很容易地增加新的组件类型,客户端代码不需要做任何修改。
- 更灵活的结构:可以动态地创建复杂的树形结构,灵活地添加或删除组件。
组合模式的缺点
- 可能导致过度设计:如果系统的层次结构过于简单,使用组合模式可能会显得过于复杂。
- 难以限制组件类型:在组合模式中,难以对叶子节点和组合节点进行类型上的限制。
组合模式的适用场景
- 表示部分-整体层次结构:需要表示对象的部分-整体层次结构。
- 统一处理单个对象和组合对象:希望客户端可以一致地处理单个对象和组合对象。
享元模式(Flyweight)
通过共享大量细粒度对象来减少内存使用和提高性能。享元模式的核心思想是将对象的状态分为内部状态和外部状态,内部状态是可以共享的,而外部状态是可以变化的。通过共享内部状态,可以减少内存的使用。享元模式从对象中剥离出不发生改变且多个实例需要的重复数据,独立出一个享元,使多个对象共享,从而节省内存以及减少对象数量。
package main
import (
"fmt"
)
// Flyweight 是享元接口,定义了一个操作方法
type Flyweight interface {
Operation(extrinsicState string)
}
// ConcreteFlyweight 是具体的享元类,实现了 Flyweight 接口
type ConcreteFlyweight struct {
intrinsicState string
}
func (f *ConcreteFlyweight) Operation(extrinsicState string) {
fmt.Printf("Intrinsic State: %s, Extrinsic State: %s\n", f.intrinsicState, extrinsicState)
}
// FlyweightFactory 是享元工厂类,负责创建和管理享元对象
type FlyweightFactory struct {
flyweights map[string]Flyweight
}
func NewFlyweightFactory() *FlyweightFactory {
return &FlyweightFactory{
flyweights: make(map[string]Flyweight),
}
}
func (f *FlyweightFactory) GetFlyweight(key string) Flyweight {
if flyweight, exists := f.flyweights[key]; exists {
return flyweight
}
flyweight := &ConcreteFlyweight{intrinsicState: key}
f.flyweights[key] = flyweight
return flyweight
}
func main() {
factory := NewFlyweightFactory()
flyweight1 := factory.GetFlyweight("state1")
flyweight1.Operation("extrinsic1")
flyweight2 := factory.GetFlyweight("state2")
flyweight2.Operation("extrinsic2")
flyweight3 := factory.GetFlyweight("state1")
flyweight3.Operation("extrinsic3")
fmt.Printf("Flyweight1 and Flyweight3 are the same instance: %v\n", flyweight1 == flyweight3)
}
享元模式的优点
- 减少内存使用:通过共享内部状态,可以显著减少内存的使用。
- 提高性能:减少了创建对象的开销,提高了系统的性能。
享元模式的缺点
- 复杂性增加:需要分离内部状态和外部状态,增加了系统的复杂性。
- 管理共享对象:需要额外的代码来管理共享对象。
享元模式的适用场景
- 大量相似对象:系统中有大量相似的对象,这些对象的大部分状态是可以共享的。
- 内存使用优化:需要优化内存使用,减少内存开销。
行为型模式
模版方法模式(Template-Method)
模版方法模式使用继承机制,把通用步骤和通用方法放到父类中,把具体实现延迟到子类中实现。使得实现符合开闭原则。
模版方法模式的角色和职责
- AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。
- ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。
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()
}
模板方法模式的优点
- 代码复用:将通用的代码放在抽象类中,避免重复代码。
- 灵活性:允许子类在不改变算法结构的情况下,重新定义算法中的某些步骤。
- 控制反转:通过模板方法模式,父类控制算法的执行流程,而子类只需实现具体的步骤。
- 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
模板方法模式的缺点
- 增加复杂性:引入模板方法模式会增加系统的复杂性,因为需要额外的抽象类和子类。
- 难以维护:如果算法步骤过多,可能会导致代码难以维护。
模版方法模式的适用场景
- 算法的多个步骤:一个操作的算法可以分为多个步骤,并且这些步骤在不同的实现中可能会有所不同。
- 代码复用:需要在多个子类中复用通用的代码。
命令模式(Command)
命令模式本质是把某个对象的方法调用封装到对象中,方便传递、存储、调用。
命令模式的角色和职责
- Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
- ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
- Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
- Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
package main
import "fmt"
// Command 是命令接口,定义了执行命令的方法
type Command interface {
Execute()
}
// Light 是接收者类,具有具体的操作
type Light struct{}
func (l *Light) On() {
fmt.Println("Light is On")
}
func (l *Light) Off() {
fmt.Println("Light is Off")
}
// LightOnCommand 是具体的命令类,实现了 Command 接口
type LightOnCommand struct {
light *Light
}
func (c *LightOnCommand) Execute() {
c.light.On()
}
// LightOffCommand 是具体的命令类,实现了 Command 接口
type LightOffCommand struct {
light *Light
}
func (c *LightOffCommand) Execute() {
c.light.Off()
}
// RemoteControl 是调用者类,持有命令对象
type RemoteControl struct {
command Command
}
func (r *RemoteControl) SetCommand(command Command) {
r.command = command
}
func (r *RemoteControl) PressButton() {
r.command.Execute()
}
func main() {
light := &Light{}
lightOnCommand := &LightOnCommand{light: light}
lightOffCommand := &LightOffCommand{light: light}
remote := &RemoteControl{}
// 打开灯
remote.SetCommand(lightOnCommand)
remote.PressButton()
// 关闭灯
remote.SetCommand(lightOffCommand)
remote.PressButton()
}
命令模式的优点
- 解耦请求的发送者和接收者:发送者和接收者通过命令对象进行通信,彼此之间不需要直接引用。
- 支持撤销和重做操作:可以很容易地实现命令的撤销和重做功能。
- 支持日志记录:可以记录命令的执行历史,从而支持命令的重放。
- 支持组合命令:可以将多个命令组合成一个复合命令,从而简化复杂操作的实现。
命令模式的缺点
- 增加系统复杂性:引入命令模式会增加系统的复杂性,因为需要定义大量的命令类。
- 命令对象的管理:需要管理命令对象的生命周期,可能会增加内存开销。
命令模式的适用场景
- 需要对请求排队或记录请求日志:可以记录命令的执行历史,从而支持命令的重放。
- 需要支持撤销和重做操作:可以很容易地实现命令的撤销和重做功能。
- 需要将一组操作组合成一个操作:可以将多个命令组合成一个复合命令,从而简化复杂操作的实现。
策略模式(Strategy)
定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。策略模式使得算法可以独立于使用它的客户端而变化。
策略模式的角色和职责
- Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
- Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
- ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。
package main
import "fmt"
// Strategy 是定义了算法接口
type Strategy interface {
Execute(a, b int) int
}
// ConcreteStrategyAdd 是具体的加法策略
type ConcreteStrategyAdd struct{}
func (s *ConcreteStrategyAdd) Execute(a, b int) int {
return a + b
}
// ConcreteStrategySubtract 是具体的减法策略
type ConcreteStrategySubtract struct{}
func (s *ConcreteStrategySubtract) Execute(a, b int) int {
return a - b
}
// ConcreteStrategyMultiply 是具体的乘法策略
type ConcreteStrategyMultiply struct{}
func (s *ConcreteStrategyMultiply) Execute(a, b int) int {
return a * b
}
// Context 是上下文类,使用策略来执行算法
type Context struct {
strategy Strategy
}
func (c *Context) SetStrategy(strategy Strategy) {
c.strategy = strategy
}
func (c *Context) ExecuteStrategy(a, b int) int {
return c.strategy.Execute(a, b)
}
func main() {
context := &Context{}
// 使用加法策略
context.SetStrategy(&ConcreteStrategyAdd{})
fmt.Println("10 + 5 =", context.ExecuteStrategy(10, 5))
// 使用减法策略
context.SetStrategy(&ConcreteStrategySubtract{})
fmt.Println("10 - 5 =", context.ExecuteStrategy(10, 5))
// 使用乘法策略
context.SetStrategy(&ConcreteStrategyMultiply{})
fmt.Println("10 * 5 =", context.ExecuteStrategy(10, 5))
}
策略模式的优点
- 算法可以自由切换:不同的算法可以独立于客户端进行切换。
- 避免使用多重条件判断:通过使用策略模式,可以避免在客户端代码中使用多重条件判断来选择算法。
- 扩展性好:增加新的算法时,只需要添加新的策略类,而不需要修改现有的代码。
策略模式的缺点
- 增加对象数量:每个策略都是一个独立的类,会增加系统中类的数量。
- 客户端必须了解不同的策略:客户端必须知道所有的策略,并且自行选择合适的策略。
策略模式的适用场景
- 多种算法的场景:需要在多种算法中进行选择的场景。
- 算法需要自由切换的场景:算法需要在运行时根据不同的条件进行切换的场景。
- 避免多重条件判断的场景:需要避免在客户端代码中使用多重条件判断来选择算法的场景。
观察者模式(Observer)
定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象的状态发生变化时,所有依赖于它的观察者对象都会得到通知并自动更新。
观察者模式角色和职责
- Subject(被观察者或目标,抽象主题):被观察的对象。当需要被观察的状态发生变化时,需要通知队列中所有观察者对象。Subject需要维持(添加,删除,通知)一个观察者对象的队列列表。
- ConcreteSubject(具体被观察者或目标,具体主题):被观察者的具体实现。包含一些基本的属性状态及其他操作。
- Observer(观察者):接口或抽象类。当Subject的状态发生变化时,Observer对象将通过一个callback函数得到通知。
- ConcreteObserver(具体观察者):观察者的具体实现。得到通知后将完成一些具体的业务逻辑处理。
package main
import "fmt"
// Observer 是观察者接口,定义了 Update 方法
type Observer interface {
Update(message string)
}
// Subject 是主题接口,定义了注册、注销和通知观察者的方法
type Subject interface {
RegisterObserver(observer Observer)
RemoveObserver(observer Observer)
NotifyObservers()
}
// ConcreteSubject 是具体的主题类,维护了一个观察者列表
type ConcreteSubject struct {
observers []Observer
message string
}
func (s *ConcreteSubject) RegisterObserver(observer Observer) {
s.observers = append(s.observers, observer)
}
func (s *ConcreteSubject) RemoveObserver(observer Observer) {
for i, o := range s.observers {
if o == observer {
s.observers = append(s.observers[:i], s.observers[i+1:]...)
break
}
}
}
func (s *ConcreteSubject) NotifyObservers() {
for _, observer := range s.observers {
observer.Update(s.message)
}
}
func (s *ConcreteSubject) SetMessage(message string) {
s.message = message
s.NotifyObservers()
}
// ConcreteObserver 是具体的观察者类,实现了 Observer 接口
type ConcreteObserver struct {
name string
}
func (o *ConcreteObserver) Update(message string) {
fmt.Printf("%s received message: %s\n", o.name, message)
}
func main() {
subject := &ConcreteSubject{}
observer1 := &ConcreteObserver{name: "Observer 1"}
observer2 := &ConcreteObserver{name: "Observer 2"}
subject.RegisterObserver(observer1)
subject.RegisterObserver(observer2)
subject.SetMessage("Hello, Observers!")
subject.RemoveObserver(observer1)
subject.SetMessage("Hello, Observer 2!")
}
观察者模式的优点
- 解耦:观察者模式将观察者和被观察者解耦,使得它们可以独立变化。
- 灵活性:可以在运行时动态地添加或删除观察者。
- 广播通信:主题对象会自动通知所有的观察者,无需单独通知每个观察者。
观察者模式的缺点
- 性能开销:如果观察者很多,通知所有观察者可能会有性能开销。
- 复杂性增加:引入观察者模式会增加系统的复杂性,特别是在观察者和被观察者之间的依赖关系较为复杂时。
- 通知顺序不确定:观察者的通知顺序可能不确定,可能会导致一些难以调试的问题。
观察者模式的适用场景
- 事件处理系统:需要在某个事件发生时通知多个对象。
- 数据变化通知:需要在数据变化时通知多个依赖于数据的对象。
- 订阅-发布系统:需要实现订阅-发布机制的系统。
职责链模式(Chain of Responsibility)
允许多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合。这些对象通过形成一条链来传递请求,直到有一个对象处理它为止。
package main
import "fmt"
// Handler 是处理请求的接口
type Handler interface {
SetNext(handler Handler)
Handle(request string)
}
// BaseHandler 是 Handler 接口的基础实现
type BaseHandler struct {
next Handler
}
func (h *BaseHandler) SetNext(handler Handler) {
h.next = handler
}
func (h *BaseHandler) Handle(request string) {
if h.next != nil {
h.next.Handle(request)
}
}
// ConcreteHandlerA 是具体的处理者A
type ConcreteHandlerA struct {
BaseHandler
}
func (h *ConcreteHandlerA) Handle(request string) {
if request == "A" {
fmt.Println("ConcreteHandlerA handled the request")
} else {
h.BaseHandler.Handle(request)
}
}
// ConcreteHandlerB 是具体的处理者B
type ConcreteHandlerB struct {
BaseHandler
}
func (h *ConcreteHandlerB) Handle(request string) {
if request == "B" {
fmt.Println("ConcreteHandlerB handled the request")
} else {
h.BaseHandler.Handle(request)
}
}
func main() {
handlerA := &ConcreteHandlerA{}
handlerB := &ConcreteHandlerB{}
handlerA.SetNext(handlerB)
requests := []string{"A", "B", "C"}
for _, request := range requests {
handlerA.Handle(request)
}
}
职责链模式的优点
- 降低耦合度:请求的发送者和接收者解耦,发送者不需要知道具体的接收者是谁。
- 动态组合职责:可以动态地添加或删除处理请求的对象,灵活性高。
- 职责分担:每个对象只需处理自己负责的部分,其余的交给下一个对象处理。
职责链模式的缺点
- 性能问题:如果链条过长,可能会导致性能问题,因为请求需要经过多个对象传递。
- 调试困难:由于请求的处理过程是动态的,调试和跟踪请求的处理过程可能比较困难。
职责链模式的适用场景
- 多个对象可以处理同一个请求:系统中有多个对象可以处理同一个请求,但具体由哪个对象处理在运行时动态决定。
- 动态指定处理对象:需要在运行时动态地指定处理请求的对象。
- 职责分离:需要将不同的职责分离到不同的对象中。
中介者模式(Mediator)
定义了一个中介者对象来封装一组对象之间的交互。中介者模式通过使对象之间不直接相互引用,从而使其耦合松散,并可以独立地改变它们之间的交互。
package main
import "fmt"
// Mediator 是中介者接口,定义了对象之间交互的方法
type Mediator interface {
Send(message string, colleague Colleague)
}
// Colleague 是同事类接口,定义了与中介者交互的方法
type Colleague interface {
SetMediator(mediator Mediator)
Send(message string)
Receive(message string)
}
// ConcreteMediator 是具体的中介者类,实现了 Mediator 接口
type ConcreteMediator struct {
colleague1 *ConcreteColleague1
colleague2 *ConcreteColleague2
}
func (m *ConcreteMediator) Send(message string, colleague Colleague) {
if colleague == m.colleague1 {
m.colleague2.Receive(message)
} else {
m.colleague1.Receive(message)
}
}
// ConcreteColleague1 是具体的同事类1,实现了 Colleague 接口
type ConcreteColleague1 struct {
mediator Mediator
}
func (c *ConcreteColleague1) SetMediator(mediator Mediator) {
c.mediator = mediator
}
func (c *ConcreteColleague1) Send(message string) {
fmt.Println("Colleague1 sends message:", message)
c.mediator.Send(message, c)
}
func (c *ConcreteColleague1) Receive(message string) {
fmt.Println("Colleague1 receives message:", message)
}
// ConcreteColleague2 是具体的同事类2,实现了 Colleague 接口
type ConcreteColleague2 struct {
mediator Mediator
}
func (c *ConcreteColleague2) SetMediator(mediator Mediator) {
c.mediator = mediator
}
func (c *ConcreteColleague2) Send(message string) {
fmt.Println("Colleague2 sends message:", message)
c.mediator.Send(message, c)
}
func (c *ConcreteColleague2) Receive(message string) {
fmt.Println("Colleague2 receives message:", message)
}
func main() {
mediator := &ConcreteMediator{}
colleague1 := &ConcreteColleague1{}
colleague2 := &ConcreteColleague2{}
colleague1.SetMediator(mediator)
colleague2.SetMediator(mediator)
mediator.colleague1 = colleague1
mediator.colleague2 = colleague2
colleague1.Send("Hello, Colleague2!")
colleague2.Send("Hi, Colleague1!")
}
中介者模式的优点
- 降低耦合:中介者模式通过引入中介者对象,使得对象之间的耦合度降低,便于独立地修改和扩展各个对象。
- 集中控制:中介者模式将对象之间的交互逻辑集中到中介者对象中,便于管理和维护。
- 简化对象协议:对象之间不再需要直接交互,只需与中介者交互,简化了对象之间的协议。
中介者模式的缺点
- 中介者复杂性:随着系统的复杂性增加,中介者对象本身可能会变得复杂,难以维护。
- 性能问题:所有的交互都通过中介者进行,可能会带来性能上的开销。
中介者模式的适用场景
- 对象之间存在复杂的交互:对象之间的交互复杂且多样,通过中介者模式可以简化对象之间的交互。
- 需要解耦对象之间的依赖关系:希望通过引入中介者对象来降低对象之间的耦合度。
备忘录模式(Memento)
允许在不破坏封装的前提下捕获和恢复对象的内部状态。备忘录模式通过保存对象的状态,使得对象可以在需要时恢复到之前的状态。
package main
import "fmt"
// Memento 是一个空接口,用于表示备忘录对象
type Memento interface{}
// Game 是需要保存和恢复状态的对象
type Game struct {
hp, mp int
}
// gameMemento 是一个私有结构体,用于保存 Game 的状态
type gameMemento struct {
hp, mp int
}
// Play 方法用于改变游戏的状态
func (g *Game) Play(hpDelta, mpDelta int) {
g.hp += hpDelta
g.mp += mpDelta
}
// Save 方法用于创建一个 gameMemento 对象,保存当前的 hp 和 mp
func (g *Game) Save() Memento {
return &gameMemento{
hp: g.hp,
mp: g.mp,
}
}
// Load 方法用于从一个 Memento 对象中恢复 hp 和 mp
func (g *Game) Load(m Memento) {
if gm, ok := m.(*gameMemento); ok {
g.hp = gm.hp
g.mp = gm.mp
}
}
// Status 方法用于打印当前的 hp 和 mp
func (g *Game) Status() {
fmt.Printf("Current HP: %d, MP: %d\n", g.hp, g.mp)
}
func main() {
// 初始化游戏对象
game := &Game{hp: 100, mp: 50}
game.Status()
// 改变游戏状态
game.Play(-10, 20)
game.Status()
// 保存当前状态
savedState := game.Save()
// 再次改变游戏状态
game.Play(-20, -30)
game.Status()
// 恢复之前保存的状态
game.Load(savedState)
game.Status()
}
备忘录模式的优点
- 状态恢复:可以在需要时恢复对象的状态,而不需要暴露对象的内部结构。
- 封装性:备忘录模式保持了对象的封装性,外部对象无法访问对象的内部状态。
- 简化撤销操作:通过保存对象的状态,可以简化撤销操作的实现。
备忘录模式的缺点
- 内存开销:保存对象的状态可能会占用较多的内存,特别是当对象的状态较大时。
- 实现复杂性:实现备忘录模式可能会增加系统的复杂性,特别是在需要保存多个对象的状态时。
备忘录模式的适用场景
- 需要保存和恢复对象状态:需要在某个时刻保存对象的状态,并在需要时恢复。
- 实现撤销操作:需要实现撤销操作,通过保存对象的状态,可以轻松实现撤销功能。
状态模式(State)
允许对象在内部状态改变时改变其行为。状态模式将状态的行为封装在独立的状态类中,并将状态的转换逻辑委托给这些状态类,从而使得对象的行为可以随着状态的变化而变化。
package main
import "fmt"
// State 是状态接口,定义了所有具体状态类的共同接口
type State interface {
Handle(context *Context)
}
// ConcreteStateA 是具体状态类A
type ConcreteStateA struct{}
func (s *ConcreteStateA) Handle(context *Context) {
fmt.Println("State A handling request and changing state to B")
context.SetState(&ConcreteStateB{})
}
// ConcreteStateB 是具体状态类B
type ConcreteStateB struct{}
func (s *ConcreteStateB) Handle(context *Context) {
fmt.Println("State B handling request and changing state to A")
context.SetState(&ConcreteStateA{})
}
// Context 是上下文类,维护一个State实例
type Context struct {
state State
}
func (c *Context) SetState(state State) {
c.state = state
}
func (c *Context) Request() {
c.state.Handle(c)
}
func main() {
context := &Context{state: &ConcreteStateA{}}
context.Request() // Output: State A handling request and changing state to B
context.Request() // Output: State B handling request and changing state to A
context.Request() // Output: State A handling request and changing state to B
context.Request() // Output: State B handling request and changing state to A
}
状态模式的优点
- 状态切换明确:将状态的行为封装在独立的状态类中,使得状态切换更加明确和易于管理。
- 简化复杂状态逻辑:通过将状态相关的行为分散到不同的状态类中,简化了复杂的状态逻辑。
- 增加新的状态容易:增加新的状态只需要添加新的状态类,不需要修改现有的状态类和上下文类。
状态模式的缺点
- 增加类的数量:状态模式会增加类的数量,因为每个状态都需要一个独立的类。
- 状态切换逻辑分散:状态切换的逻辑分散在各个状态类中,可能会导致代码难以理解和维护。
状态模式的适用场景
- 对象的行为依赖于其状态:对象的行为依赖于其状态,并且需要在运行时根据状态改变行为。
- 状态切换频繁:对象的状态切换频繁,并且状态之间的转换逻辑复杂。
解释器模式(Interpreter)
解释器模式定义一套语言文法,并设计该语言解释器,使用户能使用特定文法控制解释器行为。解释器模式的意义在于,它分离多种复杂功能的实现,每个功能只需关注自身的解释。对于调用者不用关心内部的解释器的工作,只需要用简单的方式组合命令就可以。
package main
import (
"fmt"
"strconv"
"strings"
)
// Expression 是定义了解释方法的接口
type Expression interface {
Interpret() int
}
// Number 是表示数字的终结符表达式
type Number struct {
value int
}
func (n *Number) Interpret() int {
return n.value
}
// Add 是表示加法的非终结符表达式
type Add struct {
left Expression
right Expression
}
func (a *Add) Interpret() int {
return a.left.Interpret() + a.right.Interpret()
}
// Subtract 是表示减法的非终结符表达式
type Subtract struct {
left Expression
right Expression
}
func (s *Subtract) Interpret() int {
return s.left.Interpret() - s.right.Interpret()
}
// Parse 是一个简单的解析器,用于解析输入的表达式
func Parse(expression string) Expression {
tokens := strings.Split(expression, " ")
stack := []Expression{}
for _, token := range tokens {
switch token {
case "+":
right := stack[len(stack)-1]
stack = stack[:len(stack)-1]
left := stack[len(stack)-1]
stack = stack[:len(stack)-1]
stack = append(stack, &Add{left: left, right: right})
case "-":
right := stack[len(stack)-1]
stack = stack[:len(stack)-1]
left := stack[len(stack)-1]
stack = stack[:len(stack)-1]
stack = append(stack, &Subtract{left: left, right: right})
default:
value, _ := strconv.Atoi(token)
stack = append(stack, &Number{value: value})
}
}
return stack[0]
}
func main() {
expression := "3 4 + 2 -"
parsedExpression := Parse(expression)
result := parsedExpression.Interpret()
fmt.Println("Result:", result) // 输出结果: 5
}
解释器模式的优点
- 易于扩展:可以很容易地扩展文法规则,只需增加新的解释器类即可。
- 灵活性:可以很容易地修改和扩展文法规则,而不需要修改现有的解释器代码。
- 易于实现:对于简单的文法规则,解释器模式的实现相对简单。
解释器模式的缺点
- 性能问题:对于复杂的文法规则,解释器模式的性能可能较差,因为每个文法规则都需要一个解释器类。
- 复杂性:对于复杂的文法规则,解释器模式的实现可能会变得非常复杂。
解释器模式的适用场景
- 简单的文法规则:适用于简单的文法规则,例如数学表达式求值、简单的脚本语言等。
- 重复出现的问题:适用于那些需要重复解决的问题,例如配置文件解析、命令解释等。
迭代器模式(Iterator)
提供一种方法顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表示。迭代器模式使用相同方式送代不同类型集合或者隐藏集合类型的具体实现,将遍历聚合对象的责任从聚合对象中分离出来,简化了聚合对象的接口和实现。
package main
import "fmt"
// Iterator 是定义了遍历方法的接口
type Iterator interface {
HasNext() bool
Next() interface{}
}
// Aggregate 是定义了创建迭代器方法的接口
type Aggregate interface {
CreateIterator() Iterator
}
// ConcreteAggregate 是具体的聚合类
type ConcreteAggregate struct {
items []interface{}
}
func (a *ConcreteAggregate) CreateIterator() Iterator {
return &ConcreteIterator{aggregate: a, currentIndex: 0}
}
// ConcreteIterator 是具体的迭代器类
type ConcreteIterator struct {
aggregate *ConcreteAggregate
currentIndex int
}
func (i *ConcreteIterator) HasNext() bool {
return i.currentIndex < len(i.aggregate.items)
}
func (i *ConcreteIterator) Next() interface{} {
if i.HasNext() {
item := i.aggregate.items[i.currentIndex]
i.currentIndex++
return item
}
return nil
}
func main() {
aggregate := &ConcreteAggregate{
items: []interface{}{"Item1", "Item2", "Item3"},
}
iterator := aggregate.CreateIterator()
for iterator.HasNext() {
item := iterator.Next()
fmt.Println(item)
}
}
迭代器模式的优点
- 简化聚合类:将遍历聚合对象的责任从聚合对象中分离出来,简化了聚合对象的接口和实现。
- 一致的遍历接口:提供了一致的遍历接口,使得不同的聚合对象可以使用相同的遍历方式。
- 灵活性:可以在不修改聚合对象的情况下,定义新的迭代器来遍历聚合对象。
迭代器模式的缺点
- 增加类的数量:引入迭代器模式会增加类的数量,因为需要额外的迭代器类。
- 性能开销:迭代器模式可能会增加系统的性能开销,特别是在遍历大型聚合对象时。
迭代器模式的适用场景
- 遍历聚合对象:需要遍历一个聚合对象中的各个元素,而不需要暴露该对象的内部表示。
- 不同的遍历方式:需要以不同的方式遍历一个聚合对象中的各个元素。
访问者模式(Visitor)
允许在不修改对象结构的情况下,增加新的操作。访问者模式将操作分离到独立的对象中,使得新的操作可以很容易地添加到对象结构中。对象只要预留访问者接口Accept则后期为对象添加功能的时候就不需要改动对象。
package main
import "fmt"
// Element 是定义了接受访问者的方法的接口
type Element interface {
Accept(visitor Visitor)
}
// File 是具体的文件类
type File struct {
name string
size int
}
func (f *File) Accept(visitor Visitor) {
visitor.VisitFile(f)
}
// Directory 是具体的目录类
type Directory struct {
name string
contents []Element
}
func (d *Directory) Accept(visitor Visitor) {
visitor.VisitDirectory(d)
}
// Visitor 是定义了访问方法的接口
type Visitor interface {
VisitFile(file *File)
VisitDirectory(directory *Directory)
}
// SizeVisitor 是具体的访问者类,用于计算总大小
type SizeVisitor struct {
totalSize int
}
func (v *SizeVisitor) VisitFile(file *File) {
v.totalSize += file.size
}
func (v *SizeVisitor) VisitDirectory(directory *Directory) {
for _, element := range directory.contents {
element.Accept(v)
}
}
func (v *SizeVisitor) TotalSize() int {
return v.totalSize
}
// NameVisitor 是具体的访问者类,用于列出所有文件的名称
type NameVisitor struct {
names []string
}
func (v *NameVisitor) VisitFile(file *File) {
v.names = append(v.names, file.name)
}
func (v *NameVisitor) VisitDirectory(directory *Directory) {
for _, element := range directory.contents {
element.Accept(v)
}
}
func (v *NameVisitor) Names() []string {
return v.names
}
func main() {
// 创建文件和目录
file1 := &File{name: "file1.txt", size: 100}
file2 := &File{name: "file2.txt", size: 200}
dir := &Directory{name: "dir", contents: []Element{file1, file2}}
// 使用 SizeVisitor 计算总大小
sizeVisitor := &SizeVisitor{}
dir.Accept(sizeVisitor)
fmt.Println("Total size:", sizeVisitor.TotalSize())
// 使用 NameVisitor 列出所有文件的名称
nameVisitor := &NameVisitor{}
dir.Accept(nameVisitor)
fmt.Println("File names:", nameVisitor.Names())
}
访问者模式的优点
- 增加新的操作:可以在不修改对象结构的情况下增加新的操作。
- 分离关注点:将操作与对象结构分离,使得代码更清晰、更易维护。
- 扩展性好:可以很容易地增加新的访问者来实现新的操作。
访问者模式的缺点
- 违反单一职责原则:访问者模式将不同的操作集中到访问者中,可能会导致访问者类职责过重。
- 对象结构的变化:如果对象结构发生变化,需要修改所有的访问者类。
- 双重分派:访问者模式需要双重分派(double dispatch),这在某些编程语言中可能不太直观。
访问者模式的适用场景
- 对象结构稳定:对象结构相对稳定,但需要在对象结构上定义新的操作。
- 需要对对象结构中的元素进行多种不同且不相关的操作:可以使用访问者模式将这些操作分离到独立的访问者中。
参考资料
Easy搞定Golang设计模式
单例模式及为何构造函数私有化
版权声明:本博客所有文章除特别声明外,均采用 CC BY 4.0许可协议,转载请注明出处
本文链接:https://blog.redamancy.tech/technique/46