Effective Java の Builderパターンとその拡張

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

ちょいと前から Effective Java 第二版の読書会的なものをやってます。

その中で Builderパターンが非常に美しいね、という話になりました。

Builderパターンとは数多くのパラメータを持つオブジェクトの生成に関するパターンです。

オブジェクト生成に通常のコンストラクタを使用する場合、パラメータ間の整合性は保てますが、数が多いと引数の順番を知っている必要があり書きづらく読みづらいコードになります。

それ以外でよく使われるのが JavaBeansパターンです。
デフォルトコンストラクタを使用し、setter を用いて値を設定する方法ですね。
この方法では、コードの意味合いは明確になりますが、パラメータ間の整合性を保てず、クラスを不変にする可能性を排除してしまいます。

そこで以下の様な Builder パターンの登場となります。

// ■オブジェクト側
public final class Commodity {
    private final int id;             // 必須
    private final String name;        // 必須
    private final long catalogPrice;  // 必須
    private final long lowestPrice;   // オプショナル
    private final long averagePrice;  // オプショナル
    private final String makerName;   // オプショナル

    public static class Builder {
        private final int id;
        private final String name;
        private final long catalogPrice;
        private long lowestPrice;
        private long averagePrice;
        private String makerName;
        public Builder(int id, String name, long catalogPrice) {
            // 必須項目は Builder のコンストラクタで指定
            this.id = id;
            this.name = name;
            this.catalogPrice = catalogPrice;
        }
        public Builder lowestPrice(long lowestPrice) {
            this.lowestPrice = lowestPrice; return this;
        }
        public Builder averagePrice(long averagePrice) {
            this.averagePrice = averagePrice; return this;
        }
        public Builder makerName(long makerName) {
            this.makerName = makerName; return this;
        }
        public Commodity build() {
            // パラメータ間の整合性は build メソッドで解決
            if (lowestPrice > averagePrice) throw new IllegalStateException("lowestPrice > averagePrice");
            return new Commodity(this);
        }
    }
    
    private Commodity(Builder builder) {
        id = builder.id;
        name = builder.name;
        lowestPrice = builder.lowestPrice;
        averagePrice = builder.averagePrice;
        makerName = builder.makerName;
    }

    // getter 省略
}

// ■クライアント側
Commodity commodity = new Commodity.Builder(10, "hoge", 400).lowestPrice(100).averagePrice(300).build();

パラメータ間の整合性を保ったまま、クラスを不変にすることも可能にしています。すばらしいですね。

しかしこれ、必須のパラメータはコンストラクタで渡していますが、必須のパラメータが多い場合にはコンストラクタでオブジェクトを生成する場合と同じ問題が起ってしまいます。

そこで他のパラメータと同じようにメソッドで設定できるようにしたい、けれども必ず必須の項目が設定されるようにしたい、という問題を解決するために以下の様な形を考えてみました。

// ■オブジェクト側
public final class Commodity {
    private final int id;             // 必須
    private final String name;        // 必須
    private final long catalogPrice;  // 必須
    private final long lowestPrice;   // オプショナル
    private final long averagePrice;  // オプショナル
    private final String makerName;   // オプショナル

    public static interface IdBuilder {
        NameBuilder id(int id);
    }
    public static interface NameBuilder {
        CatalogPriceBuilder name(String name);
    }
    public static interface CatalogPriceBuilder {
        Builder catalogPrice(long catalogPrice);
    }
    public static class Builder implements IdBuilder, NameBuilder, CatalogPriceBuilder {
        private int id;
        private String name;
        private long catalogPrice;
        private long lowestPrice;
        private long averagePrice;
        private String makerName;
        private Builder() {
            // Builder の生成はファクトリメソッドを使用するため private
        }
        public NameBuilder id(long id) {
            this.id = id; return this;
        }
        public CatalogPriceBuilder name(String name) {
            this.name = name; return this;
        }
        public Builder catalogPrice(long catalogPrice) {
            this.catalogPrice = catalogPrice; return this;
        }
        public Builder lowestPrice(long lowestPrice) {
            this.lowestPrice = lowestPrice; return this;
        }
        public Builder averagePrice(long averagePrice) {
            this.averagePrice = averagePrice; return this;
        }
        public Builder makerName(String makerName) {
            this.makerName = makerName; return this;
        }
        public Commodity build() {
            // パラメータ間の整合性は build メソッドで解決
            if (lowestPrice > averagePrice) throw new IllegalStateException("lowestPrice > averagePrice");
            return new Commodity(this);
        }
    }

    public static IdBuilder builder() {
        return new Builder();
    }
    
    private Commodity(Builder builder) {
        id = builder.id;
        name = builder.name;
        catalogPrice = builder.catalogPrice;
        lowestPrice = builder.lowestPrice;
        averagePrice = builder.averagePrice;
        makerName = builder.makerName;
    }

    // getter 省略
}

// ■クライアント側
Commodity commodity = Commodity.builder().id(10).name("hoge").catalogPrice(400)
    .lowestPrice(100).averagePrice(300).build();

必須パラメータにはそれぞれ buildメソッドを持たない interface を用意します。
Builderのファクトリメソッド及び、パラメータの設定メソッドがそれらの interface を数珠繋ぎに返します。
これによって、すべての必須パラメータに値を設定することが保障されます。

おまけとして IDE の補完機能を使えば何を設定したらいいか JavaDoc を見ずとも教えてくれるので Fluent Interface のメリットも最大限に享受できますね。

問題はコード量が増えてしまうところ。単純に読みづらいですし、IDE の支援がある JavaBeans パターンと違って書くのもメンドクサイです。書くのに関しては必須とオプショナルの区別がつけば自動生成は簡単そうなので Eclipse Plugin なりエディタのマクロなり組めばその辺の労力は JavaBeans と同じにできそうです。

2009-10-17 03:05

private → public や long → String の書き間違いを修正