Javaを鍛える|デザインパターン全23回 #5

Builder:複雑な検索条件オブジェクトの段階的構築

  • Java
  • デザインパターン
  • Builder
  • Javaを鍛える

📋 この記事の目次
  1. Builderは何のためにあるか
  2. Before:使わない項目にもnullを渡す必要がある
  3. After:Builderで必要な項目だけを指定する
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftのチケット検索機能には、statusassigneeseveritykeywordという4つの検索条件があります。これらをまとめて扱うTicketSearchConditionクラスは、こう作られています。

public class TicketSearchCondition {
    private final String status;
    private final String assignee;
    private final String severity;
    private final String keyword;

    public TicketSearchCondition(String status, String assignee, String severity, String keyword) {
        this.status = status;
        this.assignee = assignee;
        this.severity = severity;
        this.keyword = keyword;
    }

    public String describe() {
        return "status=" + status + ", assignee=" + assignee + ", severity=" + severity + ", keyword=" + keyword;
    }
}

今は4項目ですが、今後「期限」「タグ」なども追加したいという話が出ています。項目が増えるたびに、コンストラクタの引数もどんどん増えていきます。

Builderは何のためにあるか

Builderは、「多くの部品からなる複雑なオブジェクトを、必要な部分だけ順に指定しながら組み立てられるようにする」 パターンです。フルコースの注文で全品目を毎回指定するのではなく、「これは要らない」「これだけ追加で」と必要な分だけ伝えて注文を組み立てるイメージです。

Before:使わない項目にもnullを渡す必要がある

public class Main {
    public static void main(String[] args) {
        // severityを指定したいだけなのに、使わないassigneeとkeywordにもnullを渡す必要がある
        TicketSearchCondition condition = new TicketSearchCondition("open", null, "high", null);
        System.out.println(condition.describe());
    }
}

実行結果:

status=open, assignee=null, severity=high, keyword=null

statusseverityだけ指定したい」という単純な意図に対して、nullを2つも渡さなければならず、しかも引数の順番を間違えても気づきにくいという危険もあります(assigneeseverityの位置を取り違えても、コンパイルエラーにはなりません)。

After:Builderで必要な項目だけを指定する

public class TicketSearchCondition {
    private final String status;
    private final String assignee;
    private final String severity;
    private final String keyword;

    private TicketSearchCondition(Builder builder) {
        this.status = builder.status;
        this.assignee = builder.assignee;
        this.severity = builder.severity;
        this.keyword = builder.keyword;
    }

    public String describe() {
        return "status=" + status + ", assignee=" + assignee + ", severity=" + severity + ", keyword=" + keyword;
    }

    public static class Builder {
        private String status;
        private String assignee;
        private String severity;
        private String keyword;

        public Builder status(String status) { this.status = status; return this; }
        public Builder assignee(String assignee) { this.assignee = assignee; return this; }
        public Builder severity(String severity) { this.severity = severity; return this; }
        public Builder keyword(String keyword) { this.keyword = keyword; return this; }

        public TicketSearchCondition build() {
            return new TicketSearchCondition(this);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        TicketSearchCondition condition = new TicketSearchCondition.Builder()
            .status("open")
            .severity("high")
            .build();
        System.out.println(condition.describe());
    }
}

実行結果:

status=open, assignee=null, severity=high, keyword=null

結果はBeforeとまったく同じですが、呼び出し側のコードはstatus(...)severity(...)というそれぞれの項目名がそのまま読める形になり、指定していないassigneekeywordを気にする必要がなくなりました。将来「期限」を追加したくなっても、BuilderdueBefore(...)のようなメソッドを1つ足すだけで済み、既存の呼び出しコードは一切変更する必要がありません。

🧓 ベテランの現場コラム 「コンストラクタの引数が4つを超えてきたら、Builderを検討する」というのは実務でもよく言われる目安です。特に、項目の多くが「指定してもしなくてもよい(オプション)」性質を持つ場合、Builderの効果は顕著です。逆に、必須項目が2〜3個だけのシンプルなクラスにまでBuilderを導入すると、コード量が増えるだけで見合わないこともあります。

こんな場面で使う/使わない

  • 使う: オプションの項目が多く、組み合わせによって指定する項目が変わるオブジェクトを作る場合
  • 使わない: 必須項目が少数で、コンストラクタがすでに読みやすい場合。無理にBuilder化するとコード量が増えるだけになる

まとめ

  • Builderは「必要な部分だけを順に指定しながら組み立てる」パターン
  • 引数の多いコンストラクタや、引数の順序間違いのリスクを解消できる
  • オプション項目が増えても、呼び出し側のコードを変えずに拡張できる

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。