摘要:设计模式(Design Pattern)是一套被重复应用、少数人通晓的、通过分类编目标、代码设计教训的总结,应用设计模式是为了可重用代码、让代码更容易被别人了解并且保障代码可靠性。
本文分享自华为云社区《快来,这里有 23 种设计模式的 Go 语言实现》,原文作者:元闰子。
前言
从 1995 年 GoF 提出 23 种设计模式到当初,25 年过来了,设计模式仍旧是软件畛域的热门话题。在当下,如果你不会一点设计模式,都不好意思说本人是一个合格的程序员。设计模式通常被定义为:
设计模式(Design Pattern)是一套被重复应用、少数人通晓的、通过分类编目标、代码设计教训的总结,应用设计模式是为了可重用代码、让代码更容易被别人了解并且保障代码可靠性。
从定义上看,设计模式其实是一种教训的总结,是针对特定问题的简洁而优雅的解决方案 。既然是经验总结,那么学习设计模式最间接的益处就在于能够站在伟人的肩膀上解决软件开发过程中的一些特定问题。然而,学习设计模式的最高境界是习得其中解决问题所用到的思维,当你把它们的实质思维吃透了,也就能做到 即便曾经忘掉某个设计模式的名称和构造,也能在解决特定问题时信手拈来。
好的货色有人吹捧,当然也会招黑。设计模式被鞭挞次要因为以下两点:
1、设计模式会减少代码量,把程序逻辑变得复杂。这一点是不可避免的,然而咱们并不能仅仅只思考开发阶段的老本。最简略的程序当然是一个函数从头写到尾,然而这样前期的保护老本会变得十分大;而设计模式尽管减少了一点开发成本,然而能让人们写出可复用、可维护性高的程序。援用《软件设计的哲学》里的概念,前者就是战术编程,后者就是策略编程,咱们应该对战术编程 Say No!
2、滥用设计模式。这是初学者最容易犯的谬误,当学到一个模式时,巴不得在所有的代码都用上,从而在不该应用模式的中央刻意地应用了模式,导致了程序变得异样简单。其实每个设计模式都有几个要害因素:实用场景、解决办法、优缺点。模式并不是万能药,它只有在特定的问题上能力显现出成果。所以,在应用一个模式前,先问问本人,以后的这个场景实用这个模式吗?
《设计模式》一书的副标题是“可复用面向对象软件的根底”,但并不意味着只有面向对象语言能力应用设计模式。模式只是一种解决特定问题的思维,跟语言无关。就像 Go 语言一样,它并非是像 C ++ 和 Java 一样的面向对象语言,然而设计模式同样实用。本系列文章将应用 Go 语言来实现 GoF 提出的 23 种设计模式,依照创立型模式(Creational Pattern)、结构型模式(Structural Pattern)和行为型模式(Behavioral Pattern)三种类别进行组织,文本次要介绍其中的创立型模式。
单例模式(Singleton Pattern)
简述
单例模式算是 23 中设计模式里最简略的一个了,它次要用于 保障一个类仅有一个实例,并提供一个拜访它的全局拜访点。
在程序设计中,有一些对象通常咱们只须要一个共享的实例,比方线程池、全局缓存、对象池等,这种场景下就适宜应用单例模式。
然而,并非所有全局惟一的场景都适宜应用单例模式。比方,思考须要统计一个 API 调用的状况,有两个指标,胜利调用次数和失败调用次数。这两个指标都是全局惟一的,所以有人可能会将其建模成两个单例 SuccessApiMetric 和 FailApiMetric。依照这个思路,随着指标数量的增多,你会发现代码里类的定义会越来越多,也越来越臃肿。这也是单例模式最常见的误用场景,更好的办法是将两个指标设计成一个对象 ApiMetric 下的两个实例 ApiMetic success 和 ApiMetic fail。
如何判断一个对象是否应该被建模成单例?
通常,被建模成单例的对象都有“中心点”的含意,比方线程池就是治理所有线程的核心。所以,在判断一个对象是否适宜单例模式时,先思考下,这个对象是一个中心点吗?
Go 实现
在对某个对象实现单例模式时,有两个点必须要留神:(1)限度调用者间接实例化该对象;(2)为该对象的单例提供一个全局惟一的拜访办法。
对于 C ++/Java 而言,只需把类的构造函数设计成公有的,并提供一个 static 办法去拜访该类点惟一实例即可。但对于 Go 语言来说,即没有构造函数的概念,也没有 static 办法,所以须要另寻前途。
咱们能够利用 Go 语言 package 的拜访规定来实现,将单例构造体设计成首字母小写,就能限定其拜访范畴只在以后 package 下,模仿了 C ++/Java 中的公有构造函数;再在以后 package 下实现一个首字母大写的拜访函数,就相当于 static 办法的作用了。
在理论开发中,咱们常常会遇到须要频繁创立和销毁的对象。频繁的创立和销毁一则耗费 CPU,二则内存的利用率也不高,通常咱们都会应用对象池技术来进行优化。思考咱们须要实现一个音讯对象池,因为是全局的中心点,治理所有的 Message 实例,所以将其实现成单例,实现代码如下:
package msgpool
...
// 音讯池
type messagePool struct {pool *sync.Pool}
// 音讯池单例
var msgPool = &messagePool{
// 如果音讯池里没有音讯,则新建一个 Count 值为 0 的 Message 实例
pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
}
// 拜访音讯池单例的惟一办法
func Instance() *messagePool {return msgPool}
// 往音讯池里增加音讯
func (m *messagePool) AddMsg(msg *Message) {m.pool.Put(msg)
}
// 从音讯池里获取音讯
func (m *messagePool) GetMsg() *Message {return m.pool.Get().(*Message)
}
...
测试代码如下:
package test
...
func TestMessagePool(t *testing.T) {msg0 := msgpool.Instance().GetMsg()
if msg0.Count != 0 {t.Errorf("expect msg count %d, but actual %d.", 0, msg0.Count)
}
msg0.Count = 1
msgpool.Instance().AddMsg(msg0)
msg1 := msgpool.Instance().GetMsg()
if msg1.Count != 1 {t.Errorf("expect msg count %d, but actual %d.", 1, msg1.Count)
}
}
// 运行后果
=== RUN TestMessagePool
--- PASS: TestMessagePool (0.00s)
PASS
以上的单例模式就是典型的“饿汉模式”,实例在零碎加载的时候就曾经实现了初始化。对应地,还有一种“懒汉模式”,只有等到对象被应用的时候,才会去初始化它,从而肯定水平上节俭了内存。家喻户晓,“懒汉模式”会带来线程平安问题,能够通过一般加锁,或者更高效的双重测验锁来优化。对于“懒汉模式”,Go 语言有一个更优雅的实现形式,那就是利用 sync.Once,它有一个 Do 办法,其入参是一个办法,Go 语言会保障仅仅只调用一次该办法。
// 单例模式的“懒汉模式”实现
package msgpool
...
var once = &sync.Once{}
// 音讯池单例,在首次调用时初始化
var msgPool *messagePool
// 全局惟一获取音讯池 pool 到办法
func Instance() *messagePool {
// 在匿名函数中实现初始化逻辑,Go 语言保障只会调用一次
once.Do(func() {
msgPool = &messagePool{
// 如果音讯池里没有音讯,则新建一个 Count 值为 0 的 Message 实例
pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
}
})
return msgPool
}
...
建造者模式(Builder Pattern)
简述
在程序设计中,咱们会常常遇到一些简单的对象,其中有很多成员属性,甚至嵌套着多个简单的对象。这种状况下,创立这个简单对象就会变得很繁琐。对于 C ++/Java 而言,最常见的体现就是构造函数有着长长的参数列表:
MyObject obj = new MyObject(param1, param2, param3, param4, param5, param6, ...)
而对于 Go 语言来说,最常见的体现就是多层的嵌套实例化:
obj := &MyObject{
Field1: &Field1 {
Param1: &Param1 {Val: 0,},
Param2: &Param2 {Val: 1,},
...
},
Field2: &Field2 {
Param3: &Param3 {Val: 2,},
...
},
...
}
上述的对象创立办法有两个显著的毛病:(1)对对象使用者不敌对,使用者在创建对象时须要晓得的细节太多;(2)代码可读性很差。
针对这种对象成员较多,创建对象逻辑较为繁琐的场景,就适宜应用建造者模式来进行优化。
建造者模式的作用有如下几个:
1、封装简单对象的创立过程,使对象使用者不感知简单的创立逻辑。
2、能够一步步依照程序对成员进行赋值,或者创立嵌套对象,并最终实现指标对象的创立。
3、对多个对象复用同样的对象创立逻辑。
其中,第 1 和第 2 点比拟罕用,上面对建造者模式的实现也次要是针对这两点进行示例。
Go 实现
思考如下的一个 Message 构造体,其次要有 Header 和 Body 组成:
package msg
...
type Message struct {
Header *Header
Body *Body
}
type Header struct {
SrcAddr string
SrcPort uint64
DestAddr string
DestPort uint64
Items map[string]string
}
type Body struct {Items []string
}
...
如果依照间接的对象创立形式,创立逻辑应该是这样的:
// 多层的嵌套实例化
message := msg.Message{
Header: &msg.Header{
SrcAddr: "192.168.0.1",
SrcPort: 1234,
DestAddr: "192.168.0.2",
DestPort: 8080,
Items: make(map[string]string),
},
Body: &msg.Body{Items: make([]string, 0),
},
}
// 须要晓得对象的实现细节
message.Header.Items["contents"] = "application/json"
message.Body.Items = append(message.Body.Items, "record1")
message.Body.Items = append(message.Body.Items, "record2")
尽管 Message 构造体嵌套的档次不多,然而从其创立的代码来看,的确存在对对象使用者不敌对和代码可读性差的毛病。上面咱们引入建造者模式对代码进行重构:
package msg
...
// Message 对象的 Builder 对象
type builder struct {
once *sync.Once
msg *Message
}
// 返回 Builder 对象
func Builder() *builder {
return &builder{once: &sync.Once{},
msg: &Message{Header: &Header{}, Body: &Body{}},
}
}
// 以下是对 Message 成员对构建办法
func (b *builder) WithSrcAddr(srcAddr string) *builder {
b.msg.Header.SrcAddr = srcAddr
return b
}
func (b *builder) WithSrcPort(srcPort uint64) *builder {
b.msg.Header.SrcPort = srcPort
return b
}
func (b *builder) WithDestAddr(destAddr string) *builder {
b.msg.Header.DestAddr = destAddr
return b
}
func (b *builder) WithDestPort(destPort uint64) *builder {
b.msg.Header.DestPort = destPort
return b
}
func (b *builder) WithHeaderItem(key, value string) *builder {
// 保障 map 只初始化一次
b.once.Do(func() {b.msg.Header.Items = make(map[string]string)
})
b.msg.Header.Items[key] = value
return b
}
func (b *builder) WithBodyItem(record string) *builder {b.msg.Body.Items = append(b.msg.Body.Items, record)
return b
}
// 创立 Message 对象,在最初一步调用
func (b *builder) Build() *Message {return b.msg}
测试代码如下:
package test
...
func TestMessageBuilder(t *testing.T) {
// 应用音讯建造者进行对象创立
message := msg.Builder().
WithSrcAddr("192.168.0.1").
WithSrcPort(1234).
WithDestAddr("192.168.0.2").
WithDestPort(8080).
WithHeaderItem("contents", "application/json").
WithBodyItem("record1").
WithBodyItem("record2").
Build()
if message.Header.SrcAddr != "192.168.0.1" {t.Errorf("expect src address 192.168.0.1, but actual %s.", message.Header.SrcAddr)
}
if message.Body.Items[0] != "record1" {t.Errorf("expect body item0 record1, but actual %s.", message.Body.Items[0])
}
}
// 运行后果
=== RUN TestMessageBuilder
--- PASS: TestMessageBuilder (0.00s)
PASS
从测试代码可知,应用建造者模式来进行对象创立,使用者不再须要晓得对象具体的实现细节,代码可读性也更好。
工厂办法模式(Factory Method Pattern)
简述
工厂办法模式跟上一节探讨的建造者模式相似,都是将对象创立的逻辑封装起来,为使用者提供一个简略易用的对象创立接口。两者在利用场景上稍有区别,建造者模式更罕用于须要传递多个参数来进行实例化的场景。
应用工厂办法来创建对象次要有两个益处:
1、代码可读性更好。相比于应用 C ++/Java 中的构造函数,或者 Go 中的 {} 来创建对象,工厂办法因为能够通过函数名来表白代码含意,从而具备更好的可读性。比方,应用工厂办法 productA := CreateProductA()创立一个 ProductA 对象,比间接应用 productA := ProductA{}的可读性要好。
2、与使用者代码解耦。很多状况下,对象的创立往往是一个容易变动的点,通过工厂办法来封装对象的创立过程,能够在创立逻辑变更时,防止霰弹式批改。
工厂办法模式也有两种实现形式:(1)提供一个工厂对象,通过调用工厂对象的工厂办法来创立产品对象;(2)将工厂办法集成到产品对象中(C++/Java 中对象的 static 办法,Go 中同一 package 下的函数)
Go 实现
思考有一个事件对象 Event,别离有两种无效的工夫类型 Start 和 End:
package event
...
type Type uint8
// 事件类型定义
const (
Start Type = iota
End
)
// 事件形象接口
type Event interface {EventType() Type
Content() string}
// 开始事件,实现了 Event 接口
type StartEvent struct{content string}
...
// 完结事件,实现了 Event 接口
type EndEvent struct{content string}
...
1、依照第一种实现形式,为 Event 提供一个工厂对象,具体代码如下:
package event
...
// 事件工厂对象
type Factory struct{}
// 更具事件类型创立具体事件
func (e *Factory) Create(etype Type) Event {
switch etype {
case Start:
return &StartEvent{content: "this is start event",}
case End:
return &EndEvent{content: "this is end event",}
default:
return nil
}
}
测试代码如下:
package test
...
func TestEventFactory(t *testing.T) {factory := event.Factory{}
e := factory.Create(event.Start)
if e.EventType() != event.Start {t.Errorf("expect event.Start, but actual %v.", e.EventType())
}
e = factory.Create(event.End)
if e.EventType() != event.End {t.Errorf("expect event.End, but actual %v.", e.EventType())
}
}
// 运行后果
=== RUN TestEventFactory
--- PASS: TestEventFactory (0.00s)
PASS
2、依照第二种实现形式,别离给 Start 和 End 类型的 Event 独自提供一个工厂办法,代码如下:
package event
...
// Start 类型 Event 的工厂办法
func OfStart() Event {
return &StartEvent{content: "this is start event",}
}
// End 类型 Event 的工厂办法
func OfEnd() Event {
return &EndEvent{content: "this is end event",}
}
测试代码如下:
package event
...
func TestEvent(t *testing.T) {e := event.OfStart()
if e.EventType() != event.Start {t.Errorf("expect event.Start, but actual %v.", e.EventType())
}
e = event.OfEnd()
if e.EventType() != event.End {t.Errorf("expect event.End, but actual %v.", e.EventType())
}
}
// 运行后果
=== RUN TestEvent
--- PASS: TestEvent (0.00s)
PASS
形象工厂模式(Abstract Factory Pattern)
简述
在工厂办法模式中,咱们通过一个工厂对象来创立一个产品族,具体创立哪个产品,则通过 swtich-case 的形式去判断。这也意味着该产品组上,每新增一类产品对象,都必须批改原来工厂对象的代码;而且随着产品的一直增多,工厂对象的职责也越来越重,违反了繁多职责准则。
形象工厂模式通过给工厂类新增一个形象层解决了该问题,如上图所示,FactoryA 和 FactoryB 都实现·形象工厂接口,别离用于创立 ProductA 和 ProductB。如果后续新增了 ProductC,只需新增一个 FactoryC 即可,无需批改原有的代码;因为每个工厂只负责创立一个产品,因而也遵循了繁多职责准则。
Go 实现
思考须要如下一个插件架构格调的音讯解决零碎,pipeline 是音讯解决的管道,其中蕴含了 input、filter 和 output 三个插件。咱们须要实现依据配置来创立 pipeline,加载插件过程的实现非常适合应用工厂模式,其中 input、filter 和 output 三类插件的创立应用形象工厂模式,而 pipeline 的创立则应用工厂办法模式。
各类插件和 pipeline 的接口定义如下:
package plugin
...
// 插件形象接口定义
type Plugin interface {}
// 输出插件,用于接管音讯
type Input interface {
Plugin
Receive() string}
// 过滤插件,用于解决音讯
type Filter interface {
Plugin
Process(msg string) string
}
// 输入插件,用于发送音讯
type Output interface {
Plugin
Send(msg string)
}
package pipeline
...
// 音讯管道的定义
type Pipeline struct {
input plugin.Input
filter plugin.Filter
output plugin.Output
}
// 一个音讯的解决流程为 input -> filter -> output
func (p *Pipeline) Exec() {msg := p.input.Receive()
msg = p.filter.Process(msg)
p.output.Send(msg)
}
接着,咱们定义 input、filter、output 三类插件接口的具体实现:
package plugin
...
// input 插件名称与类型的映射关系,次要用于通过反射创立 input 对象
var inputNames = make(map[string]reflect.Type)
// Hello input 插件,接管“Hello World”音讯
type HelloInput struct {}
func (h *HelloInput) Receive() string {return "Hello World"}
// 初始化 input 插件映射关系表
func init() {inputNames["hello"] = reflect.TypeOf(HelloInput{})
}
package plugin
...
// filter 插件名称与类型的映射关系,次要用于通过反射创立 filter 对象
var filterNames = make(map[string]reflect.Type)
// Upper filter 插件,将音讯全副字母转成大写
type UpperFilter struct {}
func (u *UpperFilter) Process(msg string) string {return strings.ToUpper(msg)
}
// 初始化 filter 插件映射关系表
func init() {filterNames["upper"] = reflect.TypeOf(UpperFilter{})
}
package plugin
...
// output 插件名称与类型的映射关系,次要用于通过反射创立 output 对象
var outputNames = make(map[string]reflect.Type)
// Console output 插件,将音讯输入到管制台上
type ConsoleOutput struct {}
func (c *ConsoleOutput) Send(msg string) {fmt.Println(msg)
}
// 初始化 output 插件映射关系表
func init() {outputNames["console"] = reflect.TypeOf(ConsoleOutput{})
}
而后,咱们定义插件形象工厂接口,以及对应插件的工厂实现:
package plugin
...
// 插件形象工厂接口
type Factory interface {Create(conf Config) Plugin
}
// input 插件工厂对象,实现 Factory 接口
type InputFactory struct{}
// 读取配置,通过反射机制进行对象实例化
func (i *InputFactory) Create(conf Config) Plugin {t, _ := inputNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
// filter 和 output 插件工厂实现相似
type FilterFactory struct{}
func (f *FilterFactory) Create(conf Config) Plugin {t, _ := filterNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
type OutputFactory struct{}
func (o *OutputFactory) Create(conf Config) Plugin {t, _ := outputNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
最初定义 pipeline 的工厂办法,调用 plugin.Factory 形象工厂实现 pipelien 对象的实例化:
package pipeline
...
// 保留用于创立 Plugin 的工厂实例,其中 map 的 key 为插件类型,value 为形象工厂接口
var pluginFactories = make(map[plugin.Type]plugin.Factory)
// 依据 plugin.Type 返回对应 Plugin 类型的工厂实例
func factoryOf(t plugin.Type) plugin.Factory {factory, _ := pluginFactories[t]
return factory
}
// pipeline 工厂办法,依据配置创立一个 Pipeline 实例
func Of(conf Config) *Pipeline {p := &Pipeline{}
p.input = factoryOf(plugin.InputType).Create(conf.Input).(plugin.Input)
p.filter = factoryOf(plugin.FilterType).Create(conf.Filter).(plugin.Filter)
p.output = factoryOf(plugin.OutputType).Create(conf.Output).(plugin.Output)
return p
}
// 初始化插件工厂对象
func init() {pluginFactories[plugin.InputType] = &plugin.InputFactory{}
pluginFactories[plugin.FilterType] = &plugin.FilterFactory{}
pluginFactories[plugin.OutputType] = &plugin.OutputFactory{}}
测试代码如下:
package test
...
func TestPipeline(t *testing.T) {// 其中 pipeline.DefaultConfig()的配置内容见【形象工厂模式示例图】// 音讯解决流程为 HelloInput -> UpperFilter -> ConsoleOutput
p := pipeline.Of(pipeline.DefaultConfig())
p.Exec()}
// 运行后果
=== RUN TestPipeline
HELLO WORLD
--- PASS: TestPipeline (0.00s)
PASS
原型模式(Prototype Pattern)
简述
原型模式次要解决对象复制的问题,它的外围就是 clone()办法,返回 Prototype 对象的复制品。在程序设计过程中,往往会遇到有一些场景须要大量雷同的对象,如果不应用原型模式,那么咱们可能会这样进行对象的创立:新创建一个雷同对象的实例,而后遍历原始对象的所有成员变量,并将成员变量值复制到新对象中。这种办法的毛病很显著,那就是使用者必须晓得对象的实现细节,导致代码之间的耦合。另外,对象很有可能存在除了对象自身以外不可见的变量,这种状况下该办法就行不通了。
对于这种状况,更好的办法就是应用原型模式,将复制逻辑委托给对象自身,这样,上述两个问题也都迎刃而解了。
Go 实现
还是以建造者模式一节中的 Message 作为例子,当初设计一个 Prototype 形象接口:
package prototype
...
// 原型复制形象接口
type Prototype interface {clone() Prototype
}
type Message struct {
Header *Header
Body *Body
}
func (m *Message) clone() Prototype {
msg := *m
return &msg
}
测试代码如下:
package test
...
func TestPrototype(t *testing.T) {message := msg.Builder().
WithSrcAddr("192.168.0.1").
WithSrcPort(1234).
WithDestAddr("192.168.0.2").
WithDestPort(8080).
WithHeaderItem("contents", "application/json").
WithBodyItem("record1").
WithBodyItem("record2").
Build()
// 复制一份音讯
newMessage := message.Clone().(*msg.Message)
if newMessage.Header.SrcAddr != message.Header.SrcAddr {t.Errorf("Clone Message failed.")
}
if newMessage.Body.Items[0] != message.Body.Items[0] {t.Errorf("Clone Message failed.")
}
}
// 运行后果
=== RUN TestPrototype
--- PASS: TestPrototype (0.00s)
PASS
总结
本文次要介绍了 GoF 的 23 种设计模式中的 5 种创立型模式,创立型模式的目标都是提供一个简略的接口,让对象的创立过程与使用者解耦。其中,单例模式次要用于保障一个类仅有一个实例,并提供一个拜访它的全局拜访点;建造者模式次要解决须要创建对象时须要传入多个参数,或者对初始化程序有要求的场景;工厂办法模式通过提供一个工厂对象或者工厂办法,为使用者暗藏了对象创立的细节;形象工厂模式是对工厂办法模式的优化,通过为工厂对象新增一个形象层,让工厂对象遵循繁多职责准则,也防止了霰弹式批改;原型模式则让对象复制更加简略。
点击关注,第一工夫理解华为云陈腐技术~