结构型 - 1. 代理模式

131 阅读3分钟

结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。

1. 代理模式的原理与实现

代理模式(Proxy Design Pattern)在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能

  • 一般情况下,让代理类和原始类实现同样的接口。
  • 原始类没有定义接口,并且原始类代码不是我们维护的。通过代理类继承原始类来实现代理模式。
type Server interface {
   HandleRequest(url, method string) (int, string)
}


type Application struct {
}

func (a *Application) HandleRequest(url, method string) (int, string) {
   if url == "/app/status" && method == "GET" {
      return 200, "Ok"
   }

   if url == "/create/user" && method == "POST" {
      return 201, "User Created"
   }
   return 404, "Not Ok"
}


type Nginx struct {
   application       *Application
   maxAllowedRequest int
   rateLimiter       map[string]int
}

func NewNginxServer() *Nginx {
   return &Nginx{
      application:       &Application{},
      maxAllowedRequest: 2,
      rateLimiter:       make(map[string]int),
   }
}

func (n *Nginx) HandleRequest(url, method string) (int, string) {
   allowed := n.checkRateLimiting(url)
   if !allowed {
      return 403, "Not Allowed"
   }
   return n.application.HandleRequest(url, method)
}

func (n *Nginx) checkRateLimiting(url string) bool {
   if n.rateLimiter[url] == 0 {
      n.rateLimiter[url] = 1
   }
   if n.rateLimiter[url] > n.maxAllowedRequest {
      return false
   }
   n.rateLimiter[url] = n.rateLimiter[url] + 1
   return true
}

// 客户端使用
func TestNginx(t *testing.T) {
   nginxServer := NewNginxServer()
   appStatusURL := "/app/status"
   createUserURL := "/create/user"

   httpCode, body := nginxServer.HandleRequest(appStatusURL, "GET")
   t.Logf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

   httpCode, body = nginxServer.HandleRequest(appStatusURL, "GET")
   t.Logf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

   httpCode, body = nginxServer.HandleRequest(appStatusURL, "GET")
   t.Logf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

   httpCode, body = nginxServer.HandleRequest(createUserURL, "POST")
   t.Logf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

   httpCode, body = nginxServer.HandleRequest(createUserURL, "GET")
   t.Logf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
}

2. 动态代理的原理与实现

让代理类和原始类实现同样的接口,可能存在的问题:

  • 在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。
  • 如果要添加的附加功能的类不止有一个,需要装对每个类都创建一个代理类。并且,每个代理类中的代码都有点像「模板式」的“重复”代码,增加了不必要的开发成本。

解决办法就是:动态代理(Dynamic Proxy),不事先为每个原始类编写代理类,而是在运行时,动态地创建原始类对应的代理类。

3. 代理模式的应用场景

3.1 业务系统的非功能性开发

代理模式最常用的一个应用场景就是,在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理,让程序员只需要关注业务方面的开发。

3.2 代理模式在RPC,缓存中的应用

实际上,RPC 框架也可以看作是一种代理模式:隐藏网络通信、数据编解码等细节。RPC 服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户端的交互细节。

如果开发一个接口请求的缓存功能,简单的实现方式是,给每个需要支持缓存的查询需求都开发两个不同的接口,一个支持缓存,一个支持实时查询。这样做显然增加了开发成本,而且会让代码看起来非常臃肿(接口个数成倍增加),也不方便缓存接口的集中管理(增加、删除缓存接口)、集中配置(比如配置每个接口缓存过期时间)

针对这些问题,代理模式就能派上用场了,确切地说,应该是动态代理。

如果是基于 Spring 框架来开发的话,那就可以在 AOP 切面中完成接口缓存的功能。在应用启动的时候,我们从配置文件中加载需要支持缓存的接口,以及相应的缓存策略(比如过期时间)等。当请求到来的时候,我们在 AOP 切面中拦截请求,如果请求中带有支持缓存的字段(比如 http://…?..&cached=true),我们便从缓存(内存缓存或者 Redis 缓存等)中获取数据直接返回。