五、proxy 代理模式
proxy 模式
https://refactoringguru.cn/design-patterns/proxy
如果 client 需要操作一个 rawObject, 但希望 proxy 它时, 则可使用 proxy 模式.
可抽象 proxy interface, 使 rawObject 和 proxyObject 都实现该 proxy interface.
有如下场景:
- 延迟初始化: 如果 rawObject 是一个消耗大量资源的巨型对象, 我们只是偶尔使用它的话. 可以用 proxyObject 封装一层, 当真正调用时再调用 rawObject 去初始化
- 缓存结果, 记录日志, 访问控制等.
通常, proxyObject 会管理 rawObject 的整个生命周期.
例如, 用户直接访问腾讯视频太慢了, 我们可以写一个代理, 它缓存视频. 详见 051 示例
5.1 tencent_video_proxy
用户直接从腾讯视频下载视频需要花钱, 而且慢. 但如果盗版网站代理的话, 提供同样的服务, 而且免费, 快速.
示例: https://refactoringguru.cn/design-patterns/proxy
目录层级
bash
05proxy/051tencent_video_proxy
├── imovie_website.go
├── imovie_website_test.go
├── readme.md
├── tencent_video.go
└── video_website.go
5.1.1 接口
go
package _51tencent_video_proxy
import "fmt"
// IMovieWebsite 私人电影网站
type IMovieWebsite struct {
// 原始内容提供商, 是代理的对象: 如腾讯视频
rawVideoWebsite VideoWebsite
// 缓存的 videos
cachedVideos map[int]string
}
func NewIMovieWebsite(rawVideoWebsite VideoWebsite) VideoWebsite {
return &IMovieWebsite{
rawVideoWebsite: rawVideoWebsite,
cachedVideos: make(map[int]string),
}
}
// 私有方法, 拉取全部原始视频
func (im *IMovieWebsite) fetchRawVideos() {
fmt.Println("[拉取原始视频列表] 开始")
defer fmt.Println("[拉取原始视频列表] 结束")
im.cachedVideos = im.rawVideoWebsite.listVideos()
}
// 私有方法, 尝试拉取某原始视频
func (im *IMovieWebsite) fetchRawVideoIfNotExist(id int) {
// fmt.Printf("[尝试拉取某原始视频%v] 开始\n", id)
// defer fmt.Printf("[尝试拉取某原始视频%v] 结束\n", id)
// 尝试从缓存中寻找
_, ok := im.cachedVideos[id]
if !ok {
// 如果不存在则拉取
im.fetchRawVideos()
}
}
func (im *IMovieWebsite) listVideos() map[int]string {
// 如果无缓存, 则重新拉取
if len(im.cachedVideos) == 0 {
im.fetchRawVideos()
}
// 返回缓存的内容
return im.cachedVideos
}
func (im *IMovieWebsite) startVideo(id int) {
im.fetchRawVideoIfNotExist(id)
// 从缓存中寻找
name, ok := im.cachedVideos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("开始播放id为%v的视频%v\n", id, name)
}
func (im *IMovieWebsite) stopVideo(id int) {
im.fetchRawVideoIfNotExist(id)
// 从缓存中寻找
name, ok := im.cachedVideos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("暂停播放id为%v的视频%v\n", id, name)
}
func (im *IMovieWebsite) seekVideo(id int, pos float64) {
im.fetchRawVideoIfNotExist(id)
// 从缓存中寻找
name, ok := im.cachedVideos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("跳转id为%v的视频%v, 进度到%v\n", id, name, pos)
}
5.1.2 原始:腾讯视频实现
go
package _51tencent_video_proxy
import "fmt"
// TencentVideoWebsite 腾讯视频网站
type TencentVideoWebsite struct {
// 视频内容, k: 视频id, v: 视频name
videos map[int]string
}
func NewTencentVideoWebsite() VideoWebsite {
return &TencentVideoWebsite{videos: map[int]string{1: "热辣滚烫", 2: "飞驰人生"}}
}
func (t *TencentVideoWebsite) listVideos() map[int]string {
return t.videos
}
func (t *TencentVideoWebsite) startVideo(id int) {
name, ok := t.videos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("开始播放id为%v的视频%v\n", id, name)
}
func (t *TencentVideoWebsite) stopVideo(id int) {
name, ok := t.videos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("暂停播放id为%v的视频%v\n", id, name)
}
func (t *TencentVideoWebsite) seekVideo(id int, pos float64) {
name, ok := t.videos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
}
fmt.Printf("跳转id为%v的视频%v, 进度到%v\n", id, name, pos)
}
5.1.3 代理:实现
go
package _51tencent_video_proxy
import "fmt"
// IMovieWebsite 私人电影网站
type IMovieWebsite struct {
// 原始内容提供商, 是代理的对象: 如腾讯视频
rawVideoWebsite VideoWebsite
// 缓存的 videos
cachedVideos map[int]string
}
func NewIMovieWebsite(rawVideoWebsite VideoWebsite) VideoWebsite {
return &IMovieWebsite{
rawVideoWebsite: rawVideoWebsite,
cachedVideos: make(map[int]string),
}
}
// 私有方法, 拉取全部原始视频
func (im *IMovieWebsite) fetchRawVideos() {
fmt.Println("[拉取原始视频列表] 开始")
defer fmt.Println("[拉取原始视频列表] 结束")
im.cachedVideos = im.rawVideoWebsite.listVideos()
}
// 私有方法, 尝试拉取某原始视频
func (im *IMovieWebsite) fetchRawVideoIfNotExist(id int) {
// fmt.Printf("[尝试拉取某原始视频%v] 开始\n", id)
// defer fmt.Printf("[尝试拉取某原始视频%v] 结束\n", id)
// 尝试从缓存中寻找
_, ok := im.cachedVideos[id]
if !ok {
// 如果不存在则拉取
im.fetchRawVideos()
}
}
func (im *IMovieWebsite) listVideos() map[int]string {
// 如果无缓存, 则重新拉取
if len(im.cachedVideos) == 0 {
im.fetchRawVideos()
}
// 返回缓存的内容
return im.cachedVideos
}
func (im *IMovieWebsite) startVideo(id int) {
im.fetchRawVideoIfNotExist(id)
// 从缓存中寻找
name, ok := im.cachedVideos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("开始播放id为%v的视频%v\n", id, name)
}
func (im *IMovieWebsite) stopVideo(id int) {
im.fetchRawVideoIfNotExist(id)
// 从缓存中寻找
name, ok := im.cachedVideos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("暂停播放id为%v的视频%v\n", id, name)
}
func (im *IMovieWebsite) seekVideo(id int, pos float64) {
im.fetchRawVideoIfNotExist(id)
// 从缓存中寻找
name, ok := im.cachedVideos[id]
if !ok {
fmt.Printf("视频%v不存在\n", id)
return
}
fmt.Printf("跳转id为%v的视频%v, 进度到%v\n", id, name, pos)
}
5.1.4 单测
go
package _51tencent_video_proxy
import (
"fmt"
"testing"
)
// 第一次就拉取原始视频
/*
=== RUN TestIMovieWebsite_FetchFirst
[拉取原始视频列表] 开始
[拉取原始视频列表] 结束
---StartVideo---
开始播放id为1的视频热辣滚烫
---stopVideo---
暂停播放id为1的视频热辣滚烫
---seekVideo---
跳转id为1的视频热辣滚烫, 进度到1.2
---StartVideo---
开始播放id为2的视频飞驰人生
---stopVideo---
暂停播放id为2的视频飞驰人生
---seekVideo---
跳转id为2的视频飞驰人生, 进度到1.2
--- PASS: TestIMovieWebsite_FetchFirst (0.00s)
PASS
*/
func TestIMovieWebsite_FetchFirst(t *testing.T) {
tencentVideoWebsite := NewTencentVideoWebsite()
iMovieWebsite := NewIMovieWebsite(tencentVideoWebsite)
videos := iMovieWebsite.listVideos()
for id := range videos {
fmt.Println("---StartVideo---")
iMovieWebsite.startVideo(id)
fmt.Println("---stopVideo---")
iMovieWebsite.stopVideo(id)
fmt.Println("---seekVideo---")
iMovieWebsite.seekVideo(id, 1.2)
}
}
// 不提前拉取原始视频, 而是懒加载
/*
=== RUN TestIMovieWebsite_LazyFetch
---StartVideo---
[拉取原始视频列表] 开始
[拉取原始视频列表] 结束
开始播放id为1的视频热辣滚烫
---stopVideo---
暂停播放id为1的视频热辣滚烫
---seekVideo---
跳转id为1的视频热辣滚烫, 进度到1.2
---StartVideo---
开始播放id为2的视频飞驰人生
---stopVideo---
暂停播放id为2的视频飞驰人生
---seekVideo---
跳转id为2的视频飞驰人生, 进度到1.2
---StartVideo---
[拉取原始视频列表] 开始
[拉取原始视频列表] 结束
视频3不存在
---stopVideo---
[拉取原始视频列表] 开始
[拉取原始视频列表] 结束
视频3不存在
---seekVideo---
[拉取原始视频列表] 开始
[拉取原始视频列表] 结束
视频3不存在
--- PASS: TestIMovieWebsite_LazyFetch (0.00s)
PASS
*/
func TestIMovieWebsite_LazyFetch(t *testing.T) {
tencentVideoWebsite := NewTencentVideoWebsite()
iMovieWebsite := NewIMovieWebsite(tencentVideoWebsite)
for _, id := range []int{1, 2, 3} {
fmt.Println("---StartVideo---")
iMovieWebsite.startVideo(id)
fmt.Println("---stopVideo---")
iMovieWebsite.stopVideo(id)
fmt.Println("---seekVideo---")
iMovieWebsite.seekVideo(id, 1.2)
}
}
5.2 nginx
https://refactoringguru.cn/design-patterns/proxy/go/example
nginx 可作为 server 的代理:
- 控制可访问的 server 范围
- 限速
- 缓存请求
bash
05proxy/052nginx
├── app.go
├── nginx.go
├── nginx_test.go
├── readme.md
└── server.go
5.2.1 Server
go
package _52nginx
// Server 的接口, rawServerObject 和 proxyServerObject 均会实现此接口
type Server interface {
handleRequest(method, url string) (code int, body string)
}
5.2.2 App
go
package _52nginx
// app 是 Server
type app struct {
}
func NewApp() Server {
return &app{}
}
func (a *app) handleRequest(method, url string) (code int, body string) {
if method == "GET" && url == "/status" {
return 200, "OK"
}
if method == "POST" && url == "/create/user" {
return 201, "User Created"
}
return 404, "Not Found"
}
5.2.3 Nginx
go
package _52nginx
type Nginx struct {
app Server
// 次数限制器
url2Cnt map[string]int
// 各 URL 允许访问的最大次数
maxCnt int
}
func NewNginx(app Server, maxCnt int) Nginx {
return Nginx{
app: app,
url2Cnt: make(map[string]int),
maxCnt: maxCnt,
}
}
func (n *Nginx) handleRequest(method, url string) (code int, body string) {
if !n.checkCntLimiterAllowed(method, url) {
return 403, "Not Allowed"
}
return n.app.handleRequest(method, url)
}
// 访问次数控制
func (n *Nginx) checkCntLimiterAllowed(method, url string) bool {
// 次数超限
if cnt := n.url2Cnt[url]; cnt >= n.maxCnt {
return false
}
// 次数正常
n.url2Cnt[url]++
return true
}
5.2.4 nginx_test
go
package _52nginx
import (
"github.com/stretchr/testify/require"
"testing"
)
// 测试超过最大的访问次数时, Nginx 的效果
func TestNginx(t *testing.T) {
maxCnt := 2
n := NewNginx(NewApp(), maxCnt)
// 可被 App 匹配的 URL, 超过最大访问次数时, 被 Nginx 拦截
for i := 0; i < 5; i++ {
method, url := "GET", "/status"
c, b := n.handleRequest(method, url)
if i < maxCnt {
// Nginx 放行, 实际由 App 响应
require.EqualValues(t, 200, c)
require.EqualValues(t, "OK", b)
} else {
// Nginx 拦截, 实际由 Nginx 响应
require.EqualValues(t, 403, c)
require.EqualValues(t, "Not Allowed", b)
}
}
// 不被 App 匹配的 URL, 超过最大访问次数时, 被 Nginx 拦截
for i := 0; i < 5; i++ {
method, url := "POST", "/a-not-exist-url"
c, b := n.handleRequest(method, url)
if i < maxCnt {
// Nginx 放行, 实际由 App 响应
require.EqualValues(t, 404, c)
require.EqualValues(t, "Not Found", b)
} else {
// Nginx 拦截, 实际由 Nginx 响应
require.EqualValues(t, 403, c)
require.EqualValues(t, "Not Allowed", b)
}
}
}