導入
「締切を過ぎたタスクを目立たせてほしい」という要望が来たとします。実務でこの手の機能追加をするときも、進め方はこれまでと同じです。「期限切れかどうか」の判定はドメイン(TaskItem)の責務としてTDDで先にテストし、ViewModelやViewはその結果(true/false)を受け取って表示を変えるだけ、という責務の分け方を崩さないことが大切です。
図解
flowchart LR
D["dueDay(締切の通し番号)<br/>と clock.today()(現在)"] --> J["isOverdue(Clock)<br/>(TaskItemが持つ)"]
J --> V["View / ViewModel<br/>(結果を表示するだけ)"]
style J fill:#e1f5fe
説明
00-index.mdの拡張仕様どおり、TaskItemにlong dueDay(0は期限なし)とisOverdue(Clock clock)を足します。日付はjava.timeを使わず、Clock#today()が返す通し番号(1日を1とする単純な整数)で表します。
Clock clock = () -> 20L; // 「今日」は20日目
dueDayは既存のコンストラクタを変えず、dueDayを受け取る新しいコンストラクタを1つ追加する形で足します。既存のコンストラクタはthis(...)でこの新しいコンストラクタに委譲すれば、dueDay = 0(期限なし)として扱えますisOverdue(Clock clock)は「期限が設定されていて(dueDay != 0)、まだ完了しておらず、締切(dueDay)が現在(clock.today())より前」ならtrueを返しますClockを引数として受け取るのは、第9章で確認した「静的な非決定値への依存を避ける」ための設計です。テストでは() -> 20Lのように、好きな「今日」を差し込めます
やってみよう
まず何も書かずに「▶ 実行」を押し、isOverdueとdueDayが無くてコンパイルエラーになる(Red)ことを確認してから演習に進みましょう。
演習
Clock.javaは完成済みとしてそのまま使い、今回の担当はDomain.javaのTaskItemだけです。
public interface Clock {
long today(); // 「今日」を通し番号(1日=1)で返す
}
// --- 前の章までに作ったクラスのコピー ---
final class TaskTitle {
private final String value;
TaskTitle(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("タイトルは空にできません");
}
if (value.length() > 50) {
throw new IllegalArgumentException("タイトルは50文字以内です");
}
this.value = value;
}
String getValue() { return value; }
public String toString() { return value; }
}
enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
private final String label;
Priority(String label) { this.label = label; }
String getLabel() { return label; }
}
// TODO: dueDay(0は期限なし)を受け取るコンストラクタと、isOverdue(Clock) を実装してください。
// 既存の3引数コンストラクタは変更禁止(dueDay=0として、新しいコンストラクタに委譲してください)
class TaskItem {
private final String id;
private TaskTitle title;
private final Priority priority;
private boolean completed;
TaskItem(String id, TaskTitle title, Priority priority) {
this.id = id;
this.title = title;
this.priority = priority;
}
String getId() { return id; }
TaskTitle getTitle() { return title; }
Priority getPriority() { return priority; }
boolean isCompleted() { return completed; }
void complete() { completed = true; }
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskItemTest {
@Test
void kigenwoSugitaraOverdueNiNaru() {
Clock clock = () -> 20L;
TaskItem task = new TaskItem("1", new TaskTitle("レポート提出"), Priority.HIGH, 10L);
assertTrue(task.isOverdue(clock), "締切(10日目)を過ぎた20日目はoverdue");
}
@Test
void kigenMaeHaOverdueDenai() {
Clock clock = () -> 5L;
TaskItem task = new TaskItem("2", new TaskTitle("企画書"), Priority.MEDIUM, 10L);
assertFalse(task.isOverdue(clock), "締切(10日目)前の5日目はoverdueではない");
}
@Test
void kigenNashiHaOverdueNiNaranai() {
Clock clock = () -> 100L;
TaskItem task = new TaskItem("3", new TaskTitle("いつかやる"), Priority.LOW);
assertFalse(task.isOverdue(clock), "dueDayが0(期限なし)ならoverdueにならない");
}
@Test
void kanryouZumiHaOverdueNiNaranai() {
Clock clock = () -> 20L;
TaskItem task = new TaskItem("4", new TaskTitle("提出済み"), Priority.HIGH, 10L);
task.complete();
assertFalse(task.isOverdue(clock), "完了済みならoverdueにならない");
}
}
- 期待される結果: 4件のテストが成功
ヒント1を見る
TaskItem(String id, TaskTitle title, Priority priority) { this(id, title, priority, 0); }のように、既存のコンストラクタから新しいコンストラクタへthis(...)で委譲します
ヒント2を見る
boolean isOverdue(Clock clock) { return dueDay != 0 && !completed && dueDay < clock.today(); }
まとめ
- 新機能を追加するときも「判定ロジックはドメインでTDD」「ViewModel/Viewは結果を表示するだけ」という責務分担は崩さない
- 既存のコンストラクタは変更せず、
this(...)で新しいコンストラクタに委譲すれば、呼び出し側のコードを壊さずに項目を追加できる Clockを引数として渡す設計のおかげで、「今日」を差し替えるだけで境界値(締切当日・前日・翌日など)を何パターンでも画面なしにテストできる
次回: 「カテゴリ分け」機能を、同じ進め方でもう1つ追加します。