Design Patterns in Practice GoF: The Proxy Pattern

Summary: The   proxy pattern provides a proxy for an object to control access to that object.

This article is shared from HUAWEI CLOUD Community " [Go Implementation] Practice 23 Design Patterns of GoF: Proxy Pattern ", author: Yuan Runzi.

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)
}

In this example, Subject is the Db interface, Proxy is the CacheProxy object, and SubjectImpl is the memoryDb object:

Summarize several key points for implementing the proxy pattern:

  1. Define a proxy object that implements the interface of the proxied object. In this example, the former is the CacheProxy object and the latter is the Db interface.
  2. The proxy object combines the proxy object, and the combination here should be an abstract interface, which makes the proxy more scalable. In this example, the CacheProxy object combines the Db interface.
  3. The proxy object embeds the logic of the proxy itself on the specific interface implementation. In this example, CacheProxy adds the read and write logic of cache sync.Map in methods such as Query and Insert.
  4. Proxy objects can also have their own unique methods to provide some auxiliary functions. In this example, CacheProxy adds methods such as Hit and Miss to count cache hit rates.
  5. Finally, in the initialization phase, the instantiation of the proxy is done, and the dependency is injected into the client. This requires that clients rely on abstract interfaces rather than concrete implementations, otherwise the proxy will not be transparent.

expand

Reverse proxies in the Go standard library

The most typical application scenario of the proxy mode is the remote proxy , among which the reverse proxy is the most commonly used one.

Taking a web application as an example, the reverse proxy is located in front of the web server and forwards the client (eg web browser) request to the back-end web server. Reverse proxies are often used to help improve security, performance, and reliability , such as load balancing, SSL secure links.

The net package of the Go standard library also provides a reverse proxy, ReverseProxy, located under net/http/httputil/reverseproxy.go, which implements the http.Handler interface. http.Handler provides the ability to process Http requests, which is equivalent to Http server. Then, corresponding to the UML structure diagram, http.Handler is the Subject, and ReverseProxy is the Proxy:

Some core codes of ReverseProxy are listed below:

// 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 is a typical proxy mode implementation, in which the remote proxy cannot directly refer to the object reference of the backend. Therefore, by introducing the Transport to remotely access the backend service, the Transport can be understood as the Subject.

ReverseProxy can be used like this:

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")
}

Typical application scenarios

  • Remote proxy (remote proxy), the remote proxy is suitable for the object that provides the service on the remote machine. The service cannot be used through ordinary function calls, and it needs to be done through the remote proxy. Because the ontology object cannot be directly accessed, all remote proxy objects usually do not directly hold the reference of the ontology object, but hold the address of the remote machine to access the ontology object through the network protocol .
  • Virtual proxy (virtual proxy), there are often some heavyweight service objects in program design. If the object instance is held all the time, it will consume a lot of system resources. At this time, the virtual proxy can be used to delay initialization of the object.
  • Protection proxy, which is used to control access to ontology objects, and is often used in scenarios where permission verification is required for client access.
  • Cache proxy (cache proxy), the cache proxy mainly adds a layer of cache between the Client and the ontology object to speed up the access of the ontology object, which is common in the scenario of connecting to the database.
  • Smart reference (smart reference), smart reference provides additional actions for the access of the ontology object. The common implementation is the smart pointer in C++, which provides a counting function for the access of the object. When the count of the accessed object is 0, the object is destroyed. .

Advantages and disadvantages

advantage

  • It is possible to control access objects, such as remote access, increase cache, security, etc., without the client's perception.
  • In line with the open-closed principle , a new proxy can be added without modifying the client and the proxy object; the proxy object can also be replaced without modifying the client and proxy.

shortcoming

  • When acting as a remote proxy, the delay of the request will be affected because of one more forwarding.

Links to other schemas

From a structural point of view, the decoration pattern  and the proxy pattern have a high similarity, but the points emphasized by the two are different. The former emphasizes adding new functions to ontology objects, while the latter emphasizes access control to ontology objects .

Article with pictures

You can  find the drawing method of the article in the hand-drawn style with Keynote . 

refer to

[1] [Go Implementation] Practice 23 Design Patterns of GoF: SOLID Principle , Yuan Runzi 

[2] [Go Implementation] Practice GoF's 23 Design Patterns: Decorative Patterns , Yuan Runzi 

[3] Design Patterns, Chapter 4. Structural Patterns, GoF

[4] Proxy mode,  refactoringguru.cn

[5] What is a reverse proxy? , cloudflare

 

Click Follow to learn about HUAWEI CLOUD's new technologies for the first time~

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4526289/blog/5584867