第28章:総合演習3 疎結合にして「完成」させる(DIP/DI/Test)🔌🧪🚀
ここまででクラス構造はかなり綺麗になってるはず!✨ でも、まだ 「new があちこちに散らばってる」 と、最後の一歩として 疎結合 が完成しないの😵💫 この章では DIPで依存の向きを直して、DIで組み立てを1か所に集めて、テストで“挙動が変わってない”を証明 してゴールしよう〜!🥳🎉
(C# 14 / .NET 10 / Visual Studio 2026 で進めてOKだよ〜) (Microsoft Learn)
1) この章のゴール🎯✨
最終ゴールはこれ👇💕
- ✅ “業務ロジック(上位)” が “DB/HTTP/外部API(下位)” に振り回されない(DIP) (Microsoft Learn)
- ✅ 依存の組み立てを Program.cs(Composition Root)に集約(DI) (Microsoft Learn)
- ✅ 外部I/Oを Fake/Mock で差し替え可能にして高速テスト(安心して改修できる)🧪 (Microsoft Learn)
- ✅ Before/After の挙動が同じだと テストで証明(これ超大事!)✅
2) 完成形イメージ(ざっくり設計図)🗺️✨

「どこに何を書くか」を最後に固定しちゃうよ💡
- Core(ドメイン):エンティティ / 値オブジェクト / ドメインサービス
- Application(ユースケース):注文作成・支払い・発送など、アプリのやりたいこと
- Infrastructure:DB、外部決済、メール送信、配送API…(現実の面倒なところ)
- App(Program.cs):全部を “組み立てるだけ” 🧱✨(ここが Composition Root)
この「組み立て1か所ルール」が、後々ずーーーっと効くよ😎✨ (Microsoft Learn)
3) Step0:まずは “守るべき挙動” をテストで固定しよ🧷🧪
リファクタの最終局面でありがちなのがこれ👇
「綺麗になったけど、実は挙動が変わってた😇」
だから先に、重要なユースケースだけでも テストで“杭(くい)”を打つよ🔨✨
例:ミニECならこのへん👇
- 注文できる(在庫OK)
- 割引が正しく入る
- 支払い成功ならステータスが Paid になる
- 支払い失敗なら Paid にならない
- 発送ラベルが作られる(配送先が入る)
テスティングは xUnit でいくのが定番で、xUnit v3 は .NET 8+ 対応だから .NET 10 でもOK👌 (xunit.net)
4) Step1:外部I/Oを “抽象(interface)” に逃がす(DIP)🏰🔁
4-1. 「外部っぽいもの」を洗い出す👀📝
まず、コードからこの匂いを探すよ😈
new SqlConnection(...)/DbContextHttpClient直叩きDateTime.Now(時間依存)Guid.NewGuid()(乱数依存)File.ReadAllText(ファイル依存)- 外部SDK直呼び
これらを “インターフェース越し” にするのがコツ✨ .NET の DI はこの形を前提に設計されてるよ💡 (Microsoft Learn)
4-2. Core or Application 側に「欲しい能力」を interface で置く📌
例(決済・注文保存・時計)👇
public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(Order order, CancellationToken ct);
}
public interface IOrderRepository
{
Task SaveAsync(Order order, CancellationToken ct);
Task<Order?> FindAsync(OrderId id, CancellationToken ct);
}
public interface IClock
{
DateTimeOffset Now { get; }
}
👉ポイント:“どう実現するか” は書かない 「欲しい能力」だけを書くのがDIPの気持ちだよ🫶✨ (Microsoft Learn)
5) Step2:new を消して “コンストラクタ注入” にする🎁✨
Application のユースケース(例:PlaceOrderUseCase)が、直接インフラを作ってたらアウト🙅♀️ こうする👇
public sealed class PlaceOrderUseCase
{
private readonly IPaymentGateway _payment;
private readonly IOrderRepository _orders;
private readonly IClock _clock;
public PlaceOrderUseCase(
IPaymentGateway payment,
IOrderRepository orders,
IClock clock)
{
_payment = payment;
_orders = orders;
_clock = clock;
}
public async Task<OrderId> ExecuteAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId, cmd.Items, _clock.Now);
var payment = await _payment.ChargeAsync(order, ct);
if (!payment.Success)
throw new InvalidOperationException("支払い失敗");
order.MarkPaid(payment.TransactionId);
await _orders.SaveAsync(order, ct);
return order.Id;
}
}
✅ これで「上位(ユースケース)」は「下位(決済/DB)」の都合から自由になる🥳 まさにDIP〜! (Microsoft Learn)
6) Step3:Program.cs に “組み立て” を集める(DI/Composition Root)🧱🧩
.NET の DI は標準機能として用意されてるよ📦✨ (Microsoft Learn) そして「どこで登録するか?」の答えが Program.cs(Composition Root)!
6-1. いちばん素直な登録例✅
(ConsoleでもWebでも考え方は同じだよ)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// --- インフラ ---
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddTransient<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
// --- アプリ(ユースケース)---
builder.Services.AddTransient<PlaceOrderUseCase>();
var host = builder.Build();
// 例:実行してみる
var useCase = host.Services.GetRequiredService<PlaceOrderUseCase>();
// await useCase.ExecuteAsync(...);
6-2. lifetime(Singleton/Scoped/Transient)の選び方💡
ここ、事故りやすいから要点だけ押さえよ!🧯
- Transient:軽いサービス、状態持たない、毎回作ってOK
- Scoped:1リクエスト(or 1処理単位)で同じインスタンスにしたい(DB系は多い)
- Singleton:アプリ全体で共有(重い生成コスト・グローバル共有・スレッドセーフが前提)
このへんの注意点は公式のガイドにもまとまってるよ✅ (Microsoft Learn)
7) Step4:テストで “Fake差し替え” をやる(ここが気持ちいい!)🧪✨
7-1. Fake を用意する(手書きでOK)✍️
public sealed class FakePaymentGateway : IPaymentGateway
{
public bool ShouldSucceed { get; set; } = true;
public Task<PaymentResult> ChargeAsync(Order order, CancellationToken ct)
=> Task.FromResult(ShouldSucceed
? PaymentResult.Success("tx_123")
: PaymentResult.Failure("NG"));
}
public sealed class FakeClock : IClock
{
public DateTimeOffset Now { get; set; } = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
}
7-2. テスト側だけ “別のDI構成” にする🎛️
using Microsoft.Extensions.DependencyInjection;
using Xunit;
public class PlaceOrderUseCaseTests
{
[Fact]
public async Task 支払い成功ならPaidになる()
{
var services = new ServiceCollection();
// Fake差し替え
var fakePayment = new FakePaymentGateway { ShouldSucceed = true };
var fakeClock = new FakeClock();
services.AddSingleton<IPaymentGateway>(fakePayment);
services.AddSingleton<IClock>(fakeClock);
services.AddSingleton<IOrderRepository, InMemoryOrderRepository>();
services.AddTransient<PlaceOrderUseCase>();
using var sp = services.BuildServiceProvider();
var sut = sp.GetRequiredService<PlaceOrderUseCase>();
// Act
var id = await sut.ExecuteAsync(new PlaceOrderCommand(/*...*/),
CancellationToken.None);
// Assert(例)
var repo = sp.GetRequiredService<IOrderRepository>();
var saved = await repo.FindAsync(id, CancellationToken.None);
Assert.NotNull(saved);
Assert.True(saved!.IsPaid);
}
}
これができると、外部APIが落ちてようがDBがなくても、秒でテスト回せる🥹✨ .NET のDIは「こういう差し替え」を超やりやすくするためにある感じだよ💞 (Microsoft Learn)
8) Step5:“Before/After の挙動が同じ” を証明する✅🧪
最後にやるのはこれ!
- 大事なユースケースのテストが全部通る ✅
- 「例外ケース」のテストも通る ✅
- (できれば)割引計算や料金計算みたいな純粋ロジックは、ユニットテストが分厚い🧁✨
ここまで揃うと、もう 変更が怖くない状態 に到達だよ🥳🎉
9) つまずきやすいポイント集(先に潰そう)🧯💥
❌ 1) Singleton が Scoped を抱えちゃった
「Singleton → Scoped」を直接注入すると破綻しやすいよ😵💫 スコープ検証なども含めて、公式ガイドの注意があるので要チェック✅ (Microsoft Learn)
❌ 2) interface を作りすぎて迷子
「外部I/O」「差し替えたいもの」からでOK! “全部インターフェース” はやりすぎになりがち😅
❌ 3) DateTime.Now でテストが不安定
時計は IClock に逃がすと世界が平和になる🕊️✨
10) AI(Copilot/Codex系)に投げると爆速になるお願い文🤖✨
コピペして使ってOKだよ〜!💕
- 「このクラスが触っている外部依存(DB/HTTP/時間/乱数/ファイル)を列挙して、interface化の案を出して」🤖📝
- 「new を消してコンストラクタ注入に直したい。最小差分でリファクタ案を出して」🧹✨
- 「このユースケースの重要なテスト観点を10個。正常系/異常系を半々で」🧪✅
- 「Fake実装を手書きで用意したい。stateを保持して検証できる InMemoryRepository を作って」📦✨
11) 仕上げのミニ課題(クリアできたら卒業🎓🎀)
- 🌟課題A:決済を「成功/失敗/タイムアウト」で差し替えテスト
- 🌟課題B:配送ラベル作成を Fake にして、住所のバリデーションもテスト
- 🌟課題C:DI登録を
AddMiniEc()みたいな拡張メソッドにまとめて、Program.cs をスッキリ✨(ASP.NET Core でもよく使う型だよ) (Microsoft Learn)
まとめ🎉✨
第28章で完成したのはこれ👇💕
- DIPで「依存の向き」を直した 🏰🔁 (Microsoft Learn)
- DIで「組み立て」を Program.cs に集めた 🧱✨ (Microsoft Learn)
- テストで「挙動が同じ」を証明した 🧪✅
- 結果:変更が怖くないSOLIDアプリ ができた〜!!!🥳🎉
次、もし良ければ🙏✨ あなたが第27章までで作ったサンプルの「主要クラス(Order系、Payment系、Repository系)」のコード断片を貼ってくれたら、その形に合わせて Chapter28 の“具体的なDI登録&テスト差し替え”をガチで組み立てた完成例を作るよ🤖💞