Skip to content

Instantly share code, notes, and snippets.

@RayStarkMC
Created November 6, 2020 12:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RayStarkMC/4aec65ce4ecc5c033b6f71f764917718 to your computer and use it in GitHub Desktop.
Save RayStarkMC/4aec65ce4ecc5c033b6f71f764917718 to your computer and use it in GitHub Desktop.
幽霊型を用いた不変オブジェクトの型安全な不変ビルダのサンプルコード
public class PhantomBuilderSample {
public static class Data {
private final int field1;
private final int field2;
private final int field3;
private final int optField1;
private final int optField2;
private Data(Builder<OK, OK, OK> builder) {
field1 = builder.field1;
field2 = builder.field2;
field3 = builder.field3;
optField1 = builder.optField1;
optField2 = builder.optField2;
}
public static Builder<NG, NG, NG> builder() {
return new Builder<>();
}
public static Data build(Builder<OK, OK, OK> builder) {
return new Data(builder);
}
public int getField1() { return field1; }
public int getField2() { return field2; }
public int getField3() { return field3; }
public int getOptField1() { return optField1; }
public int getOptField2() { return optField2; }
@Override
public String toString() {
return "Data{" +
"field1=" + field1 +
", field2=" + field2 +
", field3=" + field3 +
", optField1=" + optField1 +
", optField2=" + optField2 +
'}';
}
public static class Status { private Status() {} }
public static final class OK extends Status { private OK() {}}
public static final class NG extends Status { private NG() {} }
public static class Builder<
Field1 extends Status,
Field2 extends Status,
Field3 extends Status
> {
private final int field1;
private final int field2;
private final int field3;
private final int optField1;
private final int optField2;
private Builder() {
this(0, 0, 0, 0, 0);
}
private Builder(int field1, int field2, int field3, int optField1, int optField2) {
this.field1 = field1;
this.field2 = field2;
this.field3 = field3;
this.optField1 = optField1;
this.optField2 = optField2;
}
public Builder<OK, Field2, Field3> withField1(int field1) {
return new Builder<>(field1, field2, field3, optField1, optField2);
}
public Builder<Field1, OK, Field2> withField2(int field2) {
return new Builder<>(field1, field2, field3, optField1, optField2);
}
public Builder<Field1, Field2, OK> withField3(int field3) {
return new Builder<>(field1, field2, field3, optField1, optField2);
}
public Builder<Field1, Field2, Field3> withOptField1(int optField1) {
return new Builder<>(field1, field2, field3, optField1, optField2);
}
public Builder<Field1, Field2, Field3> withOptField2(int optField2) {
return new Builder<>(field1, field2, field3, optField1, optField2);
}
}
}
public static void main(String[] args) {
var data = Data.build(
Data.builder()
.withField1(1)
.withField2(2)
.withField3(3)
.withOptField1(4)
);
System.out.println(data);
}
}
@RayStarkMC
Copy link
Author

RayStarkMC commented Nov 6, 2020

解説

これは何?

幽霊型と呼ばれる型変数を用いて必須パラメータの入力について型安全なビルダーを実装したものです。
更にビルダー、データ型共に全て不変ですのでスレッドセーフに使いまわせます。

各型の解説

Data

複数のパラメータを持っていて愚直な実装だとコンストラクタが複雑になるデータ型です。
Dataに必要なパラメータについては以下の通りです。
必須パラメータ

  • field1
  • field2
  • field3

オプションパラメータ

  • optField1 default 0
  • optField2 default 0

をそれぞれ持ちます。

生成が複雑になりがちなのでBuilder型を提供し、更にbuilderメソッドとbuildメソッドを提供します。

Builder

Dataに対するビルダを表す型です。型変数としてField1, Field2, Field3を持っており、これらの型はメソッド呼び出しによって変わります。
Field1, Field2, Field3は実装には一切関与していません。このような型の事を幽霊型と言うらしいです。
Dataのbuilderメソッドによりインスタンスを取得し、適切にメソッドを呼び出した後のインスタンスをDataのbuildメソッドに渡して利用します。

Status, OK, NG

幽霊型として使われる型です。
Builderの処理には一切関与しません。 これらの型はコンパイル時のみ利用され、実行時使われることはありません。
実行時に使われることを防ぐためにコンストラクタはprivateに設定されています。
Statusはメソッド呼び出しがされたか否かのどちらかの情報を持っている型です。
OKは特定メソッドが呼び出された事を表します。
NGは特定メソッドが呼び出されていない事を表します。

PhantomBuilderSample

サンプルコードのクラスです。一番下のmainメソッドで実際にBuilderを利用してDataを生成しています。

幽霊型とは?

型変数としては登場するが、コンパイル時のみ利用され実行時には一切関与しない型
幽霊型で検知できるのは特定のメソッドが呼ばれたか否かを静的に検査する事です。

必須パラメータの入力を検知する方法

以下の二つが前提になります

  • Javaではメソッド呼び出し前後で型変数の型を確定する事が出来ます。
  • 更に型変数の特定の型変数であるジェネリック型インスタンスを受け取るメソッドを定義することができます。

これを用いて

  1. 予め全てのビルダの呼び出し必須のメソッドに対応する型変数を全てNGで作成しておき
  2. 必須パラメータを入力するメソッドが呼び出された時だけ型変数の型を元の型(NGとは限らない!)からOKに変えて
  3. 三つの型変数全てがOKになっているBuilderのみ受け取る事が出来るbuildメソッドにインスタンスを渡す。

という流れでDataを生成しています。
必須パラメータに対応するメソッドが呼ばれていない場合、対応する型がNGのままなのでビルダインスタンスをbuildメソッドに渡すとコンパイルエラーになってくれます。

不変性、再利用性について

Builder, Dataともに全てのフィールドはfinalであり、不変な事が保証されています。なのでこれらはスレッドセーフに再利用可能です。
ところでBuilderの全てのメソッドは元のBuilderとの差分を渡す事で差分だけ設定され他のパラメータがコピーされたクラスを返す設計になっています。つまりBuilderの特定時点の参照を保持しておいて、必要に応じて任意のパラメータについての差分だけ設定する事で何度も再利用するという事が出来るようになっています。

まとめ

書くのはめちゃくちゃ大変だけど、幽霊型を使う事で必須パラメータ入力について型安全でかつ非常に柔軟なビルダーを作ることができます。

参考

このGistはがくぞ(@gakuzzzz)氏がBurikaigi2020にて発表されていたJavaでScalaのType Safe Builderパターンをエミュレートするという内容と同じものを私が個人的にまとめたものです。本Gistでは必須パラメータ、オプションパラメータのみですがオリジナルの発表ではオブジェクトの不変条件を設定するなど更に具体的な例が説明されています。Javaの限られた型システムをフル活用してるのが最高にエモいですね!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment