導入
前レッスンで確認したFactoryの考え方を、契約どおりのTaskFactoryとして実装します。ポイントは「TaskFactory自身は検証ロジックを書かない」ことです。タイトルの検証はすでにTaskTitleのコンストラクタが持っているので、TaskFactoryはそれを呼び出して組み立てるだけに徹します。
図解
flowchart LR
C["TaskFactory.Create(title, priority = Medium)"] --> V["new TaskTitle(title)<br/>(検証。ダメなら例外)"]
V --> N["new TaskItem(Guid.NewGuid(), title, priority)"]
N --> R["検証済みのTaskItemを返す"]
サンプル
Factoryが無いと、呼び出し側ごとに「優先度を省略したときはMedium」というデフォルトを書く羽目になります。
// Factoryが無い場合、呼び出し側が毎回デフォルト値を意識する
var withoutFactory1 = new TaskItem(Guid.NewGuid(), new TaskTitle("洗濯"), Priority.Medium);
var withoutFactory2 = new TaskItem(Guid.NewGuid(), new TaskTitle("掃除"), Priority.Medium);
// Factoryがあれば、デフォルト値も1か所(引数の既定値)に集約できる
// var withFactory = TaskFactory.Create("洗濯"); // 省略時はPriority.Medium
priority = Priority.Mediumのような引数の既定値も、「省略時どうするか」という決め事の一種です。Factoryのメソッドシグネチャに書いておけば、呼び出し側は必要なときだけ指定すればよくなります。
演習
契約どおりのTaskFactoryをTDDで実装します。
var t1 = TaskFactory.Create("牛乳を買う");
Assert(t1.Title.Value == "牛乳を買う", "Titleを保持している");
Assert(t1.Priority == Priority.Medium, "省略時はMedium(既定値)");
Assert(t1.IsCompleted == false, "作成直後は未完了");
var t2 = TaskFactory.Create("卵を買う", Priority.High);
Assert(t2.Priority == Priority.High, "優先度を指定できる");
Assert(t1.Id != t2.Id, "IDは毎回異なる");
AssertThrows<ArgumentException>(() => TaskFactory.Create(""), "空のタイトルは作れない(TaskTitleの検証がそのまま効く)");
Console.WriteLine("全テスト成功");
// TODO: 次を満たす TaskFactory.Create を実装してください
// ・引数は (string title, Priority priority = Priority.Medium)
// ・new TaskTitle(title) でタイトルを検証してから TaskItem を作る
// (検証ロジック自体はここには書かない。TaskTitleに任せる)
// ・Id は Guid.NewGuid() で新規に払い出す
static class TaskFactory
{
}
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 }
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;
}
void Assert(bool condition, string name)
{
if (!condition) throw new Exception($"FAIL: {name}");
}
void AssertThrows<TException>(Action action, string name) where TException : Exception
{
try
{
action();
throw new Exception($"FAIL: {name}(例外が発生しませんでした)");
}
catch (TException) { }
}
___
- 期待される出力:
全テスト成功
ヒント1を見る
static class TaskFactory { public static TaskItem Create(string title, Priority priority = Priority.Medium) => new TaskItem(Guid.NewGuid(), new TaskTitle(title), priority); }
ヒント2を見る
バリデーションを自分で書く必要はありません。new TaskTitle(title)が空文字や51文字以上なら自動的にArgumentExceptionを投げてくれます
まとめ
TaskFactory.Create(title, priority = Priority.Medium)はタイトルの検証をTaskTitleに任せ、Idの発行とTaskItemの組み立てだけを担当する- 引数の既定値(
priority = Priority.Medium)も「決め事を1か所に集める」Factoryの役目のひとつ - 呼び出し側は
TaskFactory.Create("牛乳を買う")と書くだけでよく、TaskTitleやGuid.NewGuid()を意識しなくてよい
次回: 生成に関わるもう一つの決め事――時刻とIDの発行を、テストで差し替えられる小さなヘルパーに切り出します。