Feature Sliced Design(フロントアーキテクチャ)でクリーンなアプリケーション構造を実現
はじめに
FSD(Feature-Sliced Design)アプローチを理解するために、その概念と設計方針を説明します。
既にFSDに関して基本的な知識がある方は、「参考」に記載している記事やgithubコードを参照すると理解を促進すると思います。
この記事では必要最小限のtodoアプリを例にとり説明します。
FSDの特性から下記をご認識ください。
- フロントエンドアプリケーションを拡張性の高いものにするためのアーキテクチャ設計論になります。バックエンドへの適応は非推奨になります。
- ユーザー向けのアプリケーションを対象としている為、ライブラリやUIキットへの適応は非推奨になります。
この記事のゴール
下記の観点を理解することを目標にします。
- FSDの基本理解
- FSDの設計概念の詳細
- Todoアプリを用いて、具体的な適用方法
- Next.js App router環境でのFSDの適用方法
- FSDの良かったところを踏まえての総評
FSDとは
機能を「スライス」に分割する設計アプローチになります。
各スライスはそれぞれ独立して設計される為、機能の柔軟性や拡張性が向上します。
ユーザーアプリケーションであれば、ウェブ・モバイルその他のソフトウェア製品の設計に適用ができます。
設計概念
FSDは、レイヤー > スライス > セグメントという3つの概念と関係性で区別しています。
レイヤー
レイヤーは合計で7個あり、アプリケーションの1層目にあたります。
レイヤー同士でも依存関係が存在し、この階層構造は、レイヤーの依存関係を指し示します。
例えば、entitiesレイヤーでは、上位層にあるfeaturesの機能を利用することができません。一方でsharedレイヤーにあるものはimport可能です。
この階層構造により、レイヤー同士の依存関係は一方向に担保することができます
階層内でレイヤーが下位に位置するほど、コード内のより多くの場所で使用される可能性が高いです。その為、下位のレイヤーを変更する際はリスクが高くなることに注意してください。
各レイヤーがどのように使用されるかのイメージをgithubを用いたものがこちらになります。
参照: https://dev.to/m_midas/feature-sliced-design-the-best-frontend-architecture-4noj
Processesレイヤーに関して
This layer has been deprecated. The current version of the spec recommends avoiding it and moving its contents to features and app instead.
Processesは現在非推奨レイヤーとなっています。その為、今回は説明を省きます。
もしこのレイヤーを利用する際は、featuresとappが利用できないか検討してみてください。
App
アプリ全体ロジックの初期化、router, provider, global styleなどが含まれます
app
├── globals.css
└── providers
├── index.ts
└── redux-store.tsx
Pages
entities, features, widgetからページ全体を構築するための構成レイヤー。
場合によってはデータ取得ロジックとエラー処理が含まれています。
pagesは、その設計仕様と機能を備えた自己完結型(self-contained)のユニットとして開発されます。
Todoアプリでは、todoのリストを表示するホームページ (todo-list)、特定のタスクに関する詳細情報を提供するタスクの詳細ページ (todo-details)などが該当します。
pages
└── todo-home
├── index.ts
└── ui
├── TodoHome.css.ts
└── TodoHome.tsx
Widgets
entitiesとfeaturesにあるブロックを結合して構成するレイヤー。
ビジネスロジックは、基本的にに含まない。
再利用可能なコンポーネントを用意したい際に利用する。
Todoアプリでは、下記のようにリスト部分をwidgetsとして作成しているが、再利用する予定がない場合、pagesに記載してもよいと考える。
widgets
└── todo-list
├── index.ts
└── ui
├── Todo.tsx
├── TodoList.css.ts
└── TodoList.tsx
Features
ビジネス価値をもたらす機能を扱うレイヤー。
自己完結型の機能コンポーネントとして扱うことで、アプリケーションの主要な機能を独立した単位として整理し、他の部分との結合を最小限に抑えることが重要です。
これは、設計仕様と機能を備えたユーザーインターフェイスの一部と考えることができます。
これにより、保守性と拡張性が向上します。
Todoアプリでは、タスクのフィルターや、CRUDのUIが該当するかと思います。entitiesやsharedのロジックを用いて、独立した機能を達成します。
features
├── todo-create-form
│ ├── index.ts
│ └── ui
│ ├── TodoCreateForm.css.tsx
│ └── TodoCreateForm.tsx
└── todo-edit-form
├── index.ts
└── ui
├── TodoEditForm.css.ts
└── TodoEditForm.tsx
Entities
ビジネスロジックやモデルを扱うレイヤー。
静的UI, データストアやCRUD処理等がここに該当します。
Todoアプリでは、CRUDの処理とタイトル、内容、作成者を含んだTodo(ビジネスモデル)がここに該当します。
entities
└── todo
├── index.ts
└── models
├── index.ts
├── selectors.ts
├── slice.ts
├── thunk.ts
└── types.ts
Shared
プロジェクト/ビジネスの詳細から切り離されたレイヤー
特定のビジネスロジックに関連づけられていない再利用可能なコンポーネントやユーティリティが該当します。
アプリ全体で使用するuiコンポーネント, typeやapi等が該当します。
Todoアプリでは、フォームフィールド、投稿ボタンやapi等が該当します。
shared
├── api
│ └── todo
│ ├── index.ts
│ ├── todo.ts
│ └── types.ts
├── lib
│ └── store
│ ├── index.ts
│ ├── store.ts
│ └── types.ts
├── types
│ ├── apiStatus.ts
│ ├── errorType.ts
│ └── index.ts
└── ui
├── error-messages
│ ├── ErrorMessages.css.ts
│ └── ErrorMessages.tsx
├── index.ts
├── submit-button
│ ├── SubmitButton.css.ts
│ └── SubmitButton.tsx
├── todo-form-input
│ ├── TodoFormInput.css.ts
│ └── TodoFormInput.tsx
└── todo-update-button
├── TodoUpdateButton.css.ts
└── TodoUpdateButton.tsx
スライス
Slices are the second level in the organizational hierarchy of Feature-Sliced Design. Their main purpose is to group code by its meaning for the product, business or just the application.
スライスは二番目の階層に当たるレイヤーになります。スライスの目的は、ビジネスライヤーに基づいてコードをグループ化することです。
スライスの階層はアプリケーションに依存する為、FSDは特に標準化していない為、プロジェクト毎に決定する必要があります。
また、スライスはお互い依存してはいけません。その為、画像のようなパターンはNGとなります。もし共有したいリソースがある場合は、構造を見直すか、sharedを活用する必要があります。
todoアプリのレイヤーとスライス構成の例
筆者は下記のように構成しました。
レイヤー | スライス |
---|---|
entities | todo |
features | todo-create-form, todo-edit-form |
pages | todo-home |
widgets | todo-list |
app, shared | × |
セグメント
Segments are the third and final level in the organizational hierarchy, and their purpose is to group code by its technical nature.
セグメントは最後の3番目の階層です。セグメントの目的はコードを技術的な性質に基づいてグループ化することです。
ui/model/lib/apiと4つの標準化されたセグメントが存在します。。
- ui — UIに関するコンポーネント
- model — ビジネスロジック、データストレージおよびそのデータを操作する関数
- lib — 補助コードおよびインフラストラクチャコード
- api - 外部API、バックエンドAPIメソッドとの通信部分
カスタムセグメントについて
Custom segments are permitted, but should be created sparingly. The most common places for custom segments are the App layer and the Shared layer, where slices don't make sense
カスタムセグメントは許可されていますが、慎重に行う必要があります。基本的には、スライスが存在しないappレイヤーもしくは共有レイヤーで利用されます。
私のtodoでは、下記のようにapp/providersやshared/typesディレクトリをカスタムセグメントとして作成しております。
app
├── globals.css
└── providers
├── index.ts
└── redux-store.tsx
shared
├── api
│ └── todo
│ ├── index.ts
│ ├── todo.ts
│ └── types.ts
├── lib
│ └── store
│ ├── index.ts
│ ├── store.ts
│ └── types.ts
├── types
│ ├── apiStatus.ts
│ ├── errorType.ts
│ └── index.ts
└── ui
├── ...
レイヤーとセグメントの関係性について(一例)
https://feature-sliced.design/docs/reference/slices-segments#segments
上記URLに記載されている「Example」を日本語で整理する。
レイヤー | ui | model | lib | api |
---|---|---|---|---|
shared | uiコンポーネント | × | 複数ファイルで使用するutils | 認証やキャッシュのような追加機能を持ったAPIクライアント |
entities | ビジネスモデルのスケルトン | entityのインスタンスのデータストレージと、そのデータを操作するための関数。CRUD処理等が該当する | ストレージに関連しないエンティティのインスタンスを操作する関数 | SharedのAPIクライアントを使用したAPIメソッド |
Features | ユーザーが機能を使用する為のインタラクティブな要素 | 必要に応じて、ビジネスロジックとインフラストラクチャのデータストレージを操作する(現在のアプリのテーマなど) | modelのセグメントに記載するビジネスロジックを簡潔に記述するのに役立つutils | バックエンドでFeaturesを提供するAPIメソッド |
Widgets | entityとfeaturesを独立したUIブロックに構成する。エラー境界やロード状態も含む | インフラストラクチャーのデータストレージ(必要な場合) | ビジネスロジック以外のインタラクションや、ブロックがページ上で機能するために必要なその他のコード | 基本的に使用しない |
Pages | エンティティ、フィーチャー、ウィジェットでページを構成する。また、エラー境界やロード状態も含む | 基本的に使用しない | ビジネス以外のインタラクション | SSR指向フレームワーク用データローダー。初期データの取得等の処理が該当 |
Public API
各スライスとセグメントにはpublic APIが存在します。
Public APIを使用すると、インポートとエクスポートの操作が簡素化されるため、アプリケーションに変更を加えるときに、コード内のあらゆる場所でインポートを変更する必要がありません。
Public APIはindexファイルに公開モジュールを集約する場所となっています。スライスやセグメントから必要な機能だけを外部に抽出し、不要な機能を分離することができます。また外部ファイルからは、このindexファイルはエントリポイントとして機能します。
NGパターン1
外部のインターフェースが、モジュールの内部部分に直接アクセスしているパターン
改善メリット: 許可されているもののみをインポートするよう改善することで、アクセスされているモジュールが一目で判断できます。
export { TodoCreateForm } from "./ui/TodoCreateForm";
- import { TodoCreateForm } from "@/front/features/todo-create-form/ui/TodoCreateForm";
+ import { TodoCreateForm } from "@/front/features/todo-create-form";
NGパターン2
モジュールの内部構造が変わった際に、外部から見えるパブリックAPIに影響を与えてしまうケース。
改善メリット: featureの内部構造が外部のユーザーには露出せず、コンポーネントの移動や名前の変更が外部のユーザーに影響を与えないようになります。
- import { Form } from "features/todo-create-form/ui/form"
+ import { TodoCreateForm } from "features/todo-create-form"
NGパターン3
名前の衝突を起こしてしまうケース。
- export { Form } from "./ui/TodoCreateForm"
+ export { Form as TodoCreateForm } from "./ui/TodoCreateForm"
- export { Form } from "./ui/TodoCreateForm"
+ export { Form as TodoEditForm } from "./ui/TodoCreateForm"
Next.jsのApp router環境でFSDを適用する場合
NextJS App Routerは、appフォルダー内のファイルがURLと認識される特性があります。
このようなルーティング構造が存在する場合、純粋にFSDを適用することができません。
その際のアプローチの一つとして、下記のディレクトリ構造にするとFSDを適用することが容易に可能になります。
├── app # NextJS app folder
├── src
│ ├── app # FSD app folder
│ ├── entities
│ ├── features
│ ├── pages
│ ├── shared
│ ├── widgets
https://dev.to/m_midas/how-to-deal-with-nextjs-using-feature-sliced-design-4c67
Next.js app routerを用いたtodoの構成について
筆者は下記のように構成しました。
レイヤー | スライス | セグメント |
---|---|---|
entities | todo | models |
features | todo-create-form, todo-edit-form | ui |
pages | todo-home | ui |
widgets | todo-list | ui |
app | × | providers, globals.css |
shared | × | api, lib, types, ui |
todo構成
.
├── app
│ ├── api
│ ├── layout.tsx
│ ├── not-found.tsx
│ └── page.tsx
└── front
├── app
│ ├── globals.css
│ └── providers
│ ├── index.ts
│ └── redux-store.tsx
├── entities
│ └── todo
│ ├── index.ts
│ └── models
│ ├── index.ts
│ ├── selectors.ts
│ ├── slice.ts
│ ├── thunk.ts
│ └── types.ts
├── features
│ ├── todo-create-form
│ │ ├── index.ts
│ │ └── ui
│ │ ├── TodoCreateForm.css.tsx
│ │ └── TodoCreateForm.tsx
│ └── todo-edit-form
│ ├── index.ts
│ └── ui
│ ├── TodoEditForm.css.ts
│ └── TodoEditForm.tsx
├── pages
│ └── todo-home
│ ├── index.ts
│ └── ui
│ ├── TodoHome.css.ts
│ └── TodoHome.tsx
├── shared
│ ├── api
│ │ └── todo
│ │ ├── index.ts
│ │ ├── todo.ts
│ │ └── types.ts
│ ├── lib
│ │ └── store
│ │ ├── index.ts
│ │ ├── store.ts
│ │ └── types.ts
│ ├── types
│ │ ├── apiStatus.ts
│ │ ├── errorType.ts
│ │ └── index.ts
│ └── ui
│ ├── error-messages
│ │ ├── ErrorMessages.css.ts
│ │ └── ErrorMessages.tsx
│ ├── index.ts
│ ├── submit-button
│ │ ├── SubmitButton.css.ts
│ │ └── SubmitButton.tsx
│ ├── todo-form-input
│ │ ├── TodoFormInput.css.ts
│ │ ├── TodoFormInput.stories.tsx
│ │ └── TodoFormInput.tsx
│ └── todo-update-button
│ ├── TodoUpdateButton.css.ts
│ └── TodoUpdateButton.tsx
└── widgets
└── todo-list
├── index.ts
└── ui
├── Todo.tsx
├── TodoList.css.ts
└── TodoList.tsx
総評
明確なディレクトリ管理とほどよい疎結合性
FSDによりレイヤー、スライス、セグメントにより既に標準化された管理方法が整っています。
またレイヤー同士の依存関係が一方向であり、スライス同士は必ず疎結合になります。
その為、拡張性と変更による副作用が小さくなるという点で非常に優れているアーキテクチャと感じました。
叫ぶアーキテクチャ(Screaming Architecture)に近い
近年では、そのアプリケーションの目的やドメインをコードで明確に表現すべきと考える「叫ぶアーキテクチャ」が主流だと考えています。このアプローチのメリットは、コードの構成がその機能やビジネスロジックを直感的に反映しているため、プロジェクトの理解やメンテナンスが容易になることです。
ビジネスロジックも含め、アプリケーションの構造がその機能や目的を明確に反映すべきだという点で両者はかなり似ていると思います。
参考
文献
- Feature-Sliced Design —modern Front End Architectural Methodology on Angular.
- Feature-Sliced Design: The Best Frontend Architecture
- Usage with NextJS
- 公式
- 個人的におすすめしたいFeature-Sliced Designというフロントエンドアーキテクチャ設計方法論
- Feature-Sliced Design - スケーラブルなフロントエンドアーキテクチャを目指して
- How to deal with NextJS App Router and FSD problem
- Needs driven
Discussion