本文へスキップ
BecomeCoder

Java MVVMコース · 第4章 Repository ― データアクセスの抽象化 · レッスン16

DI ― コンストラクタインジェクション

ブラウザで完結

導入

InMemoryTaskRepository本番用)とFakeTaskRepositoryテスト用)の2つが揃いました。これを使うクラスが内部でnew InMemoryTaskRepository()と直書きしてしまうと、テストで差し替えられません。コンストラクタインジェクションで外から受け取るようにします。

図解

flowchart LR
    subgraph before["直接 new(密結合)"]
        S1["TaskRegistrationService"] -->|new| R1["InMemoryTaskRepository 固定"]
    end
    subgraph after["注入(疎結合)"]
        S2["TaskRegistrationService"] -->|コンストラクタで受け取る| I["TaskRepository 抽象"]
        I -.-> R2["InMemory / Fake"]
    end
    style after fill:#e8f5e9

説明

TaskRegistrationServiceに当てはめると、次の形になります。

public class TaskRegistrationService {
    private final TaskRepository repository;
    private final TaskFactory factory;

    public TaskRegistrationService(TaskRepository repository, TaskFactory factory) {   // 注入
        this.repository = repository;
        this.factory = factory;
    }

    public TaskItem register(String title, Priority priority) {
        TaskItem task = factory.create(title, priority);
        repository.add(task);
        return task;
    }
}
  • TaskRegistrationServiceTaskRepositoryという抽象にしか依存していない
  • 本番ではnew TaskRegistrationService(new InMemoryTaskRepository(), factory)
  • テストではnew TaskRegistrationService(new FakeTaskRepository(), factory)——TaskRegistrationServiceのコードは一切変えない

やってみよう

演習ではnew TaskRegistrationService(new InMemoryTaskRepository(), ...)new TaskRegistrationService(new FakeTaskRepository(), ...)の両方を作り、TaskRegistrationServiceのコードを1行も変えずに同じregister呼び出しが両方で動くことを確認します。

演習

「ドメインの型」「本番用Repository」「テスト用Fake」「サービス本体(ここが未実装)」「テスト」の5つに分けます。実務でもこの層は別ファイルとして分かれています。

public interface IdGenerator {
    String next();
}

public class TaskTitle {
    private final String value;

    public TaskTitle(String value) {
        if (value == null || value.isBlank()) throw new IllegalArgumentException("タイトルは空にできません");
        if (value.length() > 50) throw new IllegalArgumentException("タイトルは50文字以内です");
        this.value = value;
    }

    public String getValue() { return value; }
    public String toString() { return value; }

    public boolean equals(Object o) {
        if (!(o instanceof TaskTitle)) return false;
        return value.equals(((TaskTitle) o).value);
    }

    public int hashCode() { return value.hashCode(); }
}

public enum Priority {
    LOW("低"), MEDIUM("中"), HIGH("高");

    private final String label;

    Priority(String label) { this.label = label; }

    public String getLabel() { return label; }
}

public class TaskItem {
    private final String id;
    private TaskTitle title;
    private final Priority priority;
    private boolean completed;

    public TaskItem(String id, TaskTitle title, Priority priority) {
        this.id = id;
        this.title = title;
        this.priority = priority;
    }

    public String getId() { return id; }
    public TaskTitle getTitle() { return title; }
    public Priority getPriority() { return priority; }
    public boolean isCompleted() { return completed; }
    public void complete() { completed = true; }
    public void rename(TaskTitle newTitle) { title = newTitle; }

    public boolean equals(Object o) {
        if (!(o instanceof TaskItem)) return false;
        return id.equals(((TaskItem) o).id);
    }

    public int hashCode() { return id.hashCode(); }
}

public class TaskFactory {
    private final IdGenerator idGenerator;

    public TaskFactory(IdGenerator idGenerator) { this.idGenerator = idGenerator; }

    public TaskItem create(String title, Priority priority) {
        return new TaskItem(idGenerator.next(), new TaskTitle(title), priority);
    }

    public TaskItem create(String title) {
        return create(title, Priority.MEDIUM);
    }
}

public class SequentialIdGenerator implements IdGenerator {
    private int seq = 0;

    public String next() {
        seq++;
        return "t-" + seq;
    }
}

public interface TaskRepository {
    void add(TaskItem task);
    List<TaskItem> findAll();
    Optional<TaskItem> findById(String id);
}

public class InMemoryTaskRepository implements TaskRepository {
    private final List<TaskItem> tasks = new ArrayList<>();

    public void add(TaskItem task) { tasks.add(task); }
    public List<TaskItem> findAll() { return new ArrayList<>(tasks); }

    public Optional<TaskItem> findById(String id) {
        for (TaskItem t : tasks) {
            if (t.getId().equals(id)) return Optional.of(t);
        }
        return Optional.empty();
    }
}
// レッスン14で作ったFake(本物と同じ TaskRepository を実装している)
public class FakeTaskRepository implements TaskRepository {
    private final List<TaskItem> tasks = new ArrayList<>();
    private int addCallCount = 0;

    public void add(TaskItem task) {
        tasks.add(task);
        addCallCount++;
    }

    public List<TaskItem> findAll() { return new ArrayList<>(tasks); }

    public Optional<TaskItem> findById(String id) {
        for (TaskItem t : tasks) {
            if (t.getId().equals(id)) return Optional.of(t);
        }
        return Optional.empty();
    }

    public int getAddCallCount() { return addCallCount; }
}
// TODO: コンストラクタで TaskRepository と TaskFactory を受け取り(DI)、
//       register では TaskFactory で作ったタスクを repository に add して返してください
//       (タイトルが重複していたら IllegalStateException を投げます)
public class TaskRegistrationService {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class TaskRegistrationServiceTest {
    @Test
    void registerHaSakuseishitaTaskWoKaeshiRepositoryNimoTorokusareru() {
        TaskRepository repository = new InMemoryTaskRepository();
        TaskRegistrationService service = new TaskRegistrationService(repository, new TaskFactory(new SequentialIdGenerator()));

        TaskItem task = service.register("洗濯する", Priority.LOW);

        assertEquals("洗濯する", task.getTitle().getValue(), "追加したタスクが返る");
        assertEquals(1, repository.findAll().size(), "repositoryにも登録されている");
    }

    @Test
    void fakeNiSashikaetemoOnajiServiceNoCodeDeUgoku() {
        // TaskRegistrationService のコードは1行も変えず、渡す TaskRepository を Fake に差し替えるだけでよい
        FakeTaskRepository fake = new FakeTaskRepository();
        TaskRegistrationService fakeService = new TaskRegistrationService(fake, new TaskFactory(new SequentialIdGenerator()));
        fakeService.register("掃除する", Priority.MEDIUM);

        assertEquals(1, fake.getAddCallCount(), "Fakeに差し替えても同じServiceのコードで動く");
    }
}
  • 期待される結果: 2件のテストがすべて成功
ヒント1を見る

private final TaskRepository repository; private final TaskFactory factory; public TaskRegistrationService(TaskRepository repository, TaskFactory factory) { this.repository = repository; this.factory = factory; } public TaskItem register(String title, Priority priority) { for (TaskItem t : repository.findAll()) { if (t.getTitle().getValue().equals(title)) { throw new IllegalStateException("同じタイトルのタスクが既にあります"); } } TaskItem task = factory.create(title, priority); repository.add(task); return task; }

ヒント2を見る

フィールドに保存したrepositoryfactoryを使い、factory.createで作ったタスクをrepository.addします

まとめ

  • クラスは抽象(TaskRepository)にだけ依存し、実体は外から受け取る(コンストラクタインジェクション)
  • 本番用・テスト用の実装を、クラスのコードを変えずに差し替えられる
  • 次章でこの形をそのままTaskServiceとして業務ロジックに育てます

次章: Service層で業務ルールを積み上げていきます。

実際に動かしてみよう

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

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

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