[Go Implementation] Practice GoF's 23 Design Patterns: Proxy Pattern

Previous: [Go Implementation] Practice GoF's 23 Design Patterns: Visitor Pattern

Simple distributed application system (example code project): github.com/ruanrunxue/…

Introduction

GoF defines the Proxy Pattern as follows:

Provide a surrogate or placeholder for another object to control access to it.

That is, the proxy pattern provides a proxy for an object to control access to that object .

It is a very used design pattern, and it is also very common in real life. For example, concert ticket scalpers. Suppose you need to watch a concert, but the tickets on the official website have been sold out, so I went to the scene and bought one at a high price through scalpers. In this example, scalpers are equivalent to the agent of concert tickets. In the case that tickets cannot be purchased through official channels, you complete the goal through the agent.

From the example of concert tickets, we can also see that the key to using the proxy mode is to provide a proxy object to control the access of an object when it is inconvenient for the Client to directly access an object . The client actually accesses the proxy object, and the proxy object will transfer the client's request to the ontology object for processing.

UML structure

scene context

In a simple distributed application system (sample code project), the db module is used to store service registration and monitoring information, and it is a key-value database. In order to improve the performance of accessing the database, we decided to add a layer of cache to it:

In addition, we hope that the client is not aware of the existence of the cache when using the database, and the proxy mode can do this.

Code

 // 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
 }
 ​
 ...
复制代码

The client uses it like this:

 // 客户端只看到抽象的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 对象:

总结实现代理模式的几个关键点:

  1. 定义代理对象,实现被代理对象的接口。本例子中,前者是 CacheProxy 对象,后者是 Db 接口。

  2. 代理对象组合被代理对象,这里组合的应该是抽象接口,让代理的可扩展性更高些。本例子中,CacheProxy 对象组合了 Db 接口。

  3. 代理对象在具体接口实现上,嵌入代理本身的逻辑。本例子中,CacheProxyQueryInsert 等方法中,加入了缓存 sync.Map 的读写逻辑。

  4. 代理对象也可以有自己特有方法,提供一些辅助的功能。本例子中,CacheProxy 新增了HitMiss 等方法用于统计缓存的命中率。

  5. 最后,在初始化阶段,完成代理的实例化,并依赖注入到客户端。这要求,客户端依赖抽象接口,而不是具体实现,否则代理就不透明了。

扩展

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

For more articles, please pay attention to the WeChat public account: Yuan Runzi's invitation

Guess you like

Origin juejin.im/post/7155062296429985799