33
29

More than 3 years have passed since last update.

流れるようなインターフェース試行錯誤メモ

Posted at

※この記事には独自研究が含まれています。

流れるようなインターフェース (fluent interface) とは

流れるようなインタフェース | Martin Fowler's Bliki (ja)

流れるようなインターフェース(fluent interface)とは、メソッドチェーンを利用して DSL 的な仕組みを実現するテクニックのことを指す。

身近な例で言うと、 mockitoAssertJjOOQ、そして Spring のコンフィギュレーションなどで利用されている。

SpringSecurityの設定例
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests(authorizeRequests ->
            authorizeRequests
                .anyRequest().authenticated()
        )
        .formLogin(withDefaults())
        .httpBasic(withDefaults());
}

6.2 HttpSecurity | Spring Security Reference

流れるようなインターフェースで実装しているときの様子

fluent.gif

流れるようなインターフェースでは、メソッドチェーンを使ってオブジェクトの構築を宣言していく。
このとき、各メソッドが返す型によって次に呼び出せるメソッドが制御されている。
これにより次に設定する内容が型によって示され、間違った設定が回避できるようになる。

ただのメソッドチェーンビルダーとの違い

たとえば、次のようなクラスがあったとする。

Target
package nofluent;

public class Target {
    private final String foo;
    private String bar;

    public Target(String foo) {
        this.foo = foo;
    }

    public void setBar(String bar) {
        this.bar = bar;
    }

    public String getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

この Target クラスは、 foobar の2つのフィールドを持つ。
foo はコンストラクタで指定しなければならない必須のフィールドであるのに対して、 bar の設定は任意となっている。

このクラスのビルダーを、仮に次のように作ったとする。1

TargetBuilder
package nofluent;

public class TargetBuilder {
    private String foo;
    private String bar;

    public static TargetBuilder builder() {
        return new TargetBuilder();
    }

    public TargetBuilder foo(String value) {
        this.foo = value;
        return this;
    }

    public TargetBuilder bar(String value) {
        this.bar = value;
        return this;
    }

    public Target build() {
        Target target = new Target(this.foo);
        if (this.bar != null) {
            target.setBar(this.bar);
        }
        return target;
    }
}

このビルダーを使うと、メソッドチェーンを使って Target クラスのインスタンスを構築できる。
しかし、このビルダーには次のような問題がある。

  • foo が必須であることがわからない
  • bar が任意であることがわからない
  • foobar を複数回設定できる
間違ったインスタンスの構築が可能
Target target = TargetBuilder.builder().bar("BAR").bar("BUZZ").build();

つまり、このビルダーはメソッドチェーンで書けているだけで、 Target クラスを構築するときのルールが全く表現できていない。

一方で流れるようなインターフェースは、メソッドが返す型をコントロールすることで次の設定内容を示したり制限したりする。
色々作り方はあるかもしれないが、例えば次のようにビルダーを作ったとする。

流れるようなインターフェースを意識したビルダー
package fluent.example;

public class TargetBuilder {
    private String foo;

    public static BarBuilder foo(String value) {
        TargetBuilder builder = new TargetBuilder();
        builder.foo = value;
        return builder.new BarBuilder();
    }

    public class BarBuilder {

        public Target bar(String value) {
            Target target = new Target(foo);
            target.setBar(value);
            return target;
        }

        public Target build() {
            return new Target(foo);
        }
    }

    private TargetBuilder() {}
}
Target barIsNotNull = TargetBuilder.foo("FOO").bar("BAR");
Target barIsNull = TargetBuilder.foo("FOO").build();

このビルダーは、 foo() メソッドでしか宣言を開始できないようにしている。
これにより、 foo が必須であることが表現できている。

また、 foo() が返す次のビルダーである BarBuilder には bar()build() メソッドしか用意されていない。
したがって、実装者はこのいずれかのメソッドを呼ぶしか選択肢がない(foo を重ねて宣言したりはできない)。

bar() を使えば bar を設定できるし、 build() を使えば bar を設定せずに宣言を終了させることができる。
これにより、 bar が任意であることも表現されている。

このように、流れるようなインターフェースは対象のオブジェクトの構築ルールまで表現できるという特徴がある。

メリット

流れるようなインターフェースのメリットを整理すると、次のようなことが挙げられる。

  • 次に設定する内容がメソッドの返す型によって示される
  • 間違った設定を書こうとするとコンパイルエラーになる2
  • IDE を使っていれば入力補完で次の設定が一覧表示される

デメリット

メリットだけではなく、デメリットもある。

  • コードの自動フォーマットを利用していると残念なことになる(かも)
  • この手法に慣れていない人が見ると、 Java っぽくないコードに驚く(かも)
  • ビルダークラスの設計が結構難しい

とくに、3つ目のクラス設計が難しく、ある程度複雑なビルダーを作ろうとすると、途中で構造がややこしくなってワケが分からなくなることがよくある(自分は)。

そこで、流れるようなインターフェースを実現したクラスを、混乱せずに設計・実装していくための手順やヒントみたいなことを自分なりに整理してみる。

フローを描く

いきなり実装を始めても混乱するだけなので、まずは流れるようなインターフェースで実現したい一連のフローを図で描いてみるのがいいと思う。

文字と矢印が描ければ、テキストでも Excel でも何でもいいと思う。
自分は astah をよく使うので、アクティビティ図を利用して次のようなフローを描くようにしている。3

fluent.jpg

この図では、簡単な入力項目のチェック内容を定義するフローを描いている。

設定内容の種類

設定内容自体は、構築対象のオブジェクト次第で様々なものがありえる。
しかし、その種類は大きく「定義」と「選択」のいずれかに分類できる(と思ってる)。

「定義」は、具体的な値を設定することで、
「選択」は、フローの流れを決定することを表している。

必須かどうかを設定するのは、 "必須" と "任意" のいずれかを選択しているので「選択」に感じるかもしれない。
しかし、どちらを選んでもその後のフローは変わらない。
したがって、 "必須" か "任意" の値を設定している「定義」と考える。

一方で、項目種別は "日付", "数値", "文字列" のいずれかの値を設定していると思えるかもしれないが、その後のフローが大きく変わるので「選択」と考える。

この2種類は、実装上では次のような違いで現れる。

「定義」は値を設定するので、メソッド引数で値を渡すことになる。
一方「選択」は、選択肢を表すメソッドがいくつか用意され、それぞれのメソッドの戻り値がその後のフローを決定する型になっている。

実装のイメージ
builder
    .required(true)     // 必須かどうかを「定義」
    .number()           // 項目種別を「選択」
    .decimal()          // 整数か少数かを「選択」
    .precision(5, 2)    // 精度を「定義」
    .geaterThan(0.0)    // 最小値を「定義」
    .lessThan(10000.0); // 最大値を「定義」

ただし、「定義」は引数ありで「選択」は引数なし、と確定するわけではない
実装の簡略化を考えて、複数の連続する「選択」や、「選択」とその直後の「定義」を1つにまとめることもできる。

たとえば、上の例では項目種別の選択と整数・少数の選択、そしてその直後の精度の定義をまとめるのもアリだと思う。4

選択と直後の定義をまとめた場合
// 少数の場合
builder
    .required(true)
    .decimal(5, 2) // 項目種別選択・整数/少数選択・精度定義をまとめる
    .greaterThan(0.0)
    .lessThan(10000.0);

// 整数の場合
builder
    .required(true)
    .integer() // 項目種別選択・整数/少数選択をまとめる
    .greaterThan(0)
    .lessThan(10000);

// 日付の場合
builder
    .required(true)
    .date()
    .greaterThanEqual(2000, 1, 1)
    .lessThanEqual(2100, 12, 31);

また、選択肢が限られている定義の場合は、選択肢の数だけメソッドを用意したほうが記述がシンプルになることもある。

定義の選択肢をメソッドで表現した例
package fluent;

public class TargetBuilder {
    private SomeStrategy strategy;

    public class SomeStrategyBuilder {
        public AfterBuilder foo() {
            strategy = new FooStrategy();
            return new AfterBuilder();
        }

        public AfterBuilder bar() {
            strategy = new BarStrategy();
            return new AfterBuilder();
        }
    }

    public class AfterBuilder {...}

    private TargetBuilder() {}
}

この例では、 SomeStrategy に割り当てる具体的なクラスの定義を foo(), bar() のメソッドで分けている。

これは strategy(SomeStrategy) のように引数でインスタンスを受け取るように作ることもできる。
しかし、メソッドで分けたほうが利用側で具体的なインスタンス生成が必要なくなり、選択肢もメソッド補完のリストの中に現れて指定しやすくなる。

ただし、 SomeStrategy インスタンスの用意を呼び出し元でしたほうがいい場合もあるかもしれない。
(DI コンテナから取得したインスタンスを渡さないといけない、とか)

引数指定とメソッド指定のどちらが良いということはなく、ケースバイケースで最適な方を選ぶことになると思う。

ビルダーを作る

※以下は、自分なりに試行錯誤した結果、こうするのがいいんじゃない?と思ったことをまとめたものなので、絶対的な正解(そうしなければならないもの)ではない

内部クラスを利用する

内部クラスを利用したビルダーのイメージ
package fluent;

public class TargetBuilder {
    private String hoge;
    private String fuga;
    private String piyo;

    public static FugaBuilder hoge(String value) {
        TargetBuilder builder = new TargetBuilder();
        builder.hoge = value;
        return builder.new FugaBuilder();
    }

    public class FugaBuilder {

        public PiyoBuilder fuga(String value) {
            fuga = value;
            return new PiyoBuilder();
        }
    }

    public class PiyoBuilder {

        public TargetBuilder piyo(String value) {
            piyo = value;
            return TargetBuilder.this;
        }
    }

    public Target build() {
        return new Target(hoge, fuga, piyo);
    }

    private TargetBuilder() {}
}

流れるようなインターフェースでは、フローに合わせて様々なビルダークラスを用意しなければならない。
また、最終的にオブジェクトを作り上げるためには、そのビルダークラスたちによって設定された値を共有する必要がある。

これらのビルダークラスを全て1ファイル1クラスで定義していると、ファイル数が大量になったり、値を共有するための入れ物クラスを作ってビルダーのコンストラクタで引き回す実装を書く必要があったりと、色々面倒になりやすい。

そこで、ビルダークラスを内部クラスで定義すると実装が楽になる(と思う)。
内部クラスなら、外部クラスのインスタンスフィールドを使って値の共有ができるので、値共有のための入れ物クラスを引き回す必要もなくなる。

上の例では、 FugaBuilderPiyoBuilderTargetBuilder の内部クラスとして定義されている。
各ビルダー間で共有する値は、 TargetBuilder のインスタンスフィールドで定義している。

内部クラスは、普段はあまり利用する機会がないので、 builder.new FugaBuilder() とか TargetBuilder.this とかの実装が初見だと気持ち悪く感じるかもしれない。
でも、一応ちゃんとした Java の文法なので慣れるしかない。

ビルダーの開始

package fluent;

public class TargetBuilder {
    private String foo;

    public static AfterBuilder foo(String value) {
        TargetBuilder builder = new TargetBuilder();
        builder.foo = value;
        return builder.new AfterBuilder();
    }

    public class AfterBuilder {...}

    private TargetBuilder() {}
}

ビルダーの開始は、 static なファクトリメソッドでいきなり「定義」や「選択」を始めるのが、なんだかんだ一番記述量が減って見た目もシンプルになると思う。

普通に new TargetBuilder() とかコンストラクタで生成してから開始するのもアリだと思うが、ちょっと記述がダサくなる(個人の感想)。

ビルダーの終了

package fluent;

public class TargetBuilder {
    private String foo;

    public class BeforeBuilder {
        public FooBuilder before() {
            return new FooBuilder();
        }
    }

    public class FooBuilder {
        public Target foo(String value) {
            foo = value;
            return new Target(foo);
        }
    }

    private TargetBuilder() {}
}

ビルダーの最後は、普通は構築したオブジェクトを返す。

最後をどう記述するかには、次の2つのパターンがあると思う。

  1. build() のようにオブジェクトの構築を指示するメソッドで終わらせる
  2. build() などは挟まず、最後の設定が終わったらすぐに構築したオブジェクトを返す(上の実装例)

build() で終わらせる方法には、「ビルダーによる設定とオブジェクトの構築を分けて実行できる」という特徴がある。
利用者側はビルダーによる設定だけできればよく、構築されたオブジェクトはフレームワーク側でだけ利用するような場合は、こちらのほうが良いかもしれない。

逆に build() なしで最後の設定が終わったらすぐにオブジェクトを返す方法は、利用者側で構築されたオブジェクトをすぐに使いたい場合に、記述がシンプルになるという特徴がある。
(後述する「再利用」のケースとかが具体例となる)

また、 build() で終わらせる方法は、 API に統一感を持たせるという価値もあるかもしれない。

どちらを選択するかは、ケースバイケースになると思う。

必須定義のフロー

fluent.jpg

ビルダーの実装
package fluent;

public class TargetBuilder {
    private String foo;
    private String bar;

    public class BeforeBuilder {
        public FooBuilder before() {
            return new FooBuilder();
        }
    }

    public class FooBuilder {
        public BarBuilder foo(String value) {
            foo = value;
            return new BarBuilder();
        }
    }

    public class BarBuilder {
        public AfterBuilder bar(String value) {
            bar = value;
            return new AfterBuilder();
        }
    }

    public class AfterBuilder {
        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
builder
    .before()
    .foo("FOO")
    .bar("BAR")
    .after();

必ず設定しなければならない定義のフローは、各定義ごとにビルダーを用意してチェーンさせていく。

各定義ごとにビルダーを作るのは面倒かもしれないが、そうすることで「必須設定」であることを表現できるメリットは大きいと思う。

選択のフロー

基本

fluent.jpg

ビルダーの実装
package fluent;

public class TargetBuilder {
    private String foo;
    private String bar;

    public class BeforeBuilder {
        public SelectFooBarBuilder before() {
            return new SelectFooBarBuilder();
        }
    }

    public class SelectFooBarBuilder {
        public FooBuilder fooFlow() {
            return new FooBuilder();
        }

        public BarBuilder barFlow() {
            return new BarBuilder();
        }
    }

    public class FooBuilder {
        public AfterBuilder foo(String value) {
            foo = value;
            return new AfterBuilder();
        }
    }

    public class BarBuilder {
        public AfterBuilder bar(String value) {
            bar = value;
            return new AfterBuilder();
        }
    }

    public class AfterBuilder {
        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
// foo ルートを選択
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

// bar ルートを選択
builder
    .before()
    .barFlow()
    .bar("BAR")
    .after();

「選択」のケースでは、選択肢のメソッドを提供するビルダーを1つ挟む。

基本はフローの「選択」のみを行い、「定義」は続くビルダーで行う。
ただし、直後の「定義」をまとめて行ったほうが記述が減ってシンプルになることもある。
(無理にまとめると意味が分かりにくくなる場合は、おとなしく分けたほうが良い)

直後の「定義」をまとめた場合
package fluent;

public class TargetBuilder {
    private String foo;
    private String bar;

    public class BeforeBuilder {
        public SelectFooBarBuilder before() {
            return new SelectFooBarBuilder();
        }
    }

    public class SelectFooBarBuilder {
        public AfterBuilder foo(String value) {
            foo = value;
            return new AfterBuilder();
        }

        public AfterBuilder bar(String value) {
            bar = value;
            return new AfterBuilder();
        }
    }

    public class AfterBuilder {
        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
// foo ルートを選択
builder
    .before()
    .foo("FOO")
    .after();

// bar ルートを選択
builder
    .before()
    .bar("BAR")
    .after();

任意選択のフロー

fluent.jpg

ビルダーの実装
package fluent;

public class TargetBuilder {
    private String foo;

    public class BeforeBuilder {
        public SelectFooBuilder before() {
            return new SelectFooBuilder();
        }
    }

    public class SelectFooBuilder {
        public FooBuilder fooFlow() {
            return new FooBuilder();
        }

        public AfterBuilder and() {
            return new AfterBuilder();
        }
    }

    public class FooBuilder {
        public AfterBuilder foo(String value) {
            foo = value;
            return new AfterBuilder();
        }
    }

    public class AfterBuilder {
        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
// foo ルートを選択
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

// foo ルートを選択しない
builder
    .before()
    .and()
    .after();

任意で選択できるルートがある場合、何もしないルートも発生することになる。
その場合は、 and() のように何もしないメソッドを用意して次のビルダーにチェーンする。

ただ、任意選択が連続するような場合は設定次第で .and().and() とかになってダサくなる危険性がある。
その場合は、 notFoo() とか defaultFoo() のようにデフォルトを選択してる感を表すメソッド名にしておくとダサさが和らぐ。

ループのフロー

単一定義のループ

fluent.jpg

ビルダーの実装
package fluent;

import java.util.ArrayList;
import java.util.List;

public class TargetBuilder {
    private List<String> fooList = new ArrayList<>();

    public class BeforeBuilder {
        public FooListBuilder before() {
            return new FooListBuilder();
        }
    }

    public class FooListBuilder {
        public FooListBuilder foo(String value) {
            fooList.add(value);
            return this;
        }

        public AfterBuilder and() {
            return new AfterBuilder();
        }
    }

    public class AfterBuilder {
        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
builder
    .before()
        .foo("one")
        .foo("two")
        .foo("three")
        .and()
    .after();

何らかのリスト構造のオブジェクトを構築したい場合にループが必要になる。
まず単純なケースとして、ループの中で行う設定が単一の定義のみの場合。

FooListBuilder は、 foo(String) でリストに要素を追加してから自分自身の参照を返す。
これにより、繰り返し要素の追加ができるようになっている。

ただし、このままではループを抜けることができないので、ループを脱出するために and() メソッドも用意している。

複数定義のループ

fluent.jpg

ビルダーの実装
package fluent;

import java.util.ArrayList;
import java.util.List;

public class TargetBuilder {
    private List<Target> targetList = new ArrayList<>();

    public class BeforeBuilder {
        public TargetListBuilder.FooBuilder before() {
            return new TargetListBuilder().new FooBuilder();
        }
    }

    public class TargetListBuilder {
        private String foo;
        private String bar;

        public class FooBuilder {
            public BarBuilder foo(String value) {
                foo = value;
                return new BarBuilder();
            }
        }

        public class BarBuilder {
            public TargetListBuilder bar(String value) {
                bar = value;
                return TargetListBuilder.this;
            }
        }

        public BarBuilder foo(String value) {
            this.save();
            return new TargetListBuilder().new FooBuilder().foo(value);
        }

        public AfterBuilder and() {
            this.save();
            return new AfterBuilder();
        }

        private void save() {
            Target target = new Target(foo, bar);
            targetList.add(target);
        }
    }

    public class AfterBuilder {
        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
builder
    .before()
        .foo("Foo1").bar("Bar1")
        .foo("Foo2").bar("Bar2")
        .foo("Foo3").bar("Bar3")
        .and()
    .after();

なんか急に複雑になった。

リストの各要素オブジェクトの構築に複数の定義が必要となる場合、この複数定義のループが必要になる。

複数定義のループの場合、1回のループ内で設定された情報を一時的に記録しておく必要がある。
そして、1回のループが終わるタイミングで、記録しておいた情報を使ってオブジェクトを構築しリストに詰めるという処理が必要になる。

TargetListBuilder
    ...
    public class TargetListBuilder {
        private String foo;
        private String bar;

        ...
    }
    ...

TargetListBuilder はインスタンスフィールドを持っており、1回のループの中で設定された情報を一時的に記録しておくことができるようにしている。

TargetListBuilder
    ...
    public class TargetListBuilder {
        ...
        public class BarBuilder {
            public TargetListBuilder bar(String value) {
                bar = value;
                return TargetListBuilder.this;
            }
        }

        public BarBuilder foo(String value) {
            this.save();
            return new TargetListBuilder().new FooBuilder().foo(value);
        }

        public AfterBuilder and() {
            this.save();
            return new AfterBuilder();
        }

        private void save() {
            Target target = new Target(foo, bar);
            targetList.add(target);
        }
    }
    ...

BarBuilder.bar() メソッドで bar の定義が終わったら、外部クラスの TargetListBuilder を返している。
TargetListBuilder には、引き続き次のリスト構築を続ける foo() メソッド5と、リスト構築を終了する and() メソッドが定義されており、いずれかを選択できるようにしている。

いずれを選択しても、内部では save() メソッドが呼ばれ、その時点で一時記録されていた情報を使ってリストに Target オブジェクトを追加するようにしている。

再利用

fluent.jpg

総称型を利用した方法

SomeBuilder
package fluent;

public class SomeBuilder<AFTER> {
    private final OuterBuilder<Some> outerBuilder; 
    private final AFTER afterBuilder;
    private String foo;

    public class FooBuilder {
        public BarBuilder foo(String value) {
            foo = value;
            return new BarBuilder();
        }
    }

    public class BarBuilder {
        public AFTER bar(String bar) {
            Some some = new Some(foo, bar);
            outerBuilder.receive(some);

            return afterBuilder;
        }
    }

    SomeBuilder(OuterBuilder<Some> outerBuilder, AFTER afterBuilder) {
        this.outerBuilder = outerBuilder;
        this.afterBuilder = afterBuilder;
    }
}
OuterBuilder
package fluent;

public interface OuterBuilder<T> {
    void receive(T t);
}
TargetBuilder
package fluent;

public class TargetBuilder implements OuterBuilder<Some> {
    private Some some;

    public class BeforeBuilder {
        public SomeBuilder<AfterBuilder>.FooBuilder before() {
            SomeBuilder<AfterBuilder> someBuilder =
                new SomeBuilder<>(TargetBuilder.this, new AfterBuilder());
            return someBuilder.new FooBuilder();
        }
    }

    public class AfterBuilder {
        public void after() {...}
    }

    @Override
    public void receive(Some some) {
        this.some = some;
    }

    private TargetBuilder() {}
}
利用イメージ
builder
    .before()
    .foo("FOO")
    .bar("BAR")
    .after();

また複雑になった。

フローやビルダーが異なっていても、一部は全く同じフローを共有したくなることがある。
例えばリストの各要素となるオブジェクトを生成するときに、オブジェクトを生成する部分のフローは別のビルダーに分割して他で再利用したくなることがあったりする。

この場合まず問題となるのは、「分割したビルダーの後のメソッドチェーンをどうやってつなげるか」という点になる。
後続の処理は呼び出し元によって異なるため、分割された再利用対象のビルダー自体は知ることができない。

これを実現するため、総称型を利用している。

SomeBuilder
public class SomeBuilder<AFTER> {
    ...
    private final AFTER afterBuilder;
    ...

    public class BarBuilder {
        public AFTER bar(String bar) {
            ...
            return afterBuilder;
        }
    }
    ...
}

型パラメータ <AFTER> を宣言し、 SomeBuilder の最後の処理である bar の戻り値として利用している。
SomeBuilder 自体は、この AFTER 型のオブジェクトに対してなにか処理を要求することはなく、ただ次に処理するビルダーとして保持しておき、最後に return しているだけになる。

AFTER 型が具体的に何になるかは、呼び出し元で指定することになる。

TargetBuilder
public class TargetBuilder implements OuterBuilder<Some> {
    ...

    public class BeforeBuilder {
        public SomeBuilder<AfterBuilder>.FooBuilder before() {
            SomeBuilder<AfterBuilder> someBuilder =
                new SomeBuilder<>(..., new AfterBuilder());
            return someBuilder.new FooBuilder();
        }
    }

    public class AfterBuilder {
        ...
    }

    ...
}

TargetBuilder で利用する場合、SomeBuilder の後続は AfterBuilder となる。
したがって SomeBuilder<AFTER> には AfterBuilder を指定する。
そして、 SomeBuilder のコンストラクタで AfterBuilder のインスタンスを渡しておく。

これにより、 SomeBuilder は次のビルダーの具体的な型を知ることなく、呼び出し元に応じてメソッドチェーンをつなげることができるようになる。

次に問題となるのが、分割されたビルダーで生成された情報をどうやって呼び出し元で受け取るか、ということになる。
いくつか方法が考えられるが、ここでは OuterBuilder というインターフェースを介する方法を用いている。

OuterBuilder
package fluent;

public interface OuterBuilder<T> {
    void receive(T t);
}

OuterBuilder には receive(T) というメソッドが定義されていて、型引数で指定された型の値を受け取ることができるようになっている。
この OuterBuilder は、再利用ビルダーを使う側のビルダーが実装する。

TargetBuilder
package fluent;

public class TargetBuilder implements OuterBuilder<Some> {
    private Some some;

    ...

    public class BeforeBuilder {
        public SomeBuilder<AfterBuilder>.FooBuilder before() {
            SomeBuilder<AfterBuilder> someBuilder =
                new SomeBuilder<>(TargetBuilder.this, ...);
            return someBuilder.new FooBuilder();
        }
    }

    public class AfterBuilder {...}

    @Override
    public void receive(Some some) {
        this.some = some;
    }

    private TargetBuilder() {}
}

ここでは TargetBuilderOuterBuilder を実装している。
そして、 SomeBuilder のコンストラクタ引数で TargetBuilder.this で自身のインスタンスを渡している。

SomeBuilder
package fluent;

public class SomeBuilder<AFTER> {
    private final OuterBuilder<Some> outerBuilder; 
    ...

    public class BarBuilder {
        public AFTER bar(String bar) {
            Some some = new Some(foo, bar);
            outerBuilder.receive(some);

            return afterBuilder;
        }
    }

    SomeBuilder(OuterBuilder<Some> outerBuilder, AFTER afterBuilder) {
        this.outerBuilder = outerBuilder;
        ...
    }
}

SomeBuilder では、一連のフローが完了し呼び出し元に戻すタイミング(bar() メソッドが呼ばれたとき)で、 OuterBuilderreceive() を使って構築した Some オブジェクトを渡している。

これらの総称型を利用した仕組みにより、1つのビルダーを複数の場所で再利用することができるようになる。

引数で構築されたオブジェクトを受け取る方法

総称型を使った方法はメソッドチェーンが途切れずエレガントになるが、実装が複雑になる。
一方でメソッドチェーンが途切れても良いのであれば、よりシンプルな方法として構築されたオブジェクトを引数として受け取る方法が考えられる。

SomeBuilder
package fluent;

public class SomeBuilder {
    private String foo;

    public static BarBuilder foo(String value) {
        SomeBuilder builder = new SomeBuilder();
        builder.foo = value;
        return builder.new BarBuilder();
    }

    public class BarBuilder {
        public Some bar(String bar) {
            return new Some(foo, bar);
        }
    }

    private SomeBuilder() {}
}
TargetBuilder
package fluent;

public class TargetBuilder {
    private Some some;

    public class BeforeBuilder {
        public SomeReceiveBuilder before() {
            return new SomeReceiveBuilder();
        }
    }

    public class SomeReceiveBuilder {
        public AfterBuilder some(Some value) {
            some = value;
            return new AfterBuilder();
        }
    }

    public class AfterBuilder {

        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
builder
    .before()
    .some(foo("FOO").bar("BAR"))
    .after();

TargetBuilder 側は、再利用される SomeBuilder が構築した Some オブジェクトを受け取る口(some() メソッド)だけを用意しておく。
そして、 some() メソッドの呼び出しの中で SomeBuilder を使った Some オブジェクトの構築を行っている。

メソッドチェーンが途切れ、 SomeBuilderfoo() メソッドを static インポートする必要があるが、総称型を使った方法よりかは仕組みは単純になる。

こちらの方法を利用する場合は、 SomeBuilderbuild() で終わらないスタイルを採用したほうが、見た目はいい感じになると思う。

入れ子のオブジェクトを作る

fluent.jpg

ビルダーの実装
package fluent;

public class TargetBuilder {
    private Some some;

    public class BeforeBuilder {
        public SomeBuilder.FooBuilder before() {
            return new SomeBuilder().new FooBuilder();
        }
    }

    public class SomeBuilder {
        private String foo;
        private String bar;

        public class FooBuilder {
            public BarBuilder foo(String value) {
                foo = value;
                return new BarBuilder();
            }
        }

        public class BarBuilder {
            public AfterBuilder bar(String value) {
                bar = value;
                saveSome();
                return new AfterBuilder();
            }
        }

        private void saveSome() {
            some = new Some(foo, bar);
        }
    }

    public class AfterBuilder {

        public void after() {...}
    }

    private TargetBuilder() {}
}
利用イメージ
builder
    .before()
    .foo("BOO")
    .bar("BAR")
    .after();

これはフローよりも、構築対象のオブジェクトの構造に関係する問題。

構築対象のオブジェクトが他のオブジェクトをフィールドとして持ち、そのオブジェクトの構築に複数の「定義」や「選択」が必要なケース。
ここでの例では、構築対象の Target オブジェクトが Some という別のオブジェクトをフィールドとして持ち、その構築に foo(), bar() の2ステップが必要となっている。

ループの場合と同じで、入れ子のオブジェクトを設定している間、一時的に設定値を記録しておく必要がある。
数が少なければ一番外側のビルダー(ここでは TargetBuilder)のインスタンスフィールドで記録しておくのもありだが、数が増えてくるとどのフィールドがどの入れ子オブジェクトのためのモノか分からなくなってきてつらい。

そこで、入れ子オブジェクトのために内部クラスを1つ挟んで、入れ子オブジェクトのための位置情報はそこで管理するようにする。
ここでの例では、 SomeBuilder がそのクラスに該当する。

一番外側のクラスのインスタンスフィールドは、構築後のオブジェクトだけを持つようにしている。

実装例

ここまでのパターンを基本に、組み合わせたり必要に応じた微調整を入れれば、だいたいの流れるようなインターフェースは実装できる(気がする)。

ということで、上の方で例として挙げていた項目定義のフローを実現するビルダーを実際に実装してみる。
ここでは、ループも組み込んだ形でフローを定義する。

実現するフロー

fluent.jpg

このビルダーによって構築するオブジェクトは、 FieldSpec というクラスの List になるようにする。

FieldSpec の構造

fluent.jpg

実装

FieldSpecListBuilder
package fluent.example;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class FieldSpecListBuilder {
    private List<FieldSpec> list = new ArrayList<>();

    public static FieldSpecBuilder.RequiredBuilder field(String name) {
        FieldSpecBuilder builder = new FieldSpecListBuilder().new FieldSpecBuilder(name);
        return builder.new RequiredBuilder();
    }

    public class FieldSpecBuilder {
        private final String name;
        private boolean required;
        private FieldSpec fieldSpec;

        private FieldSpecBuilder(String name) {
            this.name = name;
        }

        public class RequiredBuilder {
            public SelectFieldTypeBuilder required(boolean value) {
                required = value;
                return new SelectFieldTypeBuilder();
            }
        }

        public class SelectFieldTypeBuilder {
            public DateSpecBuilder.DateFormatBuilder date() {
                return new DateSpecBuilder().new DateFormatBuilder();
            }

            public IntegerSpecBuilder.MinIntegerBuilder integer() {
                return new IntegerSpecBuilder().new MinIntegerBuilder();
            }

            public DecimalSpecBuilder.MinDecimalBuilder decimal(int integerSize, int decimalSize) {
                return new DecimalSpecBuilder(integerSize, decimalSize).new MinDecimalBuilder();
            }

            public TextSpecBuilder.TextTypeBuilder text() {
                return new TextSpecBuilder().new TextTypeBuilder();
            }
        }

        public class DateSpecBuilder {
            private String format;
            private LocalDate min;
            private LocalDate max;

            public class DateFormatBuilder {
                public MinDateBuilder yyyyMMdd() {
                    format = "yyyyMMdd";
                    return new MinDateBuilder();
                }

                public MinDateBuilder yyMMdd() {
                    format = "yyMMdd";
                    return new MinDateBuilder();
                }
            }

            public class MinDateBuilder {
                public MaxDateBuilder greaterThan(int year, int month, int dayOfMonth) {
                    min = LocalDate.of(year, month, dayOfMonth);
                    return new MaxDateBuilder();
                }
            }

            public class MaxDateBuilder {
                public FieldSpecBuilder lessThan(int year, int month, int dayOfMonth) {
                    max = LocalDate.of(year, month, dayOfMonth);
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }

            private void saveSpec() {
                DateSpec dateSpec = new DateSpec(format, min, max);
                fieldSpec = new FieldSpec(name, required, dateSpec);
            }
        }

        public class IntegerSpecBuilder {
            private int min;
            private boolean minInclude;
            private int max;
            private boolean maxInclude;

            public class MinIntegerBuilder {
                public MaxIntegerBuilder greaterThan(int value) {
                    min = value;
                    minInclude = false;
                    return new MaxIntegerBuilder();
                }

                public MaxIntegerBuilder greaterThanEqual(int value) {
                    min = value;
                    minInclude = true;
                    return new MaxIntegerBuilder();
                }
            }

            public class MaxIntegerBuilder {
                public FieldSpecBuilder lessThan(int value) {
                    max = value;
                    maxInclude = false;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }

                public FieldSpecBuilder lessThanEqual(int value) {
                    max = value;
                    maxInclude = true;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }

            private void saveSpec() {
                IntegerSpec integerSpec = new IntegerSpec(min, minInclude, max, maxInclude);
                fieldSpec = new FieldSpec(name, required, integerSpec);
            }
        }

        public class DecimalSpecBuilder {
            private int integerSize;
            private int decimalSize;
            private double min;
            private boolean minInclude;
            private double max;
            private boolean maxInclude;

            private DecimalSpecBuilder(int integerSize, int decimalSize) {
                this.integerSize = integerSize;
                this.decimalSize = decimalSize;
            }

            public class MinDecimalBuilder {
                public MaxDecimalBuilder greaterThan(double value) {
                    min = value;
                    minInclude = false;
                    return new MaxDecimalBuilder();
                }

                public MaxDecimalBuilder greaterThanEqual(double value) {
                    max = value;
                    minInclude = true;
                    return new MaxDecimalBuilder();
                }
            }

            public class MaxDecimalBuilder {
                public FieldSpecBuilder lessThan(double value) {
                    max = value;
                    maxInclude = false;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }

                public FieldSpecBuilder lessThanEqual(double value) {
                    max = value;
                    maxInclude = true;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }

            private void saveSpec() {
                DecimalSpec decimalSpec = new DecimalSpec(integerSize, decimalSize, min, minInclude, max, maxInclude);
                fieldSpec = new FieldSpec(name, required, decimalSpec);
            }
        }

        public class TextSpecBuilder {
            private TextSpec.TextType type;
            private int minLength;
            private int maxLength;

            public class TextTypeBuilder {
                public TextMinLengthBuilder textType(TextSpec.TextType value) {
                    type = value;
                    return new TextMinLengthBuilder();
                }
            }

            public class TextMinLengthBuilder {
                public TextMaxLengthBuilder minLength(int value) {
                    minLength = value;
                    return new TextMaxLengthBuilder();
                }
            }

            public class TextMaxLengthBuilder {
                public FieldSpecBuilder maxLength(int value) {
                    maxLength = value;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }

            private void saveSpec() {
                TextSpec textSpec = new TextSpec(type, minLength, maxLength);
                fieldSpec = new FieldSpec(name, required, textSpec);
            }
        }

        public RequiredBuilder field(String name) {
            save();
            return new FieldSpecBuilder(name).new RequiredBuilder();
        }

        public List<FieldSpec> build() {
            save();
            return list;
        }

        private void save() {
            list.add(fieldSpec);
        }
    }

    private FieldSpecListBuilder() {}
}
利用例
List<FieldSpec> fieldSpecList =
    FieldSpecListBuilder
            .field("foo")
                .required(false)
                .date()
                    .yyyyMMdd()
                    .greaterThan(2019, 1, 1)
                    .lessThan(2019, 12, 31)
            .field("bar")
                .required(true)
                .integer()
                    .greaterThan(0)
                    .lessThan(100)
            .field("fizz")
                .required(true)
                .decimal(3, 2)
                .greaterThanEqual(0.0)
                .lessThanEqual(100.0)
            .field("buzz")
                .required(false)
                .text()
                .textType(TextSpec.TextType.ALPHABET)
                .minLength(1)
                .maxLength(10)
            .build();

こんな感じになった。


  1. 説明のために簡単なクラスにしているので、そもそもこの程度のクラスにビルダーを作る必要があるかという話はおいておく 

  2. 戻り値の型に存在しないメソッドを呼ぼうとした場合など 

  3. アクティビティ図を利用しているだけで、アクティビティ図として厳密に正しいかどうかは気にしない。 

  4. 必ずまとめられるとは限らず、無理にまとめると意味が分かりにくくなるような場合もあると思う(ケースバイケース) 

  5. いきなり次の foo を設定するのではなく、FooBuilder next() のようなメソッドにして1ステップ挟む方法も考えられる。 

33
29
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
29