快來(lái),這里有23種設(shè)計(jì)模式的Go語(yǔ)言實(shí)現(xiàn)(三)

      網(wǎng)友投稿 805 2025-04-01

      前言


      上一篇文章《快來(lái),這里有23種設(shè)計(jì)模式的Go語(yǔ)言實(shí)現(xiàn)(二)》中,我們介紹了結(jié)構(gòu)型模式(Structural Pattern)中的組合模式、適配器模式和橋接模式。本文將會(huì)介紹完剩下的幾種結(jié)構(gòu)型模式,代理模式、裝飾模式、外觀模式和享元模式。本文將會(huì)繼續(xù)采用消息處理系統(tǒng)作為例子,如果對(duì)該例子不清楚,請(qǐng)移步《快來(lái),這里有23種設(shè)計(jì)模式的Go語(yǔ)言實(shí)現(xiàn)(一)》和《快來(lái),這里有23種設(shè)計(jì)模式的Go語(yǔ)言實(shí)現(xiàn)(二)》對(duì)其相關(guān)的設(shè)計(jì)和實(shí)現(xiàn)進(jìn)行了解。

      代理模式(Proxy Pattern)

      簡(jiǎn)介

      代理模式為一個(gè)對(duì)象提供一種代理以控制對(duì)該對(duì)象的訪問(wèn),它是一個(gè)使用率非常高的設(shè)計(jì)模式,即使在現(xiàn)實(shí)生活中,也是很常見(jiàn),比如演唱會(huì)門票黃牛。假設(shè)你需要看一場(chǎng)演唱會(huì),但是官網(wǎng)上門票已經(jīng)售罄,于是就當(dāng)天到現(xiàn)場(chǎng)通過(guò)黃牛高價(jià)買了一張。在這個(gè)例子中,黃牛就相當(dāng)于演唱會(huì)門票的代理,在正式渠道無(wú)法購(gòu)買門票的情況下,你通過(guò)代理完成了該目標(biāo)。

      從演唱會(huì)門票的例子我們也可以看出,使用代理模式的關(guān)鍵在于當(dāng)Client不方便直接訪問(wèn)一個(gè)對(duì)象時(shí),提供一個(gè)代理對(duì)象控制該對(duì)象的訪問(wèn)。Client實(shí)際上訪問(wèn)的是代理對(duì)象,代理對(duì)象會(huì)將Client的請(qǐng)求轉(zhuǎn)給本體對(duì)象去處理。

      在程序設(shè)計(jì)中,代理模式也分為好幾種:

      1、遠(yuǎn)程代理(remote proxy),遠(yuǎn)程代理適用于提供服務(wù)的對(duì)象處在遠(yuǎn)程的機(jī)器上,通過(guò)普通的函數(shù)調(diào)用無(wú)法使用服務(wù),需要經(jīng)過(guò)遠(yuǎn)程代理來(lái)完成。因?yàn)椴⒉荒苤苯釉L問(wèn)本體對(duì)象,所有遠(yuǎn)程代理對(duì)象通常不會(huì)直接持有本體對(duì)象的引用,而是持有遠(yuǎn)端機(jī)器的地址,通過(guò)網(wǎng)絡(luò)協(xié)議去訪問(wèn)本體對(duì)象。

      2、虛擬代理(virtual proxy),在程序設(shè)計(jì)中常常會(huì)有一些重量級(jí)的服務(wù)對(duì)象,如果一直持有該對(duì)象實(shí)例會(huì)非常消耗系統(tǒng)資源,這時(shí)可以通過(guò)虛擬代理來(lái)對(duì)該對(duì)象進(jìn)行延遲初始化。

      3、保護(hù)代理(protection proxy),保護(hù)代理用于控制對(duì)本體對(duì)象的訪問(wèn),常用于需要給Client的訪問(wèn)加上權(quán)限驗(yàn)證的場(chǎng)景。

      4、緩存代理(cache proxy),緩存代理主要在Client與本體對(duì)象之間加上一層緩存,用于加速本體對(duì)象的訪問(wèn),常見(jiàn)于連接數(shù)據(jù)庫(kù)的場(chǎng)景。

      5、智能引用(smart reference),智能引用為本體對(duì)象的訪問(wèn)提供了額外的動(dòng)作,常見(jiàn)的實(shí)現(xiàn)為C++中的智能指針,為對(duì)象的訪問(wèn)提供了計(jì)數(shù)功能,當(dāng)訪問(wèn)對(duì)象的計(jì)數(shù)為0時(shí)銷毀該對(duì)象。

      這幾種代理都是一樣的實(shí)現(xiàn)原理,下面我們將介紹遠(yuǎn)程代理的Go語(yǔ)言實(shí)現(xiàn)。

      Go實(shí)現(xiàn)

      考慮要將消息處理系統(tǒng)輸出到數(shù)據(jù)存儲(chǔ)到一個(gè)數(shù)據(jù)庫(kù)中,數(shù)據(jù)庫(kù)的接口如下:

      package db ... // Key-Value數(shù)據(jù)庫(kù)接口 type KvDb interface { // 存儲(chǔ)數(shù)據(jù) // 其中reply為操作結(jié)果,存儲(chǔ)成功為true,否則為false // 當(dāng)連接數(shù)據(jù)庫(kù)失敗時(shí)返回error,成功則返回nil Save(record Record, reply *bool) error // 根據(jù)key獲取value,其中value通過(guò)函數(shù)參數(shù)中指針類型返回 // 當(dāng)連接數(shù)據(jù)庫(kù)失敗時(shí)返回error,成功則返回nil Get(key string, value *string) error } type Record struct { Key string Value string }

      數(shù)據(jù)庫(kù)是一個(gè)Key-Value數(shù)據(jù)庫(kù),使用map存儲(chǔ)數(shù)據(jù),下面為數(shù)據(jù)庫(kù)的服務(wù)端實(shí)現(xiàn),db.Server實(shí)現(xiàn)了db.KvDb接口:

      package db ... // 數(shù)據(jù)庫(kù)服務(wù)端實(shí)現(xiàn) type Server struct { // 采用map存儲(chǔ)key-value數(shù)據(jù) data map[string]string } func (s *Server) Save(record Record, reply *bool) error { if s.data == nil{ s.data = make(map[string]string) } s.data[record.Key] = record.Value *reply = true return nil } func (s *Server) Get(key string, reply *string) error { val, ok := s.data[key] if !ok { *reply = "" return errors.New("Db has no key " + key) } *reply = val return nil }

      消息處理系統(tǒng)和數(shù)據(jù)庫(kù)并不在同一臺(tái)機(jī)器上,因此消息處理系統(tǒng)不能直接調(diào)用db.Server的方法進(jìn)行數(shù)據(jù)存儲(chǔ),像這種服務(wù)提供者和服務(wù)使用者不在同一機(jī)器上的場(chǎng)景,使用遠(yuǎn)程代理再適合不過(guò)了。

      遠(yuǎn)程代理中,最常見(jiàn)的一種實(shí)現(xiàn)是遠(yuǎn)程過(guò)程調(diào)用(Remote Procedure Call,簡(jiǎn)稱 RPC),它允許客戶端應(yīng)用可以像調(diào)用本地對(duì)象一樣直接調(diào)用另一臺(tái)不同的機(jī)器上服務(wù)端應(yīng)用的方法。在Go語(yǔ)言領(lǐng)域,除了大名鼎鼎的gRPC,Go標(biāo)準(zhǔn)庫(kù)net/rpc包里也提供了RPC的實(shí)現(xiàn)。下面,我們通過(guò)net/rpc對(duì)外提供數(shù)據(jù)庫(kù)服務(wù)端的能力:

      package db ... // 啟動(dòng)數(shù)據(jù)庫(kù),對(duì)外提供RPC接口進(jìn)行數(shù)據(jù)庫(kù)的訪問(wèn) func Start() { rpcServer := rpc.NewServer() server := &Server{data: make(map[string]string)} // 將數(shù)據(jù)庫(kù)接口注冊(cè)到RPC服務(wù)器上 if err := rpcServer.Register(server); err != nil { fmt.Printf("Register Server to rpc failed, error: %v", err) return } l, err := net.Listen("tcp", "127.0.0.1:1234") if err != nil { fmt.Printf("Listen tcp failed, error: %v", err) return } go rpcServer.Accept(l) time.Sleep(1 * time.Second) fmt.Println("Rpc server start success.") }

      到目前為止,我們已經(jīng)為數(shù)據(jù)庫(kù)提供了對(duì)外訪問(wèn)的方式。現(xiàn)在,我們需要一個(gè)遠(yuǎn)程代理來(lái)連接數(shù)據(jù)庫(kù)服務(wù)端,并進(jìn)行相關(guān)的數(shù)據(jù)庫(kù)操作。對(duì)消息處理系統(tǒng)而言,它不需要,也不應(yīng)該知道遠(yuǎn)程代理與數(shù)據(jù)庫(kù)服務(wù)端交互的底層細(xì)節(jié),這樣可以減輕系統(tǒng)之間的耦合。因此,遠(yuǎn)程代理需要實(shí)現(xiàn)db.KvDb:

      package db ... // 數(shù)據(jù)庫(kù)服務(wù)端遠(yuǎn)程代理,實(shí)現(xiàn)db.KvDb接口 type Client struct { // RPC客戶端 cli *rpc.Client } func (c *Client) Save(record Record, reply *bool) error { var ret bool // 通過(guò)RPC調(diào)用服務(wù)端的接口 err := c.cli.Call("Server.Save", record, &ret) if err != nil { fmt.Printf("Call db Server.Save rpc failed, error: %v", err) *reply = false return err } *reply = ret return nil } func (c *Client) Get(key string, reply *string) error { var ret string // 通過(guò)RPC調(diào)用服務(wù)端的接口 err := c.cli.Call("Server.Get", key, &ret) if err != nil { fmt.Printf("Call db Server.Get rpc failed, error: %v", err) *reply = "" return err } *reply = ret return nil } // 工廠方法,返回遠(yuǎn)程代理實(shí)例 func CreateClient() *Client { rpcCli, err := rpc.Dial("tcp", "127.0.0.1:1234") if err != nil { fmt.Printf("Create rpc client failed, error: %v.", err) return nil } return &Client{cli: rpcCli} }

      作為遠(yuǎn)程代理的db.Client并沒(méi)有直接持有db.Server的引用,而是持有了它的ip:port,通過(guò)RPC客戶端調(diào)用了它的方法。

      接下來(lái),我們需要為消息處理系統(tǒng)實(shí)現(xiàn)一個(gè)新的Output插件DbOutput,調(diào)用db.Client遠(yuǎn)程代理,將消息存儲(chǔ)到數(shù)據(jù)庫(kù)上。

      在《使用Go實(shí)現(xiàn)GoF的23種設(shè)計(jì)模式(二)》中我們?yōu)镻lugin引入生命周期的三個(gè)方法Start、Stop、Status之后,每新增一個(gè)新的插件,都需要實(shí)現(xiàn)這三個(gè)方法。但是大多數(shù)插件的這三個(gè)方法的邏輯基本一致,因此導(dǎo)致了一定程度的代碼冗余。對(duì)于重復(fù)代碼問(wèn)題,有什么好的解決方法呢?組合模式!

      下面,我們使用組合模式將這個(gè)方法提取成一個(gè)新的對(duì)象LifeCycle,這樣新增一個(gè)插件時(shí),只需將LifeCycle作為匿名成員(嵌入組合),就能解決冗余代碼問(wèn)題了。

      package plugin ... type LifeCycle struct { name string status Status } func (l *LifeCycle) Start() { l.status = Started fmt.Printf("%s plugin started.\n", l.name) } func (l *LifeCycle) Stop() { l.status = Stopped fmt.Printf("%s plugin stopped.\n", l.name) } func (l *LifeCycle) Status() Status { return l.status }

      DbOutput的實(shí)現(xiàn)如下,它持有一個(gè)遠(yuǎn)程代理,通過(guò)后者將消息存儲(chǔ)到遠(yuǎn)端的數(shù)據(jù)庫(kù)中。

      package plugin ... type DbOutput struct { LifeCycle // 操作數(shù)據(jù)庫(kù)的遠(yuǎn)程代理 proxy db.KvDb } func (d *DbOutput) Send(msg *msg.Message) { if d.status != Started { fmt.Printf("%s is not running, output nothing.\n", d.name) return } record := db.Record{ Key: "db", Value: msg.Body.Items[0], } reply := false err := d.proxy.Save(record, &reply) if err != nil || !reply { fmt.Println("Save msg to db server failed.") } } func (d *DbOutput) Init() { d.proxy = db.CreateClient() d.name = "db output" }

      測(cè)試代碼如下:

      package test ... func TestDbOutput(t *testing.T) { db.Start() config := pipeline.Config{ Name: "pipeline3", Input: plugin.Config{ PluginType: plugin.InputType, Name: "hello", }, Filter: plugin.Config{ PluginType: plugin.FilterType, Name: "upper", }, Output: plugin.Config{ PluginType: plugin.OutputType, Name: "db", }, } p := pipeline.Of(config) p.Start() p.Exec() // 驗(yàn)證DbOutput存儲(chǔ)的正確性 cli := db.CreateClient() var val string err := cli.Get("db", &val) if err != nil { t.Errorf("Get db failed, error: %v\n.", err) } if val != "HELLO WORLD" { t.Errorf("expect HELLO WORLD, but actual %s.", val) } } // 運(yùn)行結(jié)果 === RUN TestDbOutput Rpc server start success. db output plugin started. upper filter plugin started. hello input plugin started. Pipeline started. --- PASS: TestDbOutput (1.01s) PASS

      裝飾模式(Decorator Pattern)

      簡(jiǎn)介

      在程序設(shè)計(jì)中,我們常常需要為對(duì)象添加新的行為,很多同學(xué)的第一個(gè)想法就是擴(kuò)展本體對(duì)象,通過(guò)繼承的方式達(dá)到目的。但是使用繼承不可避免地有如下兩個(gè)弊端:(1)繼承時(shí)靜態(tài)的,在編譯期間就已經(jīng)確定,無(wú)法在運(yùn)行時(shí)改變對(duì)象的行為。(2)子類只能有一個(gè)父類,當(dāng)需要添加的新功能太多時(shí),容易導(dǎo)致類的數(shù)量劇增。

      對(duì)于這種場(chǎng)景,我們通常會(huì)使用裝飾模式(Decorator Pattern)來(lái)解決,它使用組合而非繼承的方式,能夠動(dòng)態(tài)地為本體對(duì)象疊加新的行為。理論上,只要沒(méi)有限制,它可以一直把功能疊加下去。裝飾模式最經(jīng)典的應(yīng)用當(dāng)屬Java的I/O流體系,通過(guò)裝飾模式,使用者可以動(dòng)態(tài)地為原始的輸入輸出流添加功能,比如按照字符串輸入輸出,添加緩存等,使得整個(gè)I/O流體系具有很高的可擴(kuò)展性和靈活性。

      從結(jié)構(gòu)上看,裝飾模式和代理模式具有很高的相似性,但是兩種所強(qiáng)調(diào)的點(diǎn)不一樣。前者強(qiáng)調(diào)的是為本體對(duì)象添加新的功能,后者強(qiáng)調(diào)的是對(duì)本體對(duì)象的訪問(wèn)控制。當(dāng)然,代理模式中的智能引用在筆者看來(lái)就跟裝飾模式完全一樣了。

      Go實(shí)現(xiàn)

      考慮為消息處理系統(tǒng)增加這樣的一個(gè)功能,統(tǒng)計(jì)每個(gè)消息輸入源分別產(chǎn)生了多少條消息,也就是分別統(tǒng)計(jì)每個(gè)Input產(chǎn)生Message的數(shù)量。最簡(jiǎn)單的方法是在每一個(gè)Input的Receive方法中進(jìn)行打點(diǎn)統(tǒng)計(jì),但是這樣會(huì)導(dǎo)致統(tǒng)計(jì)代碼與業(yè)務(wù)代碼的耦合。如果統(tǒng)計(jì)邏輯發(fā)生了變化,就會(huì)產(chǎn)生霰彈式修改,隨著Input類型的增多,相關(guān)代碼也會(huì)變得越來(lái)越難維護(hù)。

      更好的方法是將統(tǒng)計(jì)邏輯放到一個(gè)地方,并在每次調(diào)用Input的Receive方法后進(jìn)行打點(diǎn)統(tǒng)計(jì)。而這恰好適合采用裝飾模式,為Input(本體對(duì)象)提供打點(diǎn)統(tǒng)計(jì)功能(新的行為)。我們可以設(shè)計(jì)一個(gè)InputMetricDecorator作為Input的裝飾器,在裝飾器中完成打點(diǎn)統(tǒng)計(jì)的邏輯。

      首先,我們需要設(shè)計(jì)一個(gè)用于統(tǒng)計(jì)每個(gè)Input產(chǎn)生Message數(shù)量的對(duì)象,該對(duì)象應(yīng)該是一個(gè)全局唯一的,因此采用單例模式進(jìn)行了實(shí)現(xiàn):

      package metric ... // 消息輸入源統(tǒng)計(jì),設(shè)計(jì)為單例 type input struct { // 存放統(tǒng)計(jì)結(jié)果,key為Input類型如hello、kafka // value為對(duì)應(yīng)Input的消息統(tǒng)計(jì) metrics map[string]uint64 // 統(tǒng)計(jì)打點(diǎn)時(shí)加鎖 mu *sync.Mutex } // 給名稱為inputName的Input消息計(jì)數(shù)加1 func (i *input) Inc(inputName string) { i.mu.Lock() defer i.mu.Unlock() if _, ok := i.metrics[inputName]; !ok { i.metrics[inputName] = 0 } i.metrics[inputName] = i.metrics[inputName] + 1 } // 輸出當(dāng)前所有打點(diǎn)的情況 func (i *input) Show() { fmt.Printf("Input metric: %v\n", i.metrics) } // 單例 var inputInstance = &input{ metrics: make(map[string]uint64), mu: &sync.Mutex{}, } func Input() *input { return inputInstance }

      接下來(lái)我們開(kāi)始實(shí)現(xiàn)InputMetricDecorator,它實(shí)現(xiàn)了Input接口,并持有一個(gè)本體對(duì)象Input。在InputMetricDecorator在Receive方法中調(diào)用本體Input的Receive方法,并完成統(tǒng)計(jì)動(dòng)作。

      package plugin...type InputMetricDecorator struct { input Input}func (i *InputMetricDecorator) Receive() *msg.Message { // 調(diào)用本體對(duì)象的Receive方法 record := i.input.Receive() // 完成統(tǒng)計(jì)邏輯 if inputName, ok := record.Header.Items["input"]; ok { metric.Input().Inc(inputName) } return record}func (i *InputMetricDecorator) Start() { i.input.Start()}func (i *InputMetricDecorator) Stop() { i.input.Stop()}func (i *InputMetricDecorator) Status() Status { return i.input.Status()}func (i *InputMetricDecorator) Init() { i.input.Init()}// 工廠方法, 完成裝飾器的創(chuàng)建func CreateInputMetricDecorator(input Input) *InputMetricDecorator { return &InputMetricDecorator{input: input}}

      最后,我們?cè)赑ipeline的工廠方法上,為本體Input加上InputMetricDecorator代理:

      package pipeline...// 根據(jù)配置創(chuàng)建一個(gè)Pipeline實(shí)例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) // 為本體Input加上InputMetricDecorator裝飾器 p.input = plugin.CreateInputMetricDecorator(p.input) return p}

      測(cè)試代碼如下:

      package test...func TestInputMetricDecorator(t *testing.T) { p1 := pipeline.Of(pipeline.HelloConfig()) p2 := pipeline.Of(pipeline.KafkaInputConfig()) p1.Start() p2.Start() p1.Exec() p2.Exec() p1.Exec() metric.Input().Show()}// 運(yùn)行結(jié)果=== RUN TestInputMetricDecoratorConsole output plugin started.Upper filter plugin started.Hello input plugin started.Pipeline started.Console output plugin started.Upper filter plugin started.Kafka input plugin started.Pipeline started.Output: Header:map[content:text input:hello], Body:[HELLO WORLD]Output: Header:map[content:text input:kafka], Body:[I AM MOCK CONSUMER.]Output: Header:map[content:text input:hello], Body:[HELLO WORLD]Input metric: map[hello:2 kafka:1]--- PASS: TestInputMetricProxy (0.00s)PASS

      外觀模式(Facade Pattern)

      簡(jiǎn)介

      從結(jié)構(gòu)上看,外觀模式非常的簡(jiǎn)單,它主要是為子系統(tǒng)提供了一個(gè)更高層次的對(duì)外統(tǒng)一接口,使得Client能夠更友好地使用子系統(tǒng)的功能。圖中,Subsystem Class是子系統(tǒng)中對(duì)象的簡(jiǎn)稱,它可能是一個(gè)對(duì)象,也可能是數(shù)十個(gè)對(duì)象的集合。外觀模式降低了Client與Subsystem之間的耦合,只要Facade不變,不管Subsystem怎么變化,對(duì)于Client而言都是無(wú)感知的。

      外觀模式在程序設(shè)計(jì)中用的非常多,比如我們?cè)谏坛巧宵c(diǎn)擊購(gòu)買的按鈕,對(duì)于購(gòu)買者而言,只看到了購(gòu)買這一統(tǒng)一的接口,但是對(duì)于商城系統(tǒng)而言,其內(nèi)部則進(jìn)行了一系列的業(yè)務(wù)處理,比如庫(kù)存檢查、訂單處理、支付、物流等等。外觀模式極大地提升了用戶體驗(yàn),將用戶從復(fù)雜的業(yè)務(wù)流程中解放了出來(lái)。

      外觀模式經(jīng)常運(yùn)用于分層架構(gòu)上,通常我們都會(huì)為分層架構(gòu)中的每一個(gè)層級(jí)提供一個(gè)或多個(gè)統(tǒng)一對(duì)外的訪問(wèn)接口,這樣就能讓各個(gè)層級(jí)之間的耦合性更低,使得系統(tǒng)的架構(gòu)更加合理。

      Go實(shí)現(xiàn)

      外觀模式實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單,還是考慮前面的消息處理系統(tǒng)。在Pipeline中,每一條消息會(huì)依次經(jīng)過(guò)Input->Filter->Output的處理,代碼實(shí)現(xiàn)起來(lái)就是這樣:

      p := pipeline.Of(config)message := p.input.Receive()message = p.filter.Process(message)p.output.Send(message)

      快來(lái),這里有23種設(shè)計(jì)模式的Go語(yǔ)言實(shí)現(xiàn)(三)

      但是,對(duì)于Pipeline的使用者而言,他可能并不關(guān)心消息具體的處理流程,他只需知道消息已經(jīng)經(jīng)過(guò)Pipeline處理即可。因此,我們需要設(shè)計(jì)一個(gè)簡(jiǎn)單的對(duì)外接口:

      package pipeline...func (p *Pipeline) Exec() { msg := p.input.Receive() msg = p.filter.Process(msg) p.output.Send(msg)}

      這樣,使用者只需簡(jiǎn)單地調(diào)用Exec方法,就能完成一次消息的處理,測(cè)試代碼如下:

      package test...func TestPipeline(t *testing.T) { p := pipeline.Of(pipeline.HelloConfig()) p.Start() // 調(diào)用Exec方法完成一次消息的處理 p.Exec()}// 運(yùn)行結(jié)果=== RUN TestPipelineconsole output plugin started.upper filter plugin started.hello input plugin started.Pipeline started.Output: Header:map[content:text input:hello], Body:[HELLO WORLD]--- PASS: TestPipeline (0.00s)PASS

      享元模式(Flyweight Pattern)

      簡(jiǎn)介

      在程序設(shè)計(jì)中,我們常常會(huì)碰到一些很重型的對(duì)象,它們通常擁有很多的成員屬性,當(dāng)系統(tǒng)中充斥著大量的這些對(duì)象時(shí),系統(tǒng)的內(nèi)存將會(huì)承受巨大的壓力。此外,頻繁的創(chuàng)建這些對(duì)象也極大地消耗了系統(tǒng)的CPU。很多時(shí)候,這些重型對(duì)象里,大部分的成員屬性都是固定的,這種場(chǎng)景下, 可以使用享元模式進(jìn)行優(yōu)化,將其中固定不變的部分設(shè)計(jì)成共享對(duì)象(享元,flyweight),這樣就能節(jié)省大量的系統(tǒng)內(nèi)存和CPU。

      享元模式摒棄了在每個(gè)對(duì)象中保存所有數(shù)據(jù)的方式, 通過(guò)共享多個(gè)對(duì)象所共有的相同狀態(tài), 讓你能在有限的內(nèi)存容量中載入更多對(duì)象。

      當(dāng)我們決定對(duì)一個(gè)重型對(duì)象采用享元模式進(jìn)行優(yōu)化時(shí),首先需要將該重型對(duì)象的屬性劃分為兩類,能夠共享的和不能共享的。前者我們稱為內(nèi)部狀態(tài)(intrinsic state),存儲(chǔ)在享元中,不隨享元所處上下文的變化而變化;后者稱為外部狀態(tài)(extrinsic state),它的值取決于享元所處的上下文,因此不能共享。比如,文章A和文章B都引用了圖片A,由于文章A和文章B的文字內(nèi)容是不一樣的,因此文字就是外部狀態(tài),不能共享;但是它們所引用的圖片A是一樣的,屬于內(nèi)部狀態(tài),因此可以將圖片A設(shè)計(jì)為一個(gè)享元

      工廠模式通常都會(huì)和享元模式結(jié)對(duì)出現(xiàn),享元工廠提供了唯一獲取享元對(duì)象的接口,這樣Client就感知不到享元是如何共享的,降低了模塊的耦合性。享元模式和單例模式有些類似的地方,都是在系統(tǒng)中共享對(duì)象,但是單例模式更關(guān)心的是對(duì)象在系統(tǒng)中僅僅創(chuàng)建一次,而享元模式更關(guān)心的是如何在多個(gè)對(duì)象中共享相同的狀態(tài)。

      Go實(shí)現(xiàn)

      假設(shè)現(xiàn)在需要設(shè)計(jì)一個(gè)系統(tǒng),用于記錄NBA中的球員信息、球隊(duì)信息以及比賽結(jié)果。

      球隊(duì)Team的數(shù)據(jù)結(jié)構(gòu)定義如下:

      package nba ... type TeamId uint8 const ( Warrior TeamId = iota Laker ) type Team struct { Id TeamId // 球隊(duì)ID Name string // 球隊(duì)名稱 Players []*Player // 球隊(duì)中的球員 }

      球員Player的數(shù)據(jù)結(jié)構(gòu)定義如下:

      package nba ... type Player struct { Name string // 球員名字 Team TeamId // 球員所屬球隊(duì)ID }

      比賽結(jié)果Match的數(shù)據(jù)結(jié)構(gòu)定義如下:

      package nba ... type Match struct { Date time.Time // 比賽時(shí)間 LocalTeam *Team // 主場(chǎng)球隊(duì) VisitorTeam *Team // 客場(chǎng)球隊(duì) LocalScore uint8 // 主場(chǎng)球隊(duì)得分 VisitorScore uint8 // 客場(chǎng)球隊(duì)得分 } func (m *Match) ShowResult() { fmt.Printf("%s VS %s - %d:%d\n", m.LocalTeam.Name, m.VisitorTeam.Name, m.LocalScore, m.VisitorScore) }

      NBA中的一場(chǎng)比賽由兩個(gè)球隊(duì),主場(chǎng)球隊(duì)和客場(chǎng)球隊(duì),完成比賽,對(duì)應(yīng)著代碼就是,一個(gè)Match實(shí)例會(huì)持有2個(gè)Team實(shí)例。目前,NBA總共由30支球隊(duì),按照每個(gè)賽季每個(gè)球隊(duì)打82場(chǎng)常規(guī)賽算,一個(gè)賽季總共會(huì)有2460場(chǎng)比賽,對(duì)應(yīng)地,就會(huì)有4920個(gè)Team實(shí)例。但是,NBA的30支球隊(duì)是固定的,實(shí)際上只需30個(gè)Team實(shí)例就能完整地記錄一個(gè)賽季的所有比賽信息,剩下的4890個(gè)Team實(shí)例屬于冗余的數(shù)據(jù)。

      這種場(chǎng)景下就適合采用享元模式來(lái)進(jìn)行優(yōu)化,我們把Team設(shè)計(jì)成多個(gè)Match實(shí)例之間的享元。享元的獲取通過(guò)享元工廠來(lái)完成,享元工廠teamFactory的定義如下,Client統(tǒng)一使用teamFactory.TeamOf方法來(lái)獲取球隊(duì)Team實(shí)例。其中,每個(gè)球隊(duì)Team實(shí)例只會(huì)創(chuàng)建一次,然后添加到球隊(duì)池中,后續(xù)獲取都是直接從池中獲取,這樣就達(dá)到了共享的目的。

      package nba ... type teamFactory struct { // 球隊(duì)池,緩存球隊(duì)實(shí)例 teams map[TeamId]*Team } // 根據(jù)TeamId獲取Team實(shí)例,從池中獲取,如果池里沒(méi)有,則創(chuàng)建 func (t *teamFactory) TeamOf(id TeamId) *Team { team, ok := t.teams[id] if !ok { team = createTeam(id) t.teams[id] = team } return team } // 享元工廠的單例 var factory = &teamFactory{ teams: make(map[TeamId]*Team), } func Factory() *teamFactory { return factory } // 根據(jù)TeamId創(chuàng)建Team實(shí)例,只在TeamOf方法中調(diào)用,外部不可見(jiàn) func createTeam(id TeamId) *Team { switch id { case Warrior: w := &Team{ Id: Warrior, Name: "Golden State Warriors", } curry := &Player{ Name: "Stephen Curry", Team: Warrior, } thompson := &Player{ Name: "Klay Thompson", Team: Warrior, } w.Players = append(w.Players, curry, thompson) return w case Laker: l := &Team{ Id: Laker, Name: "Los Angeles Lakers", } james := &Player{ Name: "LeBron James", Team: Laker, } davis := &Player{ Name: "Anthony Davis", Team: Laker, } l.Players = append(l.Players, james, davis) return l default: fmt.Printf("Get an invalid team id %v.\n", id) return nil } }

      測(cè)試代碼如下:

      package test ... func TestFlyweight(t *testing.T) { game1 := &nba.Match{ Date: time.Date(2020, 1, 10, 9, 30, 0, 0, time.Local), LocalTeam: nba.Factory().TeamOf(nba.Warrior), VisitorTeam: nba.Factory().TeamOf(nba.Laker), LocalScore: 102, VisitorScore: 99, } game1.ShowResult() game2 := &nba.Match{ Date: time.Date(2020, 1, 12, 9, 30, 0, 0, time.Local), LocalTeam: nba.Factory().TeamOf(nba.Laker), VisitorTeam: nba.Factory().TeamOf(nba.Warrior), LocalScore: 110, VisitorScore: 118, } game2.ShowResult() // 兩個(gè)Match的同一個(gè)球隊(duì)?wèi)?yīng)該是同一個(gè)實(shí)例的 if game1.LocalTeam != game2.VisitorTeam { t.Errorf("Warrior team do not use flyweight pattern") } } // 運(yùn)行結(jié)果 === RUN TestFlyweight Golden State Warriors VS Los Angeles Lakers - 102:99 Los Angeles Lakers VS Golden State Warriors - 110:118 --- PASS: TestFlyweight (0.00s)

      總結(jié)

      本文我們主要介紹了結(jié)構(gòu)型模式中的代理模式、裝飾模式、外觀模式和享元模式。代理模式為一個(gè)對(duì)象提供一種代理以控制對(duì)該對(duì)象的訪問(wèn),強(qiáng)調(diào)的是對(duì)本體對(duì)象的訪問(wèn)控制;裝飾模式能夠動(dòng)態(tài)地為本體對(duì)象疊加新的行為,強(qiáng)調(diào)的是為本體對(duì)象添加新的功能;外觀模式為子系統(tǒng)提供了一個(gè)更高層次的對(duì)外統(tǒng)一接口,強(qiáng)調(diào)的是分層和解耦;享元模式通過(guò)共享對(duì)象來(lái)降低系統(tǒng)的資源消耗,強(qiáng)調(diào)的是如何在多個(gè)對(duì)象中共享相同的狀態(tài)。

      到目前為止,7種結(jié)構(gòu)型模式已經(jīng)全部介紹完,下一篇文章,我們開(kāi)始將介紹最后一類設(shè)計(jì)模式——行為型模式(Behavioral Pattern)。

      Go 數(shù)據(jù)庫(kù)

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:建筑施工企業(yè)安全生產(chǎn)管理(建筑施工企業(yè)安全生產(chǎn)管理機(jī)構(gòu)專職安全生產(chǎn)管理人員)
      下一篇:圖片太大打出來(lái)后很多看不清楚 想要分成三頁(yè)打印怎么分頁(yè)(為什么有的圖片放大后很清楚)
      相關(guān)文章
      亚洲天堂中文字幕| 亚洲成AV人片在线观看ww| 亚洲免费在线视频| 久久亚洲国产精品一区二区| 亚洲午夜国产片在线观看| 五月天婷亚洲天综合网精品偷| 亚洲AV性色在线观看| 噜噜综合亚洲AV中文无码| 亚洲av永久无码精品秋霞电影秋| 国产亚洲福利在线视频| 国内精品久久久久影院亚洲| 日韩亚洲人成在线| 亚洲日韩中文字幕一区| 亚洲国产精品网站在线播放| 亚洲精品一二三区| 亚洲乱码无人区卡1卡2卡3| 亚洲av永久无码精品网址| 国产精品日本亚洲777| 午夜亚洲国产成人不卡在线| 亚洲精品在线视频| 亚洲无av在线中文字幕| 亚洲av午夜成人片精品网站| 亚洲国产成人久久综合一| 亚洲黄色在线观看视频| 亚洲天堂福利视频| 亚洲中文字幕久久精品蜜桃| 亚洲国产综合AV在线观看| 国产成人高清亚洲一区91| 中文字幕无码精品亚洲资源网| 亚洲日韩精品一区二区三区| 无码乱人伦一区二区亚洲一| 亚洲高清中文字幕| 亚洲av无码电影网| 亚洲成AV人片在WWW| 亚洲国产成人爱av在线播放| 亚洲中文字幕无码不卡电影| 久久夜色精品国产嚕嚕亚洲av| 亚洲色图校园春色| 男人天堂2018亚洲男人天堂| WWW国产亚洲精品久久麻豆| ZZIJZZIJ亚洲日本少妇JIZJIZ|