1. 程式人生 > >【學習筆記javascript設計模式與開發實踐(代理模式)----6】

【學習筆記javascript設計模式與開發實踐(代理模式)----6】

第6章代理模式

 代理模式是為一個物件提供一個代用品或佔位符,以便控制對它的訪問。

代理模式是一種非常有意義的模式,在生活中可以找到很多代理模式的場景。比如明星都有經濟人作為代理。如果想請明星來辦一場商業演出,只能聯絡他的經紀人,經紀人會把演出的細節和報酬都談好之後,再把合同交給明星。

代理模式的關鍵是,當客戶不方便直接訪問一個物件或不滿足需要的時候,提供一個替身物件來控制對這個物件的訪問,客戶實際上訪問的是替身物件。替身物件對請求做出一些處理之後,再把請求轉交給本體物件。

6.1 第一個例子-------屌絲追MM的故事

問題描述,屌絲(小明)追妹子,由於害羞沒有自己直接追妹子而委託了一個雙方都認識的妹子代理來完成此艱鉅的任務。一天,屌絲想送妹子一束花來表白,於是他找到妹子代理,要求妹子代理幫他向MM送花表白一下。結果……(代理妹子和MM好啦……??……)

於是我們可以用程式碼來描述一下追妹子的過程,

var Flower = function(){}
var xiaoming = {
 sendFlower:function(target){
   var flower = new Flower();
   target.receiveFlower(flower);
 }
};
var A = {
 receiveFlower:function(flower){
   console.log(‘收到花’+flower);
 }
}
xiaoming.sendFlower(A);
 
接下來我們引入B,即小明通過B來給A送花
var B = {
 receiveFlower:function(flower){
   A.receiveFlower(flower);
 }
}
xiaoming.sendFlower(B);

很顯然,執行結果跟第一段程式碼一致,到此我們就完成了一個最簡單的代理模式的編寫。也許讀者會疑惑、小明自己去送花和代理B幫小明送花,二者看起來並沒有本質的區別,引入一個代理物件看起來是把事情搞複雜而已。

的確,此處的代理模式毫無用處,它所做的只是把請求簡單地轉交給本體。但不管怎樣,我們開始引入了代理,這是一個不錯的起點。

現在我們改變故事背景設定,假設當A在心情好的時候收到花,小明表白成功機率有60%,而當A在心情差的時候收到花,小明表白的成功率無限趨近於0。

而小明剛認識A兩天,還無法辨別A什麼時候心情好。如果不合時宜地把花送給A,花被直接扔掉的可能性很大,這束花可是小明吃7天泡麵換來的。

但A朋友B卻很瞭解A,所以小明只管把花交給B,B會監聽A的心情變化,所以選擇A心情好的時候把花轉交給A,如下:

var Flower = function(){};
var xiaoming = {
  sendFlower:function(target){
    var flower =newFlower();
    target.receiveFlower(flower);
  }
};
 
var B={
recevieFlower:function(flower){
   A.listenGoodMood(function(){
      A.receiveFlower(flower);
   });
}
};
 
var A = {
  receiveFlower:function(flower){
    console.log(“收到花”+flower);
  },
  listenGoodMood:function(fn){
    setTimeout(function(){
       fn();
   },10000);//10秒之後A的心情變好
  }
}
 
xiaoming.sendFlower(B);

6.2 保護代理和虛擬代理

雖然這是一個虛擬的例子,但我們可以從中找到兩種代理模式的身影,代理B可以幫A過濾掉一些請求,比如送花的人中年齡太大的或者沒有寶馬的,這種請求就可以直接在代理B處被拒絕掉。這種代理叫作保護代理。A和B一個充當白臉,和個充當黑臉。白臉A繼續保持良好的女神形象,不希望直接拒絕任何人,於是找了黑臉B來控制對A的訪問。

假設在現實中花價格不菲,導致在程式世界裡,newFlower也是一個代價最貴的操作,那麼我們可以把newFlower的操作交給代理B去執行,代理B會選擇在A心情好時再執行new Flower,這是代理模式的另一種模式,叫作虛擬代理。虛擬代理會把開銷很大的物件,延遲到真正需要它的時候才去建立。

var B = {
 receiveFlower =function(flower){
   A.listenGoodMood(function(){
      var flower = new Flower();
      A.receiveFlower(flower);
  });
 }
}

保護代理用於控制不同許可權的物件對目標物件的訪問,但在javascript並不容易實現保護代理,因為我們無法判斷誰訪問了某個物件。而虛擬代理最常用的一種代理模式。

6.3 虛擬代理實現圖片預載入

在web中開發中,圖片預載入是和種常用的技術,如果直接給某個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://imgcache.qq.com/music/photo/k/xxx.jpg’);

如果網速很慢的情況下,在圖片沒有下載完之前,頁面上會出現一塊兒空白。

現在工始引入代理物件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:///c:/loading.gif’);
       img.src = src;
     }
   }
})()
 
proxyImage.setSrc(‘http://imgcache.qq.com/music/photo/k/xxx.jpg’);

現在我們通過proxyImage間接地訪問MyImage。proxyImage控制客戶對MyImage的訪問且在此過程中加入一些額外的操作,比如在真正的圖片載入好之前,先把img節點的src設定為一張本地loading圖片。

6.4 代理的意義

也許讀者會有疑問,不過是實現一個小小的圖片預載入的功能,即使不需要引入任何模式也能辦到,那麼引入代理模式的好處究竟在哪裡呢,下面我們來看一個更常見的圖片預載入函式

不用代理的預載入圖片函式

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(){
            imgNode.src = ‘file://c:/loading.gif’;
            img.src = src;
         }
    }
})();
MyImage.setSrc(“http://wwww…..”);

為了說明代理的意義:下面引入一個面向物件設計的原則—單一職責原則

它指的是:當一個類(通常包括物件和函式)而言,應該僅有一個引起它變化的原因。如果一個物件承擔了多項職責,就意味著這個物件將變得巨大,引起變化的原因可能會有多個。面向物件設計鼓勵將行為分佈到細粒度的物件中,如果一個物件承擔的職責過多,等於把這些職責耦合到了一起,這處耦合會導致脆弱和低內聚的設計。當變化發生時,設計可能會遭到意外的破壞。

職責被定義為:引起變化的原因。上段程式碼中的MyImage物件除了負責給img節點設定src外,還要負責預載入圖片。我們在處理其中一個職責時,有可能因為其強耦合性影響另外一個職責的實現。

另外,在面向物件的程式設計中,大多數情況下,若違反其他任何原責,同時將違反開放封閉原則。如果我們只是從網路上獲取一些停機體積很小的圖片,或者5年後的網速快到根本不需要預載入,我們可能希望把預載入操作這段程式碼從MyImage物件裡刪掉。這時候就不得不改動MyImage物件了。

實際上,我們需要的只是給img節點設定src,預載入圖片只是一個錦上添花功能。如果能把這個操作放在另一個物件裡面,自然是一個非常好的方法。於是代理的作用在這裡就體現出來了,代理負責預載入圖片,預載入的操作完成之後,把請求重新交給本體MyImage。

縱觀整個程式,我們並沒有改變或者增加MyImage的介面,但是通過代理物件,實際上給系統添加了新行為。這是符合開放—封閉原則。

6.5 代理和本體介面的一致性

上一節說到,如果有一天我們不再需要預載入,那麼就不再需要代理物件,可以選擇直接請求本體。其中關鍵是代理物件和本體都對外提供了setSrc方法,在客戶看來,代理物件和本體是一致的,代理接手請求的過程對於使用者來說是透明的,使用者並不清楚代理和本體的區別,這樣做有兩個好處。

l   使用者可以放心地請求代理,他只關心是否得到想要的結果

l   在任何使用本體的地方都可以替換成使用代理

在java等語言中,代理和本體都需要顯式地實現同一個介面,一方面介面保證了它們會擁有同樣的方法,另一方面面向介面程式設計迎合依賴倒置原則,通過介面進行向上轉型,從而避開編譯器的型別檢查,代理和本體將來可以被替換使用。

在javascript這種動態型別語言中,我們有時通過鴨子型別來檢測代理和本體是否都實現了setSrc方法,另外大多數時候甚至乾脆不做檢測,全部依賴程式設計師的自覺性,這對於程式的健壯性是有影響的。

6.6 虛擬代理合並http請求

先想一個這樣的場景:每週我們都要寫一份工作週報,週報要交給總監批閱。總監手下管理著150個員工,如果我們每個人直接把週報發給總監,那總監可能要把一整週的時間都花在檢視郵件上面。

現在我們把週報傳送給各自的組長,組長作為代理,把組內的成員的週報合併提煉一份後一次性地發給總監。這樣一來,總監的郵箱便清淨多了。

如:web開發中也許最大的開銷就是網路請求。假設我們在做一個檔案同步的功能,當我們選中一個checkbox的時候,它對應的檔案就會被同步到另外一臺伺服器上面。

接下來,給這些checkbox繫結點選事件,並且在點選的同時往另一臺伺服器同步檔案

var synchronousFile = function(id){
   console.log(‘開始同步檔案,id為”+id);
}
var checkbox =document.getElementByTagName(‘input’);
for(var i=0,c;c=checkbox[i++];{
  c.onclick =function(){
    if(this.checked===true){
      synchronousFile(this.id);
    }
  }//end click func
}

當我們選中3個checkbox的時候,依次往伺服器傳送3次同步檔案的請求。問題是點的很快,頻繁的網路請求將會帶來相當大的開銷。

解決方案是, 我們可以通過一個代理函式proxySynchronousFile來收集一段時間之內的請求,最後一次性發送給伺服器。比如我們等待2秒之內需要同步的檔案ID打包發給伺服器,如果不是對實時性要求非常高的系統,2秒的延遲不會帶來太大的副作用,卻大大減輕伺服器的壓力。

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; //清空ID集合
    },2000) //2秒發一次
  }
})();
 
var checkbox =document.getElementByTagName(‘input’);
for(var i=0,c;c=checkbox[i++];{
  c.onclick =function(){
    if(this.checked===true){
      proxySynchronousFile (this.id);
    }
  }
}

6.7 虛擬代理在惰性載入中的應用

作者曾經寫過一個mini控制檯的開源專案miniConsole.js,這個控制檯可以幫助開發者在IE瀏覽器上進行一些簡單的除錯工作:

呼叫方式,miniConsole.log(1);

這句話會在頁面中建立一個div,並且把log顯示在div裡面

miniConsole.js的程式碼量大根有1000行左右,也許我們並不想一開始就載入這麼大的檔案,因為也許並不是每個使用者都需要列印log。我們希望在有必要的時候才開始載入它,比如當用戶按下F2來主動喚出控制檯的時候。

在miniConsole.js載入之前,為了能夠讓使用者正常地使用裡面的api,通常我們的解決方案是用一個佔位的miniConsole代理物件來給使用者提前使用,這個代理物件提供給使用者的介面,跟實際的miniConsole是一親的

使用者使用這個代理物件來列印log的時候,並不會真正在控制檯內列印日誌,更不會在頁面中建立任何DOM節點,即使我們想這樣做也無能為力,因為真正的miniConsole.js還沒有被載入,於是我們可以把列印log的請求都包裹在一個函式裡面,這個包裝了請求的函式就相當於其它語言中命令模式中的Command物件。隨後這些函式將全部被放到快取佇列中,這些邏輯在miniConsole代理物件中完成實現的。等使用者按下F2喚出控制檯的時候,才開始載入真正的miniConsole.js的程式碼,載入完成之後將遍歷miniConsole代理物件中的快取函式佇列,同時依次執行它們。

未載入真正的miniConsole.js之前的程式碼如下

var cache = [];
var miniConsole = {
  log:function(){
      varargs = arguments;
      cache.push(function(){
        returnminiConsole.log.apply(miniConsole,args);
      })
  }
};
 
miniConsole.log(1);

當用戶按下F2時,開始載入真正的miniConsole.js程式碼如下

var handler = function(ev){
 if(ev.keyCode===113){
    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.addEventListener(‘keydown’,handler,false);
//miniConsole.js程式碼:
miniConsole = {…} //略

當然我們要保證在F2被重複按下時候,miniConsole.js只被載入一次,另外我們整理一下miniConsole代理物件的程式碼,使它成為一個標準的虛擬代理物件

var miniConsole = (function(){
var cache = [];
var handler =function(ev){
    if(ev.keyCode===113){
       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);//僅載入一次
 
    }
};
document.body.addEventListener(‘keydown’,handler,false);
 return {
      log:function(){
         var args = arguments;
         cache.push(function(){
               return miniConsole.log.apply(miniConsole,args);
         });
   }
 }
 
})();
 

//miniConsole.js略

6.8 快取代理

快取代理可能為一些開銷在的運算結果提供暫時的儲存,在下次運算時,如果傳遞進來的引數跟之前一致,則可以直接返回前面的運算結果。

如:

var mult =  function(){
 console.log(‘開始計算乘積’);
 var a = 1;
 for(vari=0,l=arguments.length;i<l;i++){
    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 incache){
     return cache[args];
   }
   return cache[args] = mult.apply(this,arguments);
   }
})();
 
proxyMult (1,2,3,4);// 24
proxyMult (1,2,3,4);// 24
 

6.8.2 快取代理用於ajax非同步請求資料

分頁快取

6.9用高階函式動態建立代理

能過傳入高階函式這種更加靈活的方式,可以為各種計算方法建立快取代理。現在這些計算被當作引數傳入一個專門用於建立快取代理的工廠中,這樣一來,我們就可以為乘法、加法、減法等建立快取代理,如下:

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 cateProxyFactory = 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);
var 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