本文へスキップ
BecomeCoder

Java MVVMコース · 第3章 Factory ― 生成をひとまとめに · レッスン10

TaskFactory ― バリデーション付き生成をTDDで実装する

ブラウザで完結

導入

前レッスンで確認した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のタブに、前レッスンで完成したTaskTitlePriorityTaskItemをそのまま持ち込んだTaskItem.javaがあります。まずは何も書かずに実行し、テストが赤くなることを確認してから実装しましょう。

演習

TaskFactoryをTDDで実装します。TaskTitlePriorityTaskItemは前章で作った完成形をそのまま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という小さな抽象に切り出して解消します。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .java ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめて解釈して動かせます。@Test の付いたテストがあるレッスンでは、テストメソッドごとに ✅/❌ の一覧(Red/Green)が出るので、まずテストを赤くしてから実装で緑にする、というTDDの回し方をその場で体験できます(本物のJVMではなく、JUnit・Mockitoの主要な書き方まで再現した学習用シミュレータです)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。