material-fluxというFluxライブラリをREADME駆動で開発した
Flux
material-fluxというFluxアーキテクチャの実装ライブラリを書きました。
Fluxって何?と思う人は以下などを見ると良さそうな気がします。
- React: Flux Architecture - Video Tutorial Series @eggheadio
- Fluxとはなんだったのか + misc at 2014 - snyk_s log
- Fluxアーキテクチャの覚え書きを書いた - snyk_s log
- The Flux Quick Start Guide
- Getting To Know Flux, the React.js Architecture ♥ Scotch
- What the Flux? (On Flux, DDD, and CQRS) — Jack Hsu
なぜ作ったか
IDE readable(machine readable)なライブラリが欲しかったのがひとつの理由です。(d.tsなどを書けばだいたい問題ないですが、特殊なハックはしないという制限が欲しかった)
Fluxライブラリの実装比較をしてるvoronianski/flux-comparisonやThe State of Fluxなどを出てくるライブラリなどを試しましたが、殆どのライブラリは
- オブジェクトを渡したら、それに合わせたメソッドの生成
createActions
やcreateStore
のような仕組みで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駆動開発をしました。
- –– APIの定義 - ドキュメントのみ ––
- Imagine Store by azu · Pull Request #1 · azu/material-flux
- Imagine Action by azu · Pull Request #2 · azu/material-flux
- add Flux section by azu · Pull Request #3 · azu/material-flux
- — 動かないサンプルコード ––
- add examples by azu · Pull Request #5 · azu/material-flux
- — ここから実装 ––
- implementation material-flux by azu · Pull Request #7 · azu/material-flux
で、実際にやってみてなかなか難しかった感じはしました。
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でも議論されています。
- Use ES6 Classes to create React components. · Issue #613 · facebook/react
- Components as ES6 classes · Issue #3400 · facebook/react
クラスの継承の問題は古来より伝わるものらしいので詳しい人に任せますが、JavaScriptにもいわゆるclass
っぽいものがきたので、これからのライブラリAPIをどうするかはちょっと考えていかないといけなさそうな感じはしました。
- Joost's Dev Blog: Why composition is often better than inheritance
- 【翻訳】クラスの「継承」より「合成」がよい理由とは?ゲーム開発におけるコードのフレキシビリティと可読性の向上 | POSTD
ECMAScript 6のclass
は単なるシンタックスシュガーではなく、Error
やArray
、Promise
といったネイティブオブジェクトを正しく継承するためにも必要です。
ただしclass
構文じゃないとできないという訳ではなくてReflect.construct
などを使えば、同じようなことが書ける気がします?
ぐだぐだ書いてしまったので結論はありませんが、合成や継承にどんな手法を使うにしても、ヒューマンリーダブルかつマシンリーダブルな書き方というのは考えていく必要がありそうです。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。