CodeGym /コース /JAVA 25 SELF /プロジェクトのモジュール分割: ベストプラクティス

プロジェクトのモジュール分割: ベストプラクティス

JAVA 25 SELF
レベル 60 , レッスン 3
使用可能

1. いつ・なぜプロジェクトをモジュールに分割するのか

すべてを単一モジュールに詰め込むべきでない理由

小さな課題や "Hello, World!" だけを書くのであれば、モジュールシステムは過剰に見えるかもしれません。しかし、プロジェクトが大きくなるにつれて—数十・数百のクラス、数多くのパッケージ、外部ライブラリ—混沌は避けられません。棚のない図書館のようなものです。本が少ないうちは何とかなっても、その先は目当ての本を見つけるのが難しくなります。モジュールはあなたの棚です。秩序をもたらし、実装という「裏側」を隠して、外側には「ショーケース」(API)だけを見せます。

モジュール分割のメリット

  • 責務の分離: 各モジュールは自分の領域(例: DB、ビジネスロジック、UI)を担当する。
  • コードの再利用: モジュールは別プロジェクトからも利用できる。
  • テスタビリティの向上: モジュールを独立にテストできる。
  • セキュリティとカプセル化: 外部に見えるのは API のみで、実装は隠蔽される。
  • 保守容易性の向上: 「魔法の」依存が減り、依存関係のマップが明確になる。
  • ビルド・デプロイの高速化: 変更されたモジュールだけを再ビルドする。

いつ分割すべきか

  • プロジェクトが開発者1人では扱いきれない規模になった(または IDE が重くなってきた)。
  • 明確な部分が見えてきた: core, ui, utils, api, impl
  • コードを他プロジェクトで再利用する計画がある。
  • 特定の部分だけが必要とする外部依存がある。
  • 実装の詳細(アルゴリズム、内部クラス)を隠したい。

2. モジュール化の典型パターン

以下は学習用・実務用のどちらにも適した代表的な分割パターンです。

「オニオンアーキテクチャ」(Onion Architecture)

外側の層は内側に依存しますが、その逆はありません。

[ app (UI) ]
     ↓
[ core (ロジック) ]
     ↓
[ utils (ユーティリティ) ]
  • app — 外側のモジュール。グラフィカル UI、Web アプリ、エントリポイント(main)。
  • core — ビジネスロジック、モデル、サービス。
  • utils — 補助的なクラス群。

ルール: 内側の層は依存してはならない(外側に)。このため、core はさまざまなインターフェース(コンソール、Web、デスクトップ)で再利用できる。

API と実装を分離するモジュール

ライブラリでは、インターフェースとその実装を別に分けると便利です:

[ mylib.api ]  ← インターフェースのみをエクスポート
[ mylib.impl ] ← 実装を含む。エクスポートしない

テスト用モジュール

テストは本番アーティファクトに混入させないため、別モジュールに分けることがよくあります.

[ app ]
[ core ]
[ core.tests ]

学習用プロジェクトの例

myeditor/
  ├─ app/         ← エントリポイント、アプリの起動
  ├─ core/        ← ビジネスロジック(ファイルやテキストの処理)
  └─ utils/       ← ユーティリティ(ログ出力、パース)

3. モジュール間の依存関係

Java のモジュールでは、依存関係は module-info.javarequires を使って明示します。これにより可読性が上がり、コンパイラ/JVM が exports を通じて API の可視性を制御できます。

依存関係の例

core/module-info.java

module myeditor.core {
    exports myeditor.core.api; // 外部に公開するのは api パッケージのみ
    requires myeditor.utils;   // ユーティリティを利用
}

app/module-info.java

module myeditor.app {
    requires myeditor.core;    // core を利用
    requires myeditor.utils;   // ユーティリティを直接使ってもよい
}

ルールとベストプラクティス

  • 循環依存を避ける。 ArequiresB に依存し、BrequiresA に依存しているのは設計の欠陥。通常は共通の common/api に切り出して解決する。
  • 依存は最小限に。 本当に必要でないモジュールは追加しない。
  • 利用するパッケージをエクスポートする。 クラスは exports で宣言したパッケージに置く。そうでないとコンパイルエラーになる。
  • ユーティリティは最大限独立に。 utils はビジネスロジックに依存してはならない。

4. 実践: 学習用プロジェクトを 3 モジュールに分割する例

ディレクトリ構成

myeditor/
  ├─ app/
  │    ├─ src/
  │    │    └─ myeditor/app/Main.java
  │    └─ module-info.java
  ├─ core/
  │    ├─ src/
  │    │    ├─ myeditor/core/api/TextService.java
  │    │    └─ myeditor/core/impl/TextServiceImpl.java
  │    └─ module-info.java
  └─ utils/
       ├─ src/
       │    └─ myeditor/utils/Logger.java
       └─ module-info.java

module-info.java の例

core/module-info.java

module myeditor.core {
    exports myeditor.core.api;
    requires myeditor.utils;
}

app/module-info.java

module myeditor.app {
    requires myeditor.core;
    requires myeditor.utils;
}

utils/module-info.java

module myeditor.utils {
    exports myeditor.utils;
}

コード例(TextService)

myeditor/core/api/TextService.java

package myeditor.core.api;

public interface TextService {
    String toUpperCase(String text);
}

myeditor/core/impl/TextServiceImpl.java

package myeditor.core.impl;

import myeditor.core.api.TextService;

public class TextServiceImpl implements TextService {
    @Override
    public String toUpperCase(String text) {
        return text.toUpperCase();
    }
}

myeditor/app/Main.java

package myeditor.app;

import myeditor.core.api.TextService;
import myeditor.core.impl.TextServiceImpl;

public class Main {
    public static void main(String[] args) {
        TextService service = new TextServiceImpl();
        System.out.println(service.toUpperCase("hello, modules!"));
    }
}

IntelliJ IDEA での見え方

  • 各ディレクトリはプロジェクト構成上の独立した Module になる。
  • 各モジュールは src の直下に独自の module-info.java を持つ。
  • appmain を実行すると、IDE が module-path を自動で解決する。
  • エクスポートされていないパッケージのクラスを使おうとするとコンパイルエラーになる。

5. ビルドへの影響: Maven/Gradle とモジュール

Maven

マルチモジュールプロジェクトは「親」プロジェクト(parent)と複数の「子」モジュールで構成される。

myeditor/
  ├─ pom.xml        ← parent
  ├─ app/
  │    └─ pom.xml
  ├─ core/
  │    └─ pom.xml
  └─ utils/
       └─ pom.xml

parent pom.xml の例

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>myeditor</groupId>
  <artifactId>myeditor-parent</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>app</module>
    <module>core</module>
    <module>utils</module>
  </modules>
</project>

特徴:

  • Maven はコンパイル時に module-info.java を考慮する。
  • 実行時は --classpath ではなく --module-path を使用する。
  • exportsrequires を忘れるとコンパイルエラーになる。

Gradle

マルチモジュールは settings.gradle と各モジュールの個別の build.gradle で設定する。

settings.gradle

rootProject.name = 'myeditor'
include 'app', 'core', 'utils'

モジュール用の build.gradle

plugins {
    id 'java'
}

java {
    modularity.inferModulePath = true
}

IntelliJ IDEA

  • IDEA は Java モジュール作成時に module-info.java を生成できる。
  • Maven/Gradle を使う場合、モジュール構成は自動的に取り込まれる。
  • appmain を実行すると、IDE が module-path を設定する。
  • インポート/エクスポートのダイアログで、パッケージやモジュールの可視性を確認できる。

モジュール分割でよくあるミス

エラー No.1: モジュール間の循環依存。 2 つのモジュールが互いに requires していると、コンパイラはエラーを出します。これはアーキテクチャが崩れているサインです。解決策 — 共通の api モジュールを切り出すか、境界を見直す。

エラー No.2: エクスポートされていないパッケージのクラスを使用。 クラスが public でも、パッケージが module-info.javaexports に記載されていなければ、別モジュールからは見えません。結果としてコンパイルエラーになります。

エラー No.3: 利用しているモジュールに対する requires を追加し忘れる。 別モジュールからの import があっても、対応する記述が module-info.java になければコンパイルできません。依存関係は常に明示しましょう。

エラー No.4: モジュール名の重複。 モジュール名はビルドの範囲で一意である必要があります(特に Maven/Gradle)。重複はビルドを壊します。

エラー No.5: ディレクトリ構成の誤り。 ファイル module-info.java は該当モジュールの src 直下に置く必要があります。そうでないとコンパイラはモジュールを見つけません。

エラー No.6: 実行時の module-path 設定ミス。 手動で実行する場合は --module-path を使い、--classpath の代わりに指定してください。そうしないと「module not found」になります。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION