今時builderライブラリも無いだろという感じですが,書いたのです.
github.com
Motivationにも書いているのですが,普通に作ったJavaのbuilderは不完全なインスタンスを作成できる可能性を孕んでいます.注意深くコードを書けば問題ないかもしれませんが,えてして我々 (主語が大きい) はミスを犯しがちです.そしてNPEがやってきて……我々はしぬ.
というわけで今回作ったのがarnoldです.
端的にいうと「必要なすべての要素を取りこぼさないbuilder」であり,「そのジェネレータ (annotation processor)」でもあります.Basic usageにあるように,
import net.moznion.arnold.annotation.ArnoldBuilder;
import net.moznion.arnold.annotation.Required;
@ArnoldBuilder
public class Example {
@Required
private String foo;
@Required
private int bar;
@Required
private double buz;
}
というようにclassに @ArnoldBuilder
を付与することでinstantiate対象のclassであることを示し,builderでbuildしたいフィールドについては @Required
を付与することで必要なフィールドであることを示すことができます.そしてプロジェクトをビルド *1 するとAnnotation Processorが走り,builder classが自動生成されます.
自動生成されたbuilderはこのように使うことができます.
Example example = new ExampleBuilder().foo("foo")
.bar(42)
.buz(2.71828)
.build();
このBuilderは分解して書いてみると
ExampleBuilder exampleBuilder = new ExampleBuilder();
ExampleBuilder1 exampleBuilder1 = exampleBuilder.foo("foo");
ExampleBuilder2 exampleBuilder2 = exampleBuilder1.bar(42);
ExampleBuilder3 exampleBuilder3 = exampleBuilder2.buz(2.71828);
Example example = exampleBuilder3.build();
というふうになっており,つまり
ExampleBuilder
は foo
を受け取って ExampleBuilder1
を返すsetterを提供する
ExampleBuilder1
は bar
を受け取って ExampleBuilder2
を返すsetterを提供する
ExampleBuilder2
は buz
を受け取って ExampleBuilder3
を返すsetterを提供する
ExampleBuilder2
は build()
を提供する
というようなコードをAnnotation Processorが一式生成するという挙動となっています.なかなか富豪的ですね.
このように,1つのbuilder (もはやbuilderではない気もしますが) が高々1つの要素を受け取り次のbuilderを返却し,終端のbuilderが build()
して所望のclassのインスタンスをinstanciateする,という仕組みにすることで
- 「build途中の不完全な状態のインスタンス」は何らかのbuilder classになるので,build対象のインスタンスをinstantiateすることは出来ない
- 必須なフィールドがすべて揃ったらその時点でbuilderは
build()
を提供し,ようやくbuild対象のインスタンスをinstantiate出来るようになる
という安全な挙動を実現しています.
今後の課題としてはprivate classのinstantiateができないというものがあり,これはなんとかしたい *2.
それはそうと今回Javaのコード生成には square/javapoet を使ってみたんですが,使いやすく,なおかつパワフルで良いですね.
現場からは以上です.ぜひご利用下さい.