導入
InMemoryTaskRepository(本番用)とFakeTaskRepository(テスト用)の2つが揃いました。これを使うTaskServiceが内部でnew InMemoryTaskRepository()と直書きしてしまうと、テストで差し替えられません。コンストラクタインジェクションで外から受け取るようにします。
図解
flowchart LR
subgraph before["直接 new(密結合)"]
S1["TaskService"] -->|new| R1["InMemoryTaskRepository 固定"]
end
subgraph after["注入(疎結合)"]
S2["TaskService"] -->|コンストラクタで受け取る| I["ITaskRepository 抽象"]
I -.-> R2["InMemory / Fake"]
end
style after fill:#e8f5e9
サンプル
DIそのものの詳しい解説はC#デザインパターンコースのレッスン19にあります。ここではTaskServiceに当てはめます。
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)
{
var task = TaskFactory.Create(title, priority);
_repository.Add(task);
return task;
}
// CompleteTask / GetActiveTasks は次章で実装します
public void CompleteTask(Guid id) => throw new NotImplementedException();
public IReadOnlyList<TaskItem> GetActiveTasks() => throw new NotImplementedException();
}
TaskServiceはITaskRepositoryという抽象にしか依存していない- 本番では
new TaskService(new InMemoryTaskRepository()) - テストでは
new TaskService(new FakeTaskRepository())——TaskServiceのコードは一切変えない
やってみよう
new TaskService(new FakeTaskRepository())とnew TaskService(new InMemoryTaskRepository())の両方を作り、同じAddTask呼び出しが両方で動くことを確認しましょう。
演習
var repo = new InMemoryTaskRepository();
var service = new TaskService(repo);
var task = service.AddTask("洗濯する", Priority.Low);
Assert(task.Title.Value == "洗濯する", "追加したタスクが返る");
Assert(repo.All().Count == 1, "repositoryにも登録されている");
Console.WriteLine("全テスト成功");
// TODO: コンストラクタで ITaskRepository を受け取り(DI)、
// AddTask では TaskFactory 経由で作ったタスクを repository に Add して返してください
public interface ITaskService
{
TaskItem AddTask(string title, Priority priority);
void CompleteTask(Guid id);
IReadOnlyList<TaskItem> GetActiveTasks();
}
public class TaskService : ITaskService
{
___
public void CompleteTask(Guid id) => throw new NotImplementedException(); // 次章で実装
public IReadOnlyList<TaskItem> GetActiveTasks() => throw new NotImplementedException(); // 次章で実装
}
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);
}
- 期待される出力:
全テスト成功
ヒント1を見る
private readonly ITaskRepository _repository; public TaskService(ITaskRepository repository) => _repository = repository; public TaskItem AddTask(string title, Priority priority) { var task = TaskFactory.Create(title, priority); _repository.Add(task); return task; }
ヒント2を見る
フィールドに保存した_repositoryに対してTaskFactory.Createで作ったタスクをAddします
まとめ
TaskServiceはITaskRepositoryという抽象にだけ依存し、実体は外から受け取る(コンストラクタインジェクション)- 本番用・テスト用の実装を、
TaskServiceのコードを変えずに差し替えられる - 次章でこの
TaskServiceを業務ロジックとして本格的に育てます
次章: Service層で業務ルールを積み上げていきます。