導入
前章でTaskItemはnew 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を見る
バリデーション(ifでthrow)を先に書き、そのあとでnewします。順番を逆にすると不正な値でもTicketができてしまいます
まとめ
- Factoryは「
newの呼び出し」と「生成時の決め事(検証・ID付与・トリムなど)」を1か所に集約するパターン - 呼び出し側はコンストラクタの詳細を知らなくても、正しい状態のオブジェクトを作れる
- 次のレッスンでは、実際に
TaskFactoryをTaskTitleの検証込みでTDD実装する
次回: 実際にTaskFactory.CreateをTDDで実装します。