導入
第3章で「appパッケージのComposition Root」として予告した仕組みがこれです。DIコンテナを使うにせよ手動でnewするにせよ、どこかで具体的な実装(InMemoryTaskRepositoryなど)を組み立てる必要があります。それをプログラム中のただ1箇所に押し込め、それ以外のコードは抽象(TaskRepository・TaskService)にしか依存しないようにする――これがComposition Rootという考え方です。
説明
flowchart TB
subgraph CR["Composition Root(アプリ全体でただ1箇所)"]
direction LR
ID["new SequentialIdGenerator()"] --> F["new TaskFactory(idGenerator)"]
F --> REPO["new InMemoryTaskRepository()"]
REPO --> SVC["new TaskService(repository, factory)"]
SVC --> VM["new TaskListViewModel(service)"]
end
CR --> APP["アプリの残り全部<br/>(抽象にしか依存しない)"]
style CR fill:#e1f5fe
TaskServiceやTaskListViewModelのコンストラクタは「抽象を受け取る」だけで、自分では何もnewしない(第4〜6章で徹底してきた通り)- では具体的な実装は誰が
newするのか――その答えを1箇所に決めるのがComposition Root - 実務ではJavaFXアプリの
Application#startや、コンソールアプリのmainがこの役割を担う。「保存先をファイルに変える」といった変更が起きても、書き換えるのはComposition Rootだけで済む
やってみよう
ここまでの層(Repository → Service → ViewModel)を、実際に1箇所で組み立てるCompositionRootを書いてみましょう。
演習
これまでの層はすべて完成済みとして与えられています。今回の担当はCompositionRoot.javaだけです。
public interface IdGenerator { String next(); }
public interface Clock { long today(); }
// 本番用: 呼ぶたびに新しいIDを返す
public class SequentialIdGenerator implements IdGenerator {
private int count = 0;
public String next() {
count = count + 1;
return "task-" + count;
}
}
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; }
@Override
public String toString() { return value; }
}
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 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);
}
}
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
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 tasks;
}
public Optional<TaskItem> findById(String id) {
for (TaskItem task : tasks) {
if (task.getId().equals(id)) return Optional.of(task);
}
return Optional.empty();
}
}
import java.util.List;
import java.util.ArrayList;
public record TaskDto(String id, String title, String priorityLabel, boolean completed) { }
public class TaskMapper {
public static TaskDto toDto(TaskItem task) {
return new TaskDto(task.getId(), task.getTitle().getValue(), task.getPriority().getLabel(), task.isCompleted());
}
}
public class TaskService {
private final TaskRepository repository;
private final TaskFactory factory;
public TaskService(TaskRepository repository, TaskFactory factory) {
this.repository = repository;
this.factory = factory;
}
public void add(String title, Priority priority) {
TaskItem task = factory.create(title, priority);
repository.add(task);
}
public void complete(String id) {
repository.findById(id).ifPresent(task -> task.complete());
}
public List<TaskDto> list() {
List<TaskDto> result = new ArrayList<>();
for (TaskItem task : repository.findAll()) {
result.add(TaskMapper.toDto(task));
}
return result;
}
public int remainingCount() {
int count = 0;
for (TaskItem task : repository.findAll()) {
if (!task.isCompleted()) count = count + 1;
}
return count;
}
}
import java.util.List;
import java.util.ArrayList;
public interface PropertyChangedListener {
void onPropertyChanged(String propertyName);
}
public abstract class ViewModelBase {
private final List<PropertyChangedListener> listeners = new ArrayList<>();
public void addPropertyChangedListener(PropertyChangedListener listener) {
listeners.add(listener);
}
protected void raisePropertyChanged(String propertyName) {
for (PropertyChangedListener listener : listeners) {
listener.onPropertyChanged(propertyName);
}
}
}
import java.util.function.BooleanSupplier;
public interface Command {
void execute();
default boolean canExecute() { return true; }
}
public class RelayCommand implements Command {
private final Runnable action;
private final BooleanSupplier canExecute;
public RelayCommand(Runnable action) {
this(action, () -> true);
}
public RelayCommand(Runnable action, BooleanSupplier canExecute) {
this.action = action;
this.canExecute = canExecute;
}
public void execute() {
action.run();
}
public boolean canExecute() {
return canExecute.getAsBoolean();
}
}
import java.util.List;
public class TaskListViewModel extends ViewModelBase {
private final TaskService service;
private String newTitle = "";
private final Command addCommand = new RelayCommand(() -> add(), () -> canAdd());
public TaskListViewModel(TaskService service) {
this.service = service;
}
public String getNewTitle() { return newTitle; }
public void setNewTitle(String value) {
newTitle = value;
raisePropertyChanged("newTitle");
raisePropertyChanged("canAdd");
}
public List<TaskDto> getTasks() {
return service.list();
}
public int getRemainingCount() {
return service.remainingCount();
}
public Command getAddCommand() {
return addCommand;
}
public void complete(String id) {
service.complete(id);
raisePropertyChanged("tasks");
raisePropertyChanged("remainingCount");
}
private boolean canAdd() {
return newTitle != null && !newTitle.isBlank();
}
private void add() {
service.add(newTitle, Priority.MEDIUM);
setNewTitle("");
raisePropertyChanged("tasks");
raisePropertyChanged("remainingCount");
}
}
// TODO: IdGenerator → TaskFactory → TaskRepository → TaskService → TaskListViewModel の順に
// 実体を組み立てて返す compose() を実装してください(これがComposition Root)
public class CompositionRoot {
public static TaskListViewModel compose() {
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CompositionRootTest {
@Test
void composeShitaViewModelDeTaskWoTsuikaDekiru() {
TaskListViewModel viewModel = CompositionRoot.compose();
viewModel.setNewTitle("牛乳を買う");
viewModel.getAddCommand().execute();
assertEquals(1, viewModel.getTasks().size(), "組み立てたViewModelで実際にタスクを追加できる");
assertEquals("牛乳を買う", viewModel.getTasks().get(0).title(), "タイトルが反映される");
}
}
- 期待される結果: 1件のテストがすべて成功
ヒント1を見る
IdGenerator idGenerator = new SequentialIdGenerator(); TaskFactory factory = new TaskFactory(idGenerator); TaskRepository repository = new InMemoryTaskRepository(); TaskService service = new TaskService(repository, factory); return new TaskListViewModel(service);
ヒント2を見る
IdGenerator → TaskFactory → TaskRepository → TaskService → TaskListViewModel の順に、それぞれのコンストラクタへ前段で作った実体を渡すだけです
まとめ
- Composition Rootは「具体的な実装を
newする場所」をアプリ全体でただ1箇所に集約する - それ以外のクラス(
TaskService・TaskListViewModel)は抽象にしか依存しない - 実装を差し替えたくなったとき、書き換える場所がComposition Rootの1箇所だけに閉じる
次回: 共通処理を切り出す「ヘルパークラス」の設計指針です。