本文へスキップ
BecomeCoder

C# MVVMコース · 第3章 Factory ― 生成をひとまとめに · レッスン10

TaskFactory ― バリデーション付き生成をTDDで実装する

ブラウザで完結

導入

前レッスンで確認した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("牛乳を買う")と書くだけでよく、TaskTitleGuid.NewGuid()を意識しなくてよい

次回: 生成に関わるもう一つの決め事――時刻とIDの発行を、テストで差し替えられる小さなヘルパーに切り出します。

実際に動かしてみよう

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

C# — ブラウザ内で実行

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