Skip to main content

第31章:依存の差し替え②:コンストラクタ注入(DI入門)📦

(ねらい:new を減らしてテスト可能にする💪)


0) まずこの章のゴール🎯💕

画像を挿入予定

この章が終わったら、こんな状態になれます👇✨

  • ✅ 「テストで差し替えたい依存」を コンストラクタから受け取るようにできる
  • ✅ クラスの中にある new(依存の直生成)を減らせる
  • ✅ テストでは スタブ(Fake)を渡して安定・高速に回せる
  • ✅ まだ DIコンテナ無しでOK!手で渡せるようになる

1) “DI”って言葉にビビらないでOK😌✨

DI(Dependency Injection)は、超ざっくり言うと👇

「クラスが必要とするもの(依存)を、外から渡してあげる」こと📦

難しい理屈じゃなくて、感覚はこれだけです😊💕

  • ❌ 自分の中で new する(固定されて差し替え不可)
  • ✅ 外から受け取る(差し替え可能)

2) ありがちな “惜しい” 状態😵‍💫(interface 化したのに…)

前章(第30章)で IClock を作ったのに、こんな感じになりがち👇

public interface IClock
{
DateTimeOffset UtcNow { get; }
}

public sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}

public sealed class CouponApplier
{
private readonly IClock _clock = new SystemClock(); // ← ここ!!😵

public decimal Apply(decimal subtotal, Coupon coupon)
{
if (_clock.UtcNow > coupon.ExpiresAtUtc)
throw new CouponExpiredException();

return subtotal - coupon.DiscountAmount;
}
}

これ、IClock を使ってるのに 差し替えられないんです😭 (だって中で new SystemClock() して固定しちゃってるから…)


3) 解決:コンストラクタ注入(Constructor Injection)📦✨

やることは1つだけ👇

「依存は new しないで、コンストラクタでもらう」🎁

public sealed class CouponApplier
{
private readonly IClock _clock;

public CouponApplier(IClock clock)
{
_clock = clock;
}

public decimal Apply(decimal subtotal, Coupon coupon)
{
if (_clock.UtcNow > coupon.ExpiresAtUtc)
throw new CouponExpiredException();

return subtotal - coupon.DiscountAmount;
}
}

これでテストがめちゃ楽になります🧪💕


4) ハンズオン🧪☕:期限切れクーポンをテストで守る🎟️✨

仕様(今回のミニ仕様)📌

  • クーポンには ExpiresAtUtc(期限)がある
  • 期限を過ぎてたら CouponExpiredException を投げる
  • 期限内なら割引して返す

4-1) テスト用の FakeClock(スタブ)を用意🕰️🧸

「今の時刻」を自由にいじれる時計を作ります✨

public sealed class FakeClock : IClock
{
public DateTimeOffset UtcNow { get; set; }
}

4-2) xUnit テスト(期限切れは例外)🚫🧪

using Xunit;

public sealed class CouponApplierTests
{
[Fact]
public void Apply_期限切れクーポンなら例外()
{
// Arrange
var clock = new FakeClock
{
UtcNow = new DateTimeOffset(2026, 1, 18, 0, 0, 0, TimeSpan.Zero)
};
var sut = new CouponApplier(clock);

var coupon = new Coupon(
expiresAtUtc: new DateTimeOffset(2026, 1, 17, 23, 59, 59, TimeSpan.Zero),
discountAmount: 100m
);

// Act & Assert
Assert.Throws<CouponExpiredException>(() => sut.Apply(1000m, coupon));
}
}

ここが気持ちいいポイント😍 ✅ DateTimeOffset.UtcNow に依存しないから、テストが毎回安定する🎯


4-3) 期限内なら割引できる✅🧪

using Xunit;

public sealed class CouponApplierDiscountTests
{
[Fact]
public void Apply_期限内なら割引して返す()
{
// Arrange
var clock = new FakeClock
{
UtcNow = new DateTimeOffset(2026, 1, 18, 0, 0, 0, TimeSpan.Zero)
};
var sut = new CouponApplier(clock);

var coupon = new Coupon(
expiresAtUtc: new DateTimeOffset(2026, 1, 18, 0, 0, 1, TimeSpan.Zero),
discountAmount: 100m
);

// Act
var result = sut.Apply(1000m, coupon);

// Assert
Assert.Equal(900m, result);
}
}

5) 「じゃあ本番では誰が渡すの?」問題🤔➡️🙂

テストでは FakeClock を渡したけど、本番では SystemClock を渡します⏰✨ この「組み立て場所」を コンポジションルートって呼んだりします(覚えなくてOK😂)

例:Console アプリならこんな感じ👇

public static class Program
{
public static void Main()
{
IClock clock = new SystemClock();
var applier = new CouponApplier(clock);

// applier を使う
}
}

ポイントはこれ👇💕

  • ✅ “使う側” が「どれを渡すか」を決める
  • ✅ “作られる側” は「受け取って使うだけ」

6) よくある落とし穴(ここ超あるある!)😇🪤

6-1) コンストラクタで null を受け取って爆発💥

初心者あるあるなので、軽く守るなら👇

public CouponApplier(IClock clock)
{
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
}

6-2) 何でもかんでも注入しすぎる🌀

「差し替えたいもの」だけ注入でOK🙆‍♀️✨

  • ✅ 注入向き:時計、乱数、ID生成、外部API、DB、ファイル、メール送信、設定、ログ…
  • ❌ 注入いらない寄り:小さい値、単純な計算クラス、値オブジェクト(Moneyとか)

6-3) “new を消す”の目的が迷子になる😵

目的は テストで差し替えられるようにすること🧪✨ 「new が悪」じゃなくて、差し替えたい new がつらいって覚えるとちょうどいいです😊


7) AIの使いどころ(この章めちゃ相性いい🤖💖)

使えるプロンプト例🪄

  • 「このクラスの中にある new を探して、DI(コンストラクタ注入)に変える最小差分を提案して」
  • 「このテストが安定しない原因になりそうな依存(時刻・乱数・静的状態)を列挙して」
  • 「FakeClock みたいな最小スタブを作って」

⚠️ただし、採用条件はいつもこれ👇 テストが通る✅+意図に一致✅(AIの言う通りに丸呑みしないでね🙈)


8) ミニ演習🎓✨(手を動かす用)

次のどれか1つやればOK(全部やったら神💪👑)

  1. 🕰️ 「IClock を new してる場所」を全部探して、コンストラクタ注入に直す
  2. 🎲 乱数(Random)を使ってるロジックがあれば、IRandom 的に差し替え可能にする
  3. 🆔 ID採番(Guid.NewGuid)を IIdGenerator で差し替え可能にして、テストで固定IDにする

9) コミットの切り方(おすすめ)🧾✨

  • ✅ commit1: FakeClock を追加(テスト用)
  • ✅ commit2: CouponApplier をコンストラクタ注入に変更(new削除)
  • ✅ commit3: テストを追加(期限切れ/期限内)
  • ✅ commit4: リファクタ(命名・重複整理)

10) 最新メモ(本日時点)🆕✨

  • .NET 10 の最新リリースは **10.0.2(2026-01-13)**になっています。(Microsoft)
  • Visual Studio 2026 は 2026-01-13 に 18.2.0 の更新が出ています。(Microsoft Learn)
  • xUnit v3 は公式リリースノート上、3.2.2 が掲載されています。(xunit.net)

まとめ🎀✨

  • DI(コンストラクタ注入)は「外から渡すだけ」📦
  • “interface 化したのに new が残ってる” を直すのがこの章の主役😵➡️😊
  • テストでは Fake(スタブ)を渡して、安定&高速🧪⚡️
  • まだ DIコンテナは不要!まずは手で渡せれば勝ち🏆✨

次は第32章の「スタブとモックの気持ち(混乱しない説明)🎭」に行けます😊✨ この章の題材(CouponApplier)をそのまま使って、「スタブ=返すだけ」「モック=呼ばれ方も確認」を超わかりやすく繋げられるよ〜💖