静的型チェッカーflowのクラスでPrivateなフィールドを定義するメモ

flowはJavaScriptの型チェッカーだが、TypeScriptみたくPrivateフィールドを定義できるわけではなく、ちょっとした工夫が必要だったので、メモ。

なんでPrivateフィールドが必要?

  • インスタンス生成後に外部からフィールド値を変更させたくないため。
    • ドメイン駆動設計(DDD)的なクラス設計をしていると、イミュータブル(不変)のエンティティや値オブジェクトのようなものを多用する。
    • イミュータブルであることを保証できれば、安心してインスタンスを参照で保持できる。(DeepCopyをする必要がなくなる)
  • Public箇所(API)を最小限にしておおきたい。
    • リファクタリングやテスト記述が楽になる。

などなど。

flowのmunge_underscoresオプションを使う方法

flowオプションのmunge_underscoresを有効にすると、先頭に_(アンダースコア)を付けたフィールド/メソッドは、継承先で使えない。というルールを追加することができる。

.flowconfig 追記

[options]
+ munge_underscores=true

実装例

GitHub上の使用例を参考にして、Privateフィールドを実現してみる。

// @flow

type Param = {
  field1: number,
  field2: string,
}

class PrivateSample {
  _param: Param;

  constructor(param: Param) {
    this._param = param;
  }

  getField1(): number {
    return this._param.field1;
  }

  _getField2(): string {
    return this._param.field2;
  }
}

export default class Sample extends PrivateSample {}

実際にflowをかけると、以下の様なエラーになる。

const sample = new Sample({
  field1: 5,
  field2: "test",
})
assert(sample.getField1() === 5) // OK
assert(sample._getField2() === "test") // NG
assert(sample._param.field1 === 5) // NG
error| property `_param` Property not found in (:0:1,0) Sample
error| property `_getField2` Property not found in (:0:1,0) Sample

先頭に_(アンダースコア)、ハンガリアン記法的なキモさがあって、あまり使いたくないが、一番flowっぽい解決法といえる。

継承元のクラスを直接インスタンス化すると使えちゃう

ちなみに、継承元のPrivateSampleを直接使うと、エラーは出ない。継承しないと効果がないみたいなので、継承元のクラスはexportしないほうが良さそう。

const sample = new PrivateSample({
  field1: 5,
  field2: "test",
})
assert(sample.getField1() === 5) // OK
assert(sample._getField2() === "test") // OK
assert(sample._param.field1 === 5) // OK

ES6のWeakMapを使う方法

flowに限ったものではないが、ES6でPrivateなフィールドを定義する方法論がある。

ES6 class での private プロパティの定義
http://qiita.com/k_ui/items/889ec276fc04b1448674

Symbolアクセスを使う方法は、Object.getOwnPropertySymbolsを使えば、外部から値を変更することが可能なため、今回は避けた。

WeakMapでも同じファイル内ならアクセスできるが、インスタンスを作るのは概ね別ファイルなので、あまり問題ないと思った。

実装例

// @flow

type Param = {
  field1: number,
  field2: string,
}

const privates: WeakMap<Object, Param> = new WeakMap();

export default class Sample {

  constructor(param: Param) {
    privates.set(this, param);
  }

  getField1(): number {
    return privates.get(this).field1;
  }

  getField2(): string {
    return privates.get(this).field2;
  }
}

コンソールデバッグがしづらい

WeakMapの方法で、Privateフィールド化していると、コンソールでのデバッグに苦労する。

const sample = new Sample({
  field1: 5,
  field2: "test",
});
console.log(sample);

としても、フィールドの内容は表示されず、以下の様なダンプに。

Sample {}

実質、インスタンス内のプロパティには含まれていないので、表示出ないのは当たり前ではある。privatesのWeakMapをダンプすれば、以下の様な表示はされるが、ファイル外からでは参照できないので、厳しい。

WeakMap {Sample {} => Object {field1: 1234, field2: "test"}}

[おまけ] Privateなメソッドも定義できる?

同ファイル内のClass外に関数を定義して、Classメソッド内で使えば実現できなくもない。

const privates: WeakMap<Object, Param> = new WeakMap();

export default class Sample {

  constructor(param: Param) {
    privates.set(this, param);
  }

  getField1(): number {
    return privates.get(this).field1;
  }

  getField2(): string {
    return privates.get(this).field2;
  }
+
+  getPowField1(num: number) {
+    return powField1(this, num);
+  }
}

+// Private method
+function powField1(instance: Sample, num: number) {
+  return Math.pow(privates.get(instance).field1, num);
+}

ただ、ESLintを併用していると、no-use-before-defineに引っかかったりする。 ちとまどろっこしいね。

備考・注意点

コンストラクタ引数にObjectを渡して、WeakMapにそのままセットする。名前引数的に使えるので、コードの見通しがよくなる。

const sample = new Sample({
  field1: 1234,
  field2: "Text",
});

ただし、コンストラクタ引数へ渡すObjectを変更可能にしておくと、イミュータブルじゃなくなってしまうので、注意。

let param = {
  field1: 1234,
  field2: "Text",
};
const sample = new Sample(param);

// non-immutable
param.field1 = 2345;

コンストラクタ内で、ObjectのShallowCopyを行うなどして、対策すると良いかもしれない。

constructor(param: Param) {
  // ES7の`object-rest-spread`を使うと楽
  Sample.privates.set(this, { ...param });
}

まとめ

JavaScriptの言語仕様上、Private関係は実装しにくく、どうしてもまどろっこしい書き方になってしまう。それでも、TypeScriptを含め、様々なトランスパイラが生まれた今、以前に比べれば、ずいぶんとPrivateを実現しやすくなったと思う。

JavaScriptとprivateの見果てぬ夢
http://blog.tojiru.net/article/238901975.html

言語仕様を超える夢を見て、戦い続けた男たちに敬意を表したい。

comments powered by Disqus

同じシリーズの記事

この記事について

書いた人
Written by

namikingsoft

何かを残して逝きたい
フロントエンドエンジニア