Deep JavaScript
この本をサポートしてください: 購入する または 寄付する
(広告、ブロックしないでください)

15 コレクション用のイミュータブルラッパー



コレクション用のイミュータブルラッパーは、新しいオブジェクトでラップすることでそのコレクションをイミュータブルにします。この章では、その仕組みとその有用性を検討します。

15.1 オブジェクトのラップ

インターフェイスを削減するオブジェクトがある場合、次の方法で対処できます。

ラップは次のようになります。

class Wrapper {
  #wrapped;
  constructor(wrapped) {
    this.#wrapped = wrapped;
  }
  allowedMethod1(...args) {
    return this.#wrapped.allowedMethod1(...args);
  }
  allowedMethod2(...args) {
    return this.#wrapped.allowedMethod2(...args);
  }
}

関連ソフトウェア設計パターン

15.1.1 ラップを利用したコレクションのイミュータブル化

コレクションをイミュータブルにするには、ラップを使用し、そのインターフェイスからすべての破壊的な操作を削除します。

このテクニックの重要なユースケースは、内部の可変データ構造を持ち、コピーせずに安全にエクスポートしたいオブジェクトです。エクスポートが「ライブ」であることも目標です。そのオブジェクトは、内部データ構造をラップし、イミュータブルにすることで目標を達成できます。

次の 2 つのセクションは、Map と配列のイミュータブルラッパーを紹介します。どちらも次の制限があります。

15.2 Map のイミュータブルラッパー

クラス ImmutableMapWrapper は Map のラッパーを作成します。

class ImmutableMapWrapper {
  static _setUpPrototype() {
    // Only forward non-destructive methods to the wrapped Map:
    for (const methodName of ['get', 'has', 'keys', 'size']) {
      ImmutableMapWrapper.prototype[methodName] = function (...args) {
        return this.#wrappedMap[methodName](...args);
      }
    }
  }

  #wrappedMap;
  constructor(wrappedMap) {
    this.#wrappedMap = wrappedMap;
  }
}
ImmutableMapWrapper._setUpPrototype();

プロトタイプのセットアップは静的メソッドによって実行する必要があります。これは、プライベートフィールド .#wrappedMap にアクセスできるのはクラス内のみだからです。

これは動作中の ImmutableMapWrapper です。

const map = new Map([[false, 'no'], [true, 'yes']]);
const wrapped = new ImmutableMapWrapper(map);

// Non-destructive operations work as usual:
assert.equal(
  wrapped.get(true), 'yes');
assert.equal(
  wrapped.has(false), true);
assert.deepEqual(
  [...wrapped.keys()], [false, true]);

// Destructive operations are not available:
assert.throws(
  () => wrapped.set(false, 'never!'),
  /^TypeError: wrapped.set is not a function$/);
assert.throws(
  () => wrapped.clear(),
  /^TypeError: wrapped.clear is not a function$/);

15.3 配列用のイミュータブルラッパー

配列 arr では、メソッド呼び出しだけでなく arr[1] = true などのプロパティアクセスもインターセプトする必要があるため、通常のラップは十分ではありません。 JavaScript プロキシーを使用すると、これを実現できます。

const RE_INDEX_PROP_KEY = /^[0-9]+$/;
const ALLOWED_PROPERTIES = new Set([
  'length', 'constructor', 'slice', 'concat']);

function wrapArrayImmutably(arr) {
  const handler = {
    get(target, propKey, receiver) {
      // We assume that propKey is a string (not a symbol)
      if (RE_INDEX_PROP_KEY.test(propKey) // simplified check!
        || ALLOWED_PROPERTIES.has(propKey)) {
          return Reflect.get(target, propKey, receiver);
      }
      throw new TypeError(`Property "${propKey}" can’t be accessed`);
    },
    set(target, propKey, value, receiver) {
      throw new TypeError('Setting is not allowed');
    },
    deleteProperty(target, propKey) {
      throw new TypeError('Deleting is not allowed');
    },
  };
  return new Proxy(arr, handler);
}

配列をラップしてみましょう。

const arr = ['a', 'b', 'c'];
const wrapped = wrapArrayImmutably(arr);

// Non-destructive operations are allowed:
assert.deepEqual(
  wrapped.slice(1), ['b', 'c']);
assert.equal(
  wrapped[1], 'b');

// Destructive operations are not allowed:
assert.throws(
  () => wrapped[1] = 'x',
  /^TypeError: Setting is not allowed$/);
assert.throws(
  () => wrapped.shift(),
  /^TypeError: Property "shift" can’t be accessed$/);