Chapter 04

プロキシパターン

Shinya Fujino
Shinya Fujino
2022.01.07に更新
このチャプターの目次

ターゲットオブジェクトとのやり取りをインターセプトし制御する


プロキシパターン

プロキシ (Proxy) オブジェクトを使うと、特定のオブジェクトとのやり取りをより自由にコントロールできるようになります。プロキシオブジェクトは、たとえば値の取得やセットなど、オブジェクトの操作における挙動を決定することができます。


一般に、プロキシは誰かの代役を意味します。ある人に直接話しかける代わりに、その人を代理するプロキシに話しかけるという具合です。これは JavaScript でも同じです。つまり、ターゲットとなるオブジェクトを直接操作する代わりに、プロキシオブジェクトとやり取りするのです。


John Doe を表わす person オブジェクトを作りましょう。

const person = {
  name: "John Doe",
  age: 42,
  nationality: "American"
};

このオブジェクトと直接やり取りする代わりに、プロキシオブジェクトとやり取りしたいと思います。JavaScript では、Proxy のインスタンスを作成することで、簡単に新しいプロキシを作成することができます。

const person = {
  name: "John Doe",
  age: 42,
  nationality: "American"
};

const personProxy = new Proxy(person, {});

Proxy の二番目の引数は、ハンドラを表わすオブジェクトです。ハンドラオブジェクトでは、やり取りの種類に応じて特定の動作を定義することができます。プロキシのハンドラに追加できるメソッドはたくさんありますが、代表的なものは getset です:

  • get: プロパティにアクセスしようとしたときに呼び出されます
  • set: プロパティを変更しようとしたときに呼び出されます

最終的には以下のような挙動となります:

動画による説明

person オブジェクトと直接やり取りする代わりに、personProxy とやり取りするのです。

personProxy プロキシにハンドラを追加してみましょう。プロパティを変更しようとするとき、つまり Proxyset メソッドを呼び出すとき、プロパティの以前の値と新しい値をプロキシがログ出力するようにします。また、プロパティにアクセスしようとして、Proxyget メソッドを呼び出すとき、プロパティのキーと値を含むわかりやすい文をログ出力するようにします。

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${obj[prop]}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    obj[prop] = value;
  }
});

完璧です!では、プロパティを変更したり取得したりするときに何が起こるか見てみましょう。

index.js
const person = {
  name: "John Doe",
  age: 42,
  nationality: "American"
};

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${obj[prop]}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    obj[prop] = value;
    return true;
  }
});

personProxy.name;
personProxy.age = 43;

name プロパティにアクセスしようとすると、プロキシは The value of name is John Doe という、響きの良い文を返しました。

age プロパティを変更しようとすると、プロキシは、このプロパティの以前の値と新しい値を Changed age from 42 to 43 というかたちで返しました。


プロキシは、バリデーションを追加する際に便利です。ユーザーが person の年齢を文字列に変更したり、名前を空にしたりできてはいけません。あるいは、ユーザーがオブジェクト上の存在しないプロパティにアクセスしようとした場合、ユーザーにそれを知らせるべきです。

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    if (!obj[prop]) {
      console.log(
        `Hmm.. this property doesn't seem to exist on the target object`
      );
    } else {
      console.log(`The value of ${prop} is ${obj[prop]}`);
    }
  },
  set: (obj, prop, value) => {
    if (prop === "age" && typeof value !== "number") {
      console.log(`Sorry, you can only pass numeric values for age.`);
    } else if (prop === "name" && value.length < 2) {
      console.log(`You need to provide a valid name.`);
    } else {
      console.log(`Changed ${prop} from ${obj[prop]} to ${value}.`);
      obj[prop] = value;
    }
  }
});

無効な値を渡そうとしたときに何が起こるか見てみましょう!

index.js
const person = {
  name: "John Doe",
  age: 42,
  nationality: "American"
};

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    if (!obj[prop]) {
      console.log(`Hmm.. this property doesn't seem to exist`);
    } else {
      console.log(`The value of ${prop} is ${obj[prop]}`);
    }
  },
  set: (obj, prop, value) => {
    if (prop === "age" && typeof value !== "number") {
      console.log(`Sorry, you can only pass numeric values for age.`);
    } else if (prop === "name" && value.length < 2) {
      console.log(`You need to provide a valid name.`);
    } else {
      console.log(`Changed ${prop} from ${obj[prop]} to ${value}.`);
      obj[prop] = value;
    }
    return true;
  }
});

personProxy.nonExistentProperty;
personProxy.age = "44";
personProxy.name = "";

プロキシが、無効な値により person オブジェクトが変更されることを防ぎ、私たちのデータをクリーンに保ってくれました!


Reflect

JavaScript には Reflect という組み込みオブジェクトがあり、これにより、プロキシを扱う際にターゲットオブジェクトを簡単に操作できるようになります。

以前は、プロキシ内のターゲットオブジェクトのプロパティを変更・取得するには、ブラケット記法により直接的に値を取得・設定していました。その代わりとして使用できるのが Reflect オブジェクトです。Reflect オブジェクトのメソッドは、handler オブジェクトのメソッドと同じ名前をもっています。

obj[prop] によってプロパティへとアクセスし、obj[prop] = value によってプロパティを設定する代わりに、 Reflect.get()Reflect.set() によってターゲットオブジェクトのプロパティの取得や変更ができます。これらのメソッドは、ハンドラオブジェクトのメソッドと同じ引数を受け取ります。

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${Reflect.get(obj, prop)}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    Reflect.set(obj, prop, value);
  }
});

完璧です!Reflect オブジェクトにより、ターゲットオブジェクトのプロパティへのアクセスや変更が簡単になりました。

index.js
const person = {
  name: "John Doe",
  age: 42,
  nationality: "American"
};

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${Reflect.get(obj, prop)}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    return Reflect.set(obj, prop, value);
  }
});

personProxy.name;
personProxy.age = 43;
personProxy.name = "Jane Doe";

プロキシは、オブジェクトの振る舞いを制御するための強力な手段です。プロキシには、バリデーション、書式設定、通知、デバッグなど、様々なユースケースがあります。

Proxy オブジェクトを使いすぎたり、 handler メソッドを呼び出すたびに重い処理を実行したりすると、 アプリケーションのパフォーマンスに悪影響を及ぼす可能性があります。パフォーマンスが重要なコードにはプロキシを使用しないのが一番です。


参考文献