🔼

アーキテクチャ設計について備忘録

に公開

このドキュメントは、プロジェクトの層構造とアーキテクチャパターンについて説明します。

レイヤードアーキテクチャとは

アプリケーションを責務ごとに分離し、各層が特定の役割だけを担当する設計パターンです。

メリット

  • 保守性: 各層の責務が明確で、変更の影響範囲を限定できる
  • テスト容易性: 層ごとに独立してテスト可能
  • 再利用性: ビジネスロジックを他のプロジェクトでも使える
  • 変更容易性: フレームワーク変更時、ビジネスロジックは影響を受けない

一般的な層の分類

1. プレゼンテーション層(Presentation Layer / UI Layer)

役割: ユーザーとのやり取りを担当

含まれるもの:

  • UI コンポーネント
  • ページ
  • フォーム
  • ユーザー入力の受け取り

例:

  • src/app/ - Next.js のページ
  • src/features/tools/*/Tool.tsx - 各ツールの UI
  • src/components/ - 共通 UI コンポーネント

特徴:

  • React コンポーネントが中心
  • ユーザーイベント(クリック、入力等)を処理
  • 見た目とユーザー体験を担当
  • 他の層に依存するが、他の層から依存されない

コード例:

// src/components/layout/Header.tsx
export function SiteHeader() {
  const t = useTranslations("header");
  return (
    <header>
      <h1>{t("siteTitle")}</h1>
      <nav>
        <Link href="/">{t("nav.home")}</Link>
      </nav>
    </header>
  );
}

2. アプリケーション層(Application Layer / Service Layer)

役割: ビジネスロジックの調整・ワークフローの制御

含まれるもの:

  • ユースケースの実装
  • 複数のドメインロジックを組み合わせた処理
  • トランザクション管理
  • データの取得・変換・整形

例:

  • src/lib/tools/registry.ts - ツールの管理・調整
  • src/lib/seo/metadata.ts - SEO メタデータ生成
  • src/lib/seo/jsonld.ts - JSON-LD 構造化データ生成

特徴:

  • 「何を実行するか」を定義
  • 複数の純粋ロジックを組み合わせる
  • UI とビジネスロジックの橋渡し
  • ドメイン層を呼び出す

コード例:

// src/lib/tools/registry.ts
export async function getToolCards(locale: Locale): Promise<ToolCardMeta[]> {
  // 複数のツールの情報を取得・整形するワークフロー
  const cards = await Promise.all(
    toolDefinitions.map(async (definition) => {
      const result = await loadToolStrings(definition, locale);
      if (!result) return undefined;

      return {
        slug: definition.slug,
        category: definition.category,
        tags: definition.tags,
        card: result.card,
      };
    })
  );

  return cards.filter((card): card is ToolCardMeta => Boolean(card));
}

3. ドメイン層(Domain Layer / Business Logic Layer)

役割: 純粋なビジネスロジック

含まれるもの:

  • 計算処理
  • 変換処理
  • ビジネスルール
  • バリデーションロジック

例:

  • src/lib/tools/engine/ - BMI計算、和暦変換など
  • src/features/tools/*/schema.ts - Zod によるバリデーション

特徴:

  • React に依存しない純粋関数
  • テストが容易
  • 「どう計算するか」を定義
  • 他のプロジェクトでも再利用可能
  • 外部の層を知らない(依存しない)

コード例:

// src/lib/tools/engine/bmi.ts
export function calculateBmi(heightCm: number, weightKg: number): number {
  const heightM = heightCm / 100;
  return weightKg / (heightM * heightM);
}

export function getBmiCategory(bmi: number, locale: Locale): string {
  const categories = {
    ja: {
      underweight: "低体重",
      normal: "普通体重",
      overweight: "肥満(1度)",
      obese: "肥満(2度以上)",
    },
    en: {
      underweight: "Underweight",
      normal: "Normal weight",
      overweight: "Overweight",
      obese: "Obese",
    },
  };

  const cat = categories[locale] || categories.en;

  if (bmi < 18.5) return cat.underweight;
  if (bmi < 25) return cat.normal;
  if (bmi < 30) return cat.overweight;
  return cat.obese;
}

→ React も Next.js も知らない。どこでも使える純粋関数。


4. インフラストラクチャ層(Infrastructure Layer)

役割: 外部システムとのやり取り

含まれるもの:

  • データベースアクセス
  • API 呼び出し
  • ファイルシステム操作
  • 外部サービス連携
  • キャッシュ管理

例:

  • 現状は静的サイトなので該当なし
  • 将来的に API やデータベースを導入する場合はここに配置

特徴:

  • 外部依存を隔離
  • モックに置き換えやすい
  • データの永続化を担当
  • ドメイン層のインターフェースに従う

コード例(将来的な拡張例):

// src/lib/infrastructure/database.ts(未実装)
export async function saveUserPreference(userId: string, locale: Locale) {
  await db.userPreferences.upsert({
    where: { userId },
    update: { locale },
    create: { userId, locale },
  });
}

5. フレームワーク層(Framework Layer / Configuration Layer)

役割: フレームワーク固有の設定・初期化

含まれるもの:

  • フレームワークの設定ファイル
  • ミドルウェア
  • ルーティング設定
  • プラグイン設定
  • グローバル初期化

このプロジェクトでは:

  • src/i18n/ - next-intl の設定
  • middleware.ts - Next.js ミドルウェア
  • next.config.ts - Next.js 設定
  • tailwind.config.ts - Tailwind CSS 設定
  • components.json - shadcn/ui 設定

特徴:

  • フレームワーク依存が強い
  • アプリケーション全体の初期化
  • ビジネスロジックを含まない
  • フレームワークを変更すると全面的に書き換えが必要

コード例:

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

export const routing = defineRouting({
  locales: ["ja", "en"],
  defaultLocale: "en",
  localePrefix: "always",
});

export const { Link, redirect, usePathname, useRouter } =
  createNavigation(routing);

→ next-intl に完全に依存。Next.js でしか動かない。


このプロジェクトの層構成

層とディレクトリの対応表

ディレクトリ 依存関係 説明
フレームワーク層 src/i18n/, middleware.ts, *.config.ts Next.js, next-intl に依存 フレームワーク設定、ミドルウェア
プレゼンテーション層 src/app/, src/components/, src/features/*/Tool.tsx React, Next.js に依存 UI、ページ、ユーザー入力
アプリケーション層 src/lib/tools/registry.ts, src/lib/seo/ ドメイン層に依存 ツール管理、SEO生成
ドメイン層 src/lib/tools/engine/, src/features/*/schema.ts フレームワーク非依存 ビジネスロジック、バリデーション
インフラストラクチャ層 (未実装) - 将来的に API やデータベースを導入する場合

依存関係の図

┌─────────────────────────────────────────────────────────┐
│ フレームワーク層 (src/i18n/, middleware.ts)              │
│ - next-intl 設定                                          │
│ - Next.js ミドルウェア                                    │
└─────────────────────────────────────────────────────────┘
                         ↓ 依存
┌─────────────────────────────────────────────────────────┐
│ プレゼンテーション層 (src/app/, src/components/)          │
│ - UI コンポーネント                                       │
│ - ページ                                                  │
└─────────────────────────────────────────────────────────┘
                         ↓ 依存
┌─────────────────────────────────────────────────────────┐
│ アプリケーション層 (src/lib/tools/registry.ts)           │
│ - ツール管理                                              │
│ - SEO メタデータ生成                                      │
└─────────────────────────────────────────────────────────┘
                         ↓ 依存
┌─────────────────────────────────────────────────────────┐
│ ドメイン層 (src/lib/tools/engine/)                       │
│ - BMI計算                                                 │
│ - 和暦変換                                                │
│ - 純粋なビジネスロジック                                  │
└─────────────────────────────────────────────────────────┘

重要な原則:

  • 下の層は上の層を知らない(依存しない)
  • 上の層は下の層に依存する
  • ドメイン層は最も内側で、フレームワークに依存しない

なぜ src/i18n/ を独立させるのか

よくある疑問

src/i18n/ は設定だから src/lib/ に入れるべきでは?」

答え

src/lib/ = ビジネスロジック層(ドメイン・アプリケーション)
src/i18n/ = フレームワーク設定層

この2つは性質が全く異なるため、分離すべきです。

比較表

観点 src/lib/ src/i18n/
依存 フレームワーク非依存 next-intl に強く依存
再利用性 他のプロジェクトでも使える Next.js + next-intl でのみ使える
テスト 純粋関数なので簡単 フレームワークのモックが必要
変更頻度 ビジネス要件で変わる フレームワーク更新で変わる
移植性 高い(他のフレームワークでも使える) 低い(Next.js 専用)
責務 「何を計算するか」 「どう設定するか」

具体例で理解する

ドメイン層(lib/tools/engine/bmi.ts

export function calculateBmi(heightCm: number, weightKg: number): number {
  const heightM = heightCm / 100;
  return weightKg / (heightM * heightM);
}

特徴:

  • React も Next.js も知らない
  • どこでも使える純粋関数
  • Node.js、Deno、ブラウザ、どこでも動く
  • テストが超簡単
// テスト
expect(calculateBmi(170, 65)).toBeCloseTo(22.49);

フレームワーク層(i18n/routing.ts

import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  locales: ["ja", "en"],
  defaultLocale: "en",
});

特徴:

  • next-intl に完全に依存
  • Next.js でしか動かない
  • ビジネスロジックを含まない
  • フレームワーク変更時は全面書き換え

なぜ分離するのか

  1. 責務の明確化: 設定とロジックを混ぜない
  2. テスト容易性: ビジネスロジックは純粋関数でテスト簡単
  3. 移植性: ドメイン層は他のフレームワークでも使える
  4. 変更の影響範囲: フレームワーク変更時、ビジネスロジックは影響を受けない
  5. next-intl 公式推奨: 公式ドキュメントで src/i18n/ 配置を推奨

クリーンアーキテクチャとの関連

クリーンアーキテクチャの原則

有名な「クリーンアーキテクチャ」(Robert C. Martin)では、依存関係を以下のように定義します:

外側(フレームワーク・UI・インフラ)
    ↓ 依存
中間(アプリケーション・ユースケース)
    ↓ 依存
内側(ドメイン・ビジネスロジック)

重要な原則

  1. 依存関係逆転の原則(Dependency Inversion)

    • 内側の層は外側の層を知らない
    • 外側の層は内側の層に依存する
    • ビジネスロジックはフレームワークに依存しない
  2. 単一責任の原則(Single Responsibility)

    • 各層は1つの責務のみを持つ
    • 変更理由は1つだけ
  3. 開放閉鎖の原則(Open/Closed)

    • 拡張に対して開いている
    • 修正に対して閉じている

このプロジェクトでの実践

src/i18n/ (フレームワーク層)
    ↓ 依存
src/app/, src/components/ (プレゼンテーション層)
    ↓ 依存
src/lib/tools/registry.ts (アプリケーション層)
    ↓ 依存
src/lib/tools/engine/ (ドメイン層)

具体例:

engine/bmi.ts は next-intl も React も知らない。
→ だから他のプロジェクトでも使えるし、テストも簡単。

i18n/routing.ts は next-intl に依存する。
→ でもビジネスロジックは含まないので、フレームワーク変更時も影響範囲は限定的。


実装例で理解する

例1: BMI計算ツール

ドメイン層

// src/lib/tools/engine/bmi.ts
export function calculateBmi(heightCm: number, weightKg: number): number {
  const heightM = heightCm / 100;
  return weightKg / (heightM * heightM);
}
  • 純粋関数
  • フレームワーク非依存
  • どこでも使える

アプリケーション層

// src/lib/tools/registry.ts
{
  slug: "bmi",
  category: "health",
  tags: ["health", "calculator"],
  load: () => import("@/features/tools/bmi/Tool"),
  getStrings: async (locale) => {
    const strings = await import(`@/features/tools/bmi/strings/${locale}`);
    return { card: strings.default.card, ui: strings.default.ui };
  },
}
  • ツールの管理・調整
  • 複数のリソースを組み合わせる

プレゼンテーション層

// src/features/tools/bmi/Tool.tsx
export default function BmiTool({ locale, strings }: Props) {
  const [result, setResult] = useState<number | null>(null);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    // バリデーション...
    const bmi = calculateBmi(heightCm, weightKg); // ← ドメイン層を呼び出す
    setResult(bmi);
  };

  return <form onSubmit={handleSubmit}>...</form>;
}
  • UI表示
  • ユーザー入力処理
  • ドメイン層を呼び出す

フレームワーク層

// src/i18n/routing.ts
export const routing = defineRouting({
  locales: ["ja", "en"],
  defaultLocale: "en",
});
  • next-intl の設定
  • ビジネスロジックを含まない

例2: フレームワーク変更時の影響

シナリオ: Next.js から別のフレームワークに移行

変更が必要:

  • src/i18n/ - 全面書き換え(next-intl から別のライブラリへ)
  • src/app/ - 全面書き換え(Next.js App Router から別のルーティングへ)
  • src/lib/tools/registry.ts - 一部修正(動的インポート等)

変更が不要:

  • src/lib/tools/engine/ - そのまま使える(純粋関数)
  • src/features/tools/*/schema.ts - そのまま使える(Zod)

→ ビジネスロジック(ドメイン層)はフレームワークに依存しないため、資産として保護される。


まとめ

レイヤードアーキテクチャの5層

  1. フレームワーク層 - フレームワーク設定(src/i18n/
  2. プレゼンテーション層 - UI・ユーザー入力(src/app/, src/components/
  3. アプリケーション層 - ワークフロー・調整(src/lib/tools/registry.ts
  4. ドメイン層 - ビジネスロジック(src/lib/tools/engine/
  5. インフラストラクチャ層 - 外部システム連携(未実装)

なぜ分離するのか

  • 保守性: 各層の責務が明確で、変更の影響範囲を限定できる
  • テスト容易性: 層ごとに独立してテスト可能
  • 再利用性: ビジネスロジックを他のプロジェクトでも使える
  • 変更容易性: フレームワーク変更時、ビジネスロジックは影響を受けない

src/i18n/ を独立させる理由

  • src/lib/ = ビジネスロジック層(フレームワーク非依存)
  • src/i18n/ = フレームワーク設定層(next-intl に依存)
  • 性質が全く異なるため、分離することで責務を明確化

クリーンアーキテクチャの実践

依存の方向: 外側 → 内側

フレームワーク層 (src/i18n/)
        ↓
プレゼンテーション層 (src/app/, src/components/)
        ↓
アプリケーション層 (src/lib/tools/registry.ts)
        ↓
ドメイン層 (src/lib/tools/engine/)

内側の層(ドメイン層)は外側の層を知らない。
これにより、ビジネスロジックがフレームワークから独立し、長期的な保守性が向上する。


参考資料

Discussion