コンテンツにスキップ

atoms/hooks/ のような「分類のための分類」は

「小さく分ける」ベストプラクティスを妨害する

技術種別ディレクトリは「分類のための分類」であり、実際の変更境界と一致しないためモジュールの凝集度が低下する。

問題: 技術種別ディレクトリの構造

src/
├── atoms/
│   ├── Button.tsx        ← UserCard に関係
│   └── Avatar.tsx        ← UserCard に関係
├── hooks/
│   ├── useUser.ts        ← UserCard に関係
│   └── useCart.ts        ← Cart に関係
├── utils/
│   └── formatDate.ts
└── components/
    └── UserCard.tsx

UserCard を変更するとき atoms/hooks/components/ を行き来しなければならない。変更境界とディレクトリ境界がズレている。

改善: 機能・責務単位のディレクトリ

src/
├── user/
│   ├── UserCard.tsx
│   ├── Avatar.tsx        ← UserCard に凝集
│   ├── useUser.ts        ← UserCard に凝集
│   └── index.ts          ← 公開 API を明示
├── cart/
│   ├── CartList.tsx
│   ├── useCart.ts
│   └── index.ts
└── shared/
    └── formatDate.ts

機能単位でまとめることで、変更が 1 ディレクトリ内で完結する。

技術種別 vs 機能単位の比較

観点 技術種別(atoms/hooks/) 機能単位(user/cart/)
変更の局所性 複数ディレクトリを横断する 1 ディレクトリ内で完結
凝集度 低い(種類ごとに散らばる) 高い(関連するものが近い)
削除のしやすさ どこを消せばいいか不明瞭 ディレクトリごと削除できる
オーナーシップ 不明確 明確(このディレクトリの担当)

アクセス制御の補強(eslint-plugin-import-access)

  • index.ts で公開する API のみをエクスポートし、内部実装を隠す

  • eslint-plugin-import-access@package アノテーションによるアクセス制限が可能

  • 他の機能から直接内部ファイルを import するのを Lint で検出できる

要点

  • 「分類のための分類」は変更の局所性を損ない、ファイル間の移動コストを増やす

  • コンポーネント・hooks・ユーティリティを機能単位でまとめることで凝集度が上がる

  • 「再利用のために小さく分ける」ではなく「変更境界に合わせて分ける」が正しい指針