iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 13
0

我們終將得到想要的東西,只是透過不同的方式來取得。

生活實例

當我們肚子餓的時候,可以自己走到巷口買食物,可以請家人順便帶食物回來,也可以使用美食外送的服務,等著食物上門。重點在於我們所需要的是食物,但是我們可以藉由不同的方式來達成目的。至於要選擇何種方式,就跟當時的情就相關,也許家人剛好要回家,也許想嘗試新服務,也許只是懶得出門。

為什麼需要代理模式

我們希望透過代理模式,減少非必要的複雜計算,減少頻繁地客戶端與伺服器端的溝通及改善使用者體驗等情境。比較常見的是,當某件事是需要花費很多效能來執行,我們就會使用代理模式來改善效能。在使用者介面上,我們也常透過代理模式來呈現資料或圖片正在讀取的狀態。

代理模式的應用

一般而言,我們會使用代理模式來處理一些非同步的情況,或是改善一些效能上問題。目前常見的是虛擬代理快取代理

虛擬代理

以讀取圖片為例子,由於某些圖片的尺寸比較大,在讀取完成之前,畫面中圖片的位置會是一片空白,在讀取完成時,圖片才會出現。在這過程當中,使用者會經歷過一段畫面中圖片從無到有的情況,此時往往使用者無法得知目前的狀態。如果要提升使用者經驗,我們可以提供所謂的佔位圖片(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

注意之處

一般來說,我們在實作代理模式的時候,會讓所謂的介面一致。這樣的好處在於,一方面,確保最終的使用方式是一樣的,使用的人不會感到混淆,另外一方面,讓原來的方式跟後來的代理模式能夠輕易的切換。從上述的例子來看,theImageproxyImage 都有一個方法叫做 setSrcsumproxySum 都是使用方式相同的 function,我們可以稱呼這個情況為介面一致。因此當我們引入代理模式時,使用方式還是跟原來的一樣,當我們需要移除代理模式,也可以輕易地替換回原來的方式。

總結

代理模式是一個普遍的設計模式,在一些常見的開源函式庫也可以看見其身影。這個模式的精神在於,我們可以透過一些方式,讓我們能夠更有效率地得到結果或提升使用者經驗。我們也不需要一開始就引入代理模式,可以在適當時間點再用此模式去改善情況即可。

作者:Ron


上一篇
[Design Pattern] Singleton 單例模式
下一篇
[Design Pattern] Flyweight 輕量模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言