javascript設計模式 之 3代理模式

zhaoyezi發表於2018-05-30

1 代理模式的定義

代理模式:為一個物件提供一個代理用品或者佔位符,以便控制對它的訪問。生活中有很多這樣的例子:

  • 請明星代言,實際是聯絡明星的經紀人,來建立商戶 與 明星的關係
  • 二手市場買房,實際聯絡的是中介公司來搭建買房與買房之間的橋樑 因此,可以這樣理解:當客戶不方便直接訪問一個物件或者不滿足需要的時候,提供一個替身物件來控制這個物件的訪問,客戶實際訪問的是替身物件。替身物件對請求做出一些處理後,再講請求轉交給本體物件。
    javascript設計模式 之 3代理模式

#2 代理example 我們這裡使用小明送花給小美來舉例子。

  • 小明找到他與小美兩個共同的好朋友小丑,讓她幫忙送花
  • 小丑不會直接送給小美,而是會找小美心情好的時候送,增加成功機率
  • 小美收到鮮花接收了小明
function Flower(name) {
    this.name = name;
}
// 小明使用物件導向的方式建立
var Xiaoming = function() {

}
Xiaoming.prototype.sendFlower = function(xiaochou) {
    var flower = new Flower('玫瑰花');
    xiaochou.recieveFlower(flower);
}

// 小丑使用javascript的物件即類的概念建立
var xiaochou = {
    recieveFlower: function(flower) {
        var goodDay = xiaomei.listenerGoodDay();
        if (goodDay) {
            xiaomei.recieveFlower(flower);
        } else {
            console.log('小美心情不好,因此沒有送');
        }
    }
};

var xiaomei = {
    // 小美心情隨機
    listenerGoodDay: function() {
        var random = parseInt(Math.random() * 10);
        return random % 2 === 0 ? true : false;
    },
    recieveFlower: function(flower) {
        console.log(`小美看到是${flower.name}, 接收了小明`);
    }
};

var xiaoming = new Xiaoming();
xiaoming.sendFlower(xiaochou);
複製程式碼

3 保護代理和虛擬代理

從上面的例子中,我們可以知道xiaochou可以幫助小美過濾掉小美不喜歡的型別:例如沒有寶馬車,長得矮,長得醜的,小丑可以直接拒絕。這種代理叫做保護代理。 假如鮮花價格比較昂貴,new Flower()代價很高,可以把new Flower()操作交給代理xiaochou,她可以根據xiaomei的心情好的時候再去new Flower(),避免鮮花枯萎浪費。這叫做虛擬代理。虛擬代理把一些開銷很大的物件,延遲到真正需要它的時候才建立。

var xiaochou = {
    recieveFlower: function() {
        var goodDay = xiaomei.listenerGoodDay();
        if (goodDay) {
            var flower = new Flowe();
            xiaomei.recieveFlower(flower);
        } else {
            console.log('小美心情不好,因此沒有送');
        }
    }
};
複製程式碼

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

4 虛擬代理載入圖片

在web開發中,圖片預載入是一種常用的技術。如果給某個img標籤直接新增src屬性,如果某個圖片過大或者網速差,則圖片的位置會有一段時間是空白的。常見的做法是在載入圖片完畢之前使用一個loading圖片。等到圖片載入完畢後替換。下面我們使用虛擬代理來完成。

  • 建立一個本體ImageSrc方法
  • 建立一個代理ProxyImageSrc方法
// 本體
var ImageSrc = (function () {
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    };
})();

// 代理
var ProxyImageSrc = (function ProxyImageSrc() {
    var img = new Image();
    // 頁面載入完畢後,將img上存放的src設定到ImageSrc物件上
    img.onload = function() {
        ImageSrc.setSrc = this.src;
    };
    return {
        setSrc: function(src) {
            // 載入中使用loading圖片
            ImageSrc.setSrc('http://img4.imgtn.bdimg.com/it/u=1972873509,2904368741&fm=27&gp=0.jpg'); 
            img.src = src;
        }
    };
})();
// 呼叫,設定src
ProxyImageSrc.setSrc('http://img05.tooopen.com/images/20150820/tooopen_sy_139205349641.jpg');
複製程式碼

其實實現該功能,即使不引入任何模式也能夠辦到,例如通過下面的方式:

var ImageSrc = (function() {
    var imgNode = document.createElement('img');   
    document.body.appendChild(imgNode);

    var img = new Image();
    img.onload = function(src) {
        imgNode = this.src;
    }

    return {
        setSrc: function(src) {
            imgNode.src = 'loading.png';
            img.src = src;
        }
    }
})();

ImageSrc.setSrc('complete.png');
複製程式碼

該物件承擔了多項職責。負責了圖片的預載入處理,圖片完成後的設定。違背了物件導向設計的原則單一職責原則。當物件承擔更多職責,意味著物件會變的巨大,引起它變化的情況會變的很多。在設計程式碼的時候應該高內聚低耦合,儘量符合單一職責。

5 代理和本體介面一致

當某一天我們不再需要預載入,那麼就不需要設定代理物件,可以直接請求本體。其中關鍵點是:代理物件與本體都對外提供setSrc。在客戶看來,本體與代理是一致的。代理接收的請求過程對於使用者來說是透明的,使用者不清楚代理與本體的區別,這樣的好處是:

  • 使用者可以放心請求代理,他只需要關心是否能夠得到預期的結果
  • 在任何使用本體的地方都能夠替換成為使用代理
    在java語言中,代理和本體需要顯示實現同一個介面,在保證它們擁有同樣的方法的同時,面向介面程式設計迎合依賴倒置原則,通過介面向上轉型,避開編譯器的型別檢查,使得代理與本體將來能夠被替換使用。 javascript語言是動態型別語言,我們可以通過duck typing來檢查代理和本體是否擁有setSrc方法,而大多數時候不會檢查。

6 虛擬代理合並http請求

下圖是我們在網頁中上傳檔案的列表,當點選checkbox則上傳檔案。加入手速比較快的人,1秒鐘點選4,5次是不成問題的。那麼相當於1秒以內,就需要與伺服器進行4次請求,頻繁的網路請求會帶來相當大的開銷。因此我們可以等待2秒以後,將2秒以內點選了的檔案一起傳送給伺服器,如果對實時要求不是非常高的系統,2秒的延遲不會出現太大的副作用,卻能大大減輕伺服器的壓力。

javascript設計模式 之 3代理模式

// 本體,同步檔案的方法
var synchronousFile = function(ids) {
    console.log(`開始同步${ids} 檔案`);
}
// 代理,非同步呼叫方法
var proxySynchronousFile = (function() {
    var cacheIds = [];
    var timer = null;
    return function(id) {
        cacheIds.push(id);
        if (timer) {
            return;
        }
        timer = setTimeout(function() {
            synchronousFile(cacheIds.join(',')); // 2秒後向伺服器傳送請求
            clearTimeout(timer);
            timer = null;
            cache.length = 0; // 清空集合。
        },2000);
    }
})();

// 註冊上傳檔案點選事件
var checkboxs = document.getElementsByTagName('input');
checkboxs.map(function(checkbox) {
    checkbox.onclick = function(event) {
        if (this.checked === true) {
            proxySynchronousFile(this.id);
        }
    }
});
複製程式碼

7 虛擬代理實現快取

快取代理可以為一些開銷大的運算結果提供暫時儲存,在下次傳遞進來的引數如果與之前某一次的一致,直接返回儲存的結果。上面儲存一個通過ajax請求獲取的資料。

// 本體
var getAjaxData = function(arg) {
    $.ajax('url', function(data) {
        return data;
    });
}

// 代理
var proxyData = (function() {
    var cache = [];
    return function(arg) {
        if (arg in cache) {
            return cache[arg];
        } else {
            return cache[arg] = getAjaxData(arg);
        }
    }
})();

// 呼叫,第一次呼叫會將資料存放在cache中,第二次呼叫會直接使用快取中的資料
var returnData = proxyData(1);
var retrunData2 = proxyData(1);
複製程式碼

8 用高階函式建立代理

通過傳入高階函式這種更加靈活的方式,可以為各種計算方法建立快取代理。

// 本體
var mult = function() {
    return [].reduce.call(arguments, function(x, y) {
        return x * y;
    }, 1)
}
// 本體
var plus = function() {
    return [].reduce.call(arguments, function(x, y) {
        return x + y;
    }, 0)
}

// 代理
var calculateProxy = function(fn) {
    var cache = [];
    return function() {
        var args = [].join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        } else {
            return cache[args] = fn.apply(this, arguments);
        }
    };
};

// 呼叫
var proxyMult = calculateProxy(mult);
var proxyPlus = calculateProxy(plus);
console.log(proxyMult(2, 4, 5));
console.log(proxyPlus(2, 4, 5));
複製程式碼

9 小結

代理模式包括許多小分類,在javascript中常用的是虛擬代理與快取代理。雖然代理模式非常有用,但是我們在編寫業務程式碼的時候,往往不需要去預先猜測是否需要使用代理模式。而是當真正發現不方便直接訪問某個物件的時候,再編寫代理不遲。

相關文章