Derive4Jの紹介
1. 序章
Derive4Jは、Java8のさまざまな機能概念を可能にする注釈プロセッサです。
このチュートリアルでは、Derive4Jと、フレームワークによって実現される最も重要な概念を紹介します。
- 代数的データ型
- 構造パターンマッチング
- ファーストクラスの怠惰
2. Mavenの依存関係
Derive4Jを使用するには、プロジェクトに依存関係を含める必要があります。
<dependency>
<groupId>org.derive4j</groupId>
<artifactId>derive4j</artifactId>
<version>1.1.0</version>
<optional>true</optional>
</dependency>
3. 代数的データ型
3.1. 説明
代数的データ型(ADT)は一種の複合型であり、他の型またはジェネリックの組み合わせです。
ADTは通常、次の2つの主要なカテゴリに分類されます。
- 和
- 製品
代数的データ型は、HaskellやScalaなどの多くの言語でデフォルトで存在します。
3.2. 合計タイプ
列挙型は、Javaで合計型に最も近いものです。 Enum には可能な値のセットがありますが、一度に持つことができるのは1つだけです。 ただし、 JavaのEnumに追加のデータを関連付けることはできません。これは、Enumに対する代数的データ型の主な利点です。
3.3. 製品タイプ
JavaのClassは、製品タイプと見なすことができます。 製品タイプは、それらのフィールドの組み合わせによって定義されます。
ADT の詳細については、このWikipediaの記事を参照してください。
3.4. 使用法
一般的に使用される代数的データ型の1つは
抽象クラスまたはインターフェースに、ADTの構造を生成するためにDerive4Jが使用する少なくとも1つの抽象メソッドで注釈を付ける必要があります。
Derive4Jでいずれかのデータ型を作成するには、インターフェイスを作成する必要があります。
@Data
interface Either<A, B> {
<X> X match(Function<A, X> left, Function<B, X> right);
}
インターフェイスには@Dataの注釈が付けられており、Derive4Jが適切なコードを生成できるようになります。 生成されたコードには、ファクトリメソッド、レイジーコンストラクター、およびその他のさまざまなメソッドが含まれています。
デフォルトでは、生成されたコードは、注釈付きのクラスの名前を取得しますが、複数形です。 ただし、inClassパラメーターを使用して構成する可能性があります。
これで、生成されたコードを使用して Either ADTを作成し、正しく機能していることを確認できます。
public void testEitherIsCreatedFromRight() {
Either<Exception, String> either = Eithers.right("Okay");
Optional<Exception> leftOptional = Eithers.getLeft(either);
Optional<String> rightOptional = Eithers.getRight(either);
Assertions.assertThat(leftOptional).isEmpty();
Assertions.assertThat(rightOptional).hasValue("Okay");
}
生成されたmatch()メソッドを使用して、
public void testEitherIsMatchedWithRight() {
Either<Exception, String> either = Eithers.right("Okay");
Function<Exception, String> leftFunction = Mockito.mock(Function.class);
Function<String, String> rightFunction = Mockito.mock(Function.class);
either.match(leftFunction, rightFunction);
Mockito.verify(rightFunction, Mockito.times(1)).apply("Okay");
Mockito.verify(leftFunction, Mockito.times(0)).apply(Mockito.any(Exception.class));
}
4. パターンマッチング
代数的データ型の使用によって可能になる機能の1つは、パターンマッチングです。
パターンマッチングを使用するには、HTTPリクエストをモデル化するクラスを作成します。 ユーザーは、指定されたHTTPメソッドのいずれかを使用できるようになります。
- 得る
- 役職
- 消去
- 置く
HTTPRequest インターフェースから始めて、Derive4JでリクエストクラスをADTとしてモデル化してみましょう。
@Data
interface HTTPRequest {
interface Cases<R>{
R GET(String path);
R POST(String path);
R PUT(String path);
R DELETE(String path);
}
<R> R match(Cases<R> method);
}
生成されたクラスHttpRequests(複数形に注意)により、リクエストのタイプに基づいてパターンマッチングを実行できるようになります。
この目的のために、リクエストのタイプに応じて異なるStatusで応答する非常に単純なHTTPServerクラスを作成します。
まず、サーバーからクライアントへの応答として機能する単純なHTTPResponseクラスを作成しましょう。
public class HTTPResponse {
int statusCode;
String responseBody;
public HTTPResponse(int statusCode, String responseBody) {
this.statusCode = statusCode;
this.responseBody = responseBody;
}
}
次に、パターンマッチングを使用して適切な応答を送信するサーバーを作成できます。
public class HTTPServer {
public static String GET_RESPONSE_BODY = "Success!";
public static String PUT_RESPONSE_BODY = "Resource Created!";
public static String POST_RESPONSE_BODY = "Resource Updated!";
public static String DELETE_RESPONSE_BODY = "Resource Deleted!";
public HTTPResponse acceptRequest(HTTPRequest request) {
return HTTPRequests.caseOf(request)
.GET((path) -> new HTTPResponse(200, GET_RESPONSE_BODY))
.POST((path,body) -> new HTTPResponse(201, POST_RESPONSE_BODY))
.PUT((path,body) -> new HTTPResponse(200, PUT_RESPONSE_BODY))
.DELETE(path -> new HTTPResponse(200, DELETE_RESPONSE_BODY));
}
}
classのacceptRequest()メソッドは、リクエストのタイプにパターンマッチングを使用し、リクエストのタイプに基づいて異なる応答を返します。
@Test
public void whenRequestReachesServer_thenProperResponseIsReturned() {
HTTPServer server = new HTTPServer();
HTTPRequest postRequest = HTTPRequests.POST("http://test.com/post", "Resource");
HTTPResponse response = server.acceptRequest(postRequest);
Assert.assertEquals(201, response.getStatusCode());
Assert.assertEquals(HTTPServer.POST_RESPONSE_BODY, response.getResponseBody());
}
5. ファーストクラスの怠惰
Derive4Jを使用すると、怠惰の概念を導入できます。つまり、オブジェクトに対して操作を実行するまで、オブジェクトは初期化されません。 interfaceをLazyRequestとして宣言し、生成されたクラスをLazyRequestImplという名前に構成しましょう。
@Data(value = @Derive(
inClass = "{ClassName}Impl",
make = {Make.lazyConstructor, Make.constructors}
))
public interface LazyRequest {
interface Cases<R>{
R GET(String path);
R POST(String path, String body);
R PUT(String path, String body);
R DELETE(String path);
}
<R> R match(LazyRequest.Cases<R> method);
}
これで、生成されたレイジーコンストラクターが正常に機能していることを確認できます。
@Test
public void whenRequestIsReferenced_thenRequestIsLazilyContructed() {
LazyRequestSupplier mockSupplier = Mockito.spy(new LazyRequestSupplier());
LazyRequest request = LazyRequestImpl.lazy(() -> mockSupplier.get());
Mockito.verify(mockSupplier, Mockito.times(0)).get();
Assert.assertEquals(LazyRequestImpl.getPath(request), "http://test.com/get");
Mockito.verify(mockSupplier, Mockito.times(1)).get();
}
class LazyRequestSupplier implements Supplier<LazyRequest> {
@Override
public LazyRequest get() {
return LazyRequestImpl.GET("http://test.com/get");
}
}
ファーストクラスの怠惰と例の詳細については、Scalaのドキュメントを参照してください。
6. 結論
このチュートリアルでは、Derive4Jライブラリを紹介し、それを使用して、代数的データ型やパターンマッチングなど、通常はJavaでは使用できないいくつかの機能概念を実装しました。
ライブラリの詳細については、Derive4Jの公式ドキュメントのを参照してください。
いつものように、すべてのコードサンプルはGitHubで見つけることができます。