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

8章 共有可変状態の問題とその回避方法



この章では、以下の質問に答えます。

8.1 共有可変状態とは何か、なぜ問題なのか?

共有可変状態は以下のように機能します。

この定義は、関数呼び出し、協調的マルチタスキング(例えば、JavaScriptのasync関数)などに適用されることに注意してください。それぞれの場合でリスクは似ています。

以下のコードは例です。この例は現実的ではありませんが、リスクを示し、理解しやすいものです。

function logElements(arr) {
  while (arr.length > 0) {
    console.log(arr.shift());
  }
}

function main() {
  const arr = ['banana', 'orange', 'apple'];

  console.log('Before sorting:');
  logElements(arr);

  arr.sort(); // changes arr

  console.log('After sorting:');
  logElements(arr); // (A)
}
main();

// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'

この場合、2つの独立した関係者がいます。

`logElements()`は`main()`を中断し、A行に空の配列をログに記録させます。

この章の残りの部分では、共有可変状態の問題を回避する3つの方法について説明します。

特に、先ほど見た例に戻り、修正します。

8.2 データのコピーによる共有の回避

データのコピーは、共有を回避する1つの方法です。

  背景

JavaScriptでのデータのコピーに関する背景については、本書の以下の2つの章を参照してください。

8.2.1 コピーは共有可変状態にどのように役立つのか?

共有状態から*読み取る*だけなら、問題は発生しません。*変更する*前に、コピーを作成して「共有解除」する必要があります(必要な深さまで)。

*防御的コピー*は、問題が*発生する可能性がある*場合に常にコピーする手法です。その目的は、現在のエンティティ(関数、クラスなど)を安全に保つことです。

これらの対策は、自分自身を他の関係者から保護するだけでなく、他の関係者を自分自身からも保護することに注意してください。

次のセクションでは、両方の種類の防御的コピーについて説明します。

8.2.1.1 共有入力のコピー

この章の冒頭で示した動機付けの例では、`logElements()`がパラメータ`arr`を変更したために問題が発生したことを思い出してください。

function logElements(arr) {
  while (arr.length > 0) {
    console.log(arr.shift());
  }
}

この関数に防御的コピーを追加してみましょう。

function logElements(arr) {
  arr = [...arr]; // defensive copy
  while (arr.length > 0) {
    console.log(arr.shift());
  }
}

これで、`logElements()`が`main()`内で呼び出されても、問題は発生しなくなります。

function main() {
  const arr = ['banana', 'orange', 'apple'];

  console.log('Before sorting:');
  logElements(arr);

  arr.sort(); // changes arr

  console.log('After sorting:');
  logElements(arr); // (A)
}
main();

// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
// 'apple'
// 'banana'
// 'orange'
8.2.1.2 公開された内部データのコピー

公開する内部データをコピーしないクラス`StringBuilder`から始めましょう(A行)。

class StringBuilder {
  _data = [];
  add(str) {
    this._data.push(str);
  }
  getParts() {
    // We expose internals without copying them:
    return this._data; // (A)
  }
  toString() {
    return this._data.join('');
  }
}

`.getParts()`が使用されない限り、すべて正常に動作します。

const sb1 = new StringBuilder();
sb1.add('Hello');
sb1.add(' world!');
assert.equal(sb1.toString(), 'Hello world!');

しかし、`.getParts()`の結果が変更されると(A行)、`StringBuilder`は正しく動作しなくなります。

const sb2 = new StringBuilder();
sb2.add('Hello');
sb2.add(' world!');
sb2.getParts().length = 0; // (A)
assert.equal(sb2.toString(), ''); // not OK

解決策は、公開する前に内部の`._data`を防御的にコピーすることです(A行)。

class StringBuilder {
  this._data = [];
  add(str) {
    this._data.push(str);
  }
  getParts() {
    // Copy defensively
    return [...this._data]; // (A)
  }
  toString() {
    return this._data.join('');
  }
}

これで、`.getParts()`の結果を変更しても、`sb`の動作に干渉することはなくなります。

const sb = new StringBuilder();
sb.add('Hello');
sb.add(' world!');
sb.getParts().length = 0;
assert.equal(sb.toString(), 'Hello world!'); // OK

8.3 非破壊的な更新による変更の回避

データを非破壊的に更新するだけで、変更を回避できます。

  背景

データの更新について詳しくは、§7「データの破壊的および非破壊的更新」を参照してください。

8.3.1 非破壊的な更新は共有可変状態にどのように役立つのか?

非破壊的な更新では、共有データを変更することがないため、データの共有は問題になりません。(これは、データにアクセスするすべての人がそうする場合にのみ機能します!)

興味深いことに、データのコピーは非常に簡単になります。

const original = {city: 'Berlin', country: 'Germany'};
const copy = original;

これは、非破壊的な変更のみを行っているため、オンデマンドでデータをコピーしているために機能します。

8.4 データをイミュータブルにすることによる変更の防止

共有データをイミュータブルにすることで、そのデータの変更を防ぐことができます。

  背景

JavaScriptでデータをイミュータブルにする方法の背景については、本書の以下の2つの章を参照してください。

8.4.1 イミュータビリティは共有可変状態にどのように役立つのか?

データがイミュータブルであれば、リスクなしに共有できます。特に、防御的にコピーする必要はありません。

  非破壊的な更新は、イミュータブルデータの重要な補完です。

この2つを組み合わせることで、イミュータブルデータは、可変データとほぼ同じくらい汎用性が高くなりますが、関連するリスクはありません。

8.5 共有可変状態を回避するためのライブラリ

JavaScriptには、非破壊的な更新を伴うイミュータブルデータをサポートするライブラリがいくつかあります。一般的なものは以下の2つです。

これらのライブラリについては、次の2つのセクションで詳しく説明します。

8.5.1 Immutable.js

リポジトリでは、Immutable.jsライブラリは次のように説明されています。

効率性とシンプルさを向上させるJavaScript用のイミュータブルな永続データコレクション。

Immutable.jsは、次のようなイミュータブルデータ構造を提供します。

次の例では、イミュータブル`Map`を使用します。

import {Map} from 'immutable/dist/immutable.es.js';
const map0 = Map([
  [false, 'no'],
  [true, 'yes'],
]);

// We create a modified version of map0:
const map1 = map0.set(true, 'maybe');

// The modified version is different from the original:
assert.ok(map1 !== map0);
assert.equal(map1.equals(map0), false); // (A)

// We undo the change we just made:
const map2 = map1.set(true, 'yes');

// map2 is a different object than map0,
// but it has the same content
assert.ok(map2 !== map0);
assert.equal(map2.equals(map0), true); // (B)

注記

8.5.2 Immer

リポジトリでは、Immerライブラリは次のように説明されています。

現在の状態を変更することで、次のイミュータブル状態を作成します。

Immerは、(潜在的にネストされた)プレーンオブジェクト、配列、セット、マップの非破壊的な更新に役立ちます。つまり、カスタムデータ構造は含まれません。

Immerの使用方法は次のとおりです。

import {produce} from 'immer/dist/immer.module.js';

const people = [
  {name: 'Jane', work: {employer: 'Acme'}},
];

const modifiedPeople = produce(people, (draft) => {
  draft[0].work.employer = 'Cyberdyne';
  draft.push({name: 'John', work: {employer: 'Spectre'}});
});

assert.deepEqual(modifiedPeople, [
  {name: 'Jane', work: {employer: 'Cyberdyne'}},
  {name: 'John', work: {employer: 'Spectre'}},
]);
assert.deepEqual(people, [
  {name: 'Jane', work: {employer: 'Acme'}},
]);

元のデータは`people`に格納されています。`produce()`は変数`draft`を提供します。この変数が`people`であるとみなし、通常は破壊的な変更を行う操作を使用します。Immerはこれらの操作を傍受します。`draft`を変更する代わりに、`people`を非破壊的に変更します。結果は`modifiedPeople`によって参照されます。ボーナスとして、深くイミュータブルです。

Immerはプレーンオブジェクトと配列を返すため、`assert.deepEqual()`は機能します。