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

五、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

目录层级

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

  1. 控制可访问的 server 范围
  2. 限速
  3. 缓存请求
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)
		}
	}
}
相关推荐
岁岁岁平安16 分钟前
spring学习(spring-DI(字符串或对象引用注入、集合注入)(XML配置))
java·学习·spring·依赖注入·集合注入·基本数据类型注入·引用数据类型注入
北辰浮光21 分钟前
[spring]XML配置文件标签
xml·spring
ZSYP-S1 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
越甲八千1 小时前
重温设计模式--享元模式
设计模式·享元模式
qxlxi2 小时前
【Spring事务】深入浅出Spring事务从原理到源码
spring
码农爱java2 小时前
设计模式--抽象工厂模式【创建型模式】
java·设计模式·面试·抽象工厂模式·原理·23种设计模式·java 设计模式
路在脚下@3 小时前
Spring Boot @Conditional注解
java·spring boot·spring
越甲八千3 小时前
重温设计模式--中介者模式
windows·设计模式·中介者模式
犬余3 小时前
设计模式之桥接模式:抽象与实现之间的分离艺术
笔记·学习·设计模式·桥接模式
Theodore_10224 小时前
1 软件工程——概述
java·开发语言·算法·设计模式·java-ee·软件工程·个人开发