はじめに
ある程度の規模のアプリケーションのアーキテクチャを考えるときに開発時の影響範囲を限定的にしたりテスタビリティ向上のためにレイヤードアーキテクチャを適用することが多い。一方でどのレイヤーにどういう処理を置くのかや具体的なディレクトリ構成に悩むことがあるので整理してみる。
あくまで個人的な整理であり、これがベストプラクティスであるというものではない。
レイヤードアーキテクチャ
レイヤードアーキテクチャとは
まずはレイヤードアーキテクチャについてまとめる。
例えば PoEAA では 3 つのレイヤーが示されている。プレゼンテーション / ドメイン / データソースである。エヴァンスの DDD 本ではプレゼンテーション / アプリケーション / ドメイン / インフラストラクチャ の 4 層に分けられている。
レイヤーの数に違いはあるが、どちらも共通してプレゼンテーションとドメインは区別されている。マーチン・ファウラーが「One of the most useful design principles」というくらいなので「プレゼンテーションとドメインの分離」は外せないのだろう。
プレゼンテーションとドメインの分離 - Martin Fowler's Bliki (ja)
先に DDD におけるアプリケーション層について軽く触れると、このレイヤーは薄く保たれることを期待しており、ビジネスルールや知識を含まない。また、プロジェクトによってはプレゼンテーション層とアプリケーション層を厳密に区別しないこともあるとしている。
つまり、3 層と 4 層をマッピングするなら以下になるという理解をしている。
これを踏まえて上でまずは 3 層レイヤードアーキテクチャで Angular アプリケーションの役割を整理する。Angular アプリケーションに当てはめるため、前提として DB とやり取りをするバックエンドサーバーが存在し、バックエンドサーバーとは HTTP で通信をするものとする。
Angular アプリケーションでの各レイヤーの役割
プレゼンテーション層
ユーザーに情報を表示したり、ユーザーからのイベントを受け付ける役割を担う。つまり ≒ コンポーネントがプレゼンテーション層となる。コンポーネントは container component と presentational component に分けることが多いが、ここではそのどちらもプレゼンテーション層とする。
container component は 1 つ以上の presentational component を持つ。presentational component は親コンポーネントから受け取ったデータを表示し、ユーザーからのイベントを親に渡す。
データソース(インフラストラクチャ)層
代表的な役割は永続的なデータの保存である。他にも外部システムとの通信を担ったりする。Web フロントエンドの開発において直接 RDB などのデータベースとやりとりすることはないので、主な役割はバックエンドの API を呼び出すクラスを定義することとなる。ブラウザの storage とやり取りする処理を書くこともある。PoEAA ではデータソース層と呼ぶがフロントエンドではほぼバックエンドのゲートウェイとしての役割しかないため、以降はインフラストラクチャ層に統一する。
ドメイン層
ビジネスルールを表現するレイヤーとなる。プレゼンテーションから渡されたデータの妥当性確認をおこなったり、ユーザーのイベントに応じて必要なデータソース層の処理の呼び出しをおこなう。フォームのバリデーションはユーザーに素早くフィードバックするための仕組みであると捉え、プレゼンテーション層の役割とする。
PoEAA ではドメインロジックを構築するのに 3 つのパターンが示されている。ここでは 3 つの中で最もシンプルとされるトランザクションスクリプトでドメイン層を考える。 トランザクションスクリプトはプレゼンテーションから来るユーザーのアクションに対して手続き的に処理をしていく service class となる。 他にもバックエンドとのインターフェイスになる型情報などをドメイン層として管理する。
ここまでを図にすると以下のようになる。
アプリケーション層の追加
プレゼンテーション層とひとくくりにしても container component と presentational component はかなり役割が異なるので別のレイヤーとして分類する。presentational component はプレゼンテーション層で container component はアプリケーション層とする。
また状態管理が必要になってきたら store を用意することが多いが、Global Store ではなく ComponentStore で作る。ページをまたいで状態を管理したいことは少なく、container component 単位でよければ ComponentStore の方が影響範囲やライフサイクルを管理しやすいためである。
図にすると以下のようになる。
実際のアプリケーションのディレクトリ構造はどうするか
ここまでを踏まえて、実際のアプリケーションのディレクトリ構成に落とし込む。src/app/
の下は以下のようにすることが多い。なお standalone であることを前提にしているので NgModule は含まれていない。
features/
と shared/
は https://scrapbox.io/lacolaco/Feature_Pages_パターン の考え方が元となっている。
src/app/ ├── app.component.html ├── app.component.scss ├── app.component.spec.ts ├── app.component.ts ├── core/ ├── domain/ ├── features/ ├── infrastructures/gateways/ ├── routes.ts └── shared/
機能やページの関心事を features/
の下に置き、feature をまたぐような共通処理を shared/
に置く。
core/
にはそれらがないとアプリケーションが起動しないような処理を集める。例えば、ユーザー情報周りの処理や CustomErrorHandler などを置く。
domain/
には上で説明したドメイン層のバックエンドとのインターフェイスになる型情報を置く。これらは feature をまたいで必要とすることが多いので features/
の横に用意する。一方でトランザクションスクリプトは features/
ごとに必要となるのでここには配置しない。
infrastructures/
にはインフラストラクチャ層のバックエンドの API ゲートウェイを置く。
次に features/
を考える。
src/app/features/xxxx/ ├── containers/ │ ├── container1/ │ │ ├── container1.component.html │ │ ├── container1.component.scss │ │ ├── container1.component.spec.ts │ │ ├── container1.component.ts │ │ ├── container1.service.spec.ts │ │ ├── container1.service.ts │ │ ├── container1.store.spec.ts │ │ └── container1.store.ts │ └── container2/ ├── pages │ └── page │ ├── page.component.html │ ├── page.component.spec.ts │ └── page.component.ts ├── routes.ts └── views ├── presentational1/ │ ├── presentational1.component.html │ ├── presentational1.component.scss │ ├── presentational1.component.spec.ts │ └── presentational1.component.ts └── presentational2/
pages/
には routed component を置く。ここまで routed component については触れていなかったが、Router ナビゲーションの行き先となる component である。つまり container component や presentational component と役割が異なるため pages/
として分類する。
containers/
には container component とその状態を管理する ComponentStore を配置する。またレイヤーをまたぐがトランザクションスクリプトの役割を持つ service も一緒に置く。これは co-location の観点から複数の container component の各 service を一箇所に置くより、関連する container component のそばにあったほうがメリットが多いという判断である。
最後に views/
である。ここには presentational component を配置する。UI のための加工処理をプレゼンテーションロジックとして別ファイルにする場合はそれも同様にここで管理する。例えばこの feature の UI 専用の pipe などを指している。
以上がおおまかなディレクトリ構成である。
まとめ
Angular アプリケーションでレイヤードアーキテクチャと実際のディレクトリ構成を整理してみた。今の自分なりの整理なので解釈が間違っている部分もあるかもしれない。 今のところこの形がしっくりきているが、こういうのはこれで完璧、ということはないので試行錯誤していきたい。