Java : 簡易的な Builder パターン

Builder(ビルダー) パターンは、GoF によって定義されたデザインパターンの1つです。
本記事では GoF のパターンではなく、Java 標準API で使われているもう少し簡易的なパターンをご紹介します。


概要

Builder パターン(ビルダー・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする。

Wikipedia では、GoF による Builder パターンが紹介されています。

クラス図(GoFによるBuilderパターン)

GoF によるパターンでは

  • オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする

とあります。

GoF のパターンも有用だと思いますが、本記事では割愛します。
興味のある方は、上記の Wikipedia のページなどをご参照ください。

さて、Java の標準API には、名前に Builder とつくクラスやインタフェースがいくつかあります。

  • DateTimeFormatterBuilder
  • Stream.Builder
  • HttpRequest.Builder
  • などなど

本記事では、これらの Java 標準API でも使われているもう少し簡易的なパターンをご紹介します。

便宜上、本記事ではこれらのパターンを

  • 簡易的な Builder パターン

と表記します。

簡易的な Builder パターン

なぜ必要か?

簡易的な Builder パターンの目的は、

  1. オブジェクト生成 が複雑な(分かりにくい)クラスに対して
  2. オブジェクト生成 を補助するクラス(Builder)を作り
  3. オブジェクト生成 を簡単に(分かりやすく) しよう

です。

オブジェクト生成が複雑なクラスとは、例えばコンストラクタに多くのパラメータを必要とするクラスです。
具体的なコード例で見ていきましょう。

class Page {
    private final String title;
    private final String category;
    private final String publishedAt;
    private final String updatedAt;
    private final String content;

    Page(String title, String category,
         String publishedAt, String updatedAt, String content) {

        this.title = title;
        this.category = category;
        this.publishedAt = publishedAt;
        this.updatedAt = updatedAt;
        this.content = content;
    }

    @Override
    public String toString() {
        return """
                タイトル : %s
                カテゴリ : %s
                公開日 : %s
                更新日 : %s
                本文 : %s
                """
                .formatted(title, category, publishedAt, updatedAt, content);
    }
}

Pageクラスは、Webページの1ページを表します。
そして、final なフィールドを5つ持ちます。

それでは、この Page オブジェクトを生成してみましょう。

final var page = new Page(
        "今日はお花見!", "日記",
        "2022-03-20", "2022-04-01",
        "今日は近くの公園へお花見に...");

System.out.println(page);

// 結果
// ↓
//タイトル : 今日はお花見!
//カテゴリ : 日記
//公開日 : 2022-03-20
//更新日 : 2022-04-01
//本文 : 今日は近くの公園へお花見に...

Page オブジェクトを生成するには、

  1. タイトル (title)
  2. カテゴリ (category)
  3. 公開日 (publishedAt)
  4. 更新日 (updatedAt)
  5. 本文 (content)

上記5つのパラメータが必要です。

今回の例ではパラメータが5つなのでなんとかなりそうですが、さらに増えていくと単純なミスも発生しそうです。

どのパラメータが何番目だっけ…?
公開日と更新日はどっちが先だっけ…?

と、コーディングしていて不安になるかもしれません。

もしすべてのパラメータの型が違うのであれば、指定する順番を間違えてもコンパイルエラーとなりすぐに気づけます。

しかし、今回のように同じ型 (String) ばかりだと、順番を間違えてもコンパイルエラーになりません。
間違いに気づけるのはテストのときです。

プログラミングにおいて、問題はできるだけ早期に発見&解決するのが鉄則です。

もう1つ、公開日と更新日は省略可能にしたいとします。
省略された場合は、"xxxx-xx-xx" をデフォルトとしましょう。

公開日、更新日の2つとも省略したコンストラクタは、次のようにオーバーロードできます。

class Page {
    ...

    Page(String title, String category,
         String publishedAt, String updatedAt, String content) {
        ...
    }

    // 公開日、更新日を省略したコンストラクタ。
    Page(String title, String category, String content) {
        this(title, category, "xxxx-xx-xx", "xxxx-xx-xx", content);
    }
...

しかし、

  • 公開日だけを省略したコンストラクタ
  • 更新日だけを省略したコンストラクタ

この2つを共存させることはできません。
パラメータの数と型がまったく同じなのでオーバーロードできないためです。

class Page {
    ...

    // 公開日を省略したコンストラクタ。
    Page(String title, String category,
         String updatedAt, String content) {
        ...
    }

    // 更新日を省略したコンストラクタ。(★コンパイルエラー)
    Page(String title, String category,
         String publishedAt, String content) {
        ...
    }
...

この問題を回避するために、

  • null を指定したときはデフォルトにする

としてみましょう。

class Page {
    ...

    Page(String title, String category,
         String publishedAt, String updatedAt, String content) {

        this.title = title;
        this.category = category;
        this.publishedAt = publishedAt != null ? publishedAt : "xxxx-xx-xx";
        this.updatedAt = updatedAt != null ? updatedAt : "xxxx-xx-xx";
        this.content = content;
    }
...

これで、オーバーロードしなくても公開日と更新日にデフォルトを指定できるようになりました。
しかし、省略したいパラメータにも null を指定する必要があります。

結局、指定するパラメータの数は多いままですね。

final var page = new Page(
        "今日はお花見!", "日記",
        null, null,
        "今日は近くの公園へお花見に...");

System.out.println(page);

// 結果
// ↓
//タイトル : 今日はお花見!
//カテゴリ : 日記
//公開日 : xxxx-xx-xx
//更新日 : xxxx-xx-xx
//本文 : 今日は近くの公園へお花見に...

このような、

  • コンストラクタで指定するパラメータが多い
  • 省略可能なパラメータがある

という場面で便利なのが 簡易的な Builder パターンです。
特に、省略可能なパラメータが多いときに有効です。

Builderクラスとは

簡易的な Builder パターンでは、Builder が対象となるオブジェクトを生成します。

クラス図(簡易的なBuilderパターン)

例として、先ほどの Pageクラスを生成する PageBuilder を作成してみましょう。

クラス図(PageBuilder)

ただし、先ほどは公開日と更新日を省略可能としましたが、メリットを分かりやすくするために、

  • タイトル : 必須
  • それ以外 : 省略可能

とします。

class PageBuilder {

    private String title;
    private String category = "なし";
    private String publishedAt = "xxxx-xx-xx";
    private String updatedAt = "xxxx-xx-xx";
    private String content = "...";

    Page build() {
        if (title == null) {
            throw new IllegalStateException("titleを指定してください。");
        }

        return new Page(title, category, publishedAt, updatedAt, content);
    }

    PageBuilder setTitle(String title) {
        this.title = title;
        return this;
    }

    PageBuilder setCategory(String category) {
        this.category = category;
        return this;
    }

    PageBuilder setPublishedAt(String publishedAt) {
        this.publishedAt = publishedAt;
        return this;
    }

    PageBuilder setUpdatedAt(String updatedAt) {
        this.updatedAt = updatedAt;
        return this;
    }

    PageBuilder setContent(String content) {
        this.content = content;
        return this;
    }
}

少しずつコードを見ていきましょう。

PageBuilderクラスでは、Pageクラスを生成するために必要な、5つのフィールドを用意します。
タイトル以外は、省略されたときの値で初期化しています。

class PageBuilder {

    private String title;
    private String category = "なし";
    private String publishedAt = "xxxx-xx-xx";
    private String updatedAt = "xxxx-xx-xx";
    private String content = "...";

...

次に、パラメータを指定するための set~ メソッドを用意します。
また、利便性のために PageBuilder自身(this) を返します。

class PageBuilder {
    ...

    PageBuilder setTitle(String title) {
        this.title = title;
        return this;
    }

    PageBuilder setCategory(String category) {
        this.category = category;
        return this;
    }

...

そして、Pageオブジェクトを生成するための build メソッドです。
タイトルは必須なのでチェックしています。

class PageBuilder {
    ...

    Page build() {
        if (title == null) {
            throw new IllegalStateException("titleを指定してください。");
        }

        return new Page(title, category, publishedAt, updatedAt, content);
    }

...

それでは、PageBuilder クラスを使ってみましょう。

final var builder = new PageBuilder();

builder.setUpdatedAt("2022-03-20");
builder.setTitle("今日はお花見!");
builder.setCategory("日記");

// Pageをビルド!
final var page = builder.build();

System.out.println(page);

// 結果
// ↓
//タイトル : 今日はお花見!
//カテゴリ : 日記
//公開日 : xxxx-xx-xx
//更新日 : 2022-03-20
//本文 : ...

こちらも少しずつコードを見ていきましょう。

まずは PageBuilder クラスを生成します。

final var builder = new PageBuilder();

次に、必要となるパラメータを指定していきます。

builder.setUpdatedAt("2022-03-20");
builder.setTitle("今日はお花見!");
builder.setCategory("日記");

set~ メソッドの呼び出し順番は自由です。
省略可能なパラメータを省略したい場合は、set~ を呼び出す必要はありません。

最後に build メソッドで Pageオブジェクトを生成します。

final var page = builder.build();

set~ メソッドは Builder 自身を戻り値として返すので、すべてつなげて書くこともできます。

final var page = new PageBuilder().setUpdatedAt("2022-03-20")
        .setTitle("今日はお花見!").setCategory("日記").build();

System.out.println(page);

これが、Java 標準API でも使われている簡易的な Builder パターンです。
Builderパターンなし/ありを見比べてみましょう。

Builderパターンなし Builderパターンあり
final var page = new Page(
        "今日はお花見!", "日記",
        null, "2022-03-20", null);

System.out.println(page);

// 結果
// ↓
//タイトル : 今日はお花見!
//カテゴリ : 日記
//公開日 : xxxx-xx-xx
//更新日 : 2022-03-20
//本文 : ...
final var page = new PageBuilder()
        .setTitle("今日はお花見!")
        .setCategory("日記")
        .setUpdatedAt("2022-03-20")
        .build();

System.out.println(page);

// 結果
// ↓
//タイトル : 今日はお花見!
//カテゴリ : 日記
//公開日 : xxxx-xx-xx
//更新日 : 2022-03-20
//本文 : ...

Builder パターンありのほうが、どのパラメータが何なのか分かりやすく感じますでしょうか?
もしそうであれば、簡易的な Builder パターンのメリットがあったことになります。

コンストラクタで必須パラメータを指定

先ほどの PageBuilderクラスでは、build するときに必須パラメータの title をチェックしました。

class PageBuilder {
    ...

    Page build() {
        if (title == null) {
            throw new IllegalStateException("titleを指定してください。");
        }

        return new Page(title, category, publishedAt, updatedAt, content);
    }

...

もし必須パラメータが少ない場合は、Builder のコンストラクタで指定するのもおすすめです。

class PageBuilder {

    private final String title; // ★ final を追加
    private String category = "なし";
    private String publishedAt = "xxxx-xx-xx";
    private String updatedAt = "xxxx-xx-xx";
    private String content = "...";

    // ★ title をコンストラクタで指定
    PageBuilder(String title) {
        if (title == null) {
            throw new IllegalArgumentException("titleがnullです。");
        }

        this.title = title;
    }

    Page build() {
        return new Page(title, category, publishedAt, updatedAt, content);
    }

    // ★ setTitle メソッドは削除

    PageBuilder setCategory(String category) {
        this.category = category;
        return this;
    }

    PageBuilder setPublishedAt(String publishedAt) {
        this.publishedAt = publishedAt;
        return this;
    }

...
final var builder = new PageBuilder("今日はお花見!");

builder.setUpdatedAt("2022-03-20");
builder.setCategory("日記");

// Pageをビルド!
final var page = builder.build();

System.out.println(page);

// 結果
// ↓
//タイトル : 今日はお花見!
//カテゴリ : 日記
//公開日 : xxxx-xx-xx
//更新日 : 2022-03-20
//本文 : ...

コンストラクタで必須パラメータを指定したので、build メソッドではチェック不要になりました。

ただし、必須パラメータが多くなる場合は注意が必要です。
それらすべてを Builder のコンストラクタに指定すると

  • パラメータが多くて順番が分かりにくい

という問題がやはり発生してしまうからです。

デメリット

簡易的な Builder パターンも、適したところに使わないとデメリットになることがあります。

まず、Builder パターンを使おうとすると、Builder クラスを作る手間がかかります。
そして、Builder を経由してオブジェクトを生成するため、コードの記述量も増えます。

  • Builder を使わずに簡潔に記述する
  • Builder を使って分かりやすく記述する (コードは少し冗長になることも)

どちらが良いかは、必要となるパラメータの数などで変わってくるでしょう。
そのあたりはうまく見極めたいですね。

標準APIの例

Java の標準API には、簡易的な Builder パターンを採用している API があります。
HttpRequest.Builder, DateTimeFormatterBuilder, Stream.Builder などなど。

そのうちのいくつかをご紹介します。

HttpRequest.Builder

「HTTPリクエスト」のビルダー。

HttpRequest.Builder は、HttpRequest を生成するための Builder です。
HttpRequest は HTTP 通信をするときのリクエストを表します。

final var builder = HttpRequest.newBuilder(URI.create("https://example.com/"));

final var request = builder
        .POST(HttpRequest.BodyPublishers.ofString("abcd"))
        .header("Content-Type", "text/plain; charset=UTF-8")
        .timeout(Duration.ofSeconds(30))
        .build();

System.out.println(request);

// 結果
// ↓
//https://example.com/ POST

Builder を生成する HttpRequest.newBuilder では、必須パラメータである URI を指定します。
あとは、オプションのパラメータをいろいろと指定して、最後に build で HttpRequest を生成ですね。

DateTimeFormatterBuilder

日付/時間フォーマッタを作成するためのビルダー。

DateTimeFormatter は、日時クラスをどのような文字列へと変換するのか、を表すクラスです。
まずは Builder を使わない例を見てみましょう。

final var date = LocalDate.of(1999, 1, 2);
final var formatter = DateTimeFormatter.ofPattern("y年M月d日");

final var str = date.format(formatter);
System.out.println(str); // 1999年1月2日

コードの流れは

 日付クラス : LocalDate.of(1999, 1, 2)
  ↓
 フォーマッタ : "y年M月d日"
  ↓
 変換結果 : "1999年1月2日"

となります。

フォーマッタは、DateTimeFormatter.ofPattern で簡易的に生成しています。
ただし、ofPattern では、yMd が何を意味するのかを理解していないといけません。

次に DateTimeFormatterBuilder を使った例を見てみましょう。

final var builder = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR).appendLiteral("年")
        .appendValue(ChronoField.MONTH_OF_YEAR).appendLiteral("月")
        .appendValue(ChronoField.DAY_OF_MONTH).appendLiteral("日");

// DateTimeFormatter をビルド!
final var formatter = builder.toFormatter();

final var date = LocalDate.of(1999, 1, 2);

final var str = date.format(formatter);
System.out.println(str); // 1999年1月2日

記述量はだいぶ増えてしまいました。
しかし、yM, d といった書式による指定がなくなりました。

書式を知らない人がコードを見ても、理解しやすくなったと思います。

ただ、DateTimeFormatter.ofPattern と DateTimeFormatterBuilder どちらがよいか…
それは場合によるとは思います。(ofPattern による簡潔な記述も魅力的です)

補足

キーワード引数(パラメータ)

Java 以外のプログラミング言語には、言語仕様としてメソッドのパラメータを順不同で指定できるものがあります。

例えば Python のキーワード引数(パラメータ)では、次のようなことが可能です。

def func(x, y, z):
    print(f'x={x} : y={y} = z={z}')


func(z=10, y=20, x=30)

// 結果
// ↓
//x=30 : y=20 = z=10

func 関数(メソッド)の呼び出しのときに、どの引数になにを渡すのかという指定ができます。
これにより、パラメータの順番は気にせずに関数を呼び出せます。

このようなプログラミング言語では、今回ご紹介した簡易的な Builder パターンのメリットは小さくなるかもしれません。

まとめ

簡易的な Builder パターンの目的は、

  1. オブジェクト生成 が複雑な(分かりにくい)クラスに対して
  2. オブジェクト生成 を補助するクラス(Builder)を作り
  3. オブジェクト生成 を簡単に(分かりやすく) しよう

です。

メリットを受けやすいケースには、

  • コンストラクタで指定するパラメータが多い
  • 省略可能なパラメータがある

などがあります。

そんなときは、ぜひ簡易的な Builder パターンが活用できないか検討してみましょう。


関連記事

ページの先頭へ