Dev Study
← 解説「ディレクトリ構成 — feature単位で切り、sharedに寄せる」に戻る

サンプルコードで身につける: ディレクトリ構成 — feature単位で切り、sharedに寄せる

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1Before/After — 種類別フォルダから機能別フォルダへ

components/ services/ のような種類別の構成は、1つの機能を直すのに全フォルダを渡り歩くことになります。機能別に切り直すと、商品の修正で開くのは products/ だけになります。

// ── Before: ファイルの「種類」でフォルダを切った構成 ──
//
// src/app/
// ├── components/
// │   ├── product-list.ts     # 商品はここと…
// │   ├── product-card.ts
// │   ├── order-list.ts
// │   └── order-detail.ts
// ├── services/
// │   ├── product.service.ts  # …ここと…
// │   └── order.service.ts
// └── models/
//     ├── product.model.ts    # …ここに散らばる
//     └── order.model.ts
//
// 商品機能を直す → 3フォルダを行き来。機能を消す → 消し漏れが出る

// ── After: 「機能」でフォルダを切った構成 ──
//
// src/app/features/
// ├── products/               # 商品のことは全部ここ
// │   ├── product-list-page.ts
// │   ├── product-card.ts
// │   ├── product.service.ts
// │   └── product.model.ts
// └── orders/                 # 注文のことは全部ここ
//     ├── order-list-page.ts
//     ├── order-detail-page.ts
//     ├── order.service.ts
//     └── order.model.ts
//
// 修正も削除もフォルダ単位で完結する

2shared/ に置くもの、置かないもの

sharedは「2つ以上の機能から実際に使われている共通部品」の置き場です。1機能でしか使っていないものや、いつか使うかもという想像だけのものを置くと、ただの物置になってしまいます。

// ── shared/ の中身の例 ──
//
// src/app/shared/
// ├── ui/
// │   ├── badge.ts          # 商品にも注文にも出すバッジ → shared でよい
// │   └── confirm-dialog.ts # どの機能でも使う確認ダイアログ
// └── pipes/
//     └── yen.pipe.ts       # 金額表示は商品・注文・カートで使う
//
// ── shared に置かないもの ──
//
// ・product-card.ts
//     → 商品機能でしか使っていないなら features/products/ に置く。
//       2つ目の機能が使い始めた時に shared へ移せばよい
// ・「いつか使うかも」で先回りして作った部品
//     → 実際の利用者がいないコードは仕様が定まらず、物置化する
//
// 判断基準は component-split-criteria と同じ「実際に2箇所で使うか」

3依存は feature → shared の一方向に保つ

feature同士が import し合うと、片方の修正がもう片方を壊す連鎖が生まれます。共有したいものは shared に降ろしてから両方が使う形にすれば、依存は常に一方向で影響範囲が読めます。

// ── Before: feature同士が直接依存してしまった ──
//
// features/orders/order-detail-page.ts
// import { ProductCard } from '../products/product-card';
//                              ^^^^^^^^^^^^^^^^^^^^^^^^
// → 注文機能が商品機能の内部に手を伸ばしている。
//   商品側の都合で ProductCard を変えると注文画面が壊れる。
//   商品機能を消したくても注文が道連れになる

// ── After: 共有したくなった部品は shared に降ろす ──
//
// 1. product-card.ts を shared/ui/ へ移動し、
//    商品固有の依存(ProductService など)を Input に置き換える
// 2. 両方の feature が shared から import する
//
// features/products/ ──┐
//                      ├──▶ shared/ui/product-card.ts
// features/orders/  ───┘
//
// 許される依存:   features → shared
// 作らない依存:   features → features、shared → features

4ルーティングと構成を対応させる — フォルダ単位の遅延読み込み

feature単位の構成はルーティングとも相性が良く、フォルダごと loadChildren で遅延読み込みできます。URLの区切り・フォルダの区切り・読み込みの区切りが一致し、見通しがよくなります。

// app.routes.ts — featureフォルダとルートを1対1で対応させる
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'products',
    // features/products/ フォルダごと遅延読み込みする
    loadChildren: () =>
      import('./features/products/products.routes').then(
        (m) => m.PRODUCTS_ROUTES,
      ),
  },
  {
    path: 'orders',
    loadChildren: () =>
      import('./features/orders/orders.routes').then(
        (m) => m.ORDERS_ROUTES,
      ),
  },
];

// URLの /products 以下 = features/products/ の中、という対応が
// そのまま成り立つので、「この画面はどのフォルダ?」に迷わなくなる

5育てながら整理する — 3つ目の類似ファイルが合図

最初から理想の構成を作る必要はありません。平らに始めて、類似ファイルが3つ揃った時点でフォルダに整理します。実物を見てから分類するほうが、想像で作る空フォルダより正確です。

// ── 育て方の実例 ──
//
// 【最初】ファイルが少ないうちは平らでよい
// src/app/
// ├── app.ts
// ├── product-list-page.ts
// └── product.service.ts
//
// 【しばらく後】注文機能が増え、商品系ファイルが3つを超えた
// → 「3つ目の類似ファイル」が整理の合図。features/ を切る
// src/app/
// ├── features/
// │   ├── products/   # 商品系をまとめて移動
// │   └── orders/
// └── app.ts
//
// 【さらに後】バッジを商品と注文の両方で使い始めた
// → 2機能目の利用者が現れた時点で shared/ を切る
//
// 先回りで空フォルダを量産しない。実際のコードの増え方に合わせて
// 構成を育てるほうが、結果的に実態に合った形になる