第12章:VOの作り方パターン集(Factory/Parse/TryCreate)🏭🧩
この章はね、ひとことで言うと👇 **「入力が string でも、ドメインの中は“ずっと安全”にする作法」**を身につける回だよ〜!😆🛡️✨
ちなみに本日時点だと、C# 14 が最新で .NET 10 対応だよ📌(Visual Studio 側も .NET 10 を含む前提で進めてOK) (Microsoft Learn)
12.0 この章でできるようになること 🎯✨
できるようになったら勝ち!🏆
- DTOや画面入力みたいな string地獄😵💫 から、ちゃんと VOに変換できる
- Create / TryCreate / Parse / TryParse を “場面で” 使い分けられる
- 「入口で検証 → 中は安心😌」の流れをコードで作れる
- 失敗したときに、エラーを集めて返すところまでできる(超ミニ版)🧺✨
12.1 なんで「コンストラクタ直呼び」じゃダメなの?🤔💥
DTOってだいたいこう👇
- Email:
"komi@example.com"✅/❌ - Quantity:
"3"✅/❌ - Money:
"1200"とか"1,200"とか"123"とか 🤯
もし new Email(dto.Email) とかできちゃうと…
- どこでも無効なEmailを作れてしまう 😱
- 「いつ壊れた?」が追えない 🔍💦
- バグが “静かに” ドメインの奥へ侵入する 🥷
だから基本方針はこれ👇
VOは「作れた時点で正しい」💎✅ そのために、作り方(入口)を揃える🏭✨
12.2 入口4兄弟:Create / TryCreate / Parse / TryParse 👨👩👧👦✨

超ざっくり表にすると👇
| 名前 | 失敗したら | 主な用途 | 気持ち |
|---|---|---|---|
Create | 例外投げる💥 | 「ここで失敗したらバグ」な場所 | 強気😎 |
TryCreate | false+エラー返す🧾 | 画面入力・DTO・外部データ | 優しい🥺 |
Parse | 例外投げる💥 | .NETっぽいAPIに寄せたい/設定値など | きっちり📏 |
TryParse | false | パース文化(数値/日時)と同じノリにしたい | 安定🙂 |
ポイントはこれ👇 “ユーザー入力” は失敗が普通だから、Try系が主役になりやすいよ🫶✨
12.3 パターン①:Factory(Create/TryCreate)🏭✨
✅ ルール
- コンストラクタは private にして、勝手に作れないようにする🔒
TryCreateが本体(検証担当)🧪Createは “強制版” としてTryCreateを呼ぶ(失敗したら例外)💥
例:Email VO(最小でOK版)📧✨
using System.Text.RegularExpressions;
public sealed record Email
{
private static readonly Regex Simple =
new(@"^[^@\s]+@[^@\s]+\.[^@\s]+$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public string Value { get; }
private Email(string value) => Value = value;
// 強制版:ここで失敗するなら「プログラムのバグ」扱いにしたいとき
public static Email Create(string value)
=> TryCreate(value, out var email, out var error)
? email!
: throw new ArgumentException(error, nameof(value));
// 優しい版:入力が怪しい(DTO/画面/外部)なら基本こっち
public static bool TryCreate(string? value, out Email? email, out string error)
{
email = null;
if (string.IsNullOrWhiteSpace(value))
{
error = "Emailが空だよ〜🥺";
return false;
}
value = value.Trim();
if (!Simple.IsMatch(value))
{
error = "Emailの形が変かも…📧💦";
return false;
}
email = new Email(value);
error = "";
return true;
}
}
💡コツ
- **TryCreateの中は「短く・読みやすく」**が正義✨
- Emailの厳密なRFC地獄はやらない(学習・運用の落とし所🙂)
12.4 パターン②:Parse/TryParse(.NETの文化に寄せる)🧠📏
.NET には Parse / TryParse 文化があるよね(int.Parse とか)🔢✨
それに寄せると、使う人が迷いにくい👍
例:Quantity VO(TryParseっぽく)📦✨
public sealed record Quantity
{
public int Value { get; }
private Quantity(int value) => Value = value;
public static Quantity Parse(string s)
=> TryParse(s, out var q, out var error)
? q!
: throw new FormatException(error);
public static bool TryParse(string? s, out Quantity? quantity, out string error)
{
quantity = null;
if (string.IsNullOrWhiteSpace(s))
{
error = "数量が空だよ〜🥺";
return false;
}
if (!int.TryParse(s.Trim(), out var n))
{
error = "数量は数字でお願い🙏🔢";
return false;
}
if (n < 1)
{
error = "数量は1以上だよ〜📦✨";
return false;
}
quantity = new Quantity(n);
error = "";
return true;
}
}
12.5 どれを使う?迷ったときの判断🧭✨
- DTO/画面入力/API:TryCreate / TryParse(失敗が普通)🙂
- アプリ内部の計算結果:Create(失敗したらバグとして潰したい)😎
- 設定ファイル・自分たちが管理する固定値:Parse(壊れてたら起動時に落として気づきたい)💥
12.6 おまけ:IParsable<TSelf> って何?(ちょい先取り)🎁✨
.NET には **「型がParseできるよ」**を表す IParsable<TSelf> があるよ📌 (Microsoft Learn)
(数値型などがこれに対応して、汎用的にパースできる世界が広がったやつ!)
でもこの章では、まずは 自前の Parse/TryParse を揃えるだけで十分だよ〜🙆♀️✨ (必要になったら次のステップで取り入れよう!)
12.7 ミニ演習:DTO(string)→ VO化して安全にする 🧾➡️💎✨

🎯 お題:注文作成リクエストをVOに変換しよう☕️🧾
DTOはこんな感じ(外から来る前提)👇
public sealed class CreateOrderRequestDto
{
public string? CustomerEmail { get; init; }
public string? ItemCode { get; init; }
public string? Quantity { get; init; }
}
ItemCodeもVOにしちゃおう(簡易版)👇
using System.Text.RegularExpressions;
public sealed record ItemCode
{
private static readonly Regex Simple =
new(@"^[A-Z0-9\-]{3,20}$", RegexOptions.Compiled);
public string Value { get; }
private ItemCode(string value) => Value = value;
public static bool TryCreate(string? value, out ItemCode? code, out string error)
{
code = null;
if (string.IsNullOrWhiteSpace(value))
{
error = "商品コードが空だよ〜🥺";
return false;
}
value = value.Trim().ToUpperInvariant();
if (!Simple.IsMatch(value))
{
error = "商品コードの形式が変かも…🧾💦";
return false;
}
code = new ItemCode(value);
error = "";
return true;
}
}
✅ 変換結果を受け取る“中間モデル”(コマンドっぽいの)🧠✨
public sealed record CreateOrderCommand(
Email CustomerEmail,
ItemCode ItemCode,
Quantity Quantity
);
✅ エラーを集めるためのミニResult(超軽量)🧺✨
public sealed record ValidationError(string Field, string Message);
public sealed record Result<T>(bool IsSuccess, T? Value, IReadOnlyList<ValidationError> Errors)
{
public static Result<T> Ok(T value) => new(true, value, Array.Empty<ValidationError>());
public static Result<T> Fail(List<ValidationError> errors) => new(false, default, errors);
}
✅ DTO→Command 変換(ここが本題!)🔥✨
public static class CreateOrderMapper
{
public static Result<CreateOrderCommand> TryMap(CreateOrderRequestDto dto)
{
var errors = new List<ValidationError>();
if (!Email.TryCreate(dto.CustomerEmail, out var email, out var e1))
errors.Add(new ValidationError(nameof(dto.CustomerEmail), e1));
if (!ItemCode.TryCreate(dto.ItemCode, out var itemCode, out var e2))
errors.Add(new ValidationError(nameof(dto.ItemCode), e2));
if (!Quantity.TryParse(dto.Quantity, out var quantity, out var e3))
errors.Add(new ValidationError(nameof(dto.Quantity), e3));
if (errors.Count > 0)
return Result<CreateOrderCommand>.Fail(errors);
// ここまで来たら「全部正しい」世界🌈✨
return Result<CreateOrderCommand>.Ok(
new CreateOrderCommand(email!, itemCode!, quantity!)
);
}
}
🧪 テスト(xUnitの雰囲気)🧪✨
using Xunit;
public sealed class CreateOrderMapperTests
{
[Fact]
public void TryMap_InvalidInput_ReturnsErrors()
{
var dto = new CreateOrderRequestDto
{
CustomerEmail = "invalid",
ItemCode = "??",
Quantity = "0"
};
var result = CreateOrderMapper.TryMap(dto);
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.Errors);
}
[Fact]
public void TryMap_ValidInput_ReturnsCommand()
{
var dto = new CreateOrderRequestDto
{
CustomerEmail = "komi@example.com",
ItemCode = "COF-001",
Quantity = "2"
};
var result = CreateOrderMapper.TryMap(dto);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
Assert.Equal("komi@example.com", result.Value!.CustomerEmail.Value);
}
}
12.8 AI活用(Copilot/Codex想定)🤖✨:この章で効く使い方
💡 生成してもらうと速いもの
- VOの雛形(Create/TryCreate/Parse/TryParse)🏭
- テストケース案(正常・異常・境界値)🧪
- エラーメッセージの言い回し案(ユーザー向け)🫶
🗣️ 例プロンプト(そのまま投げてOK)✨
- 「
QuantityVO をTryParse形式で。1以上制約。xUnitテストも」🧪 - 「DTO→Command でエラーをリストに集める例を、読みやすく」🧺
- 「“例外を投げる版”と“Try版”の使い分けをコメントで説明して」📝
✅ AIが出したコードのチェック観点(ミニ)🔍
- 入力の
Trim()忘れてない?✂️ Try系で例外投げてない?(投げるなら意図がある?)💥- 成功時だけVOを生成してる?(失敗時に中途半端に作ってない?)🧩
12.9 この章のまとめ(1分)⏱️✨
- **VOは「作れた時点で正しい」**が命💎✅
- だから入口を揃える: Create(強制) / TryCreate(優しい) / Parse / TryParse 🏭✨
- DTO→VO は境界の仕事🌉 入口でVO化できたら、ドメイン中は安心して走れる🏃♀️💨🌈
次の章(第13章)では、この章でチラ見せした **「エラーをどう設計する?」(例外?Result?どこで返す?)**を、もう少し “設計として” 整えていくよ〜!⚠️🧠✨