読者です 読者をやめる 読者になる 読者になる

さいきんReact, Reduxでやっている設計

はじめに

ブラウザでGUIアプリケーションを作らなくても良い牧歌的な時代は終わりつつあります。個人的な意見としてはブラウザはドキュメントビューアのままでいて、複雑なGUIアプリケーションはネイティブアプリケーションとして実装されてほしいのですが、そうは言ってもお仕事で人間にとって負担の低いUIを作っていく必要があるのです。
Railsでサーバアプリケーションを書きつつ管理画面はネイティブでなんてことはコスト的に実現できません。かといって長期的に運用されるシステムを作ると、システムを運用するためのUIが操作しやすいに越したことはありません。Bootstrapを使っててきとうなフォームを並べただけの画面を作って怒られた経験はありませんか? たとえサーバ開発者だとしても、我々は使いやすいUIを求め続ける必要があります。

react, redux

複雑なGUIを作るためのフレームワークも乱立の時代を終えて、react, vue, angularなどに人気とリソースが集約されてきたような気がします。私はたまたまreact, reduxという組み合わせを習得しましたが、本質的にはfluxによるコマンドクエリ責務分離なデータフローができていればどれをつかってもなんとかなるんじゃないかと思っています。

今回作ったもの

みんな大好きTODOアプリです。
TODOアプリ簡単かと思いきや、ユーザ入力がそれなりに多いので実験としてはよい対象だと思います。
我々サーバ開発者はたくさんの入力フォームを相手にしなければならないので、実務と共通する知見も多いでしょう。

https://gyazo.com/e7bf20da140f535ef85dbef510bbec04

ソースコードはこちら github.com

model, stateたち

redux design models

特徴としてはImmutable.jsを使っている点、StateもImmutable.Recordとして実装している点でしょうか。
このTodoStateは自身と他のモデルたちの状態を適切に管理する責務を持っており、ユーザ入力に起因する状態の変更は各メソッドをactionとして呼び出してもらうようになっています。
つまり、TodoStateはTaskやその他オブジェクトのFacadeになるように設計しています。
また、Recordのメンバとしてtasksを持っているので、TodoStateインスタンスをルートコンポーネントに渡せば勝手に値を参照してもらうことができます。

reducer

redux design ducks

ユーザ入力によって発行されたアクションをTodoStateのメソッドへ適切に振り分けるのがreducerの責務です。
このファイルはducksと呼ばれるスタイルになっており、action定義、reducer, action creator, ついでにsagaをひとつのファイルに混ぜ込んでいます。

github.com

reducerにとってのinitial stateをTodoStateクラスとして定義しているので、newすれば良いだけなのも便利です。

非同期処理

redux本体が非同期処理と相性の悪いフレームワークだからか、redux-*はほんとうにたくさん存在します。
今回はその中でもredux-sagaを用いてコードの見た目としては同期的に書けるようにしています。
sagaのcall作用とtask.submit()を共存させるのが今回一番悩んだところでした。結論としては、sagaを経由するactionにTaskインスタンスをそのままぶちこんでやることになりました。(33, 34行目)
正直なところちょっとダサい方法だとは思うのですが、Taskインスタンスではなくidを突っ込んだにしても、select作用でstateを取ってきてからTaskインスタンスを取得するだけなので、パフォーマンスに影響がでない限りはこれでもいいんじゃないかと思っています。
あるいは、task.submitParams()のようなメソッドを作り、requestに必要な情報だけを切り出して非同期処理を引き起こすactionに託すのもありだと思います。

components

redux design components

さいきんPropTypes.funcとtypeするのに疲れてきたので思い切ってactionsはcontextで流すことにしました。
なにか困ったことが起きたら真面目にpropsドンブラコをしていこうと思います。お困りポイントに見当がつくかたはお知らせください。
上の例では、Container Componentで定義したchangeInputTargetアクションとcheckTaskアクションがcontext経由で受け渡されているのがわかるかと思います。

f:id:non_117:20170325203133p:plain これは内容とはまったく関係のない挿絵です

まとめ

pros, cons

pros
  • reducerが見通しやすくなる
  • stateとmodelsに対するテストが書きやすくなる
  • stateとmodelsをちゃんと設計しておけばreduxを捨てるのも簡単
cons
  • Facadeになるstateクラスが肥大化する

所感

  • stateクラスがFacadeになっていることが最重要なので、Immutableに依存する必要はない
    • ピュアES6で書くとより移植性が増す
    • しかし個人的にはImmutable + TypeScriptでやりたい
  • stateクラスが肥大化する問題はそれこそreduxが得意とする分割統治で解決してはどうか
    • reducerの分割単位をまたいで参照が必要なだけならcontextで流せなくもないと思う
    • reducerの分割単位をまたいで変更が必要な場合はUIから考え直すと良いのではないか……
    • 分割したうえでロジックが複雑化した場合はモデル内の層を増やすしかないかも
  • 実際にはTODOアプリ程度の複雑さだとまだreduxを使う要件ではない
    • ちょうどImmutableなStateができているので、Reactオンリーで十分やっていける
  • 上記のTaskといったモデルがサーバ上のモデルと一致するかというと微妙なところ
    • UIの表現をするための状態がでてくる
  • サーバから渡す初期状態をつくるには(react_on_rails)
    • UIのための状態が存在するのでas_jsonでは力不足
    • Application, Domain, Infrastructureの3層でモデルをつくりInfrastructureの管理する状態をサーバ側モデルの部分集合にすれば、as_jsonでも良い?
  • client side validation
    • validationロジックがstateクラスの管理下にあるのは間違いないが、現状のモデル構造では肥大化の要因となって厳しい気がする
    • サーバとvalidationロジック二重持ち問題発生
  • Fluxの非同期処理やコマンドクエリ責務分離って所有権と相性いいのでは?
    • 今回はリクエスト発行後にユーザ入力でnameが書き換えられても問道無用でレスポンスが勝つようになっている
    • 非同期処理が入るとレースコンディションが発生しやすいので、プログラマは何らかの方法で解決しておく必要がある
    • コマンドクエリ責務分離もView系クラスとModel系クラスが状態を変更する権限の管理という話になるはず
    • Rust + WebAssemblyに期待したい

ここまで書いて、この記事で提案したモデル構造ではモデル内の複雑さを克服できない問題が残ることに気づいてしまいました。
モデルの複雑さをサーバに押し付けることで対処が可能ではありますが、本質的には解決しないのでまた別の設計論は必要そうですね。
DDDとかに詳しい方の知見をお待ちしております。