導入
「タスク一覧を優先度の高い順に並べたい」――リリース後によくある仕様追加です。C#デザインパターンコースのTDD実践で見た通り、既存のテストを壊さずに新しい仕様をテストとして先に書き、それを満たす実装を追加します。今回はTaskServiceやITaskRepositoryには一切手を触れず、画面向けの並び替えだけを新しいクラスとして追加します。
説明
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に「順位を返す関数」を渡すと、その順位に従って要素を並び替えられる- 優先度のような「決まった選択肢の順序」は、文字列のアルファベット順に頼らず、順位表(
Dictionaryやswitch式)で明示的に決める
やってみよう
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();
まとめ
- 新しい仕様がどの層に属するかを見極め、無理に既存クラスへ詰め込まない(今回
TaskService・ITaskRepositoryは無傷のまま) - 文字列を意味のある順序で並べたいときは、アルファベット順に頼らず順位表(
switchやDictionary)で明示的に決める - 既存の
TaskService・Repositoryのテスト(第4〜5章)は1つも壊れていない――これが「安全な仕様追加」の証拠
次回: このTaskSorterを、テストに守られながらリファクタリングします。