我們終將得到想要的東西,只是透過不同的方式來取得。
當我們肚子餓的時候,可以自己走到巷口買食物,可以請家人順便帶食物回來,也可以使用美食外送的服務,等著食物上門。重點在於我們所需要的是食物,但是我們可以藉由不同的方式來達成目的。至於要選擇何種方式,就跟當時的情就相關,也許家人剛好要回家,也許想嘗試新服務,也許只是懶得出門。
我們希望透過代理模式,減少非必要的複雜計算,減少頻繁地客戶端與伺服器端的溝通及改善使用者體驗等情境。比較常見的是,當某件事是需要花費很多效能來執行,我們就會使用代理模式來改善效能。在使用者介面上,我們也常透過代理模式來呈現資料或圖片正在讀取的狀態。
一般而言,我們會使用代理模式來處理一些非同步的情況,或是改善一些效能上問題。目前常見的是虛擬代理及快取代理。
以讀取圖片為例子,由於某些圖片的尺寸比較大,在讀取完成之前,畫面中圖片的位置會是一片空白,在讀取完成時,圖片才會出現。在這過程當中,使用者會經歷過一段畫面中圖片從無到有的情況,此時往往使用者無法得知目前的狀態。如果要提升使用者經驗,我們可以提供所謂的佔位圖片(placeholder image),而佔位圖片非常小,因此很快就可以被讀取。所以當真正的圖片還在讀取時,佔位圖片可以先出現在原本的位置上,而當真正的圖片讀取完成後,再取代佔位圖片。
來看看用 Javascript 寫的例子:
// function without proxy
const theImage = (() => {
const imageNode = document.createElement("img");
document.body.appendChild(imageNode);
return {
setSrc(src) {
imageNode.src = src;
}
};
})();
theImage.setSrc("https://image.gallery.com/image1.png");
// function with proxy
const myImage = (() => {
const imageNode = document.createElement("img");
document.body.appendChild(imageNode);
return {
setSrc(src) {
imageNode.src = src;
}
};
})();
const proxyImage = (() => {
const img = new Image();
img.onload = function() {
myImage.setSrc(this.src);
};
return {
setSrc(src) {
myImage.setSrc("/image/placeholder.png");
img.src = src;
}
};
})();
proxyImage.setSrc("https://image.gallery.com/image1");
以一個 function 為例子,它的輸入是一連串的數字,它的輸出是這些數字的總和。比如說,輸入分別是 1, 2, 3,輸出則會是 6,只要輸入是一樣的,就會得到一樣的輸出。目前的問題在於,儘管我們剛執行完輸入為 1, 2, 3 的過程,並且已經得到某個結果,但如果下一次輸入還是 1, 2, 3,我們還是需要計算一次。當 function 的複雜程度遠高於這個例子,頻繁的運算可能造成效能方面的影響。
因此,如果我們能夠透過某種方式,將之前計算玩的結果紀錄下來,並賦予這個結果一個獨一無二的名稱,每次在真正執行複查的運算之前,都先去查詢是否已經有相對應的名稱,如果有,那就可以直接輸出這個值,如果沒有,就執行這個運算,之後紀錄運算後的結果,然後輸出這個結果。
藉由這個類似 cache 的模式,我們能夠避免重複的運算,提高整體的效能。
// function without proxy
const sum = (...numbers) => {
return numbers.reduce((acc, cur) => acc + cur, 0);
};
sum(1, 2, 3); // get result by calculating
sum(1, 2, 3); // get result by calculating
// function with proxy
const proxySum = (() => {
const cache = {};
return (...numbers) => {
const key = numbers.join("+");
if (cache[key] !== undefined) {
return cache[key];
}
const result = sum(...numbers);
cache[key] = result;
return result;
};
})();
proxySum(1, 2, 3); // get result by calculating
proxySum(1, 2, 3); // get result by cache
一般來說,我們在實作代理模式的時候,會讓所謂的介面一致。這樣的好處在於,一方面,確保最終的使用方式是一樣的,使用的人不會感到混淆,另外一方面,讓原來的方式跟後來的代理模式能夠輕易的切換。從上述的例子來看,theImage
跟 proxyImage
都有一個方法叫做 setSrc
,sum
跟 proxySum
都是使用方式相同的 function,我們可以稱呼這個情況為介面一致。因此當我們引入代理模式時,使用方式還是跟原來的一樣,當我們需要移除代理模式,也可以輕易地替換回原來的方式。
代理模式是一個普遍的設計模式,在一些常見的開源函式庫也可以看見其身影。這個模式的精神在於,我們可以透過一些方式,讓我們能夠更有效率地得到結果或提升使用者經驗。我們也不需要一開始就引入代理模式,可以在適當時間點再用此模式去改善情況即可。
作者:Ron