JavaScriptのプロキシパターンのパワー



私のキャリアの後半に学んだより興味深いパターンの一つはProxy .
プロキシパターンの例を探すときには、さまざまな実装の実装を見ることができます.プロキシが1ユースケースに限定されないためです.つのプロキシは、バリデータとして機能するかもしれませんが、もう一方はパフォーマンスの向上などに興味があります.
プロキシを利用することによって、ラップされた関数が呼ばれる前に、ラップされたメソッドの中に追加のロジックを追加するまで、そのメソッド(またはプロパティ)がまったく同じである元のオブジェクトと同じ機能を持つ既存のオブジェクトをラップすることになります.これは完全に外の世界に隠されたプロセスです、そして、この呼び出しは常に呼び出し元に同じように見えます.
言い換えれば、プロキシはオブジェクトと実際のオブジェクトそのもののクライアントの間に適切に座ります.これは、“保護者”として行動するか、これまでにこれを知っている呼び出し元なしでキャッシュのようなカスタムロジックを加えることができるところです.このため、時々、仲介者と呼ばれることができます.また、飾り付けパターンの別の形態として分類することもあるが、いくつかの違いがある.
このポストでは、JavaScriptのプロキシデザインパターンのパワーを超えて行くとどのように有益なそれはあなたの次のアプリケーションになることができるいくつかの例を通過します.
JavaScriptがネイティブに追加されてからProxy パターンを実装するクラスは、Proxy クラスではなく、カップルのバニラ実装後のパターンを示すために.

デコレーター対プロキシ間の違い


デコレータのパターンでは、デコレータの主な責任は、それが包装されているオブジェクト(または“装飾”)を強化することです.一方、プロキシは、よりアクセシビリティを持ち、オブジェクトを制御します.
プロキシはラップしているオブジェクトを強化するか、外の世界からのアクセスを制限するなどの他の方法でそれをコントロールするのを選ぶかもしれません、しかし、装飾者は強化を通知して、適用します.
賢明な責任感は明確です.エンジニアは通常、新しい動作を追加するためにデコレーターを使用したり、古いクラスやレガシークラスのアダプタの形式として、クライアントが知っているかもしれないが、同時に気にしないインターフェイスを返す.プロキシは通常、クライアントが同じオブジェクトを使っていないと仮定することができる同じインターフェースを返すことを意図します.

検証/ヘルパー


プロキシパターンの最初の実装はここで確認されます.
この例では、入力を検証し、プロパティをプロテクトし、間違ったデータ型を設定する方法として実装されているパターンを示します.呼び出し元は常に、元のオブジェクトで動作していると仮定しなければなりません.プロキシがラップしているオブジェクトのシグネチャやインターフェイスを変更してはいけません.
class Pop {
  constructor(...items) {
    this.id = 1
  }
}

const withValidator = (obj, field, validate) => {
  let value = obj[field]

  Object.defineProperty(obj, field, {
    get() {
      return value
    },
    set(newValue) {
      const errMsg = validate(newValue)
      if (errMsg) throw new Error(errMsg)
      value = newValue
    },
  })

  return obj
}

let mello = new Pop(1, 2, 3)

mello = withValidator(mello, 'id', (newId) => {
  if (typeof newId !== 'number') {
    return `The id ${newId} is not a number. Received ${typeof newId} instead`
  }
})

mello.id = '3'
この例では、オブジェクトのフィールドを検証する単純ヘルパーを示していますTypeError 検証が失敗した場合に例外です.
プロキシはgetter and setterid プロパティを設定し、設定しようとする値を許可または拒否します.
Proxy クラスは以下のように実装できます:
const withValidator = (obj, field, validate) => {
  return new Proxy(obj, {
    set(target, prop, newValue) {
      if (prop === field) {
        const errMsg = validate(newValue)
        if (errMsg) throw new TypeError(errMsg)
        target[prop] = newValue
      }
    },
  })
}

let mello = new Pop(1, 2, 3)

mello = withValidator(mello, 'id', (newId) => {
  if (typeof newId !== 'number') {
    return `The id ${newId} is not a number. Received ${typeof newId} instead`
  }
})

mello.id = '3'
バリデータは完全に動作します.
TypeError: The id 3 is not a number. Received string instead

クリップボード


このセクションは、ブラウザーがサポートすることを確実にすることによって、ユーザーのクリップボードにテキストの選択をコピーするとき、古いブラウザーをサポートする方法としてプロキシを使用することになりますNavigator.clipboard API.そうでなければ、それは使用に戻るでしょうexecCommand 選択をコピーします.
また、クライアントはメソッドメソッドを呼び出しているオブジェクトが元のオブジェクトであると常に仮定します.
const withClipboardPolyfill = (obj, prop, cond, copyFnIfCond) => {
  const copyToClipboard = (str) => {
    if (cond()) {
      copyFnIfCond()
    } else {
      const textarea = document.createElement('textarea')
      textarea.value = str
      textarea.style.visibility = 'hidden'
      document.body.appendChild(textarea)
      textarea.select()
      document.execCommand('copy')
      document.body.removeChild(textarea)
    }
  }
  obj[prop] = copyToClipboard
  return obj
}

const api = (function () {
  const o = {
    copyToClipboard(str) {
      return navigator.clipboard.writeText(str)
    },
  }
  return o
})()

let copyBtn = document.createElement('button')
copyBtn.id = 'copy-to-clipboard'
document.body.appendChild(copyBtn)

copyBtn.onclick = api.copyToClipboard

copyBtn = withClipboardPolyfill(
  copyBtn,
  'onclick',
  () => 'clipboard' in navigator,
  api.copyToClipboard,
)

copyBtn.click()
このような状況でプロキシを適用する点は、実際に実際に実装をハードコーディングするのではなく、copyToClipboard 関数.プロキシを利用すると、スタンドアロンとして再利用でき、inversion of control .
この戦略を使用する別の利点は、元の関数を変更しないことです.

キャッチャ


キャッシュの多くの異なるシナリオで多くの異なるフォームで取ることができます.例えば、Stale While Revalidate HTTPリクエストnginx content caching , cpu caching , lazy loading caching , メモ化など
JavaScriptでは、プロキシの助けを借りてキャッシュを達成することもできます.
直接使用せずにプロキシパターンを実装するにはProxy クラスは次のようにします.
const simpleHash = (str) =>
  str.split('').reduce((acc, str) => (acc += str.charCodeAt(0)), '')

const withMemoization = (obj, prop) => {
  const origFn = obj[prop]
  const cache = {}

  const fn = (...args) => {
    const hash = simpleHash(args.map((arg) => String(arg)).join(''))
    if (!cache[hash]) cache[hash] = origFn(...args)
    return cache[hash]
  }

  Object.defineProperty(obj, prop, {
    get() {
      return fn
    },
  })

  return obj
}

const sayHelloFns = {
  prefixWithHello(str) {
    return `[hello] ${str}`
  },
}

const enhancedApi = withMemoization(sayHelloFns, 'prefixWithHello')
enhancedApi.prefixWithHello('mike')
enhancedApi.prefixWithHello('sally')
enhancedApi.prefixWithHello('mike the giant')
enhancedApi.prefixWithHello('sally the little')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
キャッシュ
{
  "109105107101": "[hello] mike",
  "11597108108121": "[hello] sally",
  "109105107101321161041013210310597110116": "[hello] mike the giant",
  "115971081081213211610410132108105116116108101": "[hello] sally the little",
  "108111114100321111023211610410132114105110103115": "[hello] lord of the rings"
}
これを直接Proxy クラスは直進です.
const withMemoization = (obj, prop) => {
  const origFn = obj[prop]
  const cache = {}

  const fn = (...args) => {
    const hash = simpleHash(args.map((arg) => String(arg)).join(''))
    if (!cache[hash]) cache[hash] = origFn(...args)
    return cache[hash]
  }

  return new Proxy(obj, {
    get(target, key) {
      if (key === prop) {
        return fn
      }
      return target[key]
    },
  })
}

プロキシクラス


私たちはいくつかのBlabone Proxyパターン実装で直接永続的なパターンを見ましたProxy クラス.JavaScriptは直接提供するのでProxy 言語へのオブジェクトとして、このポストの残りは便宜としてこれを使用するでしょう.
残りのすべての例は、Proxy , しかし、このポストのために、より簡潔で、より簡単に働くので、代わりにクラス構文に集中しています.

Singletonへのプロキシ


あなたがSingletonを聞いたことがないならば、それは関心の対象が返されて、アプリケーションの寿命を通してすでにインスタンス化されるならば、再利用されることを確実とするもう一つのデザインパターンです.実際には、これはいくつかのグローバル変数として使用されている可能性が高いでしょう.
例えば、我々がMMORPGゲームをコーディングしていて、我々には3つのクラスがあったならばEquipment , Person , and Warrior そこに1つだけがあるところWarrior 存在するなら、我々はconstruct インスタンスをインスタンス化する際の2番目の引数の中のハンドラメソッドProxyWarrior クラス
class Equipment {
  constructor(equipmentName, type, props) {
    this.id = `_${Math.random().toString(36).substring(2, 16)}`
    this.name = equipmentName
    this.type = type
    this.props = props
  }
}

class Person {
  constructor(name) {
    this.hp = 100
    this.name = name
    this.equipments = {
      defense: {},
      offense: {},
    }
  }

  attack(target) {
    target.hp -= 5
    const weapons = Object.values(this.equipments.offense)
    if (weapons.length) {
      for (const weapon of weapons) {
        console.log({ weapon })
        target.hp -= weapon.props.damage
      }
    }
  }

  equip(equipment) {
    this.equipments[equipment.type][equipment.id] = equipment
  }
}

class Warrior extends Person {
  constructor() {
    super(...arguments)
  }

  bash(target) {
    target.hp -= 15
  }
}

function useSingleton(_Constructor) {
  let _warrior

  return new Proxy(_Constructor, {
    construct(target, args, newTarget) {
      if (!_warrior) _warrior = new Warrior(...args)
      return _warrior
    },
  })
}

const WarriorSingleton = useSingleton(Warrior)
複数のインスタンスを作成しようとするとWarrior 私たちは、最初に作成されたものだけが毎回使用されていることを保証します.
const mike = new WarriorSingleton('mike')
const bob = new WarriorSingleton('bob')
const sally = new WarriorSingleton('sally')

console.log(mike)
console.log(bob)
console.log(sally)
結果:
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}

クッキースティーラー


このセクションでは、Proxy クッキーのリストから突然変異を防ぐ.これは、元のオブジェクトが変異されるのを防ぎます、そして、mutatorCookieStealer ) 彼らの邪悪な作戦が成功したと仮定します.
この例を見てみましょう.
class Food {
  constructor(name, points) {
    this.name = name
    this.points = points
  }
}

class Cookie extends Food {
  constructor() {
    super(...arguments)
  }

  setFlavor(flavor) {
    this.flavor = flavor
  }
}

class Human {
  constructor() {
    this.foods = []
  }

  saveFood(food) {
    this.foods.push(food)
  }

  eat(food) {
    if (this.foods.includes(food)) {
      const foodToEat = this.foods.splice(this.foods.indexOf(food), 1)[0]
      this.hp += foodToEat.points
    }
  }
}

const apple = new Food('apple', 2)
const banana = new Food('banana', 2)

const chocolateChipCookie = new Cookie('cookie', 2)
const sugarCookie = new Cookie('cookie', 2)
const butterCookie = new Cookie('cookie', 3)
const bakingSodaCookie = new Cookie('cookie', 3)
const fruityCookie = new Cookie('cookie', 5)

chocolateChipCookie.setFlavor('chocolateChip')
sugarCookie.setFlavor('sugar')
butterCookie.setFlavor('butter')
bakingSodaCookie.setFlavor('bakingSoda')
fruityCookie.setFlavor('fruity')

const george = new Human()

george.saveFood(apple)
george.saveFood(banana)
george.saveFood(chocolateChipCookie)
george.saveFood(sugarCookie)
george.saveFood(butterCookie)
george.saveFood(bakingSodaCookie)
george.saveFood(fruityCookie)

console.log(george)
ジョージの食べ物
 {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 },
    Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
    Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
    Cookie { name: 'cookie', points: 3, flavor: 'butter' },
    Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
    Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
  ]
}
インスタンス化george 使用Human クラスと食品の7つの項目をそのストレージに追加しました.ジョージは、彼が果物とクッキーを食べることに満足しています.彼はクッキーについて特に興奮しています.なぜなら、彼はクッキーのために彼の欲求を満たすためにすぐに彼らの好みの味を同時に得ているからです.
しかし、問題があります.
const CookieStealer = (function () {
  const myCookiesMuahahaha = []

  return {
    get cookies() {
      return myCookiesMuahahaha
    },
    isCookie(obj) {
      return obj instanceof Cookie
    },
    stealCookies(person) {
      let indexOfCookie = person.foods.findIndex(this.isCookie)
      while (indexOfCookie !== -1) {
        const food = person.foods[indexOfCookie]
        if (this.isCookie(food)) {
          const stolenCookie = person.foods.splice(indexOfCookie, 1)[0]
          myCookiesMuahahaha.push(stolenCookie)
        }
        indexOfCookie = person.foods.findIndex(this.isCookie)
      }
    },
  }
})()

CookieStealer.stealCookies(george)
The CookieStealer 彼のクッキーを盗むために、青から出てきます.The CookieStealer 今は5つのクッキーを保管しています.
[
  Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
  Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
  Cookie { name: 'cookie', points: 3, flavor: 'butter' },
  Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
  Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
ジョージ
Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 }
  ]
}
我々が巻き戻して、我々の救世主を紹介するならばSuperman 彼の方法を適用するProxy を防ぐパターンCookieStealer 彼の邪悪な行為から、それは我々の問題を解決するでしょう
class Superman {
  protectFromCookieStealers(obj, key) {
    let realFoods = obj[key]
    let fakeFoods = [...realFoods]

    return new Proxy(obj, {
      get(target, prop) {
        if (key === prop) {
          fakeFoods = [...fakeFoods]

          Object.defineProperty(fakeFoods, 'splice', {
            get() {
              return function fakeSplice(...[index, removeCount]) {
                fakeFoods = [...fakeFoods]
                return fakeFoods.splice(index, removeCount)
              }
            },
          })

          return fakeFoods
        }
        return target[prop]
      },
    })
  }
}

const superman = new Superman()
const slickGeorge = superman.protectFromCookieStealers(george, 'foods')
我々の友人superman 幸運にもprotectFromCookieStealers の力を使うProxy クッキーのリストを偽に!彼は、ジョージのクッキーを隠した食品の本当のコレクションをCookieStealer . CookieStealer 彼の邪悪な計画で進行して、彼がクッキーと離れていると思われるように見えています
CookieStealer.stealCookies(slickGeorge)

console.log(CookieStealer.cookies)
The CookieStealer まだ彼の保管のクッキーを離れて歩くと、彼はそれを得たと思う:
[
  Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
  Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
  Cookie { name: 'cookie', points: 3, flavor: 'butter' },
  Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
  Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
リトルは、彼はスーパーマンによって騙され、それらは偽のクッキーだった知っている!george まだ彼のクッキーの力に感謝していないProxy 悪の闇から彼を救う
console.log(slickGeorge)
Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 },
    Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
    Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
    Cookie { name: 'cookie', points: 3, flavor: 'butter' },
    Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
    Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
  ]
}

結論


私は、これはプロキシのパターンにいくつかの光を当てるとどのようにこの概念を活用するために役立つ今すぐ組み込みProxy JavaScriptのクラス.
あなたはこの記事があなたに有用であることを見つけて、将来の投稿のための媒体上で私に従うことを確認してください!
私を見つけるmedium