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

第15章:結合の種類② static依存(便利だけど代償)⚡💣

いまの C# 14 / .NET 10(LTS) でも、static は当然めちゃ便利です✨(でも“効きすぎ”ると後で泣く…!) ※.NET 10 は 2025/11/11 リリースの LTS だよ〜📌 (Microsoft)


この章のゴール🎯

  • static が「なぜテストと変更に弱いのか」を体感する🧪💦
  • static依存を 最小変更で弱める3段階 を覚える🪜✨
  • “直すべきstatic” と “そのままでOKなstatic” を見分ける👀✅

static が強いのは「世界に1個」だから🌍⚡

Static Trap

static はざっくり言うと…

  • どこからでも呼べる(便利)
  • 差し替えできない(つらい)
  • グローバル状態になりやすい(事故りやすい)💥

たとえば👇

こういう“外の世界”系は、テストで固定できないと テストが不安定(フレーク) になりやすいの🥲


「直すべきstatic」ランキング🏆💣(初心者はここから!)

優先度高い順にいくよ〜📌

  1. 時間DateTime.Now / UtcNow ⏰(日付またぎで落ちる〜) (Microsoft Learn)
  2. 乱数Random.Shared 🎲(毎回結果が変わる〜) (Microsoft Learn)
  3. ファイル/DB/HTTPFile.* / HttpClient(外部I/O)📁🌐 (Microsoft Learn)
  4. staticなキャッシュ/状態static List<> とか(テスト順で壊れる)🧨
  5. staticイベント:解除し忘れでメモリリーク系😱

「そのままでOKなstatic」もあるよ🙆‍♀️✨

全部が悪ではない!ここは安心してOK👇

  • 定数・不変値const / static readonly(不変)🧊
  • 純粋関数:入力→出力だけ(外部に触らない)🧼
  • 拡張メソッド:中で外部I/Oしないならだいたい平和🧩

ポイントはこれ👇

“外の世界(時間・I/O・乱数・環境)に触れるstatic” が危険⚠️


static依存を弱める「3段階」🪜✨(最小変更でいくよ!)

段階1:呼び出しを「1か所に集める」📦

いきなり全部直さない!まず 散らばったstatic呼び出し1クラス に閉じ込める。

段階2:その箱を「差し替え可能」にする🔌

箱に interface を付けて、使う側は interface だけを見る👀✨

段階3:テストでは「偽物」を渡す🧪

本番=本物 / テスト=偽物(固定時間・固定乱数・仮想ファイル)で安定💖


ハンズオン🛠️:static地獄を「最小変更で救う」😇✨

題材:レポート文字列を作る(時間・乱数・ファイルが絡むやつ)


1) まずは “変更が怖い” コード(static依存モリモリ)😱⚡

using System;
using System.IO;

public class ReportService
{
public string BuildReport(string templatePath)
{
var template = File.ReadAllText(templatePath); // static I/O 📁
var id = Random.Shared.Next(1000, 9999); // static 乱数 🎲
var now = DateTime.UtcNow; // static 時間 ⏰

return template
.Replace("{id}", id.ToString())
.Replace("{utc}", now.ToString("O"));
}
}

これ、実装はラクなんだけど…

  • テストで 時間も乱数も固定できない
  • ファイル読み込みが絡んで テストが重い・遅い・壊れやすい 🥲

2) 段階1:static呼び出しを「箱」に集める📦✨

まず “外の世界” を触る部分だけまとめるよ👇

using System;
using System.IO;

public class SystemStuff // まずは箱(まだ interface なし)
{
public virtual string ReadAllText(string path) => File.ReadAllText(path);
public virtual int NextId() => Random.Shared.Next(1000, 9999);
public virtual DateTime UtcNow() => DateTime.UtcNow;
}

そして ReportService は箱経由にする👇

public class ReportService
{
private readonly SystemStuff _sys;

public ReportService(SystemStuff sys)
{
_sys = sys;
}

public string BuildReport(string templatePath)
{
var template = _sys.ReadAllText(templatePath);
var id = _sys.NextId();
var now = _sys.UtcNow();

return template
.Replace("{id}", id.ToString())
.Replace("{utc}", now.ToString("O"));
}
}

✅これだけでも「差し替えの入口」に立てたよ〜!


3) 段階2:箱に interface を付けて “契約化”🔌✨

Static Wrapper

“本命”はこれ👇 (使う側は interface だけ見ればOKになる)

using System;

public interface ISystemStuff
{
string ReadAllText(string path);
int NextId();
DateTime UtcNow();
}

本番実装👇

using System;
using System.IO;

public class RealSystemStuff : ISystemStuff
{
public string ReadAllText(string path) => File.ReadAllText(path);
public int NextId() => Random.Shared.Next(1000, 9999);
public DateTime UtcNow() => DateTime.UtcNow;
}

ReportService 側👇

public class ReportService
{
private readonly ISystemStuff _sys;

public ReportService(ISystemStuff sys)
{
_sys = sys;
}

public string BuildReport(string templatePath)
{
var template = _sys.ReadAllText(templatePath);
var id = _sys.NextId();
var now = _sys.UtcNow();

return template
.Replace("{id}", id.ToString())
.Replace("{utc}", now.ToString("O"));
}
}

4) 段階3:テストで “偽物” を渡して固定する🧪💖

using System;
using System.Collections.Generic;

public class FakeSystemStuff : ISystemStuff
{
private readonly Dictionary<string, string> _files = new();
public DateTime FixedUtcNow { get; set; } = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public int FixedId { get; set; } = 1234;

public void AddFile(string path, string content) => _files[path] = content;

public string ReadAllText(string path) => _files[path];
public int NextId() => FixedId;
public DateTime UtcNow() => FixedUtcNow;
}

これで 時間も乱数もファイルも固定 できる〜🎉


さらに今どきの「時間」対策:TimeProvider を使う⏰✨(おすすめ)

.NET 8 から TimeProvider っていう “時間の抽象化” が標準で入ったよ! 時間依存コードをテストしやすくするための仕組み✨ (Microsoft Learn)

TimeProvider を注入する形(IClock作らなくてもOK)🔥

using System;

public class ReportService2
{
private readonly TimeProvider _time;

public ReportService2(TimeProvider time)
{
_time = time;
}

public string Stamp()
=> _time.GetUtcNow().ToString("O");
}

テストでは FakeTimeProvider が使えるよ🧪 (NuGet:Microsoft.Extensions.TimeProvider.Testing) (Microsoft Learn)


さらに今どきの「ファイル」対策:System.IO.Abstractions 📁✨(任意)

File.ReadAllText みたいな static I/O を、IFileSystem 経由にできるライブラリがあるよ。 “System.IO と同じAPI感”で、注入&テストしやすい設計になってるのが強い💪 (nuget.org)


よくあるつまずきポイント集🧷💦(先に潰す!)

  • interface は“外の世界”だけに貼る(時間/I/O/乱数/HTTP/環境)
  • ❌ 業務ロジック全部を interface まみれにしない(やりすぎ注意)🥺
  • ✅ “staticを消す”じゃなくて、“staticを近づけない” が勝ち🏆
  • ✅ 「テストで固定したい?」と自分に聞くと判断しやすいよ🧠💡

AIの使いどころ🤖✨(この章はプロンプト2個まで🎀)

  1. 🔍 static依存の洗い出し 「このC#コードの static依存(DateTime/File/Random/Environmentなど)を列挙して、テスト上のリスク順に並べて。最小変更の改善案も1つずつ添えて」

  2. 🪜 段階的リファクタ案 「static呼び出しを 段階1: 1か所に集約 → 段階2: interface化 → 段階3: Fake実装でテスト の順で直す手順を、差分が小さくなるように提案して」


まとめ🎉

  • static は便利だけど、差し替え不能=変更&テストに弱い⚡💦
  • 改善は ①集める→②interface→③偽物で固定 の3段階が最強🪜✨
  • “時間”は TimeProvider + FakeTimeProvider が今どきの正解に近い⏰🧪 (Microsoft Learn)
  • “ファイル”は System.IO.Abstractions みたいな道具もある📁✨ (nuget.org)

次は(もし続けるなら)「staticじゃないけど同じくらい危険なやつ」=Service Locator(隠れDI) とか、Singletonの罠も絡めて総まとめに繋げられるよ〜😈🔗