反レイヤードアーキテクチャ フロントエンド

前置き

yukashikadoでは、VITANOTEというアプリ(以後、アプリ)をリリースしています。アプリでは、EC機能、検査管理機能、などを提供しています。 このアプリは、ReactNative & Typescriptで作成されています。 このアプリの開発において、レイヤードアーキテクチャを採用していましたが、レイヤードアーキテクチャをやめたという話を今回はしようと思います。

前提

アプリは以下のような特徴があります

  • 書き込み要件と表示要件とを比べて、表示要件が大部分を占めるアプリであること
  • ゲームなどのようにUIの構造が複雑ではなく、取得したデータを編集する要件が少ないこと
  • ロジックをかなりの部分をサーバサイドにまかせていること
  • typescriptが導入されていること(型エラーは常に0)

目次

  1. レイヤードアーキテクチャとは
  2. アプリでのModelという概念
  3. GraphQL化で、RestfulAPIに依存したModelが不便になる
  4. Modelの問題の解決のための再設計
  5. 共通ロジックはすべてユーティリティ
  6. まとめ

1.レイヤードアーキテクチャ

レイヤードアーキテクチャとは、レイヤーごとに責務を決め、依存関係を明確にするものです。レイヤーとしては、ざっくり以下のような形です。レイヤーを分けることによってより、下位のレイヤーの変更を気にせずに開発することができます。

  • UI
  • アプリケーション
  • ビジネスルール
  • インフラ
graph RL

UI -.-> アプリケーション
アプリケーション -.-> ビジネスルール
ビジネスルール -.-> インフラ

参考: https://qiita.com/kichion/items/aca19765cb16e7e65946

2.アプリでのModelという概念

アプリでは、アプリケーションレイヤーとビジネスルールをまとまとめてしまったModelという概念を作りました。 ここでいうModelは、Restful APIで取得したデータに対して、振る舞いをつけた ただのクラスです。 アプリとしては、Reactを使っているため、 インスタンスはイミュータブルに扱う注意の必要はありますが、 こういった振る舞いを持っていれば、様々なコンポーネント使い回せることができ便利でした。 依存関係は以下の通りです。

graph RL

Model -.-> RestfulApi
UIComponent -.-> Model

レイヤードアーキテクチャとして考えると

  • RestflApiが、インフラレイヤー
  • Modelが、アプリケーションレイヤー & ビジネスルールレイヤー
  • UIComponentが、UIレイヤー

です。 例えば、CreditcardエンドポイントからGETリクエストで取得したレスポンスは、Creditcardクラスとして、インスタンス化され、UIコンポーネントで使われます。Creditcardクラスは、期限切れなのかどうかなどの振る舞いなどを持っていたりします。 リクエストも、レスポンスも、Modelを使えばよく、特定の単位で扱うのがモデルは楽でした。

3.Graphql化で、RestfulAPIに依存したModelが不便になる

そんななか、アプリでのリクエスト回数が増え、APIの待ち時間が問題になりました。 それを改善するために、リクエストにまとめてデータを取得するためにGraphQLを導入しました。 GraphQLでは、ページごとに必要な情報をだけをクエリで取得します。 それによって、モデルの境界が曖昧になり、データのまとまりの概念が曖昧になりました。 RestfulAPIに強く依存したModelは使いにくくなってきました。 例えば、GraphqlでCreditocardの番号フィールドのみを取得した場合は、CreditcardModelをインスタンス化するため必要なRestfulAPIでは手に入ったであろう「期限」などの情報が足りないため、CreditcardModelを利用することができません。 Modelというデータのまとまりを必要とするクラスは使わなくなっていき、そして、Moelに依存したコンポーネントも不便になりました。 今のModelを含めた設計は、再設計する必要があります。

4.Modelの問題の解決のための再設計

解決するための選択肢として、以下の2つがあります。

  1. Restful版のModelが負債化したので、Graphql版のModelを再定義する
  2. Modelをやめて他の手立てを考える

4.1. Restful版のModelが負債化したので、Graphql版のModelを再定義する

Graphql化して、ページごとにクエリが異なるため、データの境界は曖昧になり、データのまとまりを作ることが難しくなりました。 しかし、コンポーネント上でよく使われるデータのまとまりの要素があったりします。ただ、それを特定のデータのまとまりを定義してしまうと、オーバフェッチングに繋がります。 オーバフェッチングは単純にアプリが遅くなることに加えて、使われていないフィールドまでメンテナンスするはめになり、サーバサイドの負債化にも繋がります。 そして、同じModelの問題を再発明する可能性があります。インフラレイヤー変更が起きる可能性は低いですが、その対応できなくなります。

4.2. Modelをやめて他の手立てを考える

モデルの構成要素は以下の2つです。この役割をどう置き換えるかを考えていきます。

  • 振る舞い(ロジック)
  • データのまとまり(データの境界)

振る舞い(ロジック)のあり方

特定のデータの振る舞いやロジックは、そもそもModelの責任なのでしょうか? graphqlでは、特定データの境界が曖昧になります。特定データの境界が曖昧なら、振る舞いすら曖昧になります。 このロジックは、サーバサイドからのデータを表示のために、加工します。 そう考えると、ロジックは特定のデータの塊(Model)のものではなく、ロジックはすべてUIコンポーネントの責務と考えていいと思います。 なので、ロジックは極力UIコンポーネント内に閉じこめて開発してくのがベストです。

データのまとまりや境界

Model(ビジネスルールレイヤー&アプリケーションレイヤー)に依存したUIコンポーネントの開発が負債を産みました。 振る舞い(ロジック)のあり方で考えた通り、Modelにあったと思っていた責務は実はUIのもので、データの修正に責務など無いことが考えられます。 UI以外の観点で見たときに、データのまとまりや境界もありません。依存する中間レイヤーなどありません。 そのため、UIが依存するのは、サーバサイドとのインターフェースである API スキーマです。 また、レイヤーを作らないことは修正が早いというメリットがあります。 フロントエンドの変化の速度を考えると、変更があったとき、中間レイヤーが少ないほうが変更箇所が減り変更が容易です。 例えるなら、RailsのModelのように、様々なレイヤーをModelに集約し、DBと強く依存することで、開発速度を上げたように、フロントエンドでも、APIスキーマに強く依存することで、中間レイヤーを気にせずにすみ、開発速度を上げれます。 RailsのModelと違うのは、DBのカラムの変更があったときに、Railsは変更が全体に及んで変更箇所を探すのが大変ですが、フロントエンドでは、スキーマに変更があったとき、変更が全体に及んでいても、即座に型エラーで修正箇所を見つけ出すことができます。 フロントエンドの目指すべき設計に置いては、サーバサイドの変更を中間レイヤーをつくり、UIまで影響を受けづらいのを目指すのを考えてしまいがちですが、サーバサイドの変更を受け入れ容易に変更できるのが理想かと思いました。

graph RL
ui(UIComponent) -.-> scheme(API Schema) 

5.共通ロジックはすべてユーティリティ

上記の中で、ロジックはすべてUIの責任という話をしました。 ロジックは基本的にコンポーネントのなか、もしくはそのコンポーネントのみがアクセスできるhooksなどに定義するのが良いかと思います。 しかし、やはり、特定のデータのまとまりに対して、コンポーネントでロジックを共通化したい場合があると思います。 そういう場合は、レイヤーを作らず、ユーティリティを定義して使い回せるのが便利です。 ユーティリティは、純粋関数or定数を持つオブジェクトです。状態は持ちません。

graph RL
UIComponent -.-> scheme(API Schema)
UIComponent -.-> Utils

Utilsは3つ存在する

  • componentUtils
    • そのコンポーネントの中で利用されるユーティリティ関数や変数や定数
    • 公開範囲は、そのコンポーネント内に限る
      • 親コンポーネントのユーティリティをその子供のコンポーネントが参照することが可能
  • domainUtils
    • componentUtilsで利用されるものが、複数のコンポーネントで利用される様になった場合に、domainに関するものはdomainUtilsに移動する
    • 公開範囲はすべてのコンポーネント(componentUtilsも含む)
    • テストを書く
  • globalUtils
    • 日付、値段、型、環境変数、共通スタイルなど Appを通して全体で参照されるユーティリティ
    • 公開範囲のすべて
    • アプリのドメイン知識を考えずECサイトであれば使い回せるであろうものはここに書く
    • テストを書く

まとめ

  • データすらも柔軟に変わってしまうフロントエンドにおいて、レイヤーはいらず、UIコンポーネントの責務としてロジックを書く
  • 中間レイヤーは開発速度低下、負債化を招くので、UIはスキーマにギチギチに依存したほうがいい
  • コンポーネントのロジックはすべてUIのものなので、中間レイヤーではなく必要なのはユーティリティ