1. 序章

このクイックチュートリアルでは、Lombokでビルダーパターンを使用するときに属性のデフォルト値を提供する方法を調査します。

ロンボクの紹介もぜひチェックしてください。

2. 依存関係

We’ll use Lombok in this tutorial, so we only need one dependency:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
    <scope>provided</scope>
</dependency>

3. LombokBuilderを使用したPOJO

まず、Lombokがビルダーパターンの実装に必要なボイラープレートコードを取り除くのにどのように役立つかを見てみましょう。

簡単なPOJOから始めましょう。

public class Pojo {
    private String name;
    private boolean original;
}

For this class to be useful, we’ll need getters. Also, if we wish to use this class with an ORM, we’ll probably need a default constructor.

これらに加えて、このクラスのビルダーが必要です。 Lombokを使用すると、いくつかの簡単な注釈を付けてこれらすべてを実行できます。

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Pojo {
    private String name;
    private boolean original;
}

4. 期待の定義

単体テストの形で達成したいことに対するいくつかの期待を定義しましょう。

The first and most basic requirement is the presence of default values after we build an object with a builder:

@Test
public void givenBuilderWithDefaultValue_ThanDefaultValueIsPresent() {
    Pojo build = Pojo.builder()
        .build();
    Assert.assertEquals("foo", build.getName());
    Assert.assertTrue(build.isOriginal());
}

Of course, this test fails, since the @Builder annotation doesn’t populate values. We’ll fix this soon.

ORMを使用する場合、通常はデフォルトのコンストラクターに依存します。 So we should expect the same behavior from the default constructor as we do from the builder:

@Test
public void givenBuilderWithDefaultValue_NoArgsWorksAlso() {
    Pojo build = Pojo.builder()
        .build();
    Pojo pojo = new Pojo();
    Assert.assertEquals(build.getName(), pojo.getName());
    Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}

この段階で、このテストは合格です。

Now let’s see how we can make both tests pass.

5. ロンボクのBuilder.Defaultアノテーション

Lombok v1.16.16以降、@Builderの内部アノテーションを使用できます。

// class annotations as before
public class Pojo {
    @Builder.Default
    private String name = "foo";
    @Builder.Default
    private boolean original = true;
}

シンプルで読みやすいですが、いくつかの欠点があります。

The default values will be present with the builder, making the first test case pass. Unfortunately, the no-args constructor won’t get the default values, making the second test case fail, even if the no-args constructor isn’t generated, but explicitly written.

This side effect of the Builder.Default annotation has been present from the beginning, and it’ll probably be with us for a long time.

6. Builderを初期化します

最小限のビルダー実装でデフォルト値を定義することにより、両方のテストに合格するように試みることができます。

// class annotations as before
public class Pojo {
    private String name = "foo";
    private boolean original = true;

    public static class PojoBuilder {
        private String name = "foo";
        private boolean original = true;
    }
}

このようにして、両方のテストに合格します。

残念ながら、価格はコードの重複です。 数十のフィールドを持つPOJOの場合、二重初期化を維持するとエラーが発生しやすくなります。

But if we’re willing to pay this price, we should take care of one more thing, too. IDE内でリファクタリングを使用してクラスの名前を変更した場合、静的内部クラスの名前は自動的に変更されません。 Then Lombok won’t find it and our code will break.

このリスクを排除するために、ビルダーアノテーションを装飾することができます。

// class annotations as before
@Builder(builderClassName = "PojoBuilder")
public class Pojo {
    private String name = "foo";
    private boolean original = true;

    public static class PojoBuilder {
        private String name = "foo";
        private boolean original = true;
    }
}

7. toBuilderを使用する

@Builder は、元のクラスのインスタンスからビルダーのインスタンスを生成することもサポートしています。 This feature isn’t enabled by default. これを有効にするには、ビルダーアノテーションでtoBuilderパラメーターを設定します。

// class annotations as before
@Builder(toBuilder = true)
public class Pojo {
    private String name = "foo";
    private boolean original = true;
}

これにより、二重初期化を取り除くことができます。

Of course, there’s a price for that. We have to instantiate the class to create a builder. So we have to modify our tests also:

@Test
public void givenBuilderWithDefaultValue_ThenDefaultValueIsPresent() {
    Pojo build =  new Pojo().toBuilder()
        .build();
    Assert.assertEquals("foo", build.getName());
    Assert.assertTrue(build.isOriginal());
}

@Test
public void givenBuilderWithDefaultValue_thenNoArgsWorksAlso() {
    Pojo build = new Pojo().toBuilder()
        .build();
    Pojo pojo = new Pojo();
    Assert.assertEquals(build.getName(), pojo.getName());
    Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}

繰り返しになりますが、両方のテストに合格するため、no-argsコンストラクターを使用した場合のデフォルト値はビルダーを使用した場合と同じになります。

8. 結論

In this article, we explored several options to provide default values for the Lombok builder.

BuilderDefaultアノテーションの副作用は注目に値します。 But the other options also have their drawbacks, so we have to choose carefully based on the current situation.

いつものように、コードはGitHubから入手できます。