本文へスキップ
BecomeCoder

C# MVVMコース · 第9章 テストをやり込む ― TDDの総仕上げ · レッスン34

仕様変更にTDDで対応する ― 並び替え機能を追加する

ブラウザで完結

導入

「タスク一覧を優先度の高い順に並べたい」――リリース後によくある仕様追加です。C#デザインパターンコースのTDD実践で見た通り、既存のテストを壊さずに新しい仕様をテストとして先に書き、それを満たす実装を追加します。今回はTaskServiceITaskRepositoryには一切手を触れず、画面向けの並び替えだけを新しいクラスとして追加します。

説明

flowchart LR
    DTO["TaskDtoの一覧<br/>(TaskMapperが変換した後)"] --> SORT["TaskSorter.ByPriorityDescending"]
    SORT --> OUT["優先度High→Medium→Lowの順に<br/>並んだ一覧"]
    style SORT fill:#e1f5fe
  • 新しい仕様は「表示の並び順」だけの話で、Entity・Repository・Serviceのどの層にも属さない。だからこそ既存クラスを一切変えずに、新しいクラスTaskSorterを追加するだけで実現できる
  • 「どこにも属さない小さな仕様」は無理にTaskServiceへ詰め込まず、専用の小さなクラスに切り出してよい(第8章のヘルパー設計と同じ考え方)
  • 進め方はいつも通りTDD。まず「優先度High→Medium→Lowの順に並ぶ」というテストを先に書く(Red。まだTaskSorterが無いので失敗する)。それを通す実装を書く(Green)

サンプル

並び替えの基本パーツ、OrderByと「順位表」の組み合わせをウォームアップとして確認します。

var fruits = new[] { "grape", "apple", "banana" };
var order = new Dictionary<string, int> { ["apple"] = 0, ["banana"] = 1, ["grape"] = 2 };

var sorted = fruits.OrderBy(f => order[f]).ToList();
Assert(string.Join(",", sorted) == "apple,banana,grape", "指定した順序で並ぶ");
Console.WriteLine("全テスト成功");

void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }
  • OrderByに「順位を返す関数」を渡すと、その順位に従って要素を並び替えられる
  • 優先度のような「決まった選択肢の順序」は、文字列のアルファベット順に頼らず、順位表(Dictionaryswitch式)で明示的に決める

やってみよう

TaskDto.Priorityは文字列("Low"/"Medium"/"High")です。アルファベット順に並べるとHigh, Low, Mediumという意図しない順序になってしまうことを確認してから、演習に進みましょう。

演習

var tasks = new List<TaskDto>
{
    new(Guid.NewGuid(), "牛乳を買う", "Low", false),
    new(Guid.NewGuid(), "レポート提出", "High", false),
    new(Guid.NewGuid(), "掃除する", "Medium", false),
};

var sorted = TaskSorter.ByPriorityDescending(tasks);
Assert(sorted[0].Title == "レポート提出", "Highが先頭");
Assert(sorted[1].Title == "掃除する", "次にMedium");
Assert(sorted[2].Title == "牛乳を買う", "Lowが最後");
Console.WriteLine("全テスト成功");

public record TaskDto(Guid Id, string Title, string Priority, bool IsCompleted);

// TODO: Priority文字列("Low"/"Medium"/"High")を高い順に並べ替える
//       ByPriorityDescending を実装してください
public static class TaskSorter
{
    ___
}

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

private static int Rank(string priority) => priority switch { "High" => 2, "Medium" => 1, "Low" => 0, _ => -1 };

ヒント2を見る

public static List<TaskDto> ByPriorityDescending(IReadOnlyList<TaskDto> tasks) => tasks.OrderByDescending(t => Rank(t.Priority)).ToList();

まとめ

  • 新しい仕様がどの層に属するかを見極め、無理に既存クラスへ詰め込まない(今回TaskServiceITaskRepositoryは無傷のまま)
  • 文字列を意味のある順序で並べたいときは、アルファベット順に頼らず順位表(switchDictionary)で明示的に決める
  • 既存のTaskServiceRepositoryのテスト(第4〜5章)は1つも壊れていない――これが「安全な仕様追加」の証拠

次回: このTaskSorterを、テストに守られながらリファクタリングします。

実際に動かしてみよう

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

C# — ブラウザ内で実行

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