導入
TaskTitleとPriorityが揃ったので、TaskItemのTitleをstringからTaskTitleに差し替え、Priority・Complete()(完了にする)・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}");
}
OrderはEqualsを上書きしていないので、C#既定の参照比較になる- Entityにとってはこれで十分どころか好都合。「中身が同じだから同じ注文」と誤判定されずに済む
- ValueObject(
TaskTitle)では値で等しいと判定したい一方、Entity(TaskItem)では区別したい――この非対称がポイント
演習
TaskItemを契約どおりの最終形にします。TitleはTaskTitle型、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; }にすると「外からは読み取り専用、クラス内のメソッドからは変更可能」になります
まとめ
TaskItemはId・Title(TaskTitle)・Priority・IsCompletedを持ち、Complete()/Rename()でふるまいを表現する- Entityは
Equalsをあえて上書きせず、既定の参照比較のままにしてよい(ValueObjectとの対比) - 「重複タイトルの禁止」「完了済みタスクの扱い」のようなアプリ全体に関わるルールは、この後のRepository・Serviceの章で扱う
次章: new TaskItem(...)の呼び出しを1か所にまとめるFactoryパターンです。