メインコンテンツまでスキップ

第5章:C#でやりがちな“未来用設計”を安全に先送りする 🧯🧠✨

この章では、「C#だとつい“それっぽい設計”を先に作っちゃう問題」を、安全に・気持ちよく・あとで困らない形で先送りする練習をします😊💕

(いまの最新環境だと、.NET 10 は LTS(長期サポート)で、C# 14 も同時期に出ています📦✨)(Microsoft Learn) (Visual Studio 2026 も一般提供が始まっていて、AI支援を前提にした流れが強いです🤖💡)(Microsoft for Developers)


この章のゴール🎯✨

終わるころには、次ができるようになります😊

  • 「interface まだ要らないかも」を理由付きで言える🗣️🧠
  • DIコンテナを入れずに、手動注入で気持ちよく進める📦➡️🧩
  • パターンやジェネリクスの“先走り”を止めて、必要になった瞬間にだけ入れられる✂️✨
  • AI(Copilot/Codex系)に盛られた提案を、上手に削る🧯🤖

まず知っておく:C#の「未来用設計」あるある😅🎭

C#って、良い道具がいっぱいあるぶん、こうなりがち👇

  • Interface先行:「とりあえず IService 作っとこ!」🪓
  • DIコンテナ先行:「最初から AddScoped して Host 作っとこ!」📦
  • パターン先行:「Strategy/Factory/Repository入れとこ!」🎭
  • ジェネリクス汎用化先行:「何でも IRepository<T>!」🧬

これ、悪ではないんだけど… “困ってないのに入れる”と、理解コストと変更コストが先に爆発します💥😭

YAGNI的には、ここで合言葉:

痛みが出てから、最小の道具を足すただし、あとで足せるように“手すり”だけ付ける🧤✨


“先送り”を安全にする「4つの手すり」🧤🧱🧪🗺️

先送りって、雑にやると事故るので、最低限これだけ付けます😊

① 依存の組み立て場所を1か所に寄せる(Composition Root)🧩

new が散らばると、後からDIコンテナに移行しづらいです💦 なので最初から「組み立てはここ!」を作ると強い💪✨

② 境界は“フォルダ”でいい(最初は)📁

Simple Manual Injection

DDDが初めてでもOK🙆‍♀️ 最初はこれで十分です👇

  • Domain/(ルール・値・エンティティ)
  • UseCases/(アプリの操作)
  • Infrastructure/(DB/外部API/ファイル)

③ テストは“1〜2本”でいい(守りの要)🧪✨

先送りするなら、変更しても壊れてないを確認できる最低限が大事!

④ 変更が起きそうな場所は“引数”で渡せる形にする🎁

interface にしなくても、デリゲート設定オブジェクトで逃げ道は作れます😺


interface はいつ切る?🪓(結論:差し替えの痛みが出てからでOK)

Interfaces are for solving pain

✅ interface を“今”切るサイン(どれか当てはまったら)👀🚨

  • 実装が 2つ以上、すでに存在してる(予定じゃなくて現物)✌️
  • テストで差し替えたいのに、差し替えが辛すぎて毎回つらい😭
  • 外部(API/DB/メール等)との境界で、失敗パターンが多くテストが地獄🔥

✅ まだ切らなくていいサイン(先送りしてOK)🌿

  • 実装が1つしかない(「将来増えるかも」は未来の自分に任せよ🕊️)
  • そもそも差し替える要件がまだ無い
  • “それっぽくしたい”以外の理由が説明できない😅

interface を作らずにテスト可能にする(デリゲート注入)🧪🎁

「メール送信」とかは、interface 先に作りがちですが… 最初はデリゲートで十分なこと、めちゃ多いです😊✨

// UseCases/NotifyUser.cs
public sealed class NotifyUser
{
private readonly Func<string, string, Task> _sendEmail;

public NotifyUser(Func<string, string, Task> sendEmail)
=> _sendEmail = sendEmail;

public Task ExecuteAsync(string email, string message)
=> _sendEmail(email, message);
}

テストではこう👇(“送ったこと”だけ確認)

using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

public class NotifyUserTests
{
[Fact]
public async Task ExecuteAsync_sends_email()
{
var sent = new List<(string Email, string Message)>();

Task FakeSend(string e, string m)
{
sent.Add((e, m));
return Task.CompletedTask;
}

var useCase = new NotifyUser(FakeSend);

await useCase.ExecuteAsync("a@example.com", "hi!");

Assert.Single(sent);
Assert.Equal(("a@example.com", "hi!"), sent[0]);
}
}

🎀ポイント

  • interface無しで差し替えできる
  • 依存の形がシンプル(学習コスト小)
  • 本当に必要になったら、後で IEmailSender に昇格できる✨

DIコンテナはいつ入れる?📦(結論:手動注入で詰み始めたら)

.NET には標準のDIがあって便利だけど、最初から入れると構成が理解しづらいことも多いです😵‍💫 (IDEもどんどんAI前提になってるので、なおさら“見通し”が大事!)(Microsoft for Developers)

✅ まずは手動注入(超おすすめ)🧩✨

// Program.cs(例:コンソールでもWebでも“組み立てだけ”は同じ考え)
var sendEmail = (Func<string, string, Task>)(async (to, msg) =>
{
// ここは仮実装でOK(ログ出すだけでもOK)
Console.WriteLine($"[Email] to={to} msg={msg}");
await Task.CompletedTask;
});

var notifyUser = new NotifyUser(sendEmail);

await notifyUser.ExecuteAsync("a@example.com", "hello!");

✅ DIコンテナ導入の“痛みサイン”👀🚨

  • new が増えすぎて、組み立てが読めない📛
  • スコープ(Webのリクエスト単位等)やライフサイクル管理が必要になった🧯
  • 構成が複雑で、手動注入が“手作業地獄”になった🫠

このタイミングで入れると、「助けになる」導入になります😊✨


パターンはいつ使う?🎭(結論:困りごとから入る)

ありがちな失敗例😅

「Strategyっぽくしたい!」で最初から分けると、クラスが増えて迷子になります🐣💦

✅ まずは if / switch でOKな場面🍰

  • 種類が 1〜2個
  • 追加の頻度が不明
  • まだ仕様が揺れてる
public decimal CalcShippingFee(string prefecture, decimal price)
{
if (prefecture == "Tokyo") return 0;
if (price >= 5000) return 0;
return 500;
}

✅ Strategy に上げるサイン🚀

  • 条件分岐が 複数箇所にコピペされ始めた
  • 種類が増えて、ifが長くなって読みづらい
  • 追加が頻繁で、毎回既存コードをいじって事故る💥

そのとき初めて、最小のStrategyへ🎭✨


ジェネリクス汎用化の目安🧬(結論:重複が“痛い”まで待つ)

❌ 早すぎる例(ありがち)😇

IRepository<T> を先に作って、結局 T ごとに例外処理が増えて破綻…💣

✅ 先にやるべき順番(おすすめ)🌿

  1. まずは 具体的な UserRepository を作る
  2. “同じ構造の重複”が3回くらい出てから
  3. 初めて「共通化できる形」を検討する

これだと、「共通化の形」が現実に合うので成功しやすいです😊✨


ミニ演習📝:「先に作り込み版」をYAGNI的に削る✂️✨

お題🎯

「ユーザー登録 → ウェルカム通知」だけやりたいのに、最初から盛り盛りになってるコードを削ります😺

作り込み版(わざと過剰😅)

public interface IEmailSender { Task SendAsync(string to, string body); }

public interface INotifier<T>
{
Task NotifyAsync(T target, string message);
}

public sealed class EmailNotifier : INotifier<User>
{
private readonly IEmailSender _sender;
public EmailNotifier(IEmailSender sender) => _sender = sender;

public Task NotifyAsync(User target, string message)
=> _sender.SendAsync(target.Email, message);
}

public sealed class RegisterUserUseCase
{
private readonly INotifier<User> _notifier;
public RegisterUserUseCase(INotifier<User> notifier) => _notifier = notifier;

public async Task ExecuteAsync(User user)
{
// 登録処理(省略)
await _notifier.NotifyAsync(user, "Welcome!");
}
}

public sealed record User(string Email);

やること(チェックリスト)✅✅✅

  • INotifier<T> のジェネリクスをやめる(今は User しかない)🧬✂️
  • interface を消してもテストできる形にする(デリゲート注入)🧪
  • クラス数を減らして、読みやすくする📉✨

目標の形(例)🌟

「これだけ」でOKにする👇

  • RegisterUserUseCase
  • 依存は Func<string, string, Task>(送信関数)
  • テストは1本でOK

AI活用🤖:AIに“逆質問”させて判断材料を出す 🧠🧯

AIって放っておくと盛りがちなので、指示がコツです😆🎈

① 盛らせない指示テンプレ🧾

  • いまの要件だけで。拡張性の仕組みは入れないで」
  • 「クラス増やさないで。増えるなら理由を先に質問して」
  • 「interface/DIコンテナ/デザインパターンは禁止。必要になった“痛み”を列挙して」

② 逆質問テンプレ(おすすめ!)🕵️‍♀️

  • 「この interface を導入する“今の痛み”は何?(将来じゃなく今)」
  • 「差し替えが必要な実装は、現時点で何個ある?」
  • 「それを入れないとテストが成立しない?成立するなら最小案は?」

③ レビュー用テンプレ(過剰設計検出)🔍

  • 「このPR、YAGNI違反っぽいところを3つ挙げて。削る案も出して」
  • 「クラス数を減らすリファクタ案を提示して(振る舞いは維持)」

(Visual Studio 側でも Copilot が統合されて、検索や支援が強化されてるので、こういう“制約つき指示”が効きます🤖✨)(The GitHub Blog)


成果物📦:C#版YAGNI判断ルール集(この章のまとめ)🧠✨

最後に、あなた用のルールとしてこのままコピペOKです🫶💕

  • interface は「実装が2個以上」「テスト差し替えが地獄」になったら切る🪓
  • それまでは デリゲート注入 で逃げ道を作る🎁
  • DIコンテナは「組み立てが読めない」「手動注入が苦痛」になったら入れる📦
  • パターンは「分岐が散らかる/コピペが増える」まで我慢🎭
  • ジェネリクス共通化は「同型の重複が3回くらい」出てから🧬
  • 先送りする代わりに、組み立て場所1か所 + テスト1〜2本は付ける🧤🧪

次の章(第6章)では、この判断を“手順”として回せるように、YAGNI開発フローに落としていきます🚶‍♀️✨