大規模フロントエンドの整理:LIFT vs Feature-Sliced Design のジレンマ
Lukas Schneider
DevOps Engineer · Leapcell

はじめに
フロントエンドアプリケーションが複雑さと規模を増すにつれて、コードベースの整理方法は極めて重要になります。明確に定義された構造がないと、プロジェクトはすぐに管理不能な混乱に陥り、開発サイクルの遅延、バグの増加、新しいチームメンバーの学習曲線の急峻化につながります。これらの課題に対処するために、2つの著名なアーキテクチャパターンが登場しました。LIFT(Locating Logic Intuitively Fast)とFeature-Sliced Design(FSD)です。どちらもモジュール化と保守性に対して異なるアプローチを提供しますが、それぞれ異なる哲学とプロジェクトのニーズに対応しています。この記事では、LIFTとFSDを分解し、それらの原則、実装戦略、および適切なユースケースを比較して、次の大規模フロントエンドプロジェクトのために情報に基づいた意思決定を行うのを支援します。
コアコンセプトの説明
LIFTとFSDの複雑な詳細に入る前に、この議論の中心となるいくつかの基本的な用語を明確にしましょう。
- モジュール化(Modularity): システムのコンポーネントを分離および再結合できる度合い。高いモジュール性とは、コンポーネントが独立しており、交換可能であることを意味し、開発と保守を容易にします。
- 関心の分離(Separation of Concerns): コンピュータプログラムを、機能の重複を可能な限り少なくして、明確に区別できる機能に分割する原則。各コンポーネントまたはモジュールは、理想的には1つの特定の関心事を処理する必要があります。
- カプセル化(Encapsulation): データとそのデータを操作するメソッドをバンドルし、コンポーネントの一部の直接アクセスを制限すること。これは、ファイルまたはフォルダの境界によって達成されることがよくあります。
- ドメイン駆動設計(Domain-driven Design - DDD): ドメインモデルの定義を中心に据えたソフトウェア開発へのアプローチ。フロントエンドでは、これは技術的な懸念ではなく、ビジネスコンセプトを中心にコードを整理することに翻訳されます。
- スケーラビリティ(Scalability): システムが処理する作業量を増やすことができる能力、またはその成長に対応するために拡大できる可能性。コード整理では、これは、パターンが、扱いにくくなることなく、多数のファイルと機能を効率的にサポートできることを意味します。
LIFT:Locating Logic Intuitively Fast
原則と哲学
Locating Logic Intuitively Fastの頭字語であるLIFTは、Angularによって普及した一連のガイドラインです。その中心的な哲学は、プロジェクトの規模に関係なく、開発者が関連コードをすばやく簡単に見つけられるようにすることにあります。LIFTは4つの主要なルールを提案しています。
- 場所(Locate): 特定の機能のファイルは、直感的で単一の場所に配置する必要があります。これは通常、機能に関連するすべてのファイルを独自のディレクトリにまとめることを意味します。
- 識別(Identify): ファイル名は、それらが何を含んでいるかをすぐに示す必要があります。たとえば、
user-list.component.ts
は、ユーザーリストのAngularコンポーネントとしてファイルを明確に識別します。 - フラット(Flat): 機能が大幅に成長するまで、フォルダ構造を可能な限りフラットに保ちます。これにより、ナビゲーションが煩雑になる深いネストを回避できます。
- DRY(Don't Repeat Yourself)を試みる: コードの冗長性を回避します。
LIFTの主な目的は、開発者エクスペリエンスと発見可能性です。開発者が特定の機能で作業する必要がある場合、その専用フォルダにすばやく移動して、関連するすべてのファイルを見つけることができるはずです。
実装と例
LIFTベースの構造では、通常、機能がトップレベルディレクトリにグループ化されているのを見ます。各機能ディレクトリ内には、すべてそのコンポーネント、サービス、モデル、およびテストが含まれています。
簡単なeコマースアプリケーションを考えてみましょう。LIFT構造は次のようになります。
src/
├── app/
│ ├── core/ // アプリケーション全体で利用可能なサービス、インターセプターなど
│ │ ├── auth/
│ │ │ ├── auth.service.ts
│ │ │ └── ...
│ │ └── ...
│ ├── shared/ // 再利用可能なUIコンポーネント、ユーティリティ関数
│ │ ├── components/
│ │ │ ├── button/
│ │ │ │ ├── button.component.ts
│ │ │ │ └── button.component.html
│ │ │ └── ...
│ │ └── pipes/
│ │ └── ...
│ ├── features/ // 主要なビジネス機能
│ │ ├── product/ // 'product' 機能
│ │ │ ├── components/
│ │ │ │ ├── product-card/
│ │ │ │ │ ├── product-card.component.ts
│ │ │ │ │ └── product-card.component.html
│ │ │ │ └── product-list/
│ │ │ │ ├── product-list.component.ts
│ │ │ │ └── product-list.component.html
│ │ │ ├── services/
│ │ │ │ └── product.service.ts
│ │ │ ├── models/
│ │ │ │ └── product.model.ts
│ │ │ ├── product.module.ts
│ │ │ └── product.routes.ts
│ │ ├── cart/ // 'cart' 機能
│ │ │ ├── components/
│ │ │ │ ├── cart-item/
│ │ │ │ └── cart-view/
│ │ │ ├── services/
│ │ │ └── ...
│ │ └── user/
│ │ └── ...
│ └── app.component.ts
│ └── app.module.ts
│ └── app.routes.ts
└── environments/
└── main.ts
この例では、product
機能に関連するすべてのファイル(コンポーネント、サービス、モデル、ルーティング)が src/app/features/product
ディレクトリ内に配置されています。これにより、製品関連の機能で作業する際に、関連するすべてのコードを見つけやすくなります。
アプリケーションシナリオ
LIFTは特に以下に適しています。
- 小規模から中規模のアプリケーション: 機能の数が管理可能で、開発者が全体構造を簡単に把握できる場合。
- 高速な機能ナビゲーションを優先するチーム: コードを見つける速度が開発者の生産性にとって最優先事項である場合。
- 明確なドメイン境界を持つプロジェクト: 機能が定義されると、その内部のすべての部分が密接に結合されます。
しかし、アプリケーションが成長するにつれて、機能間の境界がぼやけ始め、機能モジュール間の依存関係の管理が困難になる可能性があります。「共有」コンポーネントやサービスがさまざまな機能フォルダに配置されたり、「共有」フォルダがゴミ箱になったりする可能性があります。
Feature-Sliced Design(FSD)
原則と哲学
Feature-Sliced Design(FSD)は、コード整理に対してより意見が分かれる、構造化されたアプローチを採用しており、大規模で複雑なアプリケーションのための高度にスケーラブルで保守可能なアーキテクチャの作成を目指しています。FSDは、「スライス」と「レイヤー」の概念を導入し、レイヤー間の通信と依存関係管理に厳格なルールを設けています。その中心的な原則は次のとおりです。
- レイヤー(Layers): アプリケーションは水平レイヤーに分割され、それぞれが特定の責任を持ちます(例:
app
、pages
、widgets
、features
、entities
、shared
)。 - スライス(Slices): 各レイヤー内では、コンポーネントがアプリケーションのドメインの自己完結型部分を表す「スライス」(例:
product-card
、user-profile
)にグループ化されます。 - 垂直方向の結束、水平方向の分離(Vertical Cohesion, Horizontal Isolation): 特定の機能またはスライスに関連するコードは、レイヤー全体で垂直にグループ化されますが、同じレイヤー内の異なるスライスは互いに分離されます。
- 厳格な依存関係ルール(Strict Dependency Rules): 上位レイヤーは下位レイヤーに依存できますが、その逆はできません。この一方向の流れは、循環依存を防ぎ、堅牢なアーキテクチャを促進します。
- 公開API(Public API): 各スライスは、カプセル化を維持し、外部とのやり取りを制御するために、明確に定義された公開APIを公開する必要があります。
FSDは、アーキテクチャの明確さ、カプセル化、およびスケーラビリティを主な目標として強調しています。厳格なルールを施行することで、コードベースの劣化を防ぎ、アプリケーションが成長するにつれて複雑さを管理しやすくすることを目指しています。
実装と例
FSDは通常、レイヤーに基づく階層構造でプロジェクトを整理し、さらにそれらのレイヤー内のスライスに分割します。
src/
├── app/ // アプリケーション全体で利用可能なロジック、ルーティング、グローバルスタイル
│ ├── providers/ // グローバルコンテキストプロバイダー(例:Reduxストア、認証コンテキスト)
│ │ ├── store.ts
│ │ └── auth.ts
│ ├── layouts/ // コアレイアウト(例:ヘッダー、フッター、サイドバー)
│ │ ├── base-layout/
│ │ │ ├── index.ts
│ │ │ └── ui.tsx
│ │ └── ...
│ ├── index.ts
│ └── styles/
│ └── global.css
├── pages/ // フルページコンポーネント、機能/ウィジェットの compositon
│ ├── home/
│ │ ├── index.ts
│ │ └── ui.tsx
│ ├── product-details/
│ │ ├── index.ts
│ │ └── ui.tsx
│ └── profile/
│ └── ...
├── widgets/ // ダッシュボードウィジェットのような、関連する機能/エンティティの組み合わせ
│ ├── product-list-widget/
│ │ ├── index.ts
│ │ └── ui.tsx
│ └── user-profile-card/
│ └── ...
├── features/ // 具体的なビジネスロジック、特定のインタラクション(例:「カートに追加」)
│ ├── add-to-cart/
│ │ ├── index.ts // `addToCartModel`のような機能の公開API
│ │ ├── api/ // API呼び出し
│ │ ├── model/ // Reduxスライス、状態管理
│ │ ├── ui.tsx // UIコンポーネント、`model`に依存する可能性がある
│ │ └── lib/ // ユーティリティ
│ ├── sign-in/
│ │ └── ...
│ └── filter-products/
│ └── ...
├── entities/ // 「product」、「user」、「order」のようなドメイン固有のエンティティ
│ ├── product/
│ │ ├── index.ts // `productModel`のようなエンティティの公開API
│ │ ├── api/ // 製品関連のAPI呼び出し
│ │ ├── model/ // 製品データのReduxスライス、状態管理
│ │ ├── ui.tsx // 製品データを表示するためのUIコンポーネント(例:生の製品アイテム)
│ │ └── lib/ // ユーティリティ
│ ├── user/
│ │ └── ...
│ └── ...
├── shared/ // 再利用可能で汎用的なコード(UIコンポーネント、ユーティリティ、設定、ライブラリ)
│ ├── ui/ // 汎用UIコンポーネント(例:Button、Input)
│ │ ├── button/
│ │ │ ├── index.ts
│ │ │ └── ui.tsx
│ │ └── input/
│ │ └── ...
│ ├── lib/ // ユーティリティ関数(例:date-formatter)
│ │ └── formatters.ts
│ ├── config/ // アプリケーション全体で利用可能な定数
│ │ └── apiUrl.ts
│ └── api/ // 汎用APIユーティリティ
│ └── baseApi.ts
└── index.tsx // エントリポイント
このFSDの例では:
product-details
page
は、product
entity
(データ表示用)とadd-to-cart
feature
(インタラクション用)を統合します。add-to-cart
機能は、product
エンティティ(追加すべきものを知るため)およびshared/ui
(ボタン用)に依存する可能性があります。- 依存関係は常に下向きに流れます:
pages
はwidgets
、features
、entities
、shared
を使用できます。features
はentities
とshared
を使用できます。entities
はshared
のみを使用できます。この厳格なルールにより、上位レベルのロジックが下位レイヤーに漏れ出すのを防ぎ、明確な境界を維持します。
アプリケーションシナリオ
FSDは以下に非常に効果的です。
- 大規模で非常に複雑なアプリケーション: 長期的な保守性とスケーラビリティが最重要視される場合。
- 大規模チーム: 多くの開発者がアプリケーションのさまざまな部分で同時に作業する場合、FSDの厳格なルールは競合を最小限に抑え、アーキテクチャの一貫性を保証します。
- 高いアーキテクチャの厳密さを必要とするプロジェクト: 技術的負債を防ぎ、クリーンアーキテクチャを強制することが最優先事項である場合。
- 進化する要件を持つアプリケーション: モジュール性により、大幅な影響なしに機能の追加、削除、または変更が容易になります。
FSDの規約を学習し、その厳格な構造を設定するためのオーバーヘッドは、最初に高くなる可能性があり、このレベルの厳密さが正当化されない非常に小規模で迅速にプロトタイプ化されたアプリケーションにはあまり適していません。
結論
LIFTとFeature-Sliced Designはどちらも大規模なフロントエンドプロジェクトの整理に価値のあるアプローチを提供しており、それぞれに長所とトレードオフがあります。LIFTは開発者の直感と高速なコードロケーションを優先し、小規模から中規模のアプリケーションや、高速な機能ナビゲーションを重視するチームに最適です。対照的に、Feature-Sliced Designは、大規模で複雑なアプリケーションや、長期的な保守性およびアーキテクチャの整合性が不可欠な大規模チームに理想的な、高度に構造化されスケーラブルなフレームワークと厳格な依存関係ルールを提供します。LIFTとFSDのどちらを選択するかは、最終的にはプロジェクトの規模、チーム構造、および長期的なアーキテクチャ目標によって決まります。それらのコア原則を理解することで、堅牢で保守性の高いフロントエンドアプリケーションの構築を可能にするパターンを選択できます。