摘要:設計模式(Design Pattern)是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結,使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解而且保證代碼可靠性。
本文分享自華爲雲社區《快來,這裏有23種設計模式的Go語言實現》,原文做者:元閏子 。程序員
從1995年GoF提出23種設計模式到如今,25年過去了,設計模式依舊是軟件領域的熱門話題。在當下,若是你不會一點設計模式,都很差意思說本身是一個合格的程序員。設計模式一般被定義爲:編程
設計模式(Design Pattern)是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結,使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解而且保證代碼可靠性。json
從定義上看,設計模式實際上是一種經驗的總結,是針對特定問題的簡潔而優雅的解決方案。既然是經驗總結,那麼學習設計模式最直接的好處就在於能夠站在巨人的肩膀上解決軟件開發過程當中的一些特定問題。然而,學習設計模式的最高境界是習得其中解決問題所用到的思想,當你把它們的本質思想吃透了,也就能作到即便已經忘掉某個設計模式的名稱和結構,也能在解決特定問題時信手拈來。segmentfault
好的東西有人吹捧,固然也會招黑。設計模式被抨擊主要由於如下兩點:設計模式
一、設計模式會增長代碼量,把程序邏輯變得複雜。這一點是不可避免的,可是咱們並不能僅僅只考慮開發階段的成本。最簡單的程序固然是一個函數從頭寫到尾,可是這樣後期的維護成本會變得很是大;而設計模式雖然增長了一點開發成本,可是能讓人們寫出可複用、可維護性高的程序。引用《軟件設計的哲學》裏的概念,前者就是戰術編程,後者就是戰略編程,咱們應該對戰術編程Say No!緩存
二、濫用設計模式。這是初學者最容易犯的錯誤,當學到一個模式時,巴不得在全部的代碼都用上,從而在不應使用模式的地方刻意地使用了模式,致使了程序變得異常複雜。其實每一個設計模式都有幾個關鍵要素:適用場景、解決方法、優缺點。模式並非萬能藥,它只有在特定的問題上才能顯現出效果。因此,在使用一個模式前,先問問本身,當前的這個場景適用這個模式嗎?安全
《設計模式》一書的副標題是「可複用面向對象軟件的基礎」,但並不意味着只有面嚮對象語言才能使用設計模式。模式只是一種解決特定問題的思想,跟語言無關。就像Go語言同樣,它並不是是像C++和Java同樣的面嚮對象語言,可是設計模式一樣適用。本系列文章將使用Go語言來實現GoF提出的23種設計模式,按照建立型模式(Creational Pattern)、結構型模式(Structural Pattern)和行爲型模式(Behavioral Pattern)三種類別進行組織,文本主要介紹其中的建立型模式。架構
單例模式算是23中設計模式裏最簡單的一個了,它主要用於保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。app
在程序設計中,有一些對象一般咱們只須要一個共享的實例,好比線程池、全局緩存、對象池等,這種場景下就適合使用單例模式。函數
可是,並不是全部全局惟一的場景都適合使用單例模式。好比,考慮須要統計一個API調用的狀況,有兩個指標,成功調用次數和失敗調用次數。這兩個指標都是全局惟一的,因此有人可能會將其建模成兩個單例SuccessApiMetric和FailApiMetric。按照這個思路,隨着指標數量的增多,你會發現代碼裏類的定義會愈來愈多,也愈來愈臃腫。這也是單例模式最多見的誤用場景,更好的方法是將兩個指標設計成一個對象ApiMetric下的兩個實例ApiMetic success和ApiMetic fail。
如何判斷一個對象是否應該被建模成單例?
一般,被建模成單例的對象都有「中心點」的含義,好比線程池就是管理全部線程的中心。因此,在判斷一個對象是否適合單例模式時,先思考下,這個對象是一箇中心點嗎?
在對某個對象實現單例模式時,有兩個點必需要注意:(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 } ...
在程序設計中,咱們會常常遇到一些複雜的對象,其中有不少成員屬性,甚至嵌套着多個複雜的對象。這種狀況下,建立這個複雜對象就會變得很繁瑣。對於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點比較經常使用,下面對建造者模式的實現也主要是針對這兩點進行示例。
考慮以下的一個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
從測試代碼可知,使用建造者模式來進行對象建立,使用者再也不須要知道對象具體的實現細節,代碼可讀性也更好。
工廠方法模式跟上一節討論的建造者模式相似,都是將對象建立的邏輯封裝起來,爲使用者提供一個簡單易用的對象建立接口。二者在應用場景上稍有區別,建造者模式更經常使用於須要傳遞多個參數來進行實例化的場景。
使用工廠方法來建立對象主要有兩個好處:
一、代碼可讀性更好。相比於使用C++/Java中的構造函數,或者Go中的{}來建立對象,工廠方法由於能夠經過函數名來表達代碼含義,從而具有更好的可讀性。好比,使用工廠方法productA := CreateProductA()建立一個ProductA對象,比直接使用productA := ProductA{}的可讀性要好。
二、與使用者代碼解耦。不少狀況下,對象的建立每每是一個容易變化的點,經過工廠方法來封裝對象的建立過程,能夠在建立邏輯變動時,避免霰彈式修改。
工廠方法模式也有兩種實現方式:(1)提供一個工廠對象,經過調用工廠對象的工廠方法來建立產品對象;(2)將工廠方法集成到產品對象中(C++/Java中對象的static方法,Go中同一package下的函數)
考慮有一個事件對象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 } ...
一、按照第一種實現方式,爲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
二、按照第二種實現方式,分別給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
在工廠方法模式中,咱們經過一個工廠對象來建立一個產品族,具體建立哪一個產品,則經過swtich-case的方式去判斷。這也意味着該產品組上,每新增一類產品對象,都必須修改原來工廠對象的代碼;並且隨着產品的不斷增多,工廠對象的職責也愈來愈重,違反了單一職責原則。
抽象工廠模式經過給工廠類新增一個抽象層解決了該問題,如上圖所示,FactoryA和FactoryB都實現·抽象工廠接口,分別用於建立ProductA和ProductB。若是後續新增了ProductC,只需新增一個FactoryC便可,無需修改原有的代碼;由於每一個工廠只負責建立一個產品,所以也遵循了單一職責原則。
考慮須要以下一個插件架構風格的消息處理系統,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
原型模式主要解決對象複製的問題,它的核心就是clone()方法,返回Prototype對象的複製品。在程序設計過程當中,每每會遇到有一些場景須要大量相同的對象,若是不使用原型模式,那麼咱們可能會這樣進行對象的建立:新建立一個相同對象的實例,而後遍歷原始對象的全部成員變量, 並將成員變量值複製到新對象中。這種方法的缺點很明顯,那就是使用者必須知道對象的實現細節,致使代碼之間的耦合。另外,對象頗有可能存在除了對象自己之外不可見的變量,這種狀況下該方法就行不通了。
對於這種狀況,更好的方法就是使用原型模式,將複製邏輯委託給對象自己,這樣,上述兩個問題也都迎刃而解了。
仍是以建造者模式一節中的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種建立型模式,建立型模式的目的都是提供一個簡單的接口,讓對象的建立過程與使用者解耦。其中,單例模式主要用於保證一個類僅有一個實例,並提供一個訪問它的全局訪問點;建造者模式主要解決須要建立對象時須要傳入多個參數,或者對初始化順序有要求的場景;工廠方法模式經過提供一個工廠對象或者工廠方法,爲使用者隱藏了對象建立的細節;抽象工廠模式是對工廠方法模式的優化,經過爲工廠對象新增一個抽象層,讓工廠對象遵循單一職責原則,也避免了霰彈式修改;原型模式則讓對象複製更加簡單。