设计模式之--代理模式

608 阅读7分钟

1. 定义

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

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

2. 例子

小红送礼物给小刚

  • 不用代理模式的情况
var Gift = function(){};

var xiaohong = {
    sendGift: function(target) {
        var gift = new Gift();
        target.receiveGift(gift);
    }
}

var xiaogang = {
    receiveGift: function(gift) {
        console.log('收到礼物' + gift)
    }
}

xiaohong.sendGift(xiaogang);
  • 使用代理模式(小红通过小明来给小刚送礼物)
var Gift = function(){};

var xiaohong = {
    sendGift: function(target) {
        var gift = new Gift();
        target.receiveGift(gift);
    }
}

var xiaoming = {
    receiveGift: function(gift) {
        xiaogang.receiveGift(gift)
    }
}

var xiaogang = {
    receiveGift: function(gift) {
        console.log('收到礼物' + gift)
    }
}

xiaohong.sendGift(xiaogang);

这就是一个简单的代理模式。二者看起来并没有本质的区别,引入一个代理对象看起来只是把把事情变复杂了而已。此处的代理模式确实毫无用处,只是把请求简单地转交给本体。

把上面的例子换个背景:小刚接受礼物随着心情变化来

var Gift = function(){};

var xiaohong = {
    sendGift: function(target) {
        var gift = new Gift();
        target.receiveGift(gift);
    }
}

var xiaoming = {
    receiveGift: function(gift) {
        xiaogang.listenGoodMood(function(){ // 监听小刚的好心情
            xiaogang.receiveGift(gift)
        })
    }
}

var xiaogang = {
    receiveGift: function(gift) {
        console.log('收到礼物' + gift)
    },
    listenGoodMood: function(fn) {
        setTimeout(function(){ // 假设10秒之后A的心情变好
            fn()
        }, 10000)
    }
}

xiaohong.sendGift(xiaogang);

3. 保护代理和虚拟代理

3.1 保护代理

代理“小明”可以帮助“小刚”过滤掉一些请求,比如送礼物的人中年龄太大的,这种请求就可以直接在代理“小明”处被拒绝掉。这种代理叫做“保护代理”

3.2 虚拟代理

假设 new Gift() 是一个代价昂贵的操作,可以把 new Gift() 的操作交给代理 “小明”去执行,代理“小明”会选择“小刚”心情好的时候再执行 new Gift() 。这种叫“虚拟代理”。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。

var xiaoming = {
    receiveGift: function(gift) {
        xiaogang.listenGoodMood(function(){ // 监听小刚的好心情
           var gift = new Gift(); // 延迟创建 gift 对象
           xiaogang.receiveGift(gift)
        })
    }
}

保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。

4. 虚拟代理实现图片预加载

如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置会有一段时间是空白的。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。

例子如下:

先创建一个本体对象,这个对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口,外界调用这个接口,便可以给img标签设置src属性:

var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();

myImage.setSrc('http:// xxx.png');

可以看到,在图片被加载好之前,页面中有一段空白时间。

引入代理对象 proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的菊花图loading.gif,来提示用户图片正在加载。

var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})(); 

var proxyImage = (function(){
    var img = new Image;
    img.onload = function(){
        myImage.setSrc(this.src);
    }
    return {
        setSrc: function(src) {
            myImage.setSrc('file//xxx/loading.gif')
            img.src = src;
        }
    }
})()

proxyImage.setSrc('http:// xxx.png')

通过 proxyImage 间接访问 MyImage. proxyImage 控制了客户对 MyImage的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把img节点的src设置为一张本地的loading图片。

5.代理的意义

或许有人会有疑问,不过是实现一个小小的图片预加载功能,即使不需要引入任何模式也能办到,那么引入代理模式的好处究竟在哪里呢? 下面我们先抛开代理,编写一个更常见的图片预加载函数。

不用代理的预加载图片函数实现如下:

var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    var img = new Image;
    
    img.onload = function(){
        imgNode.src = img.src;
    }
    
    return {
        setSrc: function(src) {
            imgNode.src = 'file//xxx/loading.gif';
            img.src = src
        }
    }
})();  

myImage.setSrc('http:// xxx.png')

为了说明代理的意义,引入一个面向对象设计的原则--单一职责原则

单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

职责被定义为“引起变化的原因”。上段代码中的MyImage对象除了负责给 img 节点设置src外,还要负责预加载图片。在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。

实际上,我们需要的只是给 img节点设置src,预加载图片只是一个锦上添花的功能。如果能把这个操作放在另外一个对象里面,是一个非常好的方法。代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体MyImage。

我们并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放-封闭原则的。给img节点设置src和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天不再需要预加载功能,只需要改成请求本体而不是请求代理对象即可。

6. 代理和本地接口的一致性

前面例子里,如果我们有一天不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了 setSrc 方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处。

  • 用户可以放心地请求代理,他只关心是否能得到想要的结果
  • 在任何使用本体的地方都可以替换成使用代理。

在 Java 等语言中,代理和本体都需要显式地实现同一个接口,一方面接口保证了它们会拥有同样的方法,另一方面,面向接口编程迎合依赖倒置原则,通过接口进行向上转型,从而避开编译器的类型检查,代理和本体将来可以被替换使用。

如果代理对象和本体对象都为一个函数(函数也是对象),函数必须都能被执行,则可以认为它们也具有一致的“接口”,

var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    
    return function(src) {
        imgNode.src = src;
    }
})();

var proxyImage = (function(){
    var img = new Image;
    
    img.onload = function(){
        myImage(this.src);
    }
    
    return function(src){
        myImage('file//xxx/loading.gif');
        img.src = src;
    }
})();

proxyImage('http:// xxx.png');

7. 虚拟代理合并HTTP请求

var synchronousFile = function(id) {
    console.log('开始同步文件,id为:' + id);
}

var proxySynchronousFile = (function(){
    var cache = [],
        timer;
    
    return function(id) {
        cache.push(id);
        if(timer) {
            return;
        }
        
        timer = setTimeout(function(){
            synchronousFile(cache.join(','));
            clearTimeout(timer);
            timer = null;
            cache.length = 0;
        }, 2000)
    } 
})()

var checkbox = document.getElementsByTagName('input');

for(var i=0,c; c=checkbox[i++];){
    c.onclick = function(){
        if(this.checked === true) {
            proxySynchronousFile(this.id);
        }
    }
}

8. 虚拟代理在惰性加载中的应用

var miniConsole = (function(){
    var cache = [];
    var handler = function(ev){  
        if(ev.keyCode === 113){ // 当用户按下F2时,开始加载真正的miniConsole.js
            var script = document.createElement('script');
            script.onload = function(){
                for(var i=0, fn; fn=cache[i++];) {
                    fn();
                }
            }
            script.src = 'miniConsole.js';
            document.getElementsByTagName('head')[0].appendChild(script);
            document.body.removeEventListener('keydown', handler); // 只加载一次 miniConsole.js
        }
    }
    
    document.body.addEventListener('keydown',handler, false);
    
    return {
        log: function(){
            var args = arguments;
            cache.push(function(){
                return miniConsole.log.apply(miniConsole, args)
            })
        }
    }
    
})()

miniConsole.log(11)

9. 缓存代理

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

9.1 计算乘积

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

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

proxyMult(1,2,3,4)
proxyMult(1,2,3,4) //直接返回了前面缓存的计算结果

10. 用高阶函数动态创建代理

通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些计算方法被当作参数传入一个专门用于创建缓存代理的工厂中,我们就可以为乘法、加法、减法等创建缓存代理

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

// 计算加和
var plus = function(){
    var a = 0;
    for(var i=0, l = arguments.length; i<l;i++) {
        a = a + arguments[i];
    }
    return a;
}

// 创建缓存代理的工厂
var createProxyFactory = function(fn) {
    var chache = {}
    return function(){
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }
        return cache[args] = fn.apply(this, arguments);
    }
}

var proxyMult = createProxyFactory(mult),
proxyPlus = createProxyFactory(plus);

alert(proxyMult(1,2,3,4))
alert(proxyMult(1,2,3,4))
alert(proxyPlus(1,2,3,4))
alert(proxyPlus(1,2,3,4))

11. 其他代理模式

  • 防火墙代理:控制网络资源的访问
  • 远程代理:为一个对象在不同的地址空间提供局部代表
  • 保护代理:用于对象应该有不同访问权限的情况
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数
  • 写时复制代理: 通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。

在JavaScript 开发中最常用的是虚拟代理和缓存代理。