本文へスキップ
BecomeCoder

C# MVVMコース · 第8章 周辺の道具立て ― DI・ヘルパー・アンチパターン · レッスン32

ヘルパークラスの設計指針 ― 静的 vs インスタンス

ブラウザで完結

導入

「新しいIDを発行する」「今の日時を取る」――こうした小さな共通処理をヘルパークラスに切り出すことはよくあります。ただし、これを静的(static)なメソッドにするか、インターフェースにしてインスタンスをDIで注入するかは、テストのしやすさに直結する設計判断です。

説明

flowchart LR
    subgraph static_["static ヘルパー"]
        S["GuidIdGenerator.NewId()"] -->|直接呼ぶ| X["呼ぶたびに違う値<br/>テストで固定できない"]
    end
    subgraph di["interface + DI"]
        I["IIdGenerator(抽象)"] -.-> Real["本番: GuidIdGenerator"]
        I -.-> Fake["テスト: SequentialIdGenerator<br/>(予測できる値を返す)"]
    end
    style Fake fill:#e8f5e9

判断の目安はシンプルです。

  • 純粋な計算(入力だけで結果が決まり、副作用がない): staticのままで十分テストできる。文字列整形や数値計算のようなヘルパーがこれにあたる
  • 呼ぶたびに結果が変わる/外部の状態に触れる(現在時刻・乱数・新規ID発行・ファイルI/O): staticのまま直接呼ぶと、テストで結果を固定できない。インターフェースに抽象化してコンストラクタで注入し、テストでは予測可能な値を返すFakeに差し替える

具体例で確認しましょう。Guid.NewGuid()のような「毎回違う値」を返す処理そのものはテストしにくいものの、それをIIdGeneratorという抽象の後ろに隠すことで、それを使う側のクラスはテストしやすくなります。

interface IIdGenerator { string NewId(); }

// 本番用: 呼ぶたびに違う値になる(そのままでは戻り値をテストで固定できない)
class GuidIdGenerator : IIdGenerator
{
    public string NewId() => Guid.NewGuid().ToString();
}

class TicketCounter
{
    private readonly IIdGenerator _idGenerator;
    public TicketCounter(IIdGenerator idGenerator) => _idGenerator = idGenerator;   // 注入
    public string IssueTicket() => _idGenerator.NewId();
}
  • TicketCounter自体はIIdGeneratorという抽象にしか依存していない
  • テストではSequentialIdGenerator"id-1", "id-2", … のように予測できる値を返すFake)を注入すれば、発行されたIDをそのままAssertできる

やってみよう

SequentialIdGeneratorを実装し、TicketCounterが発行するIDを実際に予測できることを確認してみましょう。

演習

var fakeIds = new SequentialIdGenerator();
var counter = new TicketCounter(fakeIds);

Assert(counter.IssueTicket() == "id-1", "1件目はid-1");
Assert(counter.IssueTicket() == "id-2", "2件目はid-2");
Assert(counter.Issued.Count == 2, "発行履歴が2件残る");
Console.WriteLine("全テスト成功");

interface IIdGenerator { string NewId(); }

// 本番用: 呼ぶたびに違う値になるので、そのままではテストで固定できない
class GuidIdGenerator : IIdGenerator
{
    public string NewId() => Guid.NewGuid().ToString();
}

// TODO: 呼ばれた回数から "id-1", "id-2", ... の順に返す
//       SequentialIdGenerator を実装してください
class SequentialIdGenerator : IIdGenerator
{
    ___
}

class TicketCounter
{
    private readonly IIdGenerator _idGenerator;
    private readonly List<string> _issued = new();
    public TicketCounter(IIdGenerator idGenerator) => _idGenerator = idGenerator;
    public string IssueTicket()
    {
        var id = _idGenerator.NewId();
        _issued.Add(id);
        return id;
    }
    public IReadOnlyList<string> Issued => _issued;
}

void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }
  • 期待される出力: 全テスト成功
ヒント1を見る

class SequentialIdGenerator : IIdGenerator { private int _count = 0; public string NewId() => $"id-{++_count}"; }

ヒント2を見る

フィールドでカウンタを1件ずつ増やしながら"id-" + カウンタを文字列補間で返します

まとめ

  • 副作用のない純粋なヘルパーはstaticのままで十分テストできる
  • 現在時刻・乱数・新規ID発行のような「呼ぶたびに変わる」ヘルパーはインターフェースに抽象化し、コンストラクタで注入する
  • 本番GuidIdGenerator、テストはSequentialIdGeneratorのような予測可能なFakeに差し替えられる
  • TaskFactory.Createが内部でGuid.NewGuid()を直接呼んでいるのは、Idの実際の値をテストで検証する必要がない(重複さえしなければよい)からこそ許される設計判断

次回: MVVMでやってしまいがちな失敗、アンチパターンです。

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。