導入
前レッスンで確認したFactoryの考え方を、実際にTaskFactoryとして実装します。ポイントは「TaskFactory自身は検証ロジックを書かない」ことです。タイトルの検証はすでにTaskTitleのコンストラクタが持っているので、TaskFactoryはそれを呼び出して組み立てるだけに徹します。IDの発行は、いったんクラス内部の連番でシンプルに済ませます(この連番方式の弱点は、次のレッスンで解消します)。
図解
flowchart LR
C["factory.create(title, priority = MEDIUM)"] --> V["new TaskTitle(title)<br/>(検証。ダメなら例外)"]
V --> N["new TaskItem(連番のid, title, priority)"]
N --> R["検証済みのTaskItemを返す"]
サンプル
Factoryが無いと、呼び出し側ごとに「優先度を省略したときはMEDIUM」というデフォルトを書く羽目になります。
// Factoryが無い場合、呼び出し側が毎回デフォルト値を意識する
TaskItem withoutFactory1 = new TaskItem("1", new TaskTitle("洗濯"), Priority.MEDIUM);
TaskItem withoutFactory2 = new TaskItem("2", new TaskTitle("掃除"), Priority.MEDIUM);
// Factoryがあれば、デフォルト値も1か所(オーバーロード)に集約できる
// TaskItem withFactory = factory.create("洗濯"); // 省略時はPriority.MEDIUM
Javaには引数の既定値という文法はありませんが、create(String title)からcreate(String title, Priority priority)を呼ぶというオーバーロード(同名で引数の数が違うメソッド)で同じことができます。priority = Priority.MEDIUMのような「省略時どうするか」という決め事も、Factoryのメソッドに集約しておけば、呼び出し側は必要なときだけ指定すればよくなります。
やってみよう
TaskFactory.javaのタブに、前レッスンで完成したTaskTitle・Priority・TaskItemをそのまま持ち込んだTaskItem.javaがあります。まずは何も書かずに実行し、テストが赤くなることを確認してから実装しましょう。
演習
TaskFactoryをTDDで実装します。TaskTitle・Priority・TaskItemは前章で作った完成形をそのままTaskItem.javaとして使い、TaskFactoryの実装はTaskFactory.java、テストはTaskFactoryTest.javaという別ファイルに分けて書きます。
public final class TaskTitle {
private final String value;
public TaskTitle(String value) {
if (value == null || value.isBlank()) throw new IllegalArgumentException("タイトルは空にできません");
if (value.length() > 50) throw new IllegalArgumentException("タイトルは50文字以内です");
this.value = value.trim();
}
public String getValue() { return value; }
@Override
public boolean equals(Object o) {
if (!(o instanceof TaskTitle)) return false;
return value.equals(((TaskTitle) o).value);
}
@Override
public int hashCode() { return value.hashCode(); }
@Override
public String toString() { return value; }
}
public enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
private final String label;
Priority(String label) { this.label = label; }
public String getLabel() { return label; }
}
public class TaskItem {
private final String id;
private TaskTitle title;
private Priority priority;
private boolean completed;
public TaskItem(String id, TaskTitle title, Priority priority) {
this.id = id;
this.title = title;
this.priority = priority;
}
public String getId() { return id; }
public TaskTitle getTitle() { return title; }
public Priority getPriority() { return priority; }
public boolean isCompleted() { return completed; }
public void complete() { completed = true; }
public void rename(TaskTitle newTitle) { title = newTitle; }
}
// TODO: 次を満たす TaskFactory を実装してください
// ・create(String title, Priority priority) は
// new TaskTitle(title) でタイトルを検証してから TaskItem を返す
// (検証ロジック自体はここに書かない。TaskTitleに任せる)
// IDは "T-1", "T-2", ... のように連番で発行してください。static なカウンタを1つ持てば十分です
// ・create(String title) は create(title, Priority.MEDIUM) を呼ぶ(既定値)
public class TaskFactory {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskFactoryTest {
@Test
void shoryakuJi_HaPriorityMedium() {
TaskFactory factory = new TaskFactory();
TaskItem t1 = factory.create("牛乳を買う");
assertEquals("牛乳を買う", t1.getTitle().getValue(), "titleを保持している");
assertEquals(Priority.MEDIUM, t1.getPriority(), "省略時はMEDIUM(既定値)");
assertFalse(t1.isCompleted(), "作成直後は未完了");
}
@Test
void yusenidoWoShiteiDekiru() {
TaskFactory factory = new TaskFactory();
TaskItem t2 = factory.create("卵を買う", Priority.HIGH);
assertEquals(Priority.HIGH, t2.getPriority(), "優先度を指定できる");
}
@Test
void idHaMaikaiKotonaru() {
TaskFactory factory = new TaskFactory();
TaskItem t1 = factory.create("牛乳を買う");
TaskItem t2 = factory.create("卵を買う");
assertNotEquals(t2.getId(), t1.getId(), "IDは呼び出すたびに異なる");
}
@Test
void karaBunjiHaTsukurenai() {
TaskFactory factory = new TaskFactory();
assertThrows(IllegalArgumentException.class, () -> factory.create(""), "空のタイトルは作れない(TaskTitleの検証がそのまま効く)");
}
}
- 期待される結果: 4件のテストがすべて成功
ヒント1を見る
TaskFactory.javaに実装します。public class TaskFactory { private static int nextSeq = 1; public TaskItem create(String title, Priority priority) { TaskTitle validated = new TaskTitle(title); return new TaskItem("T-" + (nextSeq++), validated, priority); } public TaskItem create(String title) { return create(title, Priority.MEDIUM); } }
ヒント2を見る
バリデーション(new TaskTitle(title))を先に済ませてからnew TaskItem(...)します。順番を逆にすると不正な値でもTaskItemができてしまいます
まとめ
TaskFactory#create(title, priority)はタイトルの検証をTaskTitleに任せ、IDの発行とTaskItemの組み立てだけを担当するcreate(String title)からcreate(String title, Priority priority)を呼ぶオーバーロードで、「省略時はMEDIUM」という決め事を1か所に集約した- ただし、この
staticカウンタでのID発行には弱点がある。テストが「何番目に呼ばれたか」というグローバルな状態に依存してしまう
次回: このID発行の弱点を、IdGeneratorという小さな抽象に切り出して解消します。