メインコンテンツまでスキップ

第10章 不正な値を作らせない:Factoryで生成を集中🏭🔒

まず今日のゴール🎯✨

この章が終わったら、こんなことができるようになります😊

  • 「new された瞬間に壊れてる」みたいな値を 作れない ようにできる🧱🔒
  • 「生成(作る)」の入口を 1か所に集約 して、ルールが散らばらないようにできる🏭✨
  • 生成に失敗したとき、例外じゃなく Result(成功/失敗) で安全に返せる📦🙂

そして次の第11〜13章(Value Object連続)へ、めちゃスムーズに繋がります📈💎


1. なんで「new」が危ないの?😱🧨

「文字列のメールアドレス」を例にすると…

  • 空文字の Email
  • @ が無い Email
  • 前後に空白がある Email(気づかずDBに入る)
  • でたらめだけど “たまたま通っちゃう” Email

こういうのって、作れた時点で負け なんです🥲💥 一度システムに入ると、あちこちで if チェックが増えて「if地獄」へ…(第9章のやつ!)😵‍💫🌀

なので発想を逆転します👇 ✅ 「不正な値を作れない」ようにする これが Factory(生成の集中)です🏭🔒


2. Factoryの基本形:private コンストラクタ + Create🏗️🛡️

Factory Line

やることはシンプル✨

  • コンストラクタを private にする(外から new できない)
  • 静的メソッド(Create / TryCreate / From など)だけが作れる
  • Create の中で検証&正規化(trim など)して、OKなら生成✅

3. 2026年の前提:C# 14 + .NET 10(LTS)を使うよ🧁✨

いまの「最新の土台」はこんな感じです👇(2026-01-20時点)

  • C# 14 は 2025年11月リリース とされ、公式の履歴にも載っています📚✨ (Microsoft Learn)
  • .NET 10 は LTS で、配布ページでは 10.0.2(2026-01-13) が “latest” として並んでいます🧩⬆️ (Microsoft)
  • さらに .NET Blog の 2026年1月の更新でも .NET 10.0.2 が明記されています🛠️🗞️ (Microsoft for Developers)

(※Factory自体は言語バージョンに左右されにくいけど、最新環境で書くと気持ちいいのでこれでいきます😊)


4. まずは「Result型」を用意しよう📦🙂

第7章でやった「失敗を戻り値で返す」を、ここでも使います💪✨ ここでは “ミニResult” を自作します(軽くて学びやすい)🎀

Resultの設計方針🧠✨

  • 成功:値(Value)が入ってる✅
  • 失敗:エラー一覧(Errors)が入ってる❌
  • 例外は「想定外」へ寄せる(第6章の話)⚡

5. 実装してみよう:Email.Create(string)🏭📧

5-1. DomainError と Result を作る🧱✨

using System;
using System.Collections.Generic;
using System.Linq;

public sealed record DomainError(string Code, string Message);

public sealed class Result<T>
{
private Result(bool isSuccess, T? value, IReadOnlyList<DomainError> errors)
{
IsSuccess = isSuccess;
Value = value;
Errors = errors;
}

public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;

public T Value { get; } // 成功時だけ使う想定
public IReadOnlyList<DomainError> Errors { get; }

public static Result<T> Ok(T value)
=> new(true, value, Array.Empty<DomainError>());

public static Result<T> Fail(params DomainError[] errors)
=> new(false, default, errors);

public static Result<T> Fail(IEnumerable<DomainError> errors)
=> new(false, default, errors.ToArray());
}

ポイント💡😊

  • 失敗時の Value は使わない前提(使うときは IsSuccess を見てね)
  • エラーは Code を持たせると、UI表示やログが整理しやすいです🏷️✨

5-2. Email を「new禁止」にする🔒🚫

using System;

public sealed class Email
{
private Email(string value)
{
Value = value;
}

public string Value { get; }

public static Result<Email> Create(string? raw)
{
// 1) null/空白チェック
if (string.IsNullOrWhiteSpace(raw))
{
return Result<Email>.Fail(
new DomainError("email.empty", "メールアドレスを入力してね🙂📧")
);
}

// 2) 正規化(まず trim)
var trimmed = raw.Trim();

// 3) 超ミニ構文チェック(正規表現でガチらない方針🙂)
// - '@' が1個
// - 先頭/末尾が '@' じゃない
var atIndex = trimmed.IndexOf('@');
if (atIndex <= 0 || atIndex != trimmed.LastIndexOf('@') || atIndex == trimmed.Length - 1)
{
return Result<Email>.Fail(
new DomainError("email.format", "メールアドレスの形式が変かも…😵‍💫 例:a@b.com")
);
}

// 4) ここまで来たら「Emailとして成立」🎉
return Result<Email>.Ok(new Email(trimmed));
}

public override string ToString() => Value;
}

ここが超大事🛡️✨ ✅ Email は private コンストラクタなので、外から new Email(" ") ができません つまり 不正な Email が“型として存在”できない ようにしてます🏰🔒


6. 使う側(境界側)はこうなる🚪🔁

たとえば「会員登録フォーム」から来た raw string を内部へ入れるとき👇

public static Result<string> RegisterMember(string? emailText)
{
var emailResult = Email.Create(emailText);
if (emailResult.IsFailure)
{
// ここはUI/API向けの表現に変換する場所(第8章の話)🎀
var message = string.Join("\n", emailResult.Errors.Select(e => e.Message));
return Result<string>.Fail(new DomainError("register.invalid", message));
}

Email email = emailResult.Value;

// 本来はDB保存など…(今は省略)
return Result<string>.Ok($"登録できたよ🎉 Email={email.Value}");
}

気持ちよさポイント😍✨

  • 変な email が入ってきても、境界で止めて 内部へ入れない🛡️
  • 内部のコードは Email 型を信じて進める(if地獄が減る)🎓

7. 命名どうする?Create / TryCreate / From 🏷️🤔

結論:迷ったらこれでOK🙆‍♀️✨

  • Create:失敗理由が欲しい(Resultで返す)📦
  • TryCreate:boolで軽く判定したい(outで値)🙂
  • From:すでに正しい値だと “強く” 信じる(内部専用にしがち)⚠️

このへんの “好み” はチームで統一が一番強いです🤝✨ (公式ドキュメント上でも、C#はAPI表現の幅が広いので「読みやすさ優先」が勝ちやすいです🙂)


8. よくある落とし穴🕳️🐾

落とし穴①:record struct にすると default が怖い😱

struct は必ず default が作れちゃいます(Email の default が “空” みたいな)💥 不変条件をガチで守りたい章の流れでは、まずは class(参照型)で作るのが安全です🛡️✨

落とし穴②:正規表現で頑張りすぎる🤯

メールの仕様は奥が深すぎて、初心者が完璧を目指すと沼りがちです🌀 この教材では「必要十分」でOK🙂✨(KISS精神)

落とし穴③:Createの外で勝手に正規化する

trim をあちこちでやると、ルールが散らばります😵‍💫 正規化はCreateに寄せる が勝ちです🏭✨


9. ミニ演習(手を動かすやつ)🧪🔥

演習A:Email.Create を強化しよう💪📧

次のルールを追加してみてね👇

  • 先頭と末尾の空白は削る(もうやった!)🧼
  • 連続した空白を弾く(例:a @b.com)🚫
  • エラーメッセージを “ユーザー向け” に調整する📣

演習B:Errorを複数返せるようにする🧾✨

今は「1個のエラーで返す」ことが多いけど、 「まとめて返したい」ケースもあります🙂 Result.Fail に複数の DomainError を渡して、UIに一覧表示してみよう✅

演習C:会員登録のコマンドを作る🎀🧱

  • RegisterMemberCommand(EmailText など)を作る
  • 境界で Email.Create を呼ぶ
  • 成功なら Email 型だけを内部に渡す

10. AI(Copilot / Codex)に頼ると強いところ🤖✨

“AIは案出し係+テスト量産係” が最強です💪🎉

そのまま投げてOKなプロンプト例🪄

  • 「Email.Create の境界値テストケースを 20 個、成功/失敗に分類して出して」🧪🤖
  • 「DomainError の Code 命名案を、短く一貫性ある形で 10 個出して」🏷️🤖
  • 「この Create を読みやすくリファクタして。初心者にも追える形で」🧹🤖
  • 「TryCreate(out Email) 版も追加して、使い分け方もコメントで」🧩🤖

11. まとめ:この章で手に入れた武器🛡️🎀

  • 不変条件は “あとで守る” じゃなくて 作る瞬間に守る 🏭🔒
  • private ctor + Create で 生成の入口が1つ になる✨
  • Resultで返すと、境界で 丁寧にエラー表示 できる🙂📣
  • 次章(MoneyなどのValue Object)で、この形がそのまま増殖していく💰💎

おまけ:自分チェックリスト✅✨

  • 型のコンストラクタは外から呼べない(new禁止)🔒
  • Create の中に「検証+正規化」がまとまってる🏭
  • 成功/失敗が Result で表現できてる📦
  • 境界で変換して、内部に raw string を持ち込んでない🚪🧼

次の第11章は、このFactoryの形をそのまま使って Money(通貨+金額) を作ります💰💎 「丸め」「負数OK?」「通貨違いの加算禁止」みたいな、型で守ると気持ちいいやつがいっぱいです🥳✨