ミルク色の記録

やったこと、やってみたこと

observable-storeとfreezable-storeというのを作ってみた話

きっかけ

ビューを作るのにReactを使っているとpropsで渡すものはなるべくシンプルなオブジェクトにしたいかな...みたいな気分になる。

(雑な例であまり良くないのだけれど)例えば、カウンタアプリケーションで現在のカウントを表示するビューみたいなのを作る時に、propsで渡されるのはCounterモデルのインスタンスで...とかではなくて、単純にcountプロパティを持つオブジェクトってだけにしたい。

// こうじゃなくて
function CounterView(props) {
  const { counter } = props;
  return <div>{counter.getCount()}</div>;
}

// こんな感じにしたい
function CounterView(props) {
  const { count } = props;
  return <div>{count}</div>;
}

propsで渡ってくるのはそういうオブジェクトだよということをPropTypesとかflowで保証して、キー名typoして残念undefined...みたいなのは防いでおく。ビュー側では参照しかしないので、とりあえず構造が合っていればモデルのこと知らなくていいみたいな感じにしたい。

逆に参照ではなくて状態を変えるところ、例えばカウンタのカウントを増やすみたいなのは、むき出しのJSONこねこねします...みたいなのじゃなくて、ちゃんとCounterモデルの操作にしたい。

またしても雑な例にすると、次みたいな感じでincrementメソッド呼ぶみたいにしたい。

const counter = new CounterModel(/* 初期化パラメータとか */);
counter.increment();

で、その辺りひっくるめてちょっと自分なりのやり方探してみようかなーと、モデルとビューの間に入って橋渡しするくんとしてobservable-storeというのを作ってみた。

observable-store

機能は次のような感じ。

  • 状態を保持する。
  • 保持している状態は参照可能にする。
  • 保持している状態は特定の方法でのみ変更可能にする。
  • 保持している状態が変わったらオブザーバーに通知する。

APIはObject.assignと短命に終わったObject.observeの間の子みたいにして、実装にはProxy使ってゴニョゴニョやった。

単純な使い方は次のような感じ

import createObservableStore from '@ushiboy/observable-store';

// observable-storeのインスタンスを生成
const store = createObservableStore({
  count: 0
});

// 保持している状態にはstateプロパティからアクセスする
const state = store.state;
console.log(`count: ${state.count}`); // count: 0;

// 状態の編集はassignメソッドから行う
store.assign({
  count: state.count + 1
});
console.log(`count: ${state.count}`); // count: 1;

// stateプロパティを直接変更しようとするとエラーになる
try {
  state.count = state.count + 1;  // throw Error
} catch (e) {
  console.log(`${e}`); // Error: Should use assign
}

// 変更を監視するオブザーバーの登録はobserveメソッドで行う
const observer1 = () => {
  console.log(`change: count:${state.count}`);
};
store.observe(observer1);

// オブザーバーの解除はunobserveメソッドで行う
store.unobserve(observer1);

当初はモデルを保持してモデルの変更を監視して...とか、参照の時にモデルのtoJSON呼んで...とか色々考えていたんだけど、モデル側の作り方を制限したくなくなってバッサリやめた。

で、これを使ってやりたかった感じにカウンタアプリ作ってみると次のようになった。

カウンタサンプルのソース

Reactと合わせて使う上でのポイントは次みたいな感じ。

  • Applicationコンポーネントでstoreの監視して、変更があったらsetStateして自身に反映する。
  • CounterViewコンポーネントには直接storeは渡さずにstateのみを渡す。
  • ボタンがクリックされた時の処理はモデルの操作をユースケースで包んで置いて直接は行わないようにする。

単純なサンプル過ぎて参考にならないんだけど、とりあえずやりたい感じになったと言えばなった。

ただ、observable-storeはassignが行われた時に保持している状態に変更があるかゴリゴリチェックしているんだけど、ここで頑張らなくてもReactの方に頼ってしまって良いかも...と感じた。

そこで、状態の変更をチェックするのをやめて、assignされたからどこか変わっているかもよ...とオブザーバーに知らせるだけに機能を削ってfreezable-storeというのを作ってみた。

freezable-store

使い方はstoreの生成方法が変わるだけで、他は一緒。

import createFreezableStore from '@ushiboy/freezable-store';

// freezable-storeのインスタンスを生成
const store = createFreezableStore({
  count: 0
});

Reactと合わせてやる分にはこれでも良いかもなーという印象。

今のところの感想

試しに作ったのがカウンタアプリ程度で扱う状態が単純すぎてなんともなーみたいな感じだけど、状態が複雑になってくるとassignのところとかヤバイかなーどうかなーみたいな感じ。とりあえずもうちょっとドッグフーディング

Proxy使ったの初めてだった。