この章では、以下の質問に答えます。
共有可変状態は以下のように機能します。
この定義は、関数呼び出し、協調的マルチタスキング(例えば、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つの方法について説明します。
特に、先ほど見た例に戻り、修正します。
データのコピーは、共有を回避する1つの方法です。
背景
JavaScriptでのデータのコピーに関する背景については、本書の以下の2つの章を参照してください。
共有状態から*読み取る*だけなら、問題は発生しません。*変更する*前に、コピーを作成して「共有解除」する必要があります(必要な深さまで)。
*防御的コピー*は、問題が*発生する可能性がある*場合に常にコピーする手法です。その目的は、現在のエンティティ(関数、クラスなど)を安全に保つことです。
これらの対策は、自分自身を他の関係者から保護するだけでなく、他の関係者を自分自身からも保護することに注意してください。
次のセクションでは、両方の種類の防御的コピーについて説明します。
この章の冒頭で示した動機付けの例では、`logElements()`がパラメータ`arr`を変更したために問題が発生したことを思い出してください。
この関数に防御的コピーを追加してみましょう。
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'
公開する内部データをコピーしないクラス`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
データを非破壊的に更新するだけで、変更を回避できます。
背景
データの更新について詳しくは、§7「データの破壊的および非破壊的更新」を参照してください。
非破壊的な更新では、共有データを変更することがないため、データの共有は問題になりません。(これは、データにアクセスするすべての人がそうする場合にのみ機能します!)
興味深いことに、データのコピーは非常に簡単になります。
これは、非破壊的な変更のみを行っているため、オンデマンドでデータをコピーしているために機能します。
共有データをイミュータブルにすることで、そのデータの変更を防ぐことができます。
背景
JavaScriptでデータをイミュータブルにする方法の背景については、本書の以下の2つの章を参照してください。
データがイミュータブルであれば、リスクなしに共有できます。特に、防御的にコピーする必要はありません。
非破壊的な更新は、イミュータブルデータの重要な補完です。
この2つを組み合わせることで、イミュータブルデータは、可変データとほぼ同じくらい汎用性が高くなりますが、関連するリスクはありません。
JavaScriptには、非破壊的な更新を伴うイミュータブルデータをサポートするライブラリがいくつかあります。一般的なものは以下の2つです。
これらのライブラリについては、次の2つのセクションで詳しく説明します。
リポジトリでは、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)
注記
リポジトリでは、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()`は機能します。