• 【设计模式】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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    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)
    
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5.2.1 Server

    package _52nginx
    
    // Server 的接口, rawServerObject 和 proxyServerObject 均会实现此接口
    type Server interface {
    	handleRequest(method, url string) (code int, body string)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    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)
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
  • 相关阅读:
    【Python学习笔记】循环
    DA14580开发板与lis2ds12三轴传感器数据显示实现
    【数据库编码集】oracle 10g数据库查询中文因编码格式不同,导致显示乱码。客户端转码解决办法,mybatis全局TypeHandler,mp多数据源
    WPF的路由事件
    SpringCloud OpenFeign
    App Inventor 2 模拟sleep函数
    大气颗粒物PMF源解析实践技术应用
    【QML】vscode安装QML格式化插件方法
    前端--vscode常用插件
    PAT 1035 Password
  • 原文地址:https://blog.csdn.net/jiaoyangwm/article/details/137961114