轻松理解 Golang 代理模式

意图

接管外界对对象的访问。

张三的老板突然在发工资的前一天跑路了,可怜张三一身房贷,被迫提起劳动仲裁,劳动局就会为其指派一位代理律师全权负责张三的仲裁事宜。那这里面就是使用了代理模式,因为在劳动仲裁这个活动中,代理律师会全权代理张三。

实战

我们来针对 Nginx 代理 Web 服务,以 Nginx 对请求限制次数的角度写一段代码(纯属虚构) 首先,我们需要一个 Server:

type server interface {
	handleRequest(string, string) (int, string)
}

之后,基于server接口,实现我们的 app

type Application struct {}

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

此时有一个新需求:接口请求限速。这种非业务功能需求并不适合在 app对象 内实现。首先就是不符合 SOLID 设计原则,再就是将功能需求和非功能需求混杂在一个是一个坏味道。 所以,我们实现一个 app 的代理 Nginx:它继承 app 类,实现了 server 接口。

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 {
	n.rateLimiter[url]++
	if n.rateLimiter[url] > n.maxAllowedRequest {
		return false
	}

	return true
}

我们跑一个测试,看 Nginx 是否实现了功能:

func TestProxy(t *testing.T) {
	nginxServer := newNginxServer()
	appStatusURL := "/app/status"

	httpCode, body := nginxServer.handleRequest(appStatusURL, "GET")
	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

	httpCode, body = nginxServer.handleRequest(appStatusURL, "GET")
	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

	httpCode, body = nginxServer.handleRequest(appStatusURL, "GET") // 触发限速
	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
}
-------------------输出--------------------
=== RUN   TestProxy

Url: /app/status
HttpCode: 200
Body: Ok

Url: /app/status
HttpCode: 200
Body: Ok

Url: /app/status
HttpCode: 403
Body: Not Allowed

优缺点

根据上面例子我们可以看到,代理模式的优点是:

  1. 单一职责,没有侵入原实现(对应例子就是限速功能没有侵入到 app类)。

至于缺点……在我看来只要合理使用,代理模式没有缺点。

应用场景

现在我们知道了如何实现代理模式,以及它的优缺点,

  1. 如果想对重复请求所需返回的相同结果进行缓存,可以使用代理模式。
  2. 业务系统的非功能需求的开发,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理。
  3. RPC 框架基于代理模式实现,RPC 将网络通信、数据编解码等细节隐藏起来。客户端在使用 RPC 服务的时候,就像使用本地函数一样,无需了解跟服务器交互的细节。

动态代理

基于目前的研究,Golang 貌似无法实现 Java 中大名鼎鼎的动态代理(如果哪位大佬有研究,欢迎在评论区指出)。

虽然实现不了,不过我们还是要知道动态代理是什么:所谓动态代理,就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。

对应的,上面的示例叫静态代理,因为在编译期已经确定了代理类。