Java

Builderを継承したBuilderを作ってハマったお話

More than 1 year has passed since last update.

はじめに

冷静に考えれば当たり前のことなんですけど、ちょっと考え込んでしまったのでメモとして……

Builderを作る

Getter / Setterは省略しますが縦と横の長さをフィールドに持つSquareクラスがあります。

Square.java
public class Square {
  private int width;
  private int height;
}

これをBuilderを使って値を設定できるようにし、最後に面積を返すようなクラスを作ると以下のようになると思います。

SquareBuilder.java
public class SquareBuilder {

  public static class Builder {

    Square square = new Square();

    public Builder() {}

    public Builder width(int width) {
      square.setWidth(width);
      return this;
    }

    public Builder height(int height) {
      square.setHeight(height);
      return this;
    }

    public int build() {
      return square.getWidth() * square.getHeight();
    }
  }
}

すると、こんな感じで面積を求めることができます

Main.java
public class Main {
  public static void main(String[] args) {
    SquareBuilder.Builder squareBuilder = new SquareBuilder.Builder();
    System.out.println("面積は:" + squareBuilder.width(5).height(10).build());
  }
}

ここまでは特に問題はありません。:slight_smile:

Builderを継承したBuilderを作る

次にSquareを継承したCubeクラスを作成します。(Getter / Setter は省略します。)

Cube.java
public class Cube extends Square {
  private int depth;
}

高さがdepthで良かったのかはさておき……:kissing:
同じくBuilderも継承します

CubeBuilder.java
public class CubeBuilder extends SquareBuilder {

  public static class Builder extends SquareBuilder.Builder {

    private Cube cube = new Cube();

    public Builder() {}

    public Builder depth(int depth) {
      cube.setDepth(depth);
      return this;
    }

    @Override
    public int build() {
      return super.build() * cube.getDepth();
    }
  }
}

すると、こんな感じで体積を求めることができます。

Main.java
public class Main {
  public static void main(String[] args) {
    CubeBuilder.Builder cubeBuilder = new CubeBuilder.Builder();
    System.out.println("体積は:" + cubeBuilder.depth(2).width(5).height(10).build());
  }
}

このコードはコンパイルを通りますし体積も求められますが、実は問題が。
体積を求めるにはdepth()を最初に呼ばないといけないのです。:worried:

理由は、width()height()の戻り値はSquareBuilder.Builder型のため、CubeBuilder.Builderでしか定義していないdepth()を呼ぶことができないからです。

ここでジェネリクスを使います。

参考:Subclassing a Java Builder class

SquareBuilder.java
public class SquareBuilder {

  public static class Builder<T extends Builder<T>> {

    Square square = new Square();

    public Builder() {}

    public T width(int width) {
      square.setWidth(width);
      return (T) this;
    }

    public T height(int height) {
      square.setHeight(height);
      return (T) this;
    }

    public int build() {
      return square.getWidth() * square.getHeight();
    }
  }
}
CubeBuilder.java
public class CubeBuilder extends SquareBuilder {

  public static class Builder extends SquareBuilder.Builder<Builder> {

    private Cube cube = new Cube();

    public Builder() {}

    public Builder depth(int depth) {
      cube.setDepth(depth);
      return this;
    }

    @Override
    public int build() {
      return super.build() * cube.getDepth();
    }
  }
}

これにより、width()height()の返り値の型がSquareBuilder.BuilderではなくSquareBuilder.Builderを継承したクラスになるのでdepth()を先に呼ぶ必要がなくなります。

Main.java
public class Main {
  public static void main(String[] args) {
    SquareBuilder.Builder squareBuilder = new SquareBuilder.Builder();
    System.out.println("面積は:" + squareBuilder.width(5).height(10).build());

    CubeBuilder.Builder cubeBuilder = new CubeBuilder.Builder();
    System.out.println("体積は:" + cubeBuilder.width(5).height(10).depth(2).build());
  }
}

ですが、Warningが出るのでちょっと気持ち悪いです。:mask:

投稿した経緯

大量のフィールドを持つPOJOに値を入れてJSON形式に出力するプログラムで、いちいちSetterで値を入れるのが面倒になり、Builderを活用としてハマったので。

おまけ

今回の例では面積を求められなくなりますが、SquareBuilderを抽象クラスにすることによってWarningを無くすこともできます。

SquareBuilder.java
public class SquareBuilder {

  public abstract static class Builder<T extends Builder<T>> {

    Square square = new Square();

    public Builder() {}

    public abstract T getThis();

    public T width(int width) {
      square.setWidth(width);
      return getThis();
    }

    public T height(int height) {
      square.setHeight(height);
      return getThis();
    }

    public int build() {
      return square.getWidth() * square.getHeight();
    }
  }
}
CubeBuilder.java
public class CubeBuilder extends SquareBuilder {

  public static class Builder extends SquareBuilder.Builder<Builder> {

    private Cube cube = new Cube();

    public Builder() {}

    @Override
    public Builder getThis() {
      return this;
    }

    public Builder depth(int depth) {
      cube.setDepth(depth);
      return this;
    }

    @Override
    public int build() {
      return super.build() * cube.getDepth();
    }
  }
}