第28章:Prototype ③:演習(注文テンプレを複製)🛒🔁

ねらい 🎯✨
- 「いつも似たような注文を作る」場面で、テンプレ(原型)から複製してちょい編集するやり方を体験します🧬
newと初期化コードが長くなる問題を、record + with でスッキリさせます🧾✨- **コピーの落とし穴(浅いコピー)**を、テストでちゃんと踏みます⚠️🧪
到達目標 🏁🌸
この章が終わったら、次ができるようになります👇
- ✅ 「ギフト注文テンプレ」みたいな 注文テンプレ(Prototype) を用意できる
- ✅ テンプレから複製して、
CustomerIdや配送先だけ差し替えられる - ✅ **テンプレと複製後が“同じ参照を共有してない”**ことをテストで確認できる🧪
- ✅ 「どこまでコピーすべき?」の判断ができる(特にコレクション)🧠✨
手順 🧭🛠️
1) プロジェクト準備 📁✨
- コンソールアプリ(例:
Ch28.Prototype) - テストプロジェクト(例:
Ch28.Prototype.Tests/ MSTest) - テスト側から、コンソール側を参照追加🔗
ここから先は、小さなECの「注文の下書き(OrderDraft)」をテンプレ化します🛒💕
2) ドメインを “最小” で作る(record中心)🧾✨
ポイント:Prototypeは「原型がすでに“それっぽく完成してる”」のが大事です🙂 なので、まず テンプレになれる形 を作ります!
// Ch28.Prototype / Domain/Money.cs
public readonly record struct Money(decimal Amount, string Currency)
{
public static Money Jpy(decimal amount) => new(amount, "JPY");
public override string ToString() => $"{Amount:N0} {Currency}";
}
// Ch28.Prototype / Domain/OrderItem.cs
public sealed record OrderItem(string Sku, string Name, Money UnitPrice, int Quantity);
// Ch28.Prototype / Domain/OrderDraft.cs
using System.Linq;
public sealed record OrderDraft(
string TemplateKey, // どのテンプレか(gift / subscription など)
string CustomerId, // 実際の注文では必須(テンプレでは空でOK)
string ShippingName,
string ShippingAddress,
OrderItem[] Items, // ← この“配列/コレクション”が浅いコピー注意ポイント⚠️
string? Note = null
)
{
public Money Total =>
Money.Jpy(Items.Sum(i => i.UnitPrice.Amount * i.Quantity));
}
3) テンプレ(Prototype)を用意する 🧬🛒
テンプレは「静的に置く」のが一番わかりやすいです🙂✨
// Ch28.Prototype / Templates/OrderTemplates.cs
public static class OrderTemplates
{
public static readonly OrderDraft Gift = new(
TemplateKey: "gift",
CustomerId: "",
ShippingName: "(テンプレ)",
ShippingAddress: "(テンプレ)",
Items: new[]
{
new OrderItem("SKU-CHOC", "チョコ", Money.Jpy(600), 2),
new OrderItem("SKU-CARD", "メッセージカード", Money.Jpy(0), 1),
},
Note: "ギフト包装🎁"
);
public static readonly OrderDraft Subscription = new(
TemplateKey: "subscription",
CustomerId: "",
ShippingName: "(テンプレ)",
ShippingAddress: "(テンプレ)",
Items: new[]
{
new OrderItem("SKU-COFFEE", "コーヒー豆", Money.Jpy(1200), 1),
},
Note: "毎月お届け☕"
);
public static OrderDraft GetByKey(string key) => key switch
{
"gift" => Gift,
"subscription" => Subscription,
_ => throw new ArgumentOutOfRangeException(nameof(key), key, "未知のテンプレだよ🥺")
};
}
4) テンプレから複製して “ちょい編集” する(with)🔁✨
Prototypeの本体です! ここで大事なのは コレクションをコピーして参照共有を避ける こと⚠️
// Ch28.Prototype / Factories/OrderDraftFactory.cs
using System.Linq;
public static class OrderDraftFactory
{
public static OrderDraft CreateFromTemplate(
string templateKey,
string customerId,
string shippingName,
string shippingAddress,
string? note = null)
{
var template = OrderTemplates.GetByKey(templateKey);
// ✅ 超重要:Items の参照を共有しない(浅いコピー事故の予防)⚠️
var itemsCopy = template.Items.ToArray();
return template with
{
CustomerId = customerId,
ShippingName = shippingName,
ShippingAddress = shippingAddress,
Items = itemsCopy,
Note = note ?? template.Note
};
}
}
5) テストで「共有してない」を確認する 🧪✅
ここが“設計の安心ポイント”です💖 「テンプレを壊してない?」をテストで守ります!
// Ch28.Prototype.Tests / OrderPrototypeTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class OrderPrototypeTests
{
[TestMethod]
public void CreateFromTemplate_CopiesItemsArray_AndKeepsTemplateSafe()
{
var order = OrderDraftFactory.CreateFromTemplate(
templateKey: "gift",
customerId: "C001",
shippingName: "山田はなこ",
shippingAddress: "東京都〇〇区△△ 1-2-3"
);
// 値が入ってる?
Assert.AreEqual("C001", order.CustomerId);
Assert.AreEqual("gift", order.TemplateKey);
// 中身は同じ?
CollectionAssert.AreEqual(OrderTemplates.Gift.Items, order.Items);
// ✅ でも配列参照は同じじゃない(共有してない)!
Assert.AreNotSame(OrderTemplates.Gift.Items, order.Items);
}
[TestMethod]
public void MutatingCloneItems_DoesNotAffectTemplate()
{
var order = OrderDraftFactory.CreateFromTemplate(
"gift", "C002", "佐藤りん", "神奈川県〇〇市△△ 9-9-9"
);
// 配列の中身を書き換え(※わざとやる!)
order.Items[0] = order.Items[0] with { Quantity = 999 };
// テンプレは無傷?
Assert.AreEqual(2, OrderTemplates.Gift.Items[0].Quantity);
}
}
よくある落とし穴 🕳️⚠️
1) with は “深いコピー” じゃないよ! 😵
recordのwithは基本 浅いコピー です- つまり、
List<T>やT[]みたいな参照型を持ってると、中身を共有しがち💥
✅ 対策
- コレクションは
ToArray()/new List<T>(old)みたいに コピーして差し替える - もしくは、そもそも 不変(immutable)寄りの設計にする🧊✨(第29章で深掘りするよ!)
2) ICloneable は “深い?浅い?” が曖昧 🌀
ICloneable.Clone()は「深い/浅い」が契約で決まってなくて事故りやすいです⚠️- この教材では、record + with + 必要箇所だけコピー を基本にします🧾✨
3) MemberwiseClone は強いけど、扱いは慎重に 🧨
MemberwiseCloneも浅いコピー- protected だし、参照共有の事故りポイントが増えやすいので、まずは今回の形でOK🙆♀️
演習 🎮🛒✨
演習A:テンプレを1つ追加しよう ➕🎁
-
OrderTemplatesに「誕生日テンプレ🎂」を追加- 例:ケーキ + キャンドル
Noteに「お誕生日おめでとう🎉」を入れる
-
テスト:
GetByKey("birthday")が取れること✅
演習B:テンプレから “追記” してもテンプレを壊さない 🧪⚠️
「複製した注文」にだけ、追加の明細を入れてみよう!
ヒント:配列を増やすなら Concat → ToArray() が楽だよ🙂✨
// 例:複製後に “紙袋” を追加するイメージ(テンプレはそのまま)
var order = OrderDraftFactory.CreateFromTemplate(...);
order = order with
{
Items = order.Items
.Concat(new[] { new OrderItem("SKU-BAG", "紙袋", Money.Jpy(30), 1) })
.ToArray()
};
✅ テストで確認すること
order.Items.Lengthは増えてるOrderTemplates.Gift.Items.Lengthは増えてない(テンプレ無傷)🛡️
演習C:テンプレ選択のUIっぽい入口を作る(最小でOK)🧩🙂
templateKeyを受け取ってCreateFromTemplateを呼ぶだけのメソッドを作る- switch/if が増えそうなら「テンプレ追加だけで済む状態」になってるか確認👀✨
自己チェック ✅🧠✨
- 「Prototypeは 原型から複製して微調整」って一言で言える?🧬
-
withが 浅いコピー だと説明できる?⚠️ - コレクションをコピーして差し替える理由が言える?🛡️
- テストで「テンプレが壊れてない」ことを守れてる?🧪💖
-
newだらけの初期化より、読みやすくなった?✨📘