[WIP] React Clean Architecture

Clean Architecture(クリーンアーキテクチャ)とは

クリーンアーキテクチャ(The Clean Architecture翻訳) あたりを参照。

要は複数層から成るレイヤードアーキテクチャの一種で、レイヤー・要素間の結合性を依存性ルールによって縛り、下記のような恩恵を期待する。

恩恵

  • アプリケーションをフレームワークから独立させる。フレームワークによって提供されている機能や設計に依存せず、同時にフレームワークの制約から解き放つ。
  • テストを容易にする。各レイヤー・要素を独立して動作させる。ある一つの要素のテストのために、他の要素を用意する必要がない。
  • UIに依存しない。UIがブラウザではなくCLIになったとしても、ビジネスルールを変更する必要がない。
  • データベースに依存しない。OracleやMySQLを問題なくMongoやPostgresQLに置き換えられる。Webのクライアントとしては、永続化層に依存しない。Web APIのURLやリクエストパラメータが変わったとしても置き換えられる。また、Web APIではなくLocalStorageなどになったとしても問題なく置き換えられる。
  • 外部作用に依存しない。ビジネスルールはアーキテクチャの外側について関与しない。

依存性ルール

それぞれの層は、自分よりも上側の層にしか依存しない。ある層のコードが、自分よりも下側の層のコードのオブジェクトに参照してはならない。上側の層にあるものは、下側の層にあるものを一切知っているべきではない。

React Clean Architecture

※リンク先にあるようなクリーンアーキテクチャの一般的な図では、この層関係を円形に表現することが多い。その場合、この文中で「上側」となっているものは「内側」、「下側」となっているものは「外側」として読み替えてほしい。

Reactにおけるクリーンアーキテクチャ

まずはredux-sagaに死を。あれはコントロールフローをReduxに乗せて行わせるためにあるライブラリで、シンプルなアプリケーションでは真価を発揮するが、要件が複雑になるに連れて、コントロールフローが異常なほど複雑になる。

Reactにおけるクリーンアーキテクチャでは、コントロールフローは層関係と制約によって充分シンプルになっているので、redux-sagaのようなライブラリを使う必要がない。Reduxにはグローバルな状態の管理を担ってもらう。

「Articleの更新を行う」といった、一般的な操作では以下のようなコントロールフローになる。

適切な図を作る時間がなかったので、明日ここにいくつかのコントロールフローの図を載せる。
ArticleのUPDATEやユーザーのサインインなど

ユーザー入力はイベントとしてArticle Editorを通じて、Article Edit RouteなどのContainer Componentに到達する。そこから「Articleを更新する」というUsecaseがコールされ、Usecaseはビジネスロジックを持つ。たとえば「異常値がないかどうか検査し、ArticleをWeb APIのような永続化層に保存する」のような。

UsecaseからはArticle Repository Service、HTTP Client(Adapter)を通じて、Web APIに対してHTTPリクエストが送信される。この一連の操作は、1つのUsecaseの中で1つしか行われないかもしれないし、複数あるかもしれない。複数ある場合は順次に行うかもしれないし、並列に行うこともあるかもしれない。

一連の操作が正常に完了したら、コール元のContainer Componentにコールバックとして通知されるか、Article Global State Serviceのような、Redux抽象にコールされる形で、どちらにせよ値がContainer Componentに通知される。Usecaseが起点となる一連の操作のどこかで例外が生じたら、即座にコール元にコールバックとしてエラーが通知される。これはJavaScriptのPromiseを使うとシンプルに記述できる。

※画像の右半分のコントールフローでUsecaseに達してない部分があるけど、これはアプリケーション初期化時に、Reduxが扱うStateとInterface Adapter層のReactコンポーネント(Routeなど)を接続宣言しておくと、値の伝搬が自動的にされるため。

Local StateとGlobal State

アプリケーション中の状態値にはローカルなものとグローバルなものがある。これを便宜上Local StateとGlobal Stateと呼ぶことにする。

Local State

ほとんどの値はLocal Stateで、これは操作や画面固有の限定的な状態値を指す。たとえば、 `/articles/123` というルートでID=123のArticleが表示されるとしたら、そのArticleは「1つのArticleを閲覧する」という操作に対する限定的な状態値である。

同じように、 `/articles` というルートで「すべてのArticleを閲覧する」操作ができるとしても、それもその操作に対する限定的な状態値である。 `/articles?filter[status]=open` というフィルタ、ソート、ページネーションなどのクエリの変更によって表示すべきものが変わるからである。

Global State

Global Stateはどういうものかというと、状態値のスコープがアプリケーション全体となったものである。セッション情報やアプリケーション設定、ロケールやタイムゾーン、モーダルダイアログやサイドバーの表示状況などである。これが本来Reduxで管理されるものである。

キャッシュとしてのGlobal State

こういう誤用がされているケースが多々ある。ReduxのStateとして値を持っておいて、フラグ値などを用いてキャッシュを行うというものだ。

キャッシュするとしたらそれは永続化層とのやり取りを行う層付近であるべきだ。それに、ReduxのStateとしてキャッシュを行ってしまうと、キャッシュサイズの管理が煩雑になる。そもそもキャッシュはそんなに気軽にするものではない。

それぞれのレイヤーや要素など

Outside of Pure JavaScript

JavaScriptより外界を指す。DOM APIやHTML5 APIなど。

Presentational Component

UI部品として扱われるReact Componentを指す。Presentationalという名のごとく、画面に表示させることを意図する。Presentational Componentは `onClick` や `onChange` などの抽象化されたイベントハンドラをPropsで受け取れるようにし、Componentを「使う側」がイベントハンドラを渡す。

Presentational Componentが(Reactの)Stateを持つかどうかは重要ではない。持たない方がシンプルだけど、Stateを持つべきComponentも存在する。

Container Component

MVCでいうコントローラーのようなReact Componentを指す。SPAのRouteや、 `position: fixed` になるようなものがこれに値する。

これらのComponentはStateを持つことが多い。上で挙げたLocal Stateは、多くの場合これらのComponentのStateとして保持する。

Container Componentは極力スタイル(CSS)を持たないのが望ましい。持つとしても、レイアウトとしての機能に留めるべきだと思う。

Adapter

外界(Outside of Pure JavaScript)とのやり取りを抽象する層。Fetch APIラッパーやLocalStorageラッパー、Google Analyticsやエラー収集サービスなどの外部SaaSのSDKのラッパーなど。

Service

細かいの処理の単位。 `ArticleRepositoryService#update(article)` すると、与えられたArticleと共に `HTTPClientAdapter.update(‘/path/to/api’, { data: article })` をコールし、Web APIを通じてデータを更新する、など。

`FooRepositoryService` 、 `ErrorReporterService` 、 `EventTrackingService` 、 `RouteTransitionService` 、 `ModalDialogService` 、 NativeMessageService など。そんな感じ。

Usecase

業務手順。 `updateArticle(data)` というUsecaseがあったなら、その中で以下のような処理を順次あるいは並列に行う。

  • `data` が正常値かどうかチェックする(バリデーション)。
  • `data` からArticleインスタンスを生成する。
  • Web APIをコールさせる。
  • 結果に応じて、画面を遷移させたりモーダルダイアログを表示させる。

これらのそれぞれの処理はUsecaseに長々と書くわけではなく、それぞれをServiceに分けていく。 ValidationService や ArticleRepositoryService のような。

Entity

これまで何度も出てきている、Articleのような存在。アプリケーション中に存在する概念としての値の実体。ArticleというEntityがあるなら、それはアプリケーション中の色んな場所でインスタンスとして引き回される。