LoginSignup
16
6

More than 3 years have passed since last update.

Webフロントエンド Clean Architecture 考

Last updated at Posted at 2020-07-26

Webフロントエンドでも Clean Architecture に関する考察や実際のサービスへの導入の事例をよく見かけるようになりました。

n番煎じですが Clean Architecture をWebフロントエンドのアプリケーションに導入する際の考察をコードを書きつつしてみました。

サンプルコード はこちらにあります。

また、今回のコードを書くにあたり、 @ttiger55 さん、@pirosikick さんのエントリーやコードを参考にしました。ありがとうございます :)

Pros/Cons

コードを書いてみた上での結論を先に述べておきます。
Pros/Cons で言えば Pros の利点が多く感じられますが、結局のところ「ビジネス要件」がある程度固まっているか否かというソフトウェア側でコントロールしにくい箇所がボトルネックとなっているのため、ここをどう捉えるかは個人によって差が出そうかなという印象を持ちました。

Pros

  • Webフロントエンドを実装する上でのビジネス要件が決まっている場合は強力
    • ビジネス要件の実装と主にUIやWeb APIを実装する両軸は基本的に Interface Adapter に該当するレイヤーを介して会話すれば良いので、互いに疎な状態で開発が可能
    • Interface Adapter, Driver 層はビジネス要件と別に切り離して設計ができるのでWeb側の仕様変更に強い
      • ビジネス要件は原則 Immutable なので Web側の仕様を変更すれば良い
    • テストなどは各層に切り分ける場合依存する Interface のモックを書けば良いので依存する実装に引きずられない
  • ビジネス要件の実装をどの External Interface でも使用できる形で提供できる
    • 物によるが...例えば、今回の例だと TypeScript で書く場合そこそこのつぶしは効く
    • ビジネス要件側は比較的凝集度が高い関数などで定義されるためテストなどもしやすい
      • 「本当に大事なロジック」と「アプリケーション側での気まぐれ」の区別がはっきりしているので実装時に余計なことを考えなくて良い

幾らかの方も仰っていますが、既存のプロジェクトのリファクタやビジネス要件がしっかり決まっている場合のプロジェクトには導入がしやすそうです。

Cons

  • ビジネス要件が固まっていないサービスでは結局は従来(これは筆者が経験してきた経歴を抽象化した感じ)のWebフロントエンドの実装形態に近い状態になりしんどい
    • ビジネス要件がコロコロ変わるようなステージのプロダクトには向いていない
      • Enterprise Business 層の I/O が変わるとほぼ全ての上位層に影響を受けてしまうで気に留めておく必要がある
      • そもそもこの状況下で Clean Architecture ベースで Webフロントエンド開発するのは中々難しいのではないか...

設計に関して

Clean Architecture は 世界一わかりやすい Clean Architecture 内でも述べられている、

  1. 依存性は上位層のみに向ける
  2. 制御の流れと依存の方向は分離しコントロール

とあるように、有名な画像ベースにある4層構成で考えなくても良いのですが、なんだかんだ4層より小さいスケールで Webフロントエンドはなかなか考えにくいかもといった感じでした。

試行錯誤の結果以下のようになりました。

pagas           # as [external interface] via useing next.js
src
 ┣ business     # as [business rule] layer
 ┃ ┣ entities   # enterprise business rules
 ┃ ┗ usecases   # application business rules
 ┣ adapters     # as [interface adaptors] layer
 ┣ web          # as [external interfaces] layer
 ┃ ┣ api        # web api
 ┃ ┗ view       # user interface
 ┣ utils        # utility module for application
 ┗ __tests__    # unit, integration testing

「ビジネス要件」に該当する層 (Business) が Entity (Enterprise) と UseCase (Application) の2層、Business と External interface のコミュニケーションを行う層を Adapters (詳しい役割は後述)、External Interface を Web (UI 及び API) として4層構成になっています。

命名法に悩む

Clean Architecture の概念を読んだ時そもそも「各層の命名が全然 Webフロントエンド実装する時の抽象概念と違うな...(自分だけかもしれないが)」というのがネックでした。

Repository や Driver 一つとってもそもそも Webフロントエンドではあまり馴染みがなく、データアクセス周りの処理って永続化って確かに永続化する場合もあるけど...Web を使った通信やブラウザメモリへのアクセスだけだしなぁ...などいろいろモヤモヤ。

もちろん、Clean Architecture での命名も拝借したところありますが、もう少し Webフロントエンドのケースで各ディレクトリが何を定義しているのか直球的な命名法にしてみました。

名前にはやはり意味があるので、あまり元の概念だけで物事を安直に考えるのも気をつけたほうが良いなと関係ないところで自戒しました。

以下各層でどういう振る舞いを考えたか書いていきます(リポジトリのソースコードを読みながらのほうがわかりやすいかもしれません)

Business (Entities, UseCases) 層

Entities

Entities はサービスで軸としたいオブジェクト、または純粋な関数で表現できる振る舞いを定義しました。サンプルでは TODO アプリを Clean Architecture で表現する場合で考えています。

- Todo のインターフェース
- Todo のプロパティにおけるビジネス上のルール
  - 今回は Todo の title, detail の文字制限などを関数で表現(バリデーションとして定義) 

このビジネス要件を比較的純粋なインターフェースやコードにまず考えるところがかなり悩みます。凝集度を高い状態で定義する層かと。

また、この層は UseCase 以上のことは考えずに定義できていることが望ましいので、「Web で使うからこうしておくか...」みたいな煩悩を捨てなければいけないところもフロントエンドやっている人は中々大変かもしれません(ただ、これはあくまで「今回のケース」はであって様々なケースで柔軟に定義は変えれるのが一番かと思います)。

UseCases

Entities で定義したインターフェースや関数を組み合わせてアプリケーション固有のルールを作成します。

サンプルでは 「Todo を作成する際、title, detail の要件を満たせていれば Data Access を介して Todo を作成できる」というものです(この記事の投稿の時点での UseCase になります)。

この場合の、Data Access はどういう実装を施すかは考えません。あくまでインターフェースを定義するだけです。ここから「制御の流れと依存の方向は分離しコントロール」を意識していく必要があります。

Adapters 層

Adapter 以降はアプリケーション固有、External Interface 固有の作りになっていきます。

さて、今回の Todo アプリ、Web で表現する場合何が考えられるかと思考を巡らせてみたところ、

- Todo に関するデータのやり取り(登録・更新・削除など)は Web の通信(API)を使用しそう
- Web の通信(API)のデータとビジネス要件のデータのインターフェースは違うことがよくある
- Web の通信を行う詳細(HttpClient の仕様は気にしたいくない)は知らないでも使えるようにしたい

のようなことを考えました。結果、「通信の詳細は知らないけど通信で取得したデータをアプリケーション用に変換する Converter のようなもの」として Interface Adapter を定義してみました。

ここまでみると、いつも一連のストリームで行っている処理(おそらく数十行で完結する処理)を各層、且つ一方的な依存関係による処理として分割している形になっているのがおわかりになりますでしょうか。

1. Todo の I/O 及び バリデーション定義 <= [Entity -> UseCase]
2. 1を満たす Input を使って Todo を作成する <= [UseCase -> Adapter]
3. Todo を作成するための Data Access の実装 <= [Adapter -> Web/API]

また、Interface Adapter では Web の通信ではないケース、例えば LocalStrage, ブラウザメモリなどで処理を完結したい場合も Adapter を置き換えるだけなので仕様変更も比較的簡単かと思います。

Web 層

Interface Adapter まで定義すればあとはいつも書いている処理とほとんど変わらなくなります。Web では UI を定義する View、Web の通信を行う API をディレクトリとして設置し、UseCase を使用する実装を行っていきます。

API

Interface Adapter より呼び出される HttpClient の実装になります(Clean Architecture の文脈だと Driver に該当するところでしょうか)。HttpClient は何で実装しても上位レイヤーは気にならないので仕様の変更をいくらでもできます。また、API のリクエスト用の URI もここの実装しか知らないため変更が容易です。

ここの層ではエラーの処理などは行わず取得したデータを JSON として返すだけの処理になります。

View

サンプルでは Next.js を使用しています。Next.js なので pages ディレクトリは使用上必須になります。

View ではこれまで定義した UseCase を使うために View 上でブートする必要があります。依存関係を自分で書いても良かったのですが、変更などを簡単に行いたいため、今回は Awilix を使用して DI の形式で行いました。

view/container.ts で DI Container を作成し、Next.js の pages/_app.tsx で Context API の Provider を使用してブートした DI Container を pages 配下でどこでも呼び出せるように処理を行っています。

なんとなく Angular の DI と似てきたなって思いつつ、Angular でも Clean Architecture のエッセンスは応用ができそうとも考えたりしました。

今回の設計では HttpClient などの処理は UseCase 側で完結しているので、View は原則 UseCase を使用するだけでシンプルになります。Redux などとも組み合わせる際は UseCase を Redux の Middleware で使える様にすればよさそうです。

テスト

コアロジックとなる箇所のテストは Unit, Integration がやりやすいため、View 側で UseCase を使う際も「テストを行っている」安定感がある中で UseCase を使用できるためとても良いなという感じです。

Data Access や Adapter はテストではモックを書くだけ(インターフェースは別途定義しているため)で済むため、インターフェースが変わらない限り、モックをどう書こうと他の層に影響が出ないのが嬉しいですね。

設計・実装してみて

各層での関心ごとが違うので、一つの大きな処理フローもハンドリングがし易い感覚を持ちました。

全てのプロダクトにこの形で導入できるとは思えませんが、Clean Architecture の発想をプロダクトの一部分などに応用は効きそうです。

16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6