Created
November 6, 2020 12:00
-
-
Save RayStarkMC/4aec65ce4ecc5c033b6f71f764917718 to your computer and use it in GitHub Desktop.
幽霊型を用いた不変オブジェクトの型安全な不変ビルダのサンプルコード
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
解説
これは何?
幽霊型と呼ばれる型変数を用いて必須パラメータの入力について型安全なビルダーを実装したものです。
更にビルダー、データ型共に全て不変ですのでスレッドセーフに使いまわせます。
各型の解説
Data
複数のパラメータを持っていて愚直な実装だとコンストラクタが複雑になるデータ型です。
Dataに必要なパラメータについては以下の通りです。
必須パラメータ
オプションパラメータ
をそれぞれ持ちます。
生成が複雑になりがちなので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を生成しています。
幽霊型とは?
型変数としては登場するが、コンパイル時のみ利用され実行時には一切関与しない型。
幽霊型で検知できるのは特定のメソッドが呼ばれたか否かを静的に検査する事です。
必須パラメータの入力を検知する方法
以下の二つが前提になります
これを用いて
という流れでDataを生成しています。
必須パラメータに対応するメソッドが呼ばれていない場合、対応する型がNGのままなのでビルダインスタンスをbuildメソッドに渡すとコンパイルエラーになってくれます。
不変性、再利用性について
Builder, Dataともに全てのフィールドはfinalであり、不変な事が保証されています。なのでこれらはスレッドセーフに再利用可能です。
ところでBuilderの全てのメソッドは元のBuilderとの差分を渡す事で差分だけ設定され他のパラメータがコピーされたクラスを返す設計になっています。つまりBuilderの特定時点の参照を保持しておいて、必要に応じて任意のパラメータについての差分だけ設定する事で何度も再利用するという事が出来るようになっています。
まとめ
書くのはめちゃくちゃ大変だけど、幽霊型を使う事で必須パラメータ入力について型安全でかつ非常に柔軟なビルダーを作ることができます。
参考
このGistはがくぞ(@gakuzzzz)氏がBurikaigi2020にて発表されていたJavaでScalaのType Safe Builderパターンをエミュレートするという内容と同じものを私が個人的にまとめたものです。本Gistでは必須パラメータ、オプションパラメータのみですがオリジナルの発表ではオブジェクトの不変条件を設定するなど更に具体的な例が説明されています。Javaの限られた型システムをフル活用してるのが最高にエモいですね!