本文へスキップ
BecomeCoder

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

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

ブラウザで完結

導入

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();
}
  • TaskServiceITaskRepositoryという抽象にしか依存していない
  • 本番では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します

まとめ

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

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

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。