Skip to content

第 9 章: SOLID 原則とモジュール設計

9.1 はじめに

前章までで、FizzBuzz のコードはデザインパターンを適用して大きく改善されました。しかし、すべてのクラスが tdd.fizzbuzz という 1 つのパッケージに詰め込まれています。

この章では、SOLID 原則 を確認しながら、クラスを適切なパッケージに分割する モジュール設計 を行います。

9.2 SOLID 原則の確認

FizzBuzz のコードが SOLID 原則にどの程度準拠しているか確認しましょう。

単一責任原則(SRP: Single Responsibility Principle)

クラスを変更する理由は 1 つだけであるべき。

クラス 責務 変更理由 SRP 準拠
FizzBuzzType01 タイプ 1 の変換ルール タイプ 1 のルール変更時 OK
FizzBuzzType02 タイプ 2 の変換ルール タイプ 2 のルール変更時 OK
FizzBuzzType03 タイプ 3 の変換ルール タイプ 3 のルール変更時 OK
FizzBuzzValue 値の保持と等価性 値の表現が変わるとき OK
FizzBuzzList コレクション操作 コレクション操作が変わるとき OK
FizzBuzzValueCommand 単一値変換の実行 実行方法が変わるとき OK
FizzBuzzListCommand リスト生成の実行 実行方法が変わるとき OK

ポリモーフィズムとデザインパターンの適用により、各クラスが単一の責務を持つようになっています。

開放閉鎖原則(OCP: Open-Closed Principle)

拡張に対して開いていて、修正に対して閉じているべき。

新しいタイプ(例: タイプ 4)を追加する場合を考えてみましょう。

// タイプ 4 を追加するのに必要な変更
// 1. 新しいクラスを追加(既存コードの修正なし)
public class FizzBuzzType04 extends FizzBuzzType {
    @Override
    public FizzBuzzValue generate(int number) {
        // タイプ 4 のルール
        return new FizzBuzzValue(number, String.valueOf(number));
    }
}

// 2. ファクトリメソッドに 1 行追加(最小限の修正)
case 4: return new FizzBuzzType04();

FizzBuzzType 階層は拡張に対して開いています。新しいタイプを追加しても、既存のタイプクラスや FizzBuzzCommand を修正する必要はありません。

依存性逆転原則(DIP: Dependency Inversion Principle)

上位モジュールは下位モジュールに依存すべきではない。どちらも抽象に依存すべき。

FizzBuzz → FizzBuzzType(抽象)← FizzBuzzType01, 02, 03
FizzBuzzCommand(抽象)← FizzBuzzValueCommand, FizzBuzzListCommand

FizzBuzz クラスは具体的な FizzBuzzType01 ではなく、抽象的な FizzBuzzType に依存しています。

9.3 パッケージ設計

現在の問題

すべてのクラスが tdd.fizzbuzz に入っています。

tdd.fizzbuzz/
├── FizzBuzz.java
├── FizzBuzzType.java
├── FizzBuzzType01.java
├── FizzBuzzType02.java
├── FizzBuzzType03.java
├── FizzBuzzValue.java
├── FizzBuzzList.java
├── FizzBuzzCommand.java
├── FizzBuzzValueCommand.java
├── FizzBuzzListCommand.java
└── Main.java

クラスが増えるにつれて、どのクラスがどの責務を持つかが分かりにくくなります。

パッケージ分割

責務に基づいてパッケージを分割します。

tdd/
├── App.java                         # エントリポイント
└── fizzbuzz/
    ├── domain/
    │   ├── type/
    │   │   ├── FizzBuzzType.java     # 抽象タイプ + Factory
    │   │   ├── FizzBuzzType01.java   # タイプ 1
    │   │   ├── FizzBuzzType02.java   # タイプ 2
    │   │   └── FizzBuzzType03.java   # タイプ 3
    │   └── model/
    │       ├── FizzBuzzValue.java    # 値オブジェクト
    │       └── FizzBuzzList.java     # コレクション
    └── application/
        ├── FizzBuzzCommand.java      # コマンドインターフェース
        ├── FizzBuzzValueCommand.java # 単一値コマンド
        └── FizzBuzzListCommand.java  # リストコマンド

パッケージの責務

パッケージ 責務 含まれるクラス
tdd.fizzbuzz.domain.type 変換ルール(ビジネスロジック) FizzBuzzType 階層
tdd.fizzbuzz.domain.model ドメインモデル(値・コレクション) FizzBuzzValue, FizzBuzzList
tdd.fizzbuzz.application アプリケーション層(操作の実行) FizzBuzzCommand 階層

依存関係

uml diagram

application パッケージは domain に依存しますが、domainapplication に依存しません。これにより、ビジネスロジックがアプリケーション層から独立して変更・テストできます。

9.4 エントリポイントの更新

パッケージ分割後の App.java です。

package tdd;

import tdd.fizzbuzz.application.FizzBuzzListCommand;
import tdd.fizzbuzz.application.FizzBuzzValueCommand;
import tdd.fizzbuzz.domain.model.FizzBuzzList;
import tdd.fizzbuzz.domain.model.FizzBuzzValue;
import tdd.fizzbuzz.domain.type.FizzBuzzType;

public class App {
    private static final int MAX_NUMBER = 100;

    public static void main(String[] args) {
        FizzBuzzType type = FizzBuzzType.create(1);

        // 単一値の取得
        FizzBuzzValueCommand valueCommand
            = new FizzBuzzValueCommand(type);
        FizzBuzzValue value = valueCommand.execute(15);
        System.out.println(value);

        // リストの生成
        FizzBuzzListCommand listCommand
            = new FizzBuzzListCommand(type);
        FizzBuzzList list = listCommand.executeList(MAX_NUMBER);
        for (FizzBuzzValue item : list.getValues()) {
            System.out.println(item.getValue());
        }
    }
}

9.5 テストの更新

テストもパッケージ構造に合わせて再配置します。

src/test/java/tdd/fizzbuzz/
├── domain/
│   ├── type/
│   │   └── FizzBuzzTypeTest.java    # タイプ別テスト
│   └── model/
│       ├── FizzBuzzValueTest.java   # 値オブジェクトテスト
│       └── FizzBuzzListTest.java    # コレクションテスト
└── application/
    └── FizzBuzzCommandTest.java     # コマンドテスト

FizzBuzzTypeTest(タイプ別テスト)

package tdd.fizzbuzz.domain.type;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class FizzBuzzTypeTest {

    @Nested
    class タイプ1の場合 {
        private final FizzBuzzType type = FizzBuzzType.create(1);

        @Test
        void 三と五の倍数でFizzBuzzを返す() {
            assertEquals("FizzBuzz", type.generate(15).getValue());
        }

        @Test
        void 三の倍数でFizzを返す() {
            assertEquals("Fizz", type.generate(3).getValue());
        }

        @Test
        void 五の倍数でBuzzを返す() {
            assertEquals("Buzz", type.generate(5).getValue());
        }

        @Test
        void その他は数値を返す() {
            assertEquals("1", type.generate(1).getValue());
        }
    }

    @Nested
    class タイプ2の場合 {
        private final FizzBuzzType type = FizzBuzzType.create(2);

        @Test
        void 常に数値を返す() {
            assertEquals("3", type.generate(3).getValue());
            assertEquals("5", type.generate(5).getValue());
            assertEquals("15", type.generate(15).getValue());
        }
    }

    @Nested
    class タイプ3の場合 {
        private final FizzBuzzType type = FizzBuzzType.create(3);

        @Test
        void 十五の倍数のみFizzBuzzを返す() {
            assertEquals("FizzBuzz", type.generate(15).getValue());
            assertEquals("3", type.generate(3).getValue());
            assertEquals("5", type.generate(5).getValue());
        }
    }

    @Nested
    class 不正なタイプの場合 {
        @Test
        void 例外をスローする() {
            assertThrows(IllegalArgumentException.class,
                () -> FizzBuzzType.create(4));
        }
    }
}

9.6 まとめ

この章では SOLID 原則を確認しながらモジュール設計を行いました。

概念 適用
単一責任原則(SRP) 各クラスが 1 つの責務を持つ
開放閉鎖原則(OCP) 新タイプ追加で既存コード変更不要
依存性逆転原則(DIP) 抽象への依存で結合度を低減
パッケージ分割 domain(ビジネスロジック)と application(操作)の分離

第 3 部の振り返り

第 3 部(第 7〜9 章)を通じて、手続き的な FizzBuzz をオブジェクト指向設計に変換しました。

テーマ 学んだこと
7 カプセル化とポリモーフィズム private / final、Strategy パターン、抽象クラス
8 デザインパターンの適用 Value Object、First-Class Collection、Command、Factory Method
9 SOLID 原則とモジュール設計 SRP / OCP / DIP、パッケージ分割、ドメインモデル
Before: 1 つのクラスに全ロジック(手続き型)
After:  責務ごとにパッケージ分割された OOP 設計

次の第 4 部では、Java の関数型プログラミング機能(Stream API、Optional、Lambda)を使って、さらにコードを発展させます。