本文へスキップ
BecomeCoder

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

ITaskRepositoryとInMemoryTaskRepository ― タスクを保持する

ブラウザで完結

導入

いよいよ本題です。「登録する(Add)」「全件取る(All)」「IDで探す(FindById)」の3つだけを持つ ITaskRepository を定義し、List<TaskItem> で保持する InMemoryTaskRepository をTDDで実装します。

図解

classDiagram
    class ITaskRepository {
        <<interface>>
        +Add(TaskItem) void
        +All() IReadOnlyList~TaskItem~
        +FindById(Guid) TaskItem
    }
    class InMemoryTaskRepository {
        -List~TaskItem~ _tasks
    }
    ITaskRepository <|.. InMemoryTaskRepository

サンプル

このコースを通じて使う契約は次の形に統一します(以降の章もこの3メソッドだけを使います)。

public interface ITaskRepository
{
    void Add(TaskItem task);
    IReadOnlyList<TaskItem> All();
    TaskItem? FindById(Guid id);
}
  • Addはタスクを1件追加する(戻り値なし)
  • Allは今持っている全タスクを読み取り専用のリストとして返す
  • FindByIdは見つからなければnullを返す(TaskItem?という戻り値がそれを表す)

やってみよう

InMemoryTaskRepository を実装したら、TaskFactory.Createで3件タスクを作ってAddし、All().Countで件数を確認してみましょう。

演習

var repo = new InMemoryTaskRepository();
var t1 = TaskFactory.Create("牛乳を買う");
var t2 = TaskFactory.Create("レポートを書く", Priority.High);
repo.Add(t1);
repo.Add(t2);

Assert(repo.All().Count == 2, "2件登録されている");
Assert(repo.FindById(t1.Id)?.Title.Value == "牛乳を買う", "IDで取得できる");
Assert(repo.FindById(Guid.NewGuid()) == null, "存在しないIDはnull");
Console.WriteLine("全テスト成功");

// TODO: ITaskRepository を実装する InMemoryTaskRepository を完成させてください
//       内部は List<TaskItem> で保持します
___

void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }

// --- 第2〜3章の型(コピー。シグネチャは変更しない) ---
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);
}
  • 期待される出力: 全テスト成功
ヒント1を見る

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); }

ヒント2を見る

FindByIdFirstOrDefault(見つからなければnull)でリストの中から探します

まとめ

  • ITaskRepositoryAdd/All/FindByIdの3メソッドだけの小さな契約
  • InMemoryTaskRepositoryList<TaskItem>で保持する素直な実装
  • TaskItemクラス(参照型)なので、FindByIdで取り出した参照を書き換えると保存済みのデータもそのまま更新される(次章で活用します)

次回: 本物ではなく偽物のFakeTaskRepositoryテストを書く理由です。

実際に動かしてみよう

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

C# — ブラウザ内で実行

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