第48章:Facade ③:演習(注文処理の窓口クラス)🛒
ねらい 🎯✨
- “注文確定”みたいな一連の処理を、呼び出し側から見て超カンタンにする(入口を1つにする)🚪😊
- 内部のゴチャゴチャ(支払い、保存、レシート、通知…)をFacadeの中に押し込めて隠す🙈✨
- テストで「入口がラクになった」「差し替えやすい」を体感する🧪🌸
到達目標 ✅🌟
- 呼び出し側が
PlaceOrderAsync(...)みたいな 1メソッドで注文処理できるようにできる🧁 - Facadeの中は複数の部品(支払い/保存/通知など)に分けて、責務が薄い状態にできる🧩✨
- テストで、成功/失敗(支払い失敗など)を部品差し替えで再現できる🔁🧪
手順 🧭🛠️
1) まず「導入前」を作って、つらさを固定する 😵💫➡️🧪
呼び出し側が全部やると、こんな感じになりがち👇(手順・例外・順序・後始末が散らばる)
// 例:導入前(呼び出し側が全部知ってる)
var paymentResult = await paymentGateway.PayAsync(order, ct);
if (!paymentResult.Succeeded)
{
return CheckoutResult.Failed("payment failed");
}
await orderRepository.SaveAsync(order, ct);
var receiptPath = await receiptWriter.WriteAsync(order, ct);
await notifier.NotifyAsync(order.CustomerEmail, "注文ありがとう!", ct);
return CheckoutResult.Succeeded(order.Id, receiptPath);
ポイント:
- 呼び出し側が **「何を」「どの順番で」**やるか全部知ってる😇
- 失敗時の扱い(保存しない、通知しない…)があちこちに増える🌋
この“つらさ”を、次でFacadeにまとめて消すよ✨
2) 部品の“契約”を小さく決める(差し替え用)🧩🔌
Facadeの中で使う部品を、小さなinterfaceで分けるよ(オレオレ汎用フレームワーク化は禁止🙅♀️、でも「業務の部品」はOK🙆♀️)
public interface IPaymentGateway
{
Task<PaymentResult> PayAsync(Order order, CancellationToken ct);
}
public interface IOrderRepository
{
Task SaveAsync(Order order, CancellationToken ct);
}
public interface IReceiptWriter
{
Task<string> WriteAsync(Order order, CancellationToken ct);
}
public interface INotifier
{
Task NotifyAsync(string toEmail, string message, CancellationToken ct);
}
public sealed record PaymentResult(bool Succeeded, string? Error = null);
3) Facade(窓口)を作る🚪🛒

呼び出し側が知らなくていいことを、ぜんぶ中へ👇 入口はユースケース単位で1つが基本(ここでは「注文確定」)
using Microsoft.Extensions.Logging;
public sealed class OrderCheckoutFacade
{
private readonly IPaymentGateway _payment;
private readonly IOrderRepository _orders;
private readonly IReceiptWriter _receipts;
private readonly INotifier _notifier;
private readonly ILogger<OrderCheckoutFacade> _logger;
public OrderCheckoutFacade(
IPaymentGateway payment,
IOrderRepository orders,
IReceiptWriter receipts,
INotifier notifier,
ILogger<OrderCheckoutFacade> logger)
{
_payment = payment;
_orders = orders;
_receipts = receipts;
_notifier = notifier;
_logger = logger;
}
public async Task<CheckoutResult> PlaceOrderAsync(Order order, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(order);
_logger.LogInformation("PlaceOrder started. OrderId={OrderId}", order.Id);
// 1) 支払い
var pay = await _payment.PayAsync(order, ct);
if (!pay.Succeeded)
{
_logger.LogWarning("Payment failed. OrderId={OrderId} Error={Error}", order.Id, pay.Error);
return CheckoutResult.Failed("支払いに失敗しました🙇♀️");
}
// 2) 保存
await _orders.SaveAsync(order, ct);
// 3) レシート作成
var receiptPath = await _receipts.WriteAsync(order, ct);
// 4) 通知
await _notifier.NotifyAsync(order.CustomerEmail, "ご注文ありがとうございます🎉", ct);
_logger.LogInformation("PlaceOrder completed. OrderId={OrderId}", order.Id);
return CheckoutResult.Succeeded(order.Id, receiptPath);
}
}
public sealed record CheckoutResult(bool Succeeded, Guid? OrderId, string? ReceiptPath, string? Error)
{
public static CheckoutResult Succeeded(Guid orderId, string receiptPath)
=> new(true, orderId, receiptPath, null);
public static CheckoutResult Failed(string error)
=> new(false, null, null, error);
}
✅ これで呼び出し側は「入口」だけ見ればOKになるよ〜!😊✨
4) 呼び出し側は“入口だけ”になる(Facadeの勝ち)🏆✨
var result = await facade.PlaceOrderAsync(order, ct);
if (!result.Succeeded)
{
Console.WriteLine(result.Error);
return;
}
Console.WriteLine($"注文OK! OrderId={result.OrderId} Receipt={result.ReceiptPath}");
5) テストで「差し替えが効く」を確認する🧪🌸
ここが超大事!✨ Facadeは“入口”だから、テストも入口からだけ叩くのが気持ちいい💗
テスト用のフェイク(最小)
public sealed class FakePaymentGateway : IPaymentGateway
{
public bool Called { get; private set; }
public PaymentResult Result { get; set; } = new(true);
public Task<PaymentResult> PayAsync(Order order, CancellationToken ct)
{
Called = true;
return Task.FromResult(Result);
}
}
public sealed class InMemoryOrderRepository : IOrderRepository
{
public List<Order> Saved { get; } = new();
public Task SaveAsync(Order order, CancellationToken ct)
{
Saved.Add(order);
return Task.CompletedTask;
}
}
public sealed class InMemoryReceiptWriter : IReceiptWriter
{
public List<Order> Written { get; } = new();
public Task<string> WriteAsync(Order order, CancellationToken ct)
{
Written.Add(order);
return Task.FromResult($"memory://receipts/{order.Id}.txt");
}
}
public sealed class CapturingNotifier : INotifier
{
public List<(string To, string Message)> Sent { get; } = new();
public Task NotifyAsync(string toEmail, string message, CancellationToken ct)
{
Sent.Add((toEmail, message));
return Task.CompletedTask;
}
}
MSTest:成功ケース🎉
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Extensions.Logging.Abstractions;
[TestClass]
public sealed class OrderCheckoutFacadeTests
{
[TestMethod]
public async Task PlaceOrder_Success_CallsAllAndReturnsReceipt()
{
var pay = new FakePaymentGateway();
var repo = new InMemoryOrderRepository();
var receipts = new InMemoryReceiptWriter();
var notifier = new CapturingNotifier();
var logger = NullLogger<OrderCheckoutFacade>.Instance;
var facade = new OrderCheckoutFacade(pay, repo, receipts, notifier, logger);
var order = new Order(Money.Jpy(1200), "a@example.com", PaymentMethod.Card);
var result = await facade.PlaceOrderAsync(order, CancellationToken.None);
Assert.IsTrue(result.Succeeded);
Assert.AreEqual(order.Id, result.OrderId);
Assert.IsTrue(pay.Called);
Assert.AreEqual(1, repo.Saved.Count);
Assert.AreEqual(1, receipts.Written.Count);
Assert.AreEqual(1, notifier.Sent.Count);
}
}
MSTest:支払い失敗ケース💥(保存も通知もしない)
[TestMethod]
public async Task PlaceOrder_PaymentFails_DoesNotSaveOrNotify()
{
var pay = new FakePaymentGateway { Result = new PaymentResult(false, "declined") };
var repo = new InMemoryOrderRepository();
var receipts = new InMemoryReceiptWriter();
var notifier = new CapturingNotifier();
var logger = NullLogger<OrderCheckoutFacade>.Instance;
var facade = new OrderCheckoutFacade(pay, repo, receipts, notifier, logger);
var order = new Order(Money.Jpy(1200), "a@example.com", PaymentMethod.Card);
var result = await facade.PlaceOrderAsync(order, CancellationToken.None);
Assert.IsFalse(result.Succeeded);
Assert.AreEqual(0, repo.Saved.Count);
Assert.AreEqual(0, receipts.Written.Count);
Assert.AreEqual(0, notifier.Sent.Count);
}
6) Copilot/Codexに頼むときの“良い頼み方”🤖✍️
(そのまま貼って使える系)
- 「
OrderCheckoutFacadeのMSTestを書いて。成功/支払い失敗の2ケース。Moqは禁止、フェイククラスはテスト内に最小で」 - 「Facadeのpublic APIは
PlaceOrderAsync(Order, CancellationToken)だけ。内部の部品を外に漏らさない構成にして」 - 「例外は増やしすぎず、失敗は
CheckoutResultで返す」
落とし穴 ⚠️😇
-
FacadeがGod Object化:何でも詰め込みすぎて、巨大クラスになる📦💥
- 対策:入口は“ユースケース単位”にする(PlaceOrder/Cancel/Refund…を無理に1個にしない)🧁
-
契約があいまい:失敗時に何が起きるか分からない(保存される?通知される?)🤔💦
- 対策:失敗パターンをテストで固定🧪
-
内部の例外をそのまま漏らす:呼び出し側が例外の種類を覚え始める😵
- 対策:Facadeの戻り値(Result)か、例外を“少数の種類”にまとめる
-
アップグレードで挙動が変わる系:言語バージョン更新でオーバーロード解決が変わることもあるよ(例:C# 14のSpan変換周り)⚙️🌀 (Microsoft Learn)
演習 📝💪(10〜30分)
- “入口がラク”を増やす🧁
CancelOrderAsync(orderId)をFacadeに追加してみよう- ただし、内部の部品(Repository/Notifierなど)をむやみに増やさず、最小で✨
- レシートを“本物のファイル”にする📄🪟
IReceiptWriterの実装をFileReceiptWriterにしてPath.Combine(Environment.GetFolderPath(...), ...)で保存先を決めよう- テストは
InMemoryReceiptWriterのままでOK(差し替えできるから!)🔁
- 呼び出し側のifを減らす✂️
- Facade導入前のコードを見て、ifが増えやすい場所にコメントしてみてね
- 「Facadeに入れるべき責務」と「呼び出し側に残すべき責務」を分ける👀✨
チェック ✅🧡
- 呼び出し側が **“入口1つ”**だけ見れば注文処理できる?🚪
- Facadeの中の部品は、**小さい契約(interface)**で差し替えできる?🔌
- 「支払い失敗」のときに、保存/レシート/通知が走らないのをテストで保証できた?🧪
- Facadeが巨大化しそうな兆候が出たら、ユースケース単位に分割する判断ができる?🧁
※ ちなみに、2026年初頭に“LTS優先”で進めるなら .NET 10 を軸にするのが自然だよ(サポート方針的に)✨ (Microsoft Learn)