T-CREATOR

ESLint でアーキテクチャ境界を守る:層間 import を規制する設計パターン

ESLint でアーキテクチャ境界を守る:層間 import を規制する設計パターン

プロジェクトが大きくなるにつれて、コードの構造を保つことが難しくなっていきますよね。特にレイヤードアーキテクチャやクリーンアーキテクチャを採用している場合、各層の責任を明確に分離することが重要です。しかし、人間がレビューだけで境界を守り続けるのは大変な作業でしょう。

そこで活躍するのが ESLint です。ESLint を使えば、コードを書いた瞬間にアーキテクチャ違反を検出できます。この記事では、ESLint を使って層間の import を規制し、アーキテクチャの境界を自動的に守る方法を解説します。

実際のプロジェクトで使える具体的な設定例も紹介しますので、ぜひ最後までお読みください。

背景

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

レイヤードアーキテクチャは、システムを複数の層(レイヤー)に分割する設計手法です。各層には明確な責任があり、特定の方向にのみ依存関係を持つことが原則となっています。

典型的な 3 層アーキテクチャでは、以下のような構成になります。

typescript// プロジェクト構造の例
src/
├── presentation/   // プレゼンテーション層(UI、コントローラー)
├── application/    // アプリケーション層(ビジネスロジック)
└── infrastructure/ // インフラストラクチャ層(DB、外部API)

各層の責任と役割を表にまとめました。

#役割具体例
1プレゼンテーション層ユーザーとのやり取りを担当React コンポーネント、API コントローラー
2アプリケーション層ビジネスロジックの実装ユースケース、サービスクラス
3インフラストラクチャ層外部システムとの接続データベースアクセス、外部 API 呼び出し

アーキテクチャの依存関係ルール

レイヤードアーキテクチャでは、依存関係の方向が重要です。基本的に、上位層は下位層に依存できますが、下位層が上位層に依存してはいけません。

以下の図で、正しい依存関係の方向を確認しましょう。

mermaidflowchart TD
  presentation["プレゼンテーション層<br/>(UI・コントローラー)"]
  application["アプリケーション層<br/>(ビジネスロジック)"]
  infrastructure["インフラストラクチャ層<br/>(DB・外部API)"]

  presentation -->|依存OK| application
  application -->|依存OK| infrastructure
  infrastructure -.->|依存NG| application
  infrastructure -.->|依存NG| presentation
  application -.->|依存NG| presentation

この図から分かるように、依存の方向は一方向です。下位層から上位層への依存は禁止されており、これにより各層の独立性が保たれます。

なぜ層間の境界が重要なのか

層間の境界を守ることで、以下のメリットが得られます。

保守性の向上が最も大きな利点でしょう。各層が独立しているため、一つの層を変更しても他の層への影響が最小限に抑えられます。

テストのしやすさも見逃せません。依存関係が明確なので、モックやスタブを使ったテストが書きやすくなりますね。

チーム開発の効率化にも貢献します。各層の責任が明確なので、複数人で並行して開発を進めやすくなるでしょう。

課題

よくあるアーキテクチャ違反のパターン

実際の開発現場では、以下のような違反がよく発生します。これらは、レビューだけでは見逃されやすい問題です。

下位層から上位層への不正な import

インフラストラクチャ層がプレゼンテーション層のコンポーネントを直接 import してしまうケースです。

typescript// infrastructure/database/userRepository.ts

// ❌ NG:インフラ層がプレゼンテーション層に依存
import { UserProfile } from '../../presentation/components/UserProfile';

export class UserRepository {
  // ...
}

このコードは、本来依存してはいけない上位層を参照してしまっています。

層をスキップした import

プレゼンテーション層がアプリケーション層を経由せず、直接インフラストラクチャ層にアクセスするパターンです。

typescript// presentation/pages/UserPage.tsx

// ❌ NG:プレゼンテーション層がインフラ層を直接参照
import { UserRepository } from '../../infrastructure/database/userRepository';

export const UserPage = () => {
  const repo = new UserRepository();
  // ...
};

アプリケーション層を経由すべきところを、直接インフラ層にアクセスしています。

循環依存の発生

二つの層が互いに依存し合ってしまうケースです。

typescript// application/services/userService.ts
import { UserController } from '../../presentation/controllers/userController';

// presentation/controllers/userController.ts
import { UserService } from '../../application/services/userService';

このような循環依存は、ビルドエラーやランタイムエラーの原因になります。

人的レビューの限界

これらの問題を人的レビューだけで防ぐのは困難です。以下のような課題があるでしょう。

#課題詳細
1レビュー負荷の増加すべての import 文を目視確認するのは時間がかかる
2見落としのリスク複雑な import パスは見落としやすい
3プロジェクト参加時の学習コスト新メンバーがルールを理解するまで時間がかかる
4リファクタリング時の事故大規模なリファクタリング時に違反を混入させやすい

以下の図で、違反が発生するフローを確認しましょう。

mermaidflowchart LR
  dev["開発者"] -->|コード記述| code["コード"]
  code -->|commit| review["レビュー"]
  review -->|見落とし| merge["マージ"]
  merge -->|違反混入| main["main ブランチ"]

  review -.->|発見| fix["修正依頼"]
  fix -.-> dev

人的レビューでは、どうしても見落としが発生してしまいます。自動化された仕組みが必要ですね。

解決策

ESLint による自動検出の仕組み

ESLint を使えば、コードを書いた瞬間にアーキテクチャ違反を検出できます。以下のような仕組みで動作します。

mermaidflowchart LR
  dev["開発者"] -->|コード記述| editor["エディタ"]
  editor -->|保存| eslint["ESLint 実行"]
  eslint -->|ルール検証| result{違反あり?}
  result -->|Yes| error["エラー表示"]
  result -->|No| ok["OK"]

  error --> dev
  ok --> commit["コミット可能"]

このフローにより、問題を早期に発見できます。

ESLint プラグインの選択肢

層間 import を規制するための ESLint プラグインには、主に以下の 3 つの選択肢があります。

#プラグイン特徴推奨用途
1no-restricted-importsESLint 標準ルールシンプルな規制
2eslint-plugin-boundaries境界定義に特化複雑なアーキテクチャ
3eslint-plugin-importimport 文全般を管理総合的な import 管理

それぞれの特徴を詳しく見ていきましょう。

方法 1:no-restricted-imports を使う

no-restricted-imports は ESLint の標準ルールで、特定のパスからの import を禁止できます。

基本的な設定

.eslintrc.js に以下のように記述します。

javascript// .eslintrc.js

module.exports = {
  rules: {
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          // 設定をここに記述
        ],
      },
    ],
  },
};

この基本設定に、具体的な禁止パターンを追加していきます。

層ごとの規制ルール設定

各層に対して、import してはいけないパスをパターンで指定しましょう。

javascript// .eslintrc.js

module.exports = {
  overrides: [
    {
      // インフラ層の規制
      files: ['src/infrastructure/**/*'],
      rules: {
        'no-restricted-imports': [
          'error',
          {
            patterns: [
              {
                group: ['**/presentation/**'],
                message:
                  'インフラ層からプレゼンテーション層への import は禁止です',
              },
              {
                group: ['**/application/**'],
                message:
                  'インフラ層からアプリケーション層への import は禁止です',
              },
            ],
          },
        ],
      },
    },
  ],
};

overrides を使うことで、特定のディレクトリにのみルールを適用できます。

アプリケーション層の規制

アプリケーション層がプレゼンテーション層に依存しないようにします。

javascript// .eslintrc.js(続き)

{
  // アプリケーション層の規制
  files: ['src/application/**/*'],
  rules: {
    'no-restricted-imports': ['error', {
      patterns: [
        {
          group: ['**/presentation/**'],
          message: 'アプリケーション層からプレゼンテーション層への import は禁止です'
        }
      ]
    }]
  }
}

これにより、ビジネスロジックが UI に依存することを防げます。

方法 2:eslint-plugin-boundaries を使う

より柔軟で強力な境界管理には、eslint-plugin-boundaries が適しています。

インストール

まず、パッケージをインストールしましょう。

bashyarn add -D eslint-plugin-boundaries

依存関係が追加されます。

基本設定

.eslintrc.js でプラグインを有効にします。

javascript// .eslintrc.js

module.exports = {
  plugins: ['boundaries'],
  extends: ['plugin:boundaries/recommended'],
  settings: {
    'boundaries/elements': [
      // エレメントの定義をここに記述
    ],
  },
};

この設定が基盤となります。

エレメント(層)の定義

各層をエレメントとして定義します。

javascript// .eslintrc.js

settings: {
  'boundaries/elements': [
    {
      type: 'presentation',
      pattern: 'src/presentation/**/*',
      mode: 'folder',
      capture: ['component']
    },
    {
      type: 'application',
      pattern: 'src/application/**/*',
      mode: 'folder',
      capture: ['service']
    },
    {
      type: 'infrastructure',
      pattern: 'src/infrastructure/**/*',
      mode: 'folder',
      capture: ['repository']
    }
  ]
}

pattern でファイルパスを指定し、type で層の種類を定義しています。

依存関係ルールの設定

定義したエレメント間の依存関係を制御します。

javascript// .eslintrc.js

settings: {
  'boundaries/elements': [
    // 前述のエレメント定義
  ],
  'boundaries/ignore': ['**/*.test.ts', '**/*.spec.ts']
},
rules: {
  'boundaries/element-types': ['error', {
    default: 'disallow',
    rules: [
      // 許可する依存関係のみを列挙
    ]
  }]
}

default: 'disallow' により、明示的に許可したもの以外は禁止されます。

許可する依存関係の定義

具体的に許可する依存関係を指定しましょう。

javascript// .eslintrc.js(続き)

rules: [
  {
    from: 'presentation',
    allow: ['application'],
  },
  {
    from: 'application',
    allow: ['infrastructure'],
  },
  {
    from: 'infrastructure',
    allow: [], // どこにも依存しない
  },
];

これにより、一方向の依存関係のみが許可されます。

方法 3:eslint-plugin-import との組み合わせ

eslint-plugin-import を併用すると、さらに細かい制御が可能です。

インストールと基本設定

パッケージをインストールします。

bashyarn add -D eslint-plugin-import

設定ファイルに追加しましょう。

javascript// .eslintrc.js

module.exports = {
  plugins: ['import'],
  extends: ['plugin:import/recommended'],
  rules: {
    // ルールをここに追加
  },
};

この基盤の上に、追加のルールを設定していきます。

循環依存の検出

import​/​no-cycle ルールで循環依存を防ぎます。

javascript// .eslintrc.js

rules: {
  'import/no-cycle': ['error', {
    maxDepth: 1,
    ignoreExternal: true
  }]
}

循環依存が発生した瞬間にエラーとなります。

未使用 import の検出

未使用の import も自動的に検出できます。

javascript// .eslintrc.js

rules: {
  'import/no-unused-modules': ['error', {
    unusedExports: true
  }]
}

コードの整理にも役立ちますね。

具体例

ケーススタディ 1:3 層アーキテクチャでの実装

実際の Next.js プロジェクトを例に、3 層アーキテクチャでの設定を見ていきます。

プロジェクト構造

以下のようなディレクトリ構成を想定します。

typescriptsrc/
├── presentation/
│   ├── components/  // React コンポーネント
│   ├── pages/       // Next.js ページ
│   └── hooks/       // カスタムフック
├── application/
│   ├── useCases/    // ユースケース
│   └── services/    // ビジネスロジック
└── infrastructure/
    ├── api/         // 外部 API クライアント
    └── repositories/ // データアクセス

この構造に対して、適切な ESLint 設定を行います。

完全な ESLint 設定ファイル

以下が、3 層アーキテクチャに対応した完全な設定例です。

javascript// .eslintrc.js

module.exports = {
  plugins: ['boundaries', 'import'],
  extends: [
    'plugin:boundaries/recommended',
    'plugin:import/recommended',
    'plugin:import/typescript',
  ],
  settings: {
    'boundaries/elements': [
      {
        type: 'presentation',
        pattern: 'src/presentation/**/*',
      },
      {
        type: 'application',
        pattern: 'src/application/**/*',
      },
      {
        type: 'infrastructure',
        pattern: 'src/infrastructure/**/*',
      },
    ],
  },
};

基本的なプラグインと層の定義を行いました。

依存関係ルールの詳細設定

各層の依存関係を詳細に設定します。

javascript// .eslintrc.js(続き)

rules: {
  'boundaries/element-types': ['error', {
    default: 'disallow',
    message: '${file.type} は ${dependency.type} に依存できません',
    rules: [
      {
        from: 'presentation',
        allow: ['application'],
        message: 'プレゼンテーション層はアプリケーション層のみに依存できます'
      },
      {
        from: 'application',
        allow: ['infrastructure'],
        message: 'アプリケーション層はインフラ層のみに依存できます'
      },
      {
        from: 'infrastructure',
        allow: [],
        message: 'インフラ層は他の層に依存できません'
      }
    ]
  }],
  'import/no-cycle': 'error',
  'import/no-unused-modules': 'error'
}

カスタムメッセージにより、違反時に分かりやすいエラーが表示されます。

実際のコード例

正しい依存関係のコード例を見てみましょう。

typescript// presentation/pages/UserListPage.tsx

import { useUserList } from '../hooks/useUserList';
import { UserCard } from '../components/UserCard';

export const UserListPage = () => {
  const { users, loading } = useUserList();

  if (loading) return <div>読み込み中...</div>;

  return (
    <div>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
};

プレゼンテーション層内での import のみを使用しています。

カスタムフックでのアプリケーション層利用

プレゼンテーション層からアプリケーション層を呼び出す例です。

typescript// presentation/hooks/useUserList.ts

// ✅ OK:プレゼンテーション層 → アプリケーション層
import { getUserListUseCase } from '../../application/useCases/getUserListUseCase';
import { useState, useEffect } from 'react';

export const useUserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getUserListUseCase()
      .then(setUsers)
      .finally(() => setLoading(false));
  }, []);

  return { users, loading };
};

許可された依存関係に沿って実装されています。

アプリケーション層の実装

ビジネスロジックを含むユースケースの実装例です。

typescript// application/useCases/getUserListUseCase.ts

// ✅ OK:アプリケーション層 → インフラ層
import { userRepository } from '../../infrastructure/repositories/userRepository';
import type { User } from '../types/user';

export const getUserListUseCase = async (): Promise<
  User[]
> => {
  // ビジネスロジック
  const users = await userRepository.findAll();

  // フィルタリングや整形などの処理
  return users.filter((user) => user.isActive);
};

アプリケーション層がインフラ層に依存しています。

インフラ層の実装

データアクセスを担当するリポジトリの実装です。

typescript// infrastructure/repositories/userRepository.ts

import type { User } from '../../application/types/user';

// ✅ OK:インフラ層は他の層に依存しない
export const userRepository = {
  findAll: async (): Promise<User[]> => {
    const response = await fetch('/api/users');
    return response.json();
  },

  findById: async (id: string): Promise<User> => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  },
};

インフラ層は外部システムとのやり取りに専念しています。

ケーススタディ 2:クリーンアーキテクチャでの実装

より複雑なクリーンアーキテクチャにも対応できます。

クリーンアーキテクチャの層構造

クリーンアーキテクチャでは、以下のような層構成になります。

mermaidflowchart TD
  frameworks["Frameworks & Drivers<br/>(外部I/O)"]
  interface["Interface Adapters<br/>(アダプター)"]
  usecase["Application Business Rules<br/>(ユースケース)"]
  entity["Enterprise Business Rules<br/>(エンティティ)"]

  frameworks -->|依存| interface
  interface -->|依存| usecase
  usecase -->|依存| entity

依存の方向は、外側から内側への一方向です。

ディレクトリ構造

クリーンアーキテクチャに対応したディレクトリ構成を定義します。

typescriptsrc/
├── frameworks/      // フレームワーク・ドライバー層
│   ├── web/        // Next.js 関連
│   └── database/   // DB 接続
├── adapters/       // インターフェースアダプター層
│   ├── controllers/
│   └── presenters/
├── usecases/       // ユースケース層
│   └── *.usecase.ts
└── entities/       // エンティティ層
    └── *.entity.ts

各層の責任が明確に分かれています。

クリーンアーキテクチャ用 ESLint 設定

4 層構造に対応した設定を行います。

javascript// .eslintrc.js

module.exports = {
  plugins: ['boundaries'],
  extends: ['plugin:boundaries/recommended'],
  settings: {
    'boundaries/elements': [
      {
        type: 'frameworks',
        pattern: 'src/frameworks/**/*',
      },
      {
        type: 'adapters',
        pattern: 'src/adapters/**/*',
      },
      {
        type: 'usecases',
        pattern: 'src/usecases/**/*',
      },
      {
        type: 'entities',
        pattern: 'src/entities/**/*',
      },
    ],
  },
};

4 つの層をそれぞれエレメントとして定義しました。

依存関係の詳細ルール

内側の層ほど依存先が少なくなるように設定します。

javascript// .eslintrc.js(続き)

rules: {
  'boundaries/element-types': ['error', {
    default: 'disallow',
    rules: [
      {
        from: 'frameworks',
        allow: ['adapters', 'usecases', 'entities']
      },
      {
        from: 'adapters',
        allow: ['usecases', 'entities']
      },
      {
        from: 'usecases',
        allow: ['entities']
      },
      {
        from: 'entities',
        allow: [] // 最内層は依存なし
      }
    ]
  }]
}

この設定により、依存性逆転の原則が守られます。

エラー例と修正方法

実際にどのようなエラーが表示されるか見てみましょう。

違反コードの例

以下のコードは ESLint エラーとなります。

typescript// infrastructure/repositories/userRepository.ts

// ❌ エラー:infrastructure は presentation に依存できません
import { UserProfile } from '../../presentation/components/UserProfile';

export class UserRepository {
  async save(profile: UserProfile) {
    // ...
  }
}

このコードを保存すると、以下のエラーが表示されます。

textError: infrastructure は presentation に依存できません
  at src/infrastructure/repositories/userRepository.ts:3:1

  infrastructure/repositories/userRepository.ts
  3:1  error  インフラ層からプレゼンテーション層への import は禁止です  boundaries/element-types

エラーメッセージが明確ですね。

修正方法

型を共通の場所に移動することで解決できます。

typescript// application/types/userProfile.ts

// 共通の型定義
export interface UserProfile {
  id: string;
  name: string;
  email: string;
}

型定義を適切な層に配置します。

修正後のコード

型を適切な場所から import するように修正しましょう。

typescript// infrastructure/repositories/userRepository.ts

// ✅ OK:infrastructure は application に依存可能
import type { UserProfile } from '../../application/types/userProfile';

export class UserRepository {
  async save(profile: UserProfile) {
    // データベースへの保存処理
    await db.users.create(profile);
  }
}

これで ESLint エラーは解消されます。

CI/CD での活用

ESLint チェックを CI/CD パイプラインに組み込むことで、違反コードのマージを防げます。

GitHub Actions での設定例

.github​/​workflows​/​lint.yml に以下を記述します。

yamlname: ESLint Check

on:
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

基本的なワークフローの設定です。

ESLint 実行ステップ

依存関係のインストールと ESLint 実行を追加します。

yaml- name: Install dependencies
  run: yarn install --frozen-lockfile

- name: Run ESLint
  run: yarn eslint . --ext .ts,.tsx

- name: Check for architecture violations
  run: |
    echo "アーキテクチャ境界チェック完了"
    echo "違反が見つかった場合は、上記のエラーを確認してください"

これにより、PR 作成時に自動的にチェックが実行されます。

VS Code での開発体験向上

エディタでリアルタイムにエラーを確認できるようにしましょう。

VS Code 拡張機能のインストール

ESLint 拡張機能をインストールすることをお勧めします。

json// .vscode/extensions.json

{
  "recommendations": ["dbaeumer.vscode-eslint"]
}

チームメンバーにも拡張機能のインストールを促せます。

プロジェクト設定

.vscode​/​settings.json で ESLint の動作を設定します。

json// .vscode/settings.json

{
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.workingDirectories": [{ "mode": "auto" }]
}

保存時に自動で修正可能なエラーが修正されます。

エディタ上でのエラー表示

コードを書いている最中に、リアルタイムでエラーが表示されるようになります。

typescript// 例:エディタ上での表示

import { UserProfile } from '../../presentation/components/UserProfile';
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       エラー: infrastructure は presentation に依存できません
       boundaries/element-types

即座にフィードバックが得られるため、開発効率が向上しますね。

まとめ

ESLint を使ったアーキテクチャ境界の管理について解説してきました。重要なポイントを振り返りましょう。

自動化の重要性が第一です。人的レビューだけでは限界があるため、ESLint による自動検出が不可欠でしょう。コードを書いた瞬間に違反が検出されるため、問題の早期発見につながります。

適切なプラグイン選択も重要ですね。プロジェクトの規模や複雑さに応じて、no-restricted-importseslint-plugin-boundarieseslint-plugin-import を使い分けることが推奨されます。

段階的な導入をお勧めします。既存プロジェクトに導入する場合は、まず一部の層から始めて徐々に拡大していくと良いでしょう。全体に一度に適用すると、大量のエラーに圧倒されてしまう可能性があります。

チーム全体での共有を忘れずに。ESLint の設定をリポジトリに含め、CI/CD パイプラインに組み込むことで、チーム全体で一貫したアーキテクチャを維持できます。

この記事で紹介した設定を参考に、ぜひあなたのプロジェクトでもアーキテクチャ境界の自動管理を始めてみてください。コードの品質と保守性が大きく向上するはずです。

関連リンク