本文へスキップ
BecomeCoder

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

なぜ生成をFactoryにまとめるか

ブラウザで完結

導入

前章でTaskItemnew TaskItem(Guid.NewGuid(), new TaskTitle(title), priority)という形で作れるようになりました。ですが、この呼び出しを「タスク追加画面」「サンプルデータの読み込み」「テストコード」など、あちこちに書き散らかすとどうなるでしょうか。あとで「優先度の既定値をMediumからLowに変えたい」となったとき、全ての呼び出し箇所を探して直すはめになります。Factoryは、この「生成のルール」を1か所に閉じ込めるパターンです。

図解

flowchart TB
    subgraph S["Factoryが無い場合"]
        A1["画面のコード<br/>new TaskItem(Guid.NewGuid(), new TaskTitle(t), Priority.Medium)"]
        A2["インポート機能のコード<br/>new TaskItem(Guid.NewGuid(), new TaskTitle(t), Priority.Medium)"]
        A3["テストのコード<br/>new TaskItem(Guid.NewGuid(), new TaskTitle(t), Priority.Medium)"]
    end
    subgraph T["Factoryがある場合"]
        F["TaskFactory.Create<br/>(検証・Id発行のルールを1か所に)"]
        B1["画面のコード<br/>TaskFactory.Create(t)"] --> F
        B2["インポート機能のコード<br/>TaskFactory.Create(t)"] --> F
        B3["テストのコード<br/>TaskFactory.Create(t)"] --> F
    end

生成のパターンそのもの(Factory Method / Abstract Factory)の詳しい解説はC#デザインパターンコースのレッスン1にあります。ここでは「タスク管理アプリのどこで使うと嬉しいか」に絞って確認します。

サンプル

var p1 = PersonFactory.Create("  田中  ", 30);
var p2 = PersonFactory.Create("鈴木", 25);

Assert(p1.Name == "田中", "前後の空白はトリムされる(決め事を1か所に)");
Assert(p1.Id != p2.Id, "IDの発行ルールも1か所にまとまっている");
Console.WriteLine("全テスト成功");

class Person
{
    public int Id { get; }
    public string Name { get; }
    public int Age { get; }
    public Person(int id, string name, int age) { Id = id; Name = name; Age = age; }
}

static class PersonFactory
{
    private static int _nextId = 1;
    public static Person Create(string name, int age)
    {
        if (age < 0) throw new ArgumentException("年齢は0以上です");
        return new Person(_nextId++, name.Trim(), age);
    }
}

void Assert(bool condition, string name)
{
    if (!condition) throw new Exception($"FAIL: {name}");
}
  • new Person(...)の呼び出しはPersonFactory.Createの中に1か所だけ
  • 「IDをどう払い出すか」「前後の空白をトリムする」「年齢は0以上」という決め事も1か所にまとまっている
  • 呼び出し側はPersonFactory.Create(name, age)と書くだけでよく、生成の詳細を知らなくてよい

演習

同じ考え方で、価格が0未満なら作れないTicket用のFactoryをTDDで実装します。

var t1 = TicketFactory.Create("SS席", 12000);
Assert(t1.Seat == "SS席", "座席名を保持している");
Assert(t1.Price == 12000, "価格を保持している");
AssertThrows<ArgumentException>(() => TicketFactory.Create("S席", -1), "価格が負なら作れない");
Console.WriteLine("全テスト成功");

class Ticket
{
    public string Seat { get; }
    public int Price { get; }
    public Ticket(string seat, int price) { Seat = seat; Price = price; }
}

// TODO: 価格(price)が0未満ならArgumentExceptionを投げ、
//       そうでなければTicketを作る TicketFactory.Create を実装してください
static class TicketFactory
{
}

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 TicketFactory { public static Ticket Create(string seat, int price) { if (price < 0) throw new ArgumentException("価格は0以上です"); return new Ticket(seat, price); } }

ヒント2を見る

バリデーション(ifthrow)を先に書き、そのあとでnewします。順番を逆にすると不正な値でもTicketができてしまいます

まとめ

  • Factoryは「newの呼び出し」と「生成時の決め事(検証・ID付与・トリムなど)」を1か所に集約するパターン
  • 呼び出し側はコンストラクタの詳細を知らなくても、正しい状態のオブジェクトを作れる
  • 次のレッスンでは、実際にTaskFactoryTaskTitleの検証込みでTDD実装する

次回: 実際にTaskFactory.CreateをTDDで実装します。

実際に動かしてみよう

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

C# — ブラウザ内で実行

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