设计模式:想用代理模式?5个示例让你掌握它

291 阅读8分钟

概念

代理模式是为一个对象提供一个代用品或占位符。 我理解的代理模式。类似于经纪人,如果想请一个明星办一场商业活动,只能通过经纪人进行沟通联系,经纪人把商业演出的细节和报酬谈好后,再把合同拿给明星签名。

代理模式的应用

下面来看日常中如何从实际情况出发来使用代理模式。

1. 图片预加载

在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性, 由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白,常见的做法是先用一张 loading 图片占位。这种场景就适合使用代理模式。

常规的实现图片预加载

常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里。

实现图片预加载如下代码:

var MyImage = function (src) {
    // dom节点设置、挂在body上
    var imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );

    // 先显示loading占位
    imgNode.src = 'XXXX/loading.gif';

    // 负责预加载图片
    const img = new image();
    img.onload= function () {
        imgNode.src = src
    }
    img.src = src;
}

MyImage('http://XXXXX/info.jpg') 

上段代码中的 MyImage 对象除了负责给 img 节点设置 src外,还要负责显示loading ,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。

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

单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。

我理解就是一个函数就干一件事,别把业务都放在一个函数逻辑里。要不然耦合度太高

如果我们只是从网络上获取一些体积很小的图片,或者 5 年后的网速快到根本不再需要预加载,我们可能希望把预加载图片的这段代码从 MyImage 对象里删掉。这时候就不得不改动MyImage 对象了。

实际上,我们需要的只是给 img 节点设置 src,预加载图片只是一个锦上添花的功能。

所以进而优化代码。

代理模式实现图片预加载

拆分功能(1.给 img 节点设置 src)和(2. 预加载图片)

用代理模式来重新写一下:

var MyImage = function (src) {
    // 给 img 节点设置 src
    var imgNode = document.createElement( 'img' ); 
    document.body.appendChild( imgNode );
    imgNode.src = src;
    return imgNode;
}
// proxyImage为代理函数
var proxyImage = function (src) {
    // 加载loading
    let imgNode = MyImage('XXXX/loading.gif');
    
    // 预加载图片
    var img = new Image;
    img.onload = function () {
        imgNode.src = src;
    };
    img.src = src;
}
proxyImage('http://XXXXX/ys0yA0Nk.jpg')

现在我们通过 proxyImage 间接地访问 MyImage。proxyImage 控制了客户对 MyImage 的访问,并 且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为 一张本地的 loading 图片。但是这仅仅是代理模式的壳子套上了 具体还有要有两点注意

    1. MyImage的函数接口不明确
    1. 没有保证代理要保和本体接口的一致性

1. 避免函数接口不明确


/**
错误示例 :
var MyImage = function (src) {
    // 给 img 节点设置 src
     var imgNode = document.createElement( 'img' ); 
    ....
    return imgNode;
} 
//返回的 imgNode 调用者可以随意更改 并没有约束,函数唯一性不明确,比如调用者可以使用
// imgNode.width = XXX 甚至将 imgNode = null  这样就会影响到后续其他调用者
**/

// MyImage函数 应该返回 一个 setStr 接口,setStr 来提供更改src 
// 而不是返回img对象 把问题抛给调用者 让自己去img对象上找相关属性
var MyImage = (function () {
    // 给 img 节点设置 src
    var imgNode = document.createElement( 'img' ); 
    ....
    return {
        // 提供修改img接口
        setStr: function(src) {
          imgNode.src = src;  
        }
    };
})()

// 如上 MyImage函数 改为立即执行函数 ,使其调用方式为 MyImage.setStr(),
// 如此既封装了内部又提供了对外的setStr接口 让调用者使用, 
// 更加规范了函数用法和调用者使用规范。
var proxyImage = function (src) {
    MyImage.setStr('XXXX/loading.gif');
    
    var img = new Image;
    img.onload = function () {
        MyImage.setStr(src);
    };
    img.src = src;
}
proxyImage('http://XXXXX/ys0yA0Nk.jpg')

2. 代理对象和本体接口的一致性

如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是本体都对外提供了 setSrc 方法,代理对象理应提供 setSrc 方法。

这样做有两个好处。
- 用户可以放心地请求代理,他只关心是否能得到想要的结果。
- 在任何使用本体的地方都可以替换成使用代理。
var proxyImage = (function (src) {
   
    var img = new Image();
    img.onload = function () {
        MyImage.setStr(this.src);
    };
   
    return {
        // 和本体接口保持一致
        setStr: function (src) {
             MyImage.setStr('XXXX/loading.gif');
             img.src = src;
        }
    }
})()

proxyImage.setSrc('http://XXXXX/ys0yA0Nk.jpg')

另外值得一提的是,如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”,代码如下:

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( 'XXX/loading.gif' );
        img.src = src;
    }
})();

proxyImage( 'http://XXXXX/A0Nk.jpg' );

2. 合并 HTTP 请求

在 Web 开发中,也许最大的开销就是网络请求。

假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同 步到另外一台备用服务器上面 我们先在页面中放置好这些 checkbox 节点:

<body>
    <input type="checkbox" id="1"></input>1
    <input type="checkbox" id="2"></input>2
    <input type="checkbox" id="3"></input>3
    <input type="checkbox" id="4"></input>4
    <input type="checkbox" id="5"></input>5
</body>

接下来,给这些 checkbox 绑定点击事件,并且在点击的同时往另一台服务器同步文件:

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

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

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

当我们选中 3 个 checkbox 的时候,依次往服务器发送了 3 次同步文件的请求。解决方案是,我们可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。

比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统,2 秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。

首先还记得 本体和代理模式要保持一致性?

synchronousFile 方法是可以直接调用的,所以 proxySynchronousFile 也应该是可以直接调用

代码如下:

var proxySynchronousFile = (function (id) {
    let timer, 
        cache = [] // 保存一段时间内需要同步的 ID
    return function (id) {
        cache.push(id)
        if(timer) return;
        timer = setTimeout(function () {
           synchronousFile(cache.join(','));
           clearTime(timer);
           cache.length = 0;
           timer = null;
        }, 2000)
    }
})()

proxySynchronousFile(id)

3. 计算乘积

先创建一个用于求乘积的函数:

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

    return a;
};

mult( 2, 3 ); // 输出:6
mult( 2, 3, 4 ); // 输出:24

现在加入代理函数:

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 ); // 输出:24
proxyMult( 1, 2, 3, 4 ); // 输出:24

4. 用于ajax异步请求数据

我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。

显然这里也可以引入代理函数,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。 具体代码不再赘述,可以自行实现。

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

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

/**************** 计算乘积 *****************/

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 cache = {};
    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 ) ); // 输出:24
alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10

总结

代理模式的变体种类非常多,限于篇幅及其在 JavaScript 中的适用性,就不一一详细展开说明了。

最重要的是 有一天我们不再需要代理对象,只需要改成代理函数名字改为本体对象, 除此之外用户不需要再做多余的操作,实现了这样调用机制才算是合格的代理模式。