摘要:设计模式(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种创立型模式,创立型模式的目标都是提供一个简略的接口,让对象的创立过程与使用者解耦。其中,单例模式次要用于保障一个类仅有一个实例,并提供一个拜访它的全局拜访点;建造者模式次要解决须要创建对象时须要传入多个参数,或者对初始化程序有要求的场景;工厂办法模式通过提供一个工厂对象或者工厂办法,为使用者暗藏了对象创立的细节;形象工厂模式是对工厂办法模式的优化,通过为工厂对象新增一个形象层,让工厂对象遵循繁多职责准则,也防止了霰弹式批改;原型模式则让对象复制更加简略。

点击关注,第一工夫理解华为云陈腐技术~