導入
レッスン3で「MyAppプロジェクトのApp起動処理」として予告した仕組みがこれです。DIコンテナを使うにせよ手動でnewするにせよ、どこかで具体的な実装(InMemoryTaskRepositoryなど)を組み立てる必要があります。それをプログラム中のただ1箇所に押し込め、それ以外のコードは抽象(ITaskRepository・ITaskService)にしか依存しないようにする――これがComposition Rootという考え方です。
説明
flowchart TB
subgraph CR["Composition Root(アプリ全体でただ1箇所)"]
direction LR
REPO["new InMemoryTaskRepository()"] --> SVC["new TaskService(repository)"]
SVC --> VM["new TaskListViewModel(service)"]
end
CR --> APP["アプリの残り全部<br/>(抽象にしか依存しない)"]
style CR fill:#e1f5fe
TaskServiceやTaskListViewModelのコンストラクタは「抽象を受け取る」だけで、自分では何もnewしない(第4〜6章で徹底してきた通り)- では具体的な実装は誰が
newするのか――その答えを1箇所に決めるのがComposition Root - 実務ではWPFの
App.xaml.cs(OnStartup)やコンソールアプリのMainがこの役割を担う。「保存先をファイルに変える」といった変更が起きても、書き換えるのはComposition Rootだけで済む
やってみよう
ここまでの層(Repository → Service → ViewModel)を、実際に1箇所で組み立てるCompositionRootを書いてみましょう。
演習
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
var vm = CompositionRoot.Compose();
vm.NewTaskTitle = "牛乳を買う";
vm.AddCommand.Execute(null);
Assert(vm.Tasks.Count == 1, "組み立てたViewModelで実際にタスクを追加できる");
Assert(vm.Tasks[0].Title == "牛乳を買う", "タイトルが反映される");
Console.WriteLine("全テスト成功");
// TODO: ITaskRepository → ITaskService → TaskListViewModel の順に実体を組み立てて
// 返す Compose() を実装してください(これがComposition Root)
static class CompositionRoot
{
___
}
void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }
// --- 型のコピー ---
public readonly struct TaskTitle : IEquatable<TaskTitle>
{
public string Value { get; }
public TaskTitle(string value)
{
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("タイトルは空にできません");
if (value.Length > 50) throw new ArgumentException("タイトルは50文字以内です");
Value = value.Trim();
}
public bool Equals(TaskTitle other) => Value == other.Value;
public override string ToString() => Value;
}
public enum Priority { Low, Medium, High }
public class TaskItem
{
public Guid Id { get; }
public TaskTitle Title { get; private set; }
public Priority Priority { get; private set; }
public bool IsCompleted { get; private set; }
public TaskItem(Guid id, TaskTitle title, Priority priority)
{ Id = id; Title = title; Priority = priority; }
public void Complete() => IsCompleted = true;
public void Rename(TaskTitle newTitle) => Title = newTitle;
}
public static class TaskFactory
{
public static TaskItem Create(string title, Priority priority = Priority.Medium)
=> new TaskItem(Guid.NewGuid(), new TaskTitle(title), priority);
}
public interface ITaskRepository
{
void Add(TaskItem task);
IReadOnlyList<TaskItem> All();
TaskItem? FindById(Guid id);
}
public class InMemoryTaskRepository : ITaskRepository
{
private readonly List<TaskItem> _tasks = new();
public void Add(TaskItem task) => _tasks.Add(task);
public IReadOnlyList<TaskItem> All() => _tasks;
public TaskItem? FindById(Guid id) => _tasks.FirstOrDefault(t => t.Id == id);
}
public interface ITaskService
{
TaskItem AddTask(string title, Priority priority);
void CompleteTask(Guid id);
IReadOnlyList<TaskItem> GetActiveTasks();
}
public class TaskService : ITaskService
{
private readonly ITaskRepository _repository;
public TaskService(ITaskRepository repository) => _repository = repository;
public TaskItem AddTask(string title, Priority priority)
{
if (_repository.All().Any(t => t.Title.Value == title))
throw new InvalidOperationException("同じタイトルのタスクが既にあります");
var task = TaskFactory.Create(title, priority);
_repository.Add(task);
return task;
}
public void CompleteTask(Guid id) => _repository.FindById(id)?.Complete();
public IReadOnlyList<TaskItem> GetActiveTasks() => _repository.All().Where(t => !t.IsCompleted).ToList();
}
public record TaskDto(Guid Id, string Title, string Priority, bool IsCompleted);
public static class TaskMapper
{
public static TaskDto ToDto(TaskItem task)
=> new(task.Id, task.Title.Value, task.Priority.ToString(), task.IsCompleted);
}
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(name);
return true;
}
}
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null) { _execute = execute; _canExecute = canExecute; }
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public class TaskListViewModel : ViewModelBase
{
private readonly ITaskService _service;
private string _newTaskTitle = "";
public ObservableCollection<TaskDto> Tasks { get; } = new();
public string NewTaskTitle
{
get => _newTaskTitle;
set => SetProperty(ref _newTaskTitle, value);
}
public ICommand AddCommand { get; }
public TaskListViewModel(ITaskService service)
{
_service = service;
AddCommand = new RelayCommand(Add, () => !string.IsNullOrWhiteSpace(NewTaskTitle));
}
private void Add()
{
var task = _service.AddTask(NewTaskTitle, Priority.Medium);
Tasks.Add(TaskMapper.ToDto(task));
NewTaskTitle = "";
}
}
- 期待される出力:
全テスト成功
ヒント1を見る
static class CompositionRoot { public static TaskListViewModel Compose() { ITaskRepository repository = new InMemoryTaskRepository(); ITaskService service = new TaskService(repository); return new TaskListViewModel(service); } }
ヒント2を見る
Repository → Service → ViewModelの順に、それぞれのコンストラクタへ前段で作った実体を渡すだけです
まとめ
- Composition Rootは「具体的な実装を
newする場所」をアプリ全体でただ1箇所に集約する - それ以外のクラス(
TaskService・TaskListViewModel)は抽象にしか依存しない - 実装を差し替えたくなったとき、書き換える場所がComposition Rootの1箇所だけに閉じる
次回: 共通処理を切り出す「ヘルパークラス」の設計指針です。