本文へスキップ
BecomeCoder

Java MVVMコース · 第8章 周辺の道具立て ― DI・ヘルパー・アンチパターン · レッスン33

ヘルパークラスの設計指針 ― 静的かインスタンスか

ブラウザで完結

導入

「今日は何日目か」「新しいIDを発行する」――こうした小さな共通処理をヘルパークラスに切り出すことはよくあります。ただし、これを**staticメソッド**として直接呼ぶか、インターフェースにしてコンストラクタで注入するかは、テストのしやすさに直結する設計判断です。

説明

flowchart LR
    subgraph static_["staticで直接呼ぶ"]
        S["SystemClock.today()"] -->|直接呼ぶ| X["呼ぶたびに違う値<br/>テストで固定できない"]
    end
    subgraph di["interface + 注入"]
        I["Clock(抽象)"] -.-> Real["本番: 実際の日付"]
        I -.-> Fake["テスト: () -&gt; 20L<br/>(決まった値を返すラムダ)"]
    end
    style Fake fill:#e8f5e9

判断の目安はシンプルです。

  • 純粋な計算(入力だけで結果が決まり、副作用がない): staticのままで十分テストできる。文字列整形や数値計算のようなヘルパーがこれにあたる(レッスン1TitleRulesがまさにこれ)
  • 呼ぶたびに結果が変わる/外部の状態に触れる(現在時刻・乱数・新規ID発行・ファイルI/O): staticのまま直接呼ぶと、テストで結果を固定できない。インターフェースに抽象化してコンストラクタで注入し、テストでは予測可能な値を返すFake(またはラムダ)に差し替える

共通のAPI契約にあるClockIdGeneratorは、まさにこの2つ目のパターンです。第7章SequentialIdGenerator本番用の実装でしたが、テストではさらに単純に、ラムダでその場限りの実装を作ることもできます。

Clock fixedClock = () -> 20L;   // "今日は20日目" というテスト専用のClock

このコースのシミュレータにはjava.timeが無いため、実際の日付を返す本番のClock実装を動かすことはできません。しかし設計そのもの(staticで直接日付を取りに行くのではなく、Clockという抽象を受け取る)は、実務のコードでもまったく同じです。

やってみよう

Clockを受け取ってメッセージを記録するActivityLogを実装し、固定のClockを渡すだけで結果を予測できることを確認してみましょう。

演習

Clock.javaは完成済みとしてそのまま使い、今回の担当はActivityLog.javaだけです。

public interface Clock { long today(); }
import java.util.List;
import java.util.ArrayList;

public class ActivityLog {
    private final Clock clock;
    private final List<String> entries = new ArrayList<>();

    public ActivityLog(Clock clock) {
        this.clock = clock;
    }

    // TODO: "◯日目: メッセージ" という形式の文字列を entries に追加する append を実装してください
    //       (◯には clock.today() の値を使います)
    public void append(String message) {
    }

    public List<String> getEntries() {
        return entries;
    }
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class ActivityLogTest {
    @Test
    void kotayonoHizukeGaKirokuNiHairu() {
        Clock fixedClock = () -> 20L;
        ActivityLog log = new ActivityLog(fixedClock);

        log.append("牛乳を買うを追加した");

        assertEquals(1, log.getEntries().size(), "1件記録される");
        assertEquals("20日目: 牛乳を買うを追加した", log.getEntries().get(0));
    }

    @Test
    void hiGaSusumuToKirokuMoKawaru() {
        Clock day1 = () -> 1L;
        ActivityLog log1 = new ActivityLog(day1);
        log1.append("最初の記録");

        Clock day2 = () -> 2L;
        ActivityLog log2 = new ActivityLog(day2);
        log2.append("別の日の記録");

        assertEquals("1日目: 最初の記録", log1.getEntries().get(0));
        assertEquals("2日目: 別の日の記録", log2.getEntries().get(0));
    }
}
  • 期待される結果: 2件のテストがすべて成功
ヒント1を見る

public void append(String message) { entries.add(clock.today() + "日目: " + message); }

ヒント2を見る

clock.today()long型なので、文字列と+でつなげればそのまま数字が文字列に変換されます

まとめ

  • 副作用のない純粋なヘルパーはstaticのままで十分テストできる
  • 現在時刻・乱数・新規ID発行のような「呼ぶたびに変わる」ヘルパーはインターフェースに抽象化し、コンストラクタで注入する
  • テストでは、本番の実装の代わりにラムダで「決まった値を返すだけの偽物」をその場で作れる(Clock fixed = () -> 20L;
  • TaskFactory.createが内部でidGenerator.next()を直接呼んでいるのは、Idの実際の値をテストで検証する必要がない(重複さえしなければよい)からこそ許される設計判断

次回: MVVMでやってしまいがちな失敗、アンチパターンです。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .java ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめて解釈して動かせます。@Test の付いたテストがあるレッスンでは、テストメソッドごとに ✅/❌ の一覧(Red/Green)が出るので、まずテストを赤くしてから実装で緑にする、というTDDの回し方をその場で体験できます(本物のJVMではなく、JUnit・Mockitoの主要な書き方まで再現した学習用シミュレータです)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。