摘要: 代理模式為一個物件提供一種代理以控制對該物件的訪問,
本文分享自華為云社區《【Go實作】實踐GoF的23種設計模式:代理模式》,作者:元閏子 ,
簡介
GoF 對代理模式(Proxy Pattern)的定義如下:
Provide a surrogate or placeholder for another object to control access to it.
也即,代理模式為一個物件提供一種代理以控制對該物件的訪問,
它是一個使用率非常高的設計模式,在現實生活中,也是很常見,比如,演唱會門票黃牛,假設你需要看一場演唱會,但官網上門票已經售罄,于是就當天到現場通過黃牛高價買了一張,在這個例子中,黃牛就相當于演唱會門票的代理,在正式渠道無法購買門票的情況下,你通過代理完成了該目標,
從演唱會門票的例子我們也能看出,使用代理模式的關鍵在于,當 Client 不方便直接訪問一個物件時,提供一個代理物件控制該物件的訪問,Client 實際上訪問的是代理物件,代理物件會將 Client 的請求轉給本體物件去處理,
UML 結構
場景背景關系
在簡單的分布式應用系統(示例代碼工程)中,db 模塊用來存盤服務注冊和監控資訊,它是一個 key-value 資料庫,為了提升訪問資料庫的性能,我們決定為它新增一層快取:
另外,我們希望客戶端在使用資料庫時,并不感知快取的存在,這些,代理模式可以做到,
代碼實作
// demo/db/cache.go package db // 關鍵點1: 定義代理物件,實作被代理物件的介面 type CacheProxy struct { // 關鍵點2: 組合被代理物件,這里應該是抽象介面,提升可擴展性 db Db cache sync.Map // key為tableName,value為sync.Map[key: primaryId, value: interface{}] hit int miss int } // 關鍵點3: 在具體介面實作上,嵌入代理本身的邏輯 func (c *CacheProxy) Query(tableName string, primaryKey interface{}, result interface{}) error { cache, ok := c.cache.Load(tableName) if ok { if record, ok := cache.(*sync.Map).Load(primaryKey); ok { c.hit++ result = record return nil } } c.miss++ if err := c.db.Query(tableName, primaryKey, result); err != nil { return err } cache.(*sync.Map).Store(primaryKey, result) return nil } func (c *CacheProxy) Insert(tableName string, primaryKey interface{}, record interface{}) error { if err := c.db.Insert(tableName, primaryKey, record); err != nil { return err } cache, ok := c.cache.Load(tableName) if !ok { return nil } cache.(*sync.Map).Store(primaryKey, record) return nil } ... // 關鍵點4: 代理也可以有自己特有方法,提供一些輔助的功能 func (c *CacheProxy) Hit() int { return c.hit } func (c *CacheProxy) Miss() int { return c.miss } ...
客戶端這樣使用:
// 客戶端只看到抽象的Db介面 func client(db Db) { table := NewTable("region"). WithType(reflect.TypeOf(new(testRegion))). WithTableIteratorFactory(NewRandomTableIteratorFactory()) db.CreateTable(table) table.Insert(1, &testRegion{Id: 1, Name: "region"}) result := new(testRegion) db.Query("region", 1, result) } func main() { // 關鍵點5: 在初始化階段,完成快取的實體化,并依賴注入到客戶端 cache := NewCacheProxy(&memoryDb{tables: sync.Map{}}) client(cache) }
本例子中,Subject 是 Db 介面,Proxy 是 CacheProxy 物件,SubjectImpl 是 memoryDb 物件:
總結實作代理模式的幾個關鍵點:
- 定義代理物件,實作被代理物件的介面,本例子中,前者是 CacheProxy 物件,后者是 Db 介面,
- 代理物件組合被代理物件,這里組合的應該是抽象介面,讓代理的可擴展性更高些,本例子中,CacheProxy 物件組合了 Db 介面,
- 代理物件在具體介面實作上,嵌入代理本身的邏輯,本例子中,CacheProxy 在 Query、Insert 等方法中,加入了快取 sync.Map 的讀寫邏輯,
- 代理物件也可以有自己特有方法,提供一些輔助的功能,本例子中,CacheProxy 新增了Hit、Miss 等方法用于統計快取的命中率,
- 最后,在初始化階段,完成代理的實體化,并依賴注入到客戶端,這要求,客戶端依賴抽象介面,而不是具體實作,否則代理就不透明了,
擴展
Go 標準庫中的反向代理
代理模式最典型的應用場景是遠程代理,其中,反向代理又是最常用的一種,
以 Web 應用為例,反向代理位于 Web 服務器前面,將客戶端(例如 Web 瀏覽器)請求轉發后端的 Web 服務器,反向代理通常用于幫助提高安全性、性能和可靠性,比如負載均衡、SSL 安全鏈接,
Go 標準庫的 net 包也提供了反向代理,ReverseProxy,位于 net/http/httputil/reverseproxy.go 下,實作 http.Handler 介面,http.Handler 提供了處理 Http 請求的能力,也即相當于 Http 服務器,那么,對應到 UML 結構圖中,http.Handler 就是 Subject,ReverseProxy 就是 Proxy:
下面列出 ReverseProxy 的一些核心代碼:
// net/http/httputil/reverseproxy.go package httputil type ReverseProxy struct { // 修改前端請求,然后通過Transport將修改后的請求轉發給后端 Director func(*http.Request) // 可理解為Subject,通過Transport來呼叫被代理物件的ServeHTTP方法處理請求 Transport http.RoundTripper // 修改后端回應,并將修改后的回應回傳給前端 ModifyResponse func(*http.Response) error // 錯誤處理 ErrorHandler func(http.ResponseWriter, *http.Request, error) ... } func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // 初始化transport transport := p.Transport if transport == nil { transport = http.DefaultTransport } ... // 修改前端請求 p.Director(outreq) ... // 將請求轉發給后端 res, err := transport.RoundTrip(outreq) ... // 修改后端回應 if !p.modifyResponse(rw, res, outreq) { return } ... // 給前端回傳回應 err = p.copyResponse(rw, res.Body, p.flushInterval(res)) ... }
ReverseProxy 就是典型的代理模式實作,其中,遠程代理無法直接參考后端的物件參考,因此這里通過引入 Transport 來遠程訪問后端服務,可以將 Transport 理解為 Subject,
可以這么使用 ReverseProxy:
func proxy(c *gin.Context) { remote, err := url.Parse("https://yrunz.com") if err != nil { panic(err) } proxy := httputil.NewSingleHostReverseProxy(remote) proxy.Director = func(req *http.Request) { req.Header = c.Request.Header req.Host = remote.Host req.URL.Scheme = remote.Scheme req.URL.Host = remote.Host req.URL.Path = c.Param("proxyPath") } proxy.ServeHTTP(c.Writer, c.Request) } func main() { r := gin.Default() r.Any("/*proxyPath", proxy) r.Run(":8080") }
典型應用場景
- 遠程代理(remote proxy),遠程代理適用于提供服務的物件處在遠程的機器上,通過普通的函式呼叫無法使用服務,需要經過遠程代理來完成,因為并不能直接訪問本體物件,所有遠程代理物件通常不會直接持有本體物件的參考,而是持有遠端機器的地址,通過網路協議去訪問本體物件,
- 虛擬代理(virtual proxy),在程式設計中常常會有一些重量級的服務物件,如果一直持有該物件實體會非常消耗系統資源,這時可以通過虛擬代理來對該物件進行延遲初始化,
- 保護代理(protection proxy),保護代理用于控制對本體物件的訪問,常用于需要給 Client 的訪問加上權限驗證的場景,
- 快取代理(cache proxy),快取代理主要在 Client 與本體物件之間加上一層快取,用于加速本體物件的訪問,常見于連接資料庫的場景,
- 智能參考(smart reference),智能參考為本體物件的訪問提供了額外的動作,常見的實作為 C++ 中的智能指標,為物件的訪問提供了計數功能,當訪問物件的計數為 0 時銷毀該物件,
優缺點
優點
- 可以在客戶端不感知的情況下,控制訪問物件,比如遠程訪問、增加快取、安全等,
- 符合開閉原則,可以在不修改客戶端和被代理物件的前提下,增加新的代理;也可以在不修改客戶端和代理的前提下,更換被代理物件,
缺點
- 作為遠程代理時,因為多了一次轉發,會影響請求的時延,
與其他模式的關聯
從結構上看,裝飾模式 和 代理模式 具有很高的相似性,但是兩種所強調的點不一樣,前者強調的是為本體物件添加新的功能,后者強調的是對本體物件的訪問控制,
文章配圖
可以在 用Keynote畫出手繪風格的配圖 中找到文章的繪圖方法,
參考
[1] 【Go實作】實踐GoF的23種設計模式:SOLID原則, 元閏子
[2] 【Go實作】實踐GoF的23種設計模式:裝飾模式, 元閏子
[3] Design Patterns, Chapter 4. Structural Patterns, GoF
[4] 代理模式, refactoringguru.cn
[5] 什么是反向代理?, cloudflare
點擊關注,第一時間了解華為云新鮮技術~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/516277.html
標籤:Go
上一篇:驅動開發:內核特征碼搜索函式封裝
