この記事はQiita Advent Calendar 2017 React #1 の 4日目の記事です。
Almin.js について最近学んだことを説明します。
概要
今、業務で開発しているシステムで、Almin.js + TypeScript + React を使ってフロントエンドの処理を実装しています。
Almin.jsの採用を検討したのは、フロントエンドでDDDに沿った設計をするのに使いたかったからです。
今回調査も兼ねて実装してみると、Almin.jsを利用することで、思っていたよりいい感じにクラスを設計できました。
その課程で Almin.js が何を提供するライブラリなのかということも理解が進んだので、学んだ内容を共有します。
必要だったもの
今回の開発にあたり、Viewを実装するためにReactを使うことは決まっていましたが、それ以外の部分についてはどうやって実装するか未定でした。
ただ、行う処理がそれなりに複雑になることがわかっていたので、ビジネスロジックとそれ以外の処理を書く場所を分けて書きたいという要望はありました。
そのために、できればフロントエンドでもDDDに沿った設計をしたいなー、と考えていました。
Reactを使ってフロントエンドでDDDに沿った設計をするにあたって、必要だと考えた機能を列挙します。
-
Componentがアクションを発行する仕組み
ユーザが操作したときの処理を書く方法を統一しておきたい。 -
UseCaseを書く場所
DDDでいうアプリケーション層にあたる処理を書く場所を決めておきたい。 -
Viewに再描画タイミングを知らせる仕組み
データが変更されたときにViewに対して再描画を通知するための手段を提供してほしい。 -
Viewに描画するデータを渡す仕組み
Reactコンポーネントが描画に使うデータを取得する手段か、Reactコンポーネントにデータを渡す方法を提供してほしい。
これらの仕組みを構築するために候補に挙がったのが、Almin.js を使うか自前でそのあたりを実装するかです。
Almin.js が上記の要望を満たせるかどうかが完全にはわかっていませんでした。
ドキュメントを読む限りはたぶん要望にフィットしそうだと思ったのですが、わからない用語が多かったので 何か僕達が求めていることよりも複雑なことを要求されるのではないか、という懸念がありました。
自前で実装すれば、自分たちの要望を満たせる仕組みを作れるとは思いますが、以下のリスクがあるなと考えていました。
- 仕組みの実装に時間をかける必要があること。今回はあまり仕組みづくりに時間をかけたくない。
- 自分が求めているものが何なのか、自分でわかってない可能性がある。
そのためにトライアンドエラーを繰り返すかもしれない。
以上の前提から、まず Almin.js を使って実装してみて、うまくいくか試してみることにしました。もし僕達の要望にAlmin.jsがフィットしないなら、そのときに必要な機能だけを切り出して自前の仕組みを作ればいいや、という感じでした。
それから半月くらい経ったのですが、まあまあいい感じなので、Almin.js を使って実装を続けていいのではないか、と考えてます。
Almin.js で実装するときに利用/実装する部品
プログラムを実装していく上で、Almin.js が提供していたり、こちらで実装する必要のある部品について説明します。
Context
Almin.js の処理に関するオブジェクトをつなぎ合わせる責務を持ったオブジェクトです。
Storeを保持し、ビューに描画タイミングを知らせます。
また、UseCaseを実行するためのUseCaseExecutorというラッパークラスを作るためにも使われます。
Context生成処理の例:
https://github.com/almin/almin/blob/beafc2546d35936f9dd1da73d5e388908cb0ce14/examples/todomvc-typescript/src/index.tsx#L18
Contextはアプリケーションのライフサイクルを通じて生存する必要があり、どこからでもアクセスできる必要があるため、Almin.jsのサンプルではAppContextLocatorというクラスをsingletonにし、ファイルをインポートするだけでアクセスできるようにしてます。
export default new AppContextLocator();
(singletonに見えないけど new して export すると、そのオブジェクトをどこでも共通して使える。)
Store, StoreGroup
Storeは、Viewが描画につかうデータ (state) を作るオブジェクト。
stateをインスタンス変数として持っておいてもいいし、毎回作ってもべつにかまわない。
僕は Storeにあまり状態を持たせたくないので、できるだけDBから値を取得して都度stateを組み立てるように実装してますが、特に問題は生じてません。
StoreGroupはStoreのコレクション。保持している各StoreのgetState() を組み立てて返します。
return new StoreGroup({
todoState: new TodoStore({ todoListRepository })
});
また、Alminでデータ変更して明示的に再描画したいことを知らせるとき、storeGroup.emitChange() するしか手がないっぽいので、しかたなくグローバルオブジェクトとしてどこからでもアクセスできるようにしてる。
UseCase
ユーザのイベントハンドラから呼び出されるユースケースを書く場所。
DDDでいうアプリケーション層に相当する処理を書く。
ドメインオブジェクトを操作し、Repositoryを通じてデータを永続化したり、サーバのAPIを呼び出すといった処理を書く。
Repository
これは状態の永続化をDDDに則ってやるなら作ったほうがいいクラスで、DDDに則らないなら別になくてよい。
Almin.js のサンプルには MemoryDB というキーバリューストアを使ったRepositoryの例が載ってて、これは良く出来てるのでそのまま使ってる。
Repositoryのinterfaceはdomain層に置くようにして、UseCaseからはinterfaceに依存するようにすれば、UseCaseのユニットテストが容易になって良い。
ドメインオブジェクト
DDDを使って設計する主目的がドメインモデルに業務の知識を集めること。なので、業務の知識が表現できるように存分に好きなように作って良い。ここはAlmin.jsとは関係なく、Almin.jsへの依存もない。
Dispatcher
Contextを作るときにDispatcherのインスタンスを渡す必要があるけど、その後Dispatcherを意識する必要はないので気にしなくていい。
ルートコンポーネント
ルートコンポーネントではContext の onChange イベントが起こったときに描画処理(setState) が走るようにします。
componentDidMount() {
const appContext = this.props.appContext;
this.releaseChange = appContext.onChange(() => {
this.setState(appContext.getState());
});
}
配下のコンポーネントに渡すpropsは、this.state の内容を渡します。
<Footer allTodos={todoState.items} filterType={todoState.filterType} />
その他のReactコンポーネント (ルートコンポーネントのrender経由で組み立てられるコンポーネント)
できるだけコンポーネント内部で状態を持たないように、props で渡されたデータだけを使って組み立てるように気をつけます。
これは React を使ったプログラムなら共通で言えることですね。
また、イベントハンドラからはUseCaseのexecuteを呼び出すようにします。
_onSave = (text: string) => {
if (text.trim()) {
AppLocator.context.useCase(AddTodoItemFactory.create()).execute(text);
}
};
Alminを使ったアプリケーションの処理の流れ
- Store, Contextを作成。Contextインスタンスはどこからでもアクセス可能なようにしておく。
- ルートコンポーネントにContextインスタンスを渡して描画する。ルートコンポーネントはContext.onChange が起こったとき、配下コンポーネントを再描画するようにイベントハンドラを仕掛けておく。
- 各コンポーネントはユーザが操作したときのイベントハンドラに、UseCaseのexecuteを実行するように書いておく。
- UseCaseはドメインモデルを操作したり、DBのデータに変更を加えたり、サーバのAPIを呼び出したりする。
Store以外に永続化DBを持つ場合は自前で用意する必要がある。 - UseCase.execute (正確にはUseCaseExecutor.execute) が呼び出され、stateに変更があると、再描画が走る。
各オブジェクトの関係をスケッチしてみました。
Almin.js を使って良かった点
Viewにコールバック関数をprops経由で渡す必要がなくなる
ViewはAppContextLocator経由で取得したContextのインスタンスと、UseCaseクラスを知ってれば良いのでコールバック関数をprops経由で渡す必要がなくなります。
これによってpropsの定義が簡単になりました。
UseCaseにユーザが操作したときに処理する内容を集められる
UseCaseを置いているディレクトリが、アプリケーションが処理するユースケースのリストになる。
UseCaseは状態を持たないので、テストも容易に作れる。
描画に使うデータ(state) をStoreで描画しやすいように加工できる
Almin.js を使う前は、state をStore経由で取ってくるのは意味あるのかな、と思ってたのですが、
実際に View から stateを使って描画するように実装してみると、DBの生のデータを触るより、描画しやすいように加工できるのがかなり快適だと気づきました。CQRSを強制できる感じです。
ドメインモデルに永続化などの業務以外の関心事を持ち込まないことに成功した
UseCase(アプリケーション層) までで、フレームワークや永続化などの業務以外の関心事をシャットアウトできたので、ドメインモデルに業務の知識だけを集めることに成功しました。
Almin.js の不満点
明示的な再描画を Context に指示できない
Alminに再描画を知らせるときに storeGroup.emitChange() を呼び出す必要があり、そのためにstoreGroupインスタンスをグローバルオブジェクトにしないといけない。
サーバからデータを引っ張ってきてDBに保存したときとか、再描画を通知したいケースがあるので。
TypeScriptとの相性
Almin.jsがTypeScriptで書かれていることもあり、型定義がないことによるコンパイルエラーなどはほぼ起こらない。
ただ、Viewに渡された state の型を、TypeScriptコンパイラが感知してくれない。
なので、なんの型がわたされてくるのかinterfaceを定義しておかないといけない。自前で定義すればいいので、そんなに問題ではないと思う。
まとめ
Almin.js を使うことで、思っていたよりいい感じにコードを整理できているな、と感じています。
ViewにはStore経由でデータを渡すという制約も、自前でなら実装してなかった機能ですが、やってみるとCQRSを強制できて、なかなかいいな、という感じです。
懸念していた「必要以上に複雑なことをしているのではないか」という点も、用語を整理してみれば概ね必要な機能だったと感じています。
フロントエンドでDDDをやりたいときには、Almin.jsの採用を検討候補に入れていいのではないでしょうか。
以上ご参考まで。