跟我一起学设计模式[三] 代理模式

341 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

记录自己学习设计模式,内容来自

《JavaScript设计模式与开发实践》

代理模式的定义

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本地对象

小明追MM

小明喜欢女孩A,小明决定送A一束花表白,因为小明害羞不敢直接送,小明决定让A的朋友B代替自己完成送花这件事

先看看不用代理模式的情况


var Flower = function() {}

var xiaoming = {
    sendFlower(target) {
        const flower = new Flower()
        target.receiveFlower(flower)
    }
}

var A = {
    receiveFlower(flower) {
        console.log('收到花', flower)
    }
}

// 接下来我们引入代理B

var B = {
    receiveFlower(flower) {
        A.receiveFlower(flower)
    }
}

xiaoming.sendFlower(B)

保护代理和虚拟的代理

假设现实世界中花价格不菲,导致在程序世界里,new Flower也是一个代价昂贵的操作,那么我们可以把new Flower的操作交给代理B去执行,代理B会选择在A心情好时再执行new Flower,这是代理模式的另一种形式,叫虚拟代理。虚拟代理会把一些开销很大的对象,延迟到真正需要它的时候才去创建。代码如下

var B = {
    receiveFlower(flower) {
        A.listenGoodMood(() => {
            const flower = new Flower()
            A.receiveFlower(flower)
        })
    }
}

虚拟代理实现图片预加载

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        const myImage = (() => {
            var imgNode = document.createElement('img')
            document.body.appendChild(imgNode)
            return {
                setSrc(src) {
                    imgNode.src = src
                }
            }
        })()

        var proxyImage = (() => {
            const img = new Image;
            img.onload = function() {
                myImage.setSrc(this.src)
            }
            return {
                setSrc(src) {
                    myImage.setSrc('./assets/loading.gif')
                    img.src = src
                }
            }
        })()

        proxyImage.setSrc('./assets/hashiqi.webp')

    </script>
</body>
</html>

虚拟代理合并HTTP请求

例如有如下场景:每周我们要写一份工作周报,周报需要给总监批阅。总监手下管理着150个员工,如果我们每个人直接把周报发给总监,那总监可能要把一整周的时候都花在查看周报上面。

现在我们把周报发给各自的组长,组长作为代理,把组内成员的周报合提炼成一份后一次性发给总监。这样一来,总监的邮箱便清净来了

这个例子再程序世界里很容易引起共鸣,在Web开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外到另外一台备用服务器上面

const synchronousFile = function(id) {
    console.log('开始同步文件,id为:', + id)
}
var checkbox = document.getElementsByTagName('input')
for(let i = 0, c; c = checkbox[i++];) {
    c.onclick = function() {
        if (this.checked === true) {
            synchronousFile(this.id)
        }
    }
}

当我们选中三个checkbox的时候,依次往服务器发送了3次同步文件的请求。而点击一个checkbox并不是很复杂的操作。可以预见,如此频繁的网络请求将会带来相当大的开销。

解决方案时,我们通过一个代理函数proxySynchronousFile来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待两秒之后才把这2秒之内需要同步的文件ID打包发送给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。

虚拟加载在惰性单例中的应用

例如有一个插件叫miniConsole.log,按F2可以打开自定义控制台

例如在控制台输入miniConsole.log(1)

这句话会在页面上创建一个div,并且把log显示在div里面。

miniConsole的代码量大概有1000行,也许我们并不想一开始就加载这么大的js文件,因为也许并不是每个用户都需要打印log。我们希望在有必要的时候才开始加载它,比如当用户按下F2来主动唤出控制台的时候。

在miniConsole.js加载之前,为了能够让用户正常的使用里面的API,通常我们的解决方案时用一个展位的miniConsole代理来给用户提前使用。

const miniConsole = (function() {
    const cache = []
    const handler = function(ev) {
        if (ev.keyCode === 112) {
            const script = document.createElement('script')
            script.onload = function() {
                for(let i = 0, fn; fn = cache[i++];) {
                    fn()
                }
            };
            script.src = 'miniConsole.js'
            document.getElementsByClassName('head')[0].appendChild(script)
            document.body.removeEventListener('keydown', handler)
        }
    }
    document.body.addEventListener('keydown', handler, false)
    return {
        log() {
            const args = arguments
            cache.push(() => {
                return miniConsole.apply(miniConsole, args)
            })
        }
    }
})

miniConsole.log(11) // 开始打印log

miniConsole = {
    log() {
        // 真正的代码略
    }
}

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果

例子1:计算乘积

const mult = function() {
    let a = 1
    for(let i = 0, l = arguments.length; i < l; i ++) {
        a = a * arguments[i]
    }
    return a
}

console.log(mult(2, 3)) // 6
console.log(mult(2, 3, 4)) // 24

// 现在加入缓存代理函数

const proxyMult = (function() {
    const cache = {}
    return function() {
        const args = Array.prototype.join.call(arguments, ',')
        if (args in cache) {
            return cache[args]
        }
        return cache[args] = mult.apply(this, arguments)
    }
})()

console.log(proxyMult(1, 2, 3, 4))
console.log(proxyMult(1, 2, 3, 4))

利用高阶函数创建代理

// 计算乘积
const mult = function() {
    let a = 1
    for(let i = 0, l = arguments.length; i < l; i ++) {
        a = a * arguments[i]
    }
    return a
}

// 计算加和

const plus = function() {
    let a = 0
    for(let i = 0, l = arguments.length; i < l; i++) {
        a = a + arguments[i]
    }
    return a
}

// 创建缓存代理的工厂

const createProxyFactory = function(fn) {
    const cache = {}
    return function() {
        const args = Array.prototype.join.call(arguments, ',')
        if (args in cache) {
            return cache[args]
        }
        console.log('计算');
        return cache[args] = fn.apply(this, arguments)
    }
}

const proxyMult = createProxyFactory(mult)
const proxyPlus = createProxyFactory(plus)

console.log(proxyMult(1, 2, 3, 4));
console.log(proxyMult(1, 2, 3, 4));
console.log(proxyPlus(1, 2, 3, 4));
console.log(proxyPlus(1, 2, 3, 4));

其他代理模式

防火墙代理:控制网络资源的访问,保护主机不让"坏人"接近

远程代理:为一个对象在不同的地址空间提供局部代理,在Java中,远程代理可以是另一个虚拟机中的对象

保护代理:用于对象应该有不同访问权限的情况

智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。

写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是典型运用场景