【设计模式】5、proxy 代理模式

发布于:2024-04-26 ⋅ 阅读:(12) ⋅ 点赞:(0)

五、proxy 代理模式

proxy 模式
https://refactoringguru.cn/design-patterns/proxy

如果 client 需要操作一个 rawObject, 但希望 proxy 它时, 则可使用 proxy 模式.

可抽象 proxy interface, 使 rawObject 和 proxyObject 都实现该 proxy interface.

有如下场景:

  1. 延迟初始化: 如果 rawObject 是一个消耗大量资源的巨型对象, 我们只是偶尔使用它的话. 可以用 proxyObject 封装一层, 当真正调用时再调用 rawObject 去初始化
  2. 缓存结果, 记录日志, 访问控制等.

通常, proxyObject 会管理 rawObject 的整个生命周期.

例如, 用户直接访问腾讯视频太慢了, 我们可以写一个代理, 它缓存视频. 详见 051 示例

5.1 tencent_video_proxy

用户直接从腾讯视频下载视频需要花钱, 而且慢. 但如果盗版网站代理的话, 提供同样的服务, 而且免费, 快速.

示例: https://refactoringguru.cn/design-patterns/proxy

目录层级

05proxy/051tencent_video_proxy
├── imovie_website.go
├── imovie_website_test.go
├── readme.md
├── tencent_video.go
└── video_website.go

5.1.1 接口

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 原始:腾讯视频实现

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 代理:实现

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 单测

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 的代理:

  1. 控制可访问的 server 范围
  2. 限速
  3. 缓存请求
05proxy/052nginx
├── app.go
├── nginx.go
├── nginx_test.go
├── readme.md
└── server.go

5.2.1 Server

package _52nginx

// Server 的接口, rawServerObject 和 proxyServerObject 均会实现此接口
type Server interface {
	handleRequest(method, url string) (code int, body string)
}

5.2.2 App

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

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

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