導入
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;
}
}
TaskRegistrationServiceはTaskRepositoryという抽象にしか依存していない- 本番では
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を見る
フィールドに保存したrepositoryとfactoryを使い、factory.createで作ったタスクをrepository.addします
まとめ
- クラスは抽象(
TaskRepository)にだけ依存し、実体は外から受け取る(コンストラクタインジェクション) - 本番用・テスト用の実装を、クラスのコードを変えずに差し替えられる
- 次章でこの形をそのまま
TaskServiceとして業務ロジックに育てます
次章: Service層で業務ルールを積み上げていきます。