自学内容网 自学内容网

【设计模式】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

目录层级

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

原文地址:https://blog.csdn.net/jiaoyangwm/article/details/137961114

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!