代理模式

在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。
在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。


代理模式的定义与特点

代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
代理模式的主要优点有:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性

其主要缺点是:

  • 代理模式会造成系统设计中类的数量增加
  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
  • 增加了系统的复杂度;
那么如何解决以上提到的缺点呢?答案是可以使用动态代理方式


代理模式的结构

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面来分析其基本结构和实现方法。

代理模式的主要角色如下。

  1. 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  2. 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  3. 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。


代理模式的实现

首先通过一个例子来简单的了解了解,故事是这样的...

以下故事纯属虚构,不要当真

大家都知道三顾茅庐(不知道的百度一下)吧,诸葛亮何许人也,厉害的不要不要的,名声在外啊。好巧不巧刘备知道了,刘备心想:“这么厉害的人跟着我,岂不美哉,统一三国起步指日可待”,于是,刘备带着礼物就去请人家了。正常流程应该这样:

// 刘备
let bei = {
  // 邀请
  invite(){
    liang.reception('草鞋')
  }
}
// 亮
let liang = {
   // 收到礼物
   reception(gift){
     console.log('亮收到礼物:' + gift)
   }
}
// 调用方法
bei.invite()

但是呢,事实不是这样的,诸葛亮心想:“刘备一卖草鞋的就想见我?”,于是呢,刘备只见到了门童:

// 刘备
let bei = {
  // 邀请
  invite(){
    mentong.reception('草鞋')
  }
}
// 门童
let mentong = {
  // 接收礼物
  reception(gift){
    console.log('门童收到礼物:' + gift)
    // 给诸葛亮
    liang.reception('草鞋')
  }
}
// 亮
let liang = {
  // 接收礼物
  reception(gift){
    console.log('亮收到礼物:' + gift)
  }
}
// 调用方法
bei.invite()

所以,刘备就只能把礼物给了门童,门童在交给了诸葛亮,然后诸葛亮一看,好家伙,草鞋。。。

到此可以看成一个简单的代理了


保护代理和虚拟代理

保护代理

诸葛亮收到草鞋后也是无语,然后叫来门童告诉他:“以后呢,送草鞋的,你就不用给我了,自己看着处理就好了”,门童心领神会,表示ojbk

// 门童
let mentong = {
  // 接收礼物
  reception(gift){
    console.log('门童收到礼物:' + gift)
    if(gift !== '草鞋'){
      // 给诸葛亮
      liang.reception(gift)
    }
  }
}

通过代理(门童)来处理一些不必要的东西,过滤掉无用信息,这可以理解为 保护代理

但在 JavaScript 并不容易实现保护代理,因为我们无法判断谁访问了某个对象。

虚拟代理

话题又回到刘备这,刘备这连着送礼都两天了,也见不到人。有人就给他出了个方法。于是啊就去找到门童说:“小兄弟这钱你拿着,你帮我买点礼物给诸葛先生”,门童也是诧异什么时候变聪明了

// 门童
let mentong = {
  // 接收礼物
  reception(){
    // 拿钱去买礼物
    let book = new Book()
    // 给诸葛亮
    liang.reception(book)
  }
}

诸葛亮这回挺开心,于是就答应见刘备了(~ ̄▽ ̄)~

这可以理解为 虚拟代理


虚拟代理实现图片预加载

注:I know 这个例子大家可能都看过了,因为没有想到更好的例子(想到了更改补上),但是这个例子我会一行一行讲解清楚的o( ̄▽ ̄)ブ

平时由于网络的不佳,导致图片出来前会有一片空白。所以我们限用一张 loading 图片占位,在异步方式加载图片

没用代理

// 创建一个本体对象
var myImage = (function(){
  // 创建标签
  var imgNode = document.createElement( 'img' );
  // 添加到页面
  document.body.appendChild( imgNode );
  return {
    // 设置图片的src
    setSrc: function( src ){
      // 更改src
      imgNode.src = src;
    }
  }
})();

myImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

引入代理对象

// 创建一个本体对象
var myImage = (function(){
  // 创建标签
  var imgNode = document.createElement( 'img' );
  // 添加到页面
  document.body.appendChild( imgNode );
  return {
    // 设置图片的src
    setSrc: function( src ){
      // 更改src
      imgNode.src = src;
    }
  }
})();

// 创建代理对象
var proxyImage = (function(){
  // 创建一个新的img标签
  var img = new Image;
  // img 加载完成事件
  img.onload = function(){
    // 调用 myImage 替换src方法
    myImage.setSrc( this.src );
  }
  return {
    // 代理设置地址
    setSrc: function( src ){
      // 预加载 loading
      myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
      // 赋值正常图片地址
      img.src = src;
    }
  }
})();

proxyImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

现在我们可以通过 proxyImage 间接地访问 MyImage 。proxyImage 控制了客户对 MyImage 的访问,并
且在此过程中提前把 img 节点的 src 设置为了一张本地的 loading 图片


现在再来看看不用代理来实现预加载

// 创建一个本体对象
var MyImage = (function(){
  // 创建标签
  var imgNode = document.createElement( 'img' );
  // 添加到页面
  document.body.appendChild( imgNode );
  //  创建一个新的img标签
  var img = new Image;
  // img 加载完成
  img.onload = function(){
    // 替换地址
    imgNode.src = img.src;
  };
  return {
    // 设置地址
    setSrc: function( src ){
      // 本地 loading 图片地址
      imgNode.src = 'file:// /C:/Users/svenzeng/Desktop/loading.gif';
      // 赋值正常图片地址
      img.src = src;
    }
  }
})();

MyImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

现在来看看没有用代理的代码

  • 违反了单一原则。MyImage除了要负责img节点的设置,还要负责预加载图片。这导致在处理其中一个职责时会因其强耦合性影响另一个职责。
  • 违反了开闭原则。倘若以后要去掉预加载,只能去更改MyImage对象,这不符合开闭原则。


代理和本体接口的一致性

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

  • 用户可以放心地请求代理,他只关心是否能得到想要的结果。
  • 在任何使用本体的地方都可以替换成使用代理。(是不是有点里氏替换的味道)
// 预加载
proxyImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

// 不用预加载
myImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );


缓存代理

再举一个经典的例子啊(没想到别的,以后想到了再换(~ ̄▽ ̄)~)

乘积函数

var mult = function(){
  console.log( '开始计算乘积' );
  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 ); // 输出:24 
proxyMult( 1, 2, 3, 4 ); // 输出:24

我们也可以动态创建代理

/**************** 计算乘积 *****************/
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开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。


链接: https://fly63.com/course/27_1264