本文へスキップ
BecomeCoder

C# MVVMコース · 第2章 Entity ― 業務データの核 · レッスン8

Entityの同一性とふるまい ― TaskItemを完成させる

ブラウザで完結

導入

TaskTitlePriorityが揃ったので、TaskItemTitlestringからTaskTitleに差し替え、PriorityComplete()(完了にする)・Rename()(タイトルを変える)を備えた最終形にします。あわせて、EntityがEqualsをあえて上書きしない理由も確認します。

図解

classDiagram
    class TaskItem {
        +Guid Id
        +TaskTitle Title
        +Priority Priority
        +bool IsCompleted
        +Complete() void
        +Rename(TaskTitle) void
    }
    class TaskTitle {
        +string Value
    }
    class Priority {
        <<enumeration>>
        Low
        Medium
        High
    }
    TaskItem --> TaskTitle
    TaskItem --> Priority

サンプル

var order1 = new Order(1, new[] { "本", "ペン" });
var order2 = new Order(2, new[] { "本", "ペン" });

Assert(!order1.Equals(order2), "中身(商品構成)が同じでもIDが違えば別の注文");
Console.WriteLine("全テスト成功");

class Order
{
    public int Id { get; }
    public string[] Items { get; }
    public Order(int id, string[] items) { Id = id; Items = items; }
    // Equalsをあえて上書きしない=デフォルトの参照比較のまま
}

void Assert(bool condition, string name)
{
    if (!condition) throw new Exception($"FAIL: {name}");
}
  • OrderEqualsを上書きしていないので、C#既定の参照比較になる
  • Entityにとってはこれで十分どころか好都合。「中身が同じだから同じ注文」と誤判定されずに済む
  • ValueObject(TaskTitle)では値で等しいと判定したい一方、Entity(TaskItem)では区別したい――この非対称がポイント

演習

TaskItemを契約どおりの最終形にします。TitleTaskTitle型、Priorityを持ち、Complete()Rename(TaskTitle)で状態を変えられるようにしてください。

var title = new TaskTitle("牛乳を買う");
var task = new TaskItem(Guid.NewGuid(), title, Priority.High);

Assert(task.Title.Value == "牛乳を買う", "Titleを保持している");
Assert(task.Priority == Priority.High, "Priorityを保持している");
Assert(task.IsCompleted == false, "作成直後は未完了");

task.Complete();
Assert(task.IsCompleted == true, "Complete()で完了になる");

task.Rename(new TaskTitle("卵も買う"));
Assert(task.Title.Value == "卵も買う", "Rename()でタイトルを変更できる");

var other = new TaskItem(Guid.NewGuid(), title, Priority.High);
Assert(!task.Equals(other), "内容が同じでもIDが違えば別のタスク(Entityは参照比較のままでよい)");

Console.WriteLine("全テスト成功");

// TODO: 次を満たす TaskItem クラスを実装してください
//   ・Id(Guid)は読み取り専用、コンストラクタで受け取る
//   ・Title(TaskTitle)・Priority(Priority)は外からは読み取り専用(private set)
//   ・IsCompleted(bool)は既定false
//   ・Complete()でIsCompletedをtrueにする
//   ・Rename(TaskTitle newTitle)でTitleを差し替える
//   ・Equalsはあえて上書きしない(既定の参照比較のままでよい)
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;
}
enum Priority { Low, Medium, High }

void Assert(bool condition, string name)
{
    if (!condition) throw new Exception($"FAIL: {name}");
}
___
  • 期待される出力: 全テスト成功
ヒント1を見る

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

ヒント2を見る

プロパティは{ get; private set; }にすると「外からは読み取り専用、クラス内のメソッドからは変更可能」になります

まとめ

  • TaskItemIdTitle(TaskTitle)・PriorityIsCompletedを持ち、Complete()/Rename()でふるまいを表現する
  • EntityはEqualsをあえて上書きせず、既定の参照比較のままにしてよい(ValueObjectとの対比)
  • 「重複タイトルの禁止」「完了済みタスクの扱い」のようなアプリ全体に関わるルールは、この後のRepository・Serviceの章で扱う

次章: new TaskItem(...)の呼び出しを1か所にまとめるFactoryパターンです。

実際に動かしてみよう

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

C# — ブラウザ内で実行

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