說透設計模式-代理模式與Proxy

LeonVincent發表於2020-04-04

什麼是代理模式

代理模式在生活中非常的常見,比如你想賣房子有房產代理人,明星有經紀人可以代理他們的一些事物,外賣小哥也在商家和你之間作為一種代理人,把外賣送到你的手上...

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

說透設計模式-代理模式與Proxy

實現一個簡單的設計模式 -李四追女神的故事

四月是春花爛漫的季節,張楚也唱到孤獨的人是可恥的,這不在一個明媚的早晨,李四在公園裡遇見了讓他小鹿亂撞的女孩,他立馬就走不動路了,於是他鼓足勇氣上去搭訕要微信,迎接他的確是一頓大耳刮子。

class WxNumber {...}
class LeeSi {
    askWx(target) {
        const res = target.getRequest()
        console.log(res)
    }
}
class Beauty {
    getRequest() {
        return '一頓大耳刮子'
    }
}

const lee = new LeeSi()
lee.askWx(new Beauty())
複製程式碼

第一次的求愛不成功,但是李四並不氣餒,因為他是一個痴情種子。他多方面打聽到原來他的一個好朋友張代麗原來是女神的閨蜜。於是他請張代麗出馬,幫他要微訊號。

class WxNumber {...}
const wxNum = new WxNumber()
class LeeSi {
    askWx(target) {
        const res = target.getRequest()
        console.log(res)
    }
}
class ZhangProxy {
    getRequest() {
        const beauty = new Beauty()
        return beauty.getRequest()
    }
}
class Beauty {
    getRequest() {
        return wxNum
    }
}
const lee = new LeeSi()
lee.askWx(new ZhangProxy())

複製程式碼

就這樣李四終於得到了女神的微信。

雖然是一個簡單的例子,但是我們可以從中得到兩種代理模式的身影,保護代理和虛擬代理。

  • 保護代理:雖然李四是張代麗的朋友,可若是李四是一個渣男,張代麗也不會幫李四要自己閨蜜的微訊號。所以就過濾了一部門渣男的請求。
  • 虛擬代理:虛擬代理把一些開銷很大的物件,延遲到請求的時候才開始建立。比如這樣:
class Beauty {
    getRequest() {
        return new WxNumber()
    }
}
複製程式碼

再來看一個例子:

虛擬代理模式實現圖片預載入

在一些網站中,有時候網速不好的時候,網頁中的圖片會在開啟的時候出現一段時間的白屏,這時候我們一般會通過預載入的技術,先在圖片的位置放置一張loading圖,等到圖片載入好了的時候,再將圖片顯示出來。

我們先來實現一個本體類

class MyImg {
    constructor() {
        this.imgNode = document.createElement('img')
    }
    addImgNode() {
        document.body.appendChild(this.imgNode)
    }
    setSrc(src) {
        this.imgNode.src = src
        this.addImgNode()
    }
}
複製程式碼

然後我們引入一個代理物件ProxyImg,通過這個代理物件,在圖片被真正載入好之前,頁面將出現一張佔點陣圖來告訴使用者正在載入。

class ProxyImg {
    constructor() {
        this.myImg = new MyImg()
        this.img = new Image
        this.src = null
        this.img.onload = () => {
            this.myImg.setSrc(this.src)
        }
    }
    setSrc(src) {
        this.MyImg.setSrc('xxx/xxx/aa.gif')
        this.img.src = src
        this.src = src
    }
}

複製程式碼

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

也許有人就會說,不過是一個小小的圖片預載入的功能,即使不使用任何的設計模式,我分分鐘手擼一個出來。那麼代理模式的作用到底體現在什麼地方呢?

我覺得的是一個物件導向設計的原則:單一職責原則

單一職責原則指的是,就一個類而言,應該僅有一個 引起它變化的原因。如果一個物件承擔了多項職責,就意味著這個物件將變得巨大,引起它變化的原因可能會有多個

比如我們不用代理實現一個圖片預載入的類:

class MyImg {
    constructor() {
        this.imgNode = document.createElement('img')
        this.img = new Image
        this.img.onload = this.loadImg()
        this.src = null
    }
    loadImg() {
        this.imgNode.src = this.src
    }
    addImgNode() {
        document.body.appendChild(this.imgNode)
    }
    setSrc(src) {
        this.imgNode.src = 'xxx/xxx/aa.gif'
        this.img.src = src
        this.src = src
        this.addImgNode()
    }
}
複製程式碼

這段程式碼除了給img節點設定src外,還要負責預載入圖片。當我們處理其中一個職責的時候,這就有可能因為其強耦合性影響另一個職責的實現。

還有一種情況,一些別的什麼原因,可能5年後網速已經快的上天了,我們可以不用再進行代理了,那我們就直接去掉代理這一層就可以,也不用再去改MyImg這個類,這就又符合開放-封閉的原則了。

從這幾個例子中我們可以看到一個規律,那就是代理暴露的方法名,和本體暴露的方法名是一致的。代理接收請求的過程對於使用者來說是透明的,使用者並不知道這其中的區別,這樣做也就可以做到在使用本體的地方都可以替換成使用代理。

快取代理

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

我們先來實現一個用於求乘積的類,然後加入快取代理

class Mult {
  cal() {
    console.log('開始計算')
    const res = [].reduce.call(arguments, ((cur, next) => {
      return cur*next
    }), 1)
    return res
  }
}

class ProxyMult {
  static cache = {}
  constructor() {
    this.mult = new Mult()
  }
  cal() {
    let args = [].join.call(arguments, ',')
    if (args in ProxyMult.cache) {
      return ProxyMult.cache[args]
    }
    return ProxyMult.cache[args] = this.mult.cal.apply(this, arguments)
  }
}
const proxyMult = new ProxyMult()

console.log(proxyMult.cal(1,2,3,4))
console.log(proxyMult.cal(1,2,3,5))
console.log(proxyMult.cal(1,2,3,4))

複製程式碼

說透設計模式-代理模式與Proxy

可以很清楚的看到Mult類中的cal方法只執行了兩次,所以快取生效。

用高階函式動態建立代理

我們寫程式碼的過程要時刻問問自己,什麼是一直在變的,什麼是不變的。變化的我們儘量遵循單一職責的原則實現分別的邏輯,不變的部分我們爭取封裝起來,讓他遵循開放-封閉原則。

開放封閉原則說的是對擴充套件開放,對修改封閉

所以我們可以通過傳入高階函式這種更加靈活的方式,可以為各種計算方法建立快取代理,所以計算方法就是可變的。 我們再來建立一個計算加和的類和建立快取代理的工廠


class Mult {
  constructor() {
    this.name = 'mult'
  }
  cal() {
    console.log('開始計算Mult')
    return [].reduce.call(arguments, ((cur, next) => {
      return cur*next
    }), 1)
  }
}

class Plus {
  constructor() {
    this.name = 'plus'
  }
  cal() {
      console.log('開始計算Plus')
      return [].reduce.call(arguments, ((cur, next) => {
          return cur + next 
      }), 0)
  }
}

class CreateProxyFactory {
  static cache = {}
  constructor(fn) {
    this.fn = new fn()
    console.log(this.fn)
  }
  cal() {
    const args = [].join.call(arguments, `,${this.fn.name}`)
    if (args in CreateProxyFactory.cache) {
      return CreateProxyFactory.cache[args]
    }
    return CreateProxyFactory.cache[args] = this.fn.cal.apply(this, arguments)
  }
}


const proxyMult = new CreateProxyFactory(Mult)
const ProxyPlus = new CreateProxyFactory(Plus)

console.log(proxyMult.cal(1,2,3,4))
console.log(proxyMult.cal(1,2,3,4))
console.log(ProxyPlus.cal(1,2,3,4))
console.log(ProxyPlus.cal(1,2,3,4))

複製程式碼

說透設計模式-代理模式與Proxy

代理模式有很多種類,但是在js中的適用性都不太高,有興趣的可以單獨去找資料拿來學習。

  • 防火牆代理
  • 遠端代理
  • 智慧引用代理
  • 寫時複製代理

Proxy

下面我們來說說ES6新加的這個Api:Proxy,光看名字我們就能想到為什麼寫代理模式的時候也要講一下這個Proxy。

在MDN上對於Proxy的解釋是:

 Proxy 物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)。
複製程式碼

首先它的語法是:

let p = new Proxy(target, handler)

分別解釋一下:

  • target 是你要代理的物件.它可以是JavaScript中的任何合法物件.如: (陣列, 物件, 函式等等)
  • handler是你要自定義操作方法的一個集合.
  • p 是一個被代理後的新物件,它擁有target的一切屬性和方法.只不過其行為和結果是在handler中自定義的,當然它也可以為target新增屬性.

在我們正式介紹 Proxy 之前,建議你對 Reflect 有一定的瞭解,它也是一個 ES6 新增的全域性物件,詳細資訊請參考 MDN Reflect

Dont BB, Show me Code

const cat = {
  color: 'yellow',
  age: 3,
  isGirl: true
}

const handle = {
  get(target, key, value) {
      if (key === 'age') {
          console.log(`I'm ${target[key]}`)
      }
      return Reflect.get(target, key, value)
  },
  set(target, key, value) {
      if (key === 'isGirl') {
        console.log(`I don't want to be transgender`)
        return Reflect.set(target, key, `I don't want to be transgender`)
      }
      return Reflect.set(target, key, value)
  }
}

const newCat = new Proxy(cat, handle)
newCat.age  // I'm 3
newCat.isGirl = false // I don't want to be transgender
newCat.age = 6
console.log(newCat)

複製程式碼

說透設計模式-代理模式與Proxy

什麼在handler,定義get和set這兩個函式名之後就代理物件上的get和set操作了呢? 實際上handler本身就是ES6所新設計的一個物件.它的作用就是用來自定義代理物件的各種可代理操作。它本身一共有13中方法,每種方法都可以代理一種操作.其13種方法如下:

handler.getPrototypeOf()

// 在讀取代理物件的原型時觸發該操作,比如在執行 Object.getPrototypeOf(proxy) 時。

handler.setPrototypeOf()

// 在設定代理物件的原型時觸發該操作,比如在執行 Object.setPrototypeOf(proxy, null) 時。

handler.isExtensible()

// 在判斷一個代理物件是否是可擴充套件時觸發該操作,比如在執行 Object.isExtensible(proxy) 時。

handler.preventExtensions()

// 在讓一個代理物件不可擴充套件時觸發該操作,比如在執行 Object.preventExtensions(proxy) 時。

handler.getOwnPropertyDescriptor()

// 在獲取代理物件某個屬性的屬性描述時觸發該操作,比如在執行 Object.getOwnPropertyDescriptor(proxy, "foo") 時。

handler.defineProperty()

// 在定義代理物件某個屬性時的屬性描述時觸發該操作,比如在執行 Object.defineProperty(proxy, "foo", {}) 時。

handler.has()

// 在判斷代理物件是否擁有某個屬性時觸發該操作,比如在執行 "foo" in proxy 時。

handler.get()

// 在讀取代理物件的某個屬性時觸發該操作,比如在執行 proxy.foo 時。

handler.set()

// 在給代理物件的某個屬性賦值時觸發該操作,比如在執行 proxy.foo = 1 時。

handler.deleteProperty()

// 在刪除代理物件的某個屬性時觸發該操作,比如在執行 delete proxy.foo 時。

handler.ownKeys()

// 在獲取代理物件的所有屬性鍵時觸發該操作,比如在執行 Object.getOwnPropertyNames(proxy) 時。

handler.apply()

// 在呼叫一個目標物件為函式的代理物件時觸發該操作,比如在執行 proxy() 時。

handler.construct()

// 在給一個目標物件為建構函式的代理物件構造例項時觸發該操作,比如在執行new proxy() 時。

複製程式碼

我把把這些方法類似的理解為一些鉤子函式,有些方法還是挺好玩的,大家可以仔細研究一下。

比如我們可以把上面的快取代理換成Proxy的方式來實現:

class Mult {
  cal() {
    console.log('開始計算Mult')
    return [].reduce.call(arguments, ((cur, next) => {
      return cur*next
    }), 1)
  }
}

const target = new Mult()
const newproxy = new Proxy(target.cal, {
  apply(target, key, value) {
    target.cache = target.cache || {}
    let args = [].join.call(value, ',')
    if (args in target.cache) {
      return target.cache[args]
    }
    return target.cache[args] = target.apply(this, value)
  }
})
console.log(newproxy(1,2,3,4))
console.log(newproxy(1,2,3,4))
console.log(newproxy(1,2,3,4,5))
console.log(newproxy)

複製程式碼

說透設計模式-代理模式與Proxy

可以看到也能實現同樣的功能,Proxy通過組合使用可以實現各種各樣的功能,當然我列出了幾種比較常見的

  • 攔截和監視外部對物件的訪問
  • 降低函式或類的複雜度
  • 在複雜操作前對操作進行校驗或對所需資源進行管理

大家可以自己去研究一下,篇幅有限,不再贅述。

如果有不對或者模糊的地方,歡迎大家指正,感謝閱讀。

相關文章