Flux

material-fluxというFluxアーキテクチャの実装ライブラリを書きました。

Fluxって何?と思う人は以下などを見ると良さそうな気がします。

なぜ作ったか

IDE readable(machine readable)なライブラリが欲しかったのがひとつの理由です。(d.tsなどを書けばだいたい問題ないですが、特殊なハックはしないという制限が欲しかった)

Fluxライブラリの実装比較をしてるvoronianski/flux-comparisonThe State of Fluxなどを出てくるライブラリなどを試しましたが、殆どのライブラリは

  • オブジェクトを渡したら、それに合わせたメソッドの生成
    • createActionscreateStoreのような仕組みでStoreなどを作る
    • d.tsでカバーできるけど。。。
    • => ユーザが触るのは普通に定義した関数に直接呼ぶ形にしたかった
  • 動的なメソッドの上書き
    • Proxy的なことをやってる
    • => ES6 Proxyがあればありかもしれないけど、現状だとただのメソッドの上書き
    • Shapeが壊れる可能性がありそう
  • Dispatcherがシングルトンになってる
    • たいていはシングルトンで問題なさそう
    • 実装もシングルトンだと簡単になる
    • けど何か気持ち悪さがある

のようなちょっとしたハックをした実装があったので、勉強も兼ねてmaterial-fluxを書きました。

material-fluxの使い方

material-fluxはES6 classを前提としたAPIを組み立てていたので、Babel などと共に使うような想定になっています。 Flummoxなどとかなり近い(実際に参考にした)形になっています。

Action

import {Action} from "material-flux"
// ただの定数なので、任意の文字列であればいいです。
// {"dosome" : Symbol("key")} とかでも問題ない
// export var doSomeKey = "unique value"; を並べる感じでも良い
export const keys = {
    "doSomething": "unique value"
};
export default class UserAction extends Action {
    // UserAction#doSomething
    doSomething(data){
        // pass the `data` to Store's `onHandler`
        // call `onHandler(data);`
        this.dispatch(keys.doSomething, data);
    }
}

Store

import {keys} from "./UserAction.js"
import {Store} from "material-flux"
export default class UserStore extends Store {
   constructor(context) {
       super(context);
       this.state = {
           userData: null
       };
       // keys文字列を使ってActionとHandlerを結ぶ
       this.register(keys.doSomething, this.onHandler);
   }

   // data is come from Action
   onHandler(data) {
       this.setState({
           userData: data
       });
   }

   // ただのgetする関数
   getUserData() {
       return this.state.userData;
   }
}

Context

ActionとStoreを結ぶための中間者。 Dispatcherはここでインスタンス化されて、一つのContextが一つのDispatcherを持っている。

Actionに対してはdipatchというDispatcherを呼ぶ関数を与えて、Storeに対してはdispatchされた時にどのHandlerがよばれるかを登録するregisterという関数を渡す。

import UserAction from "./UserAction.js"
import UserStore from "./UserStore.js"
import {Context} from 'material-flux';
export default class UserContext extends Context {
    constructor() {
        this.userAction = new UserAction(this);
        this.userStore = new UserStore(this);
    }
}

使うときは単純なES6 classなので、

var userContext = new UserContext();
userContext.userAction.doSomething("data");

という形でActionを呼ぶことができるようになる。

View(Component)

実際に使うときは、ReactなどのViewを担当するものへContextのインスタンスを渡して使ってもらう。 ReactもES6 classで書けるので例ではReact.Componentを継承しているけど、thisのバインドの問題などあるので、createClassの方が使いやすいかもしれない。

ReactのAPIはcomponentDidMountというメソッドを定義したらあるタイミングでよばれるというような一種の宣言的なAPIで、外から呼ばれるという訳でもないのでES6 classでないcreateClassを使うデメリットは少ない。(そとから呼び出すための補完がしにくくてもそこまで問題起きない)

import React from 'react';
import UserContext from './UserContext.js';
import App from './AppComponent.jsx';
var context = new UserContext();
React.render(
    React.createElement(App, { context }),
    document.getElementById('main')
);

AppComponent:

import React from 'react';
export default class AppComponent extends React.Component {
    constructor(props) {
        super(props);
        this.userStore = this.props.context.userStore;
        this.state = {
            userData: this.userStore.getUserData()
        };
    }

    _onChange() {
        this.setState({
            userData: this.userStore.getUserData()
        });
    }

    componentDidMount() {
        this.userStore.onChange(this._onChange.bind(this));
    }

    componentWillUnmount() {
        this.userStore.removeAllChangeListeners();
    }

    onClick(event) {
        var { context } = this.props;
        context.userAction.doSomething("clicked");
    }

    render() {
        return (
            <div onClick={this.onClick.bind(this)}>
                userData: {this.state.userData}
            </div>
        );
    }
}

examplesやflux-comparisonのmaterial-flux実装バージョンがあるのでそちらも見てもらえるといいかもしれません。

README駆動開発

material-fluxは最初に上げた他のFluxライブラリの懸念点を回避するために、READMEを最初に書いてから実装を始めるREADME駆動開発をしました。

で、実際にやってみてなかなか難しかった感じはしました。

README的にはAPIのことだけを考えて書いていたので、それが本当に実現できるか細かいところまで検証してなかったので、最初に考えていた案はボツになったりしています。

README駆動をするときは表面的なAPIだけじゃなくて、擬似コードか、テストファーストな感じなど内部的な動作の定義も何かしらのアプローチが必要な感じがしました。

よかった点としては、ハックをなくすという最初に考えていた目標はちゃんと維持して実現できたのは良かったと思います。

学び

学習目的で作り始めましたが、基本的にはFacebookのFluxと殆ど同じような感じで素に近い感覚の書き方ができるようなものができたと思います。

今までDispatcherが隠されてるライブラリだと、どこでそれを渡してたりするのかが実装して明確に分かったのが良かった気がします。

またmaterial-fluxはES6 Classを使いまくるという趣旨だったので、使っていますが良いところと悪いところがあります。

いいところはconstructor()というイニシャライズする場所が決まってるので覚えやすい事、悪い所はsuper()周りに気を配る必要がある継承的な問題など。

  • super()の呼び忘れはネイティブだとReferenceErrorになるが、Babel等のTranspilerではカバーできてないこと
  • super(args) のように引数を渡すパターンだとユーザーは何を渡すか意識する必要があること
    • ライブラリ側(親クラス側)でassert等を使って条件を制限する実装が必要そう
  • super(args)callWithArgs(args) のような関数呼び出しを比較すると情報量がやっぱり違うこと
    • ユーザーが親クラスを意識する必要があるのはちょっと大変な気がする
    • 意識させる実装が悪いというのもあるが、使いにくさの主な原因はこれな気がする

などが書いていて思った感想で、この辺も今後考えていく必要がありそうです。

しかし、下手な独自のインスタンス化する手法よりも、やり方がある程度一定になるので覚えることが少なくていいのはメリットだと思います。

他にはReact.ComponentではES6 classを使って書くとthisが自動的にバインドされないため書きにくくなる感じはしました。

これは以下のIssueでも議論されています。

クラスの継承の問題は古来より伝わるものらしいので詳しい人に任せますが、JavaScriptにもいわゆるclassっぽいものがきたので、これからのライブラリAPIをどうするかはちょっと考えていかないといけなさそうな感じはしました。

ECMAScript 6のclassは単なるシンタックスシュガーではなく、ErrorArrayPromiseといったネイティブオブジェクトを正しく継承するためにも必要です。

ただしclass構文じゃないとできないという訳ではなくてReflect.constructなどを使えば、同じようなことが書ける気がします?

ぐだぐだ書いてしまったので結論はありませんが、合成や継承にどんな手法を使うにしても、ヒューマンリーダブルかつマシンリーダブルな書き方というのは考えていく必要がありそうです。