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. 合計タイプ

Sumは、論理OR演算を表すデータ型です。 これは、それが1つまたは別のものである可能性があるが、両方ではない可能性があることを意味します。 簡単に言えば、合計タイプはさまざまなケースのセットです。 「合計」という名前は、個別の値の総数がケースの総数であるという事実に由来しています。

列挙型は、Javaで合計型に最も近いものです。 Enum には可能な値のセットがありますが、一度に持つことができるのは1つだけです。 ただし、 JavaのEnumに追加のデータを関連付けることはできません。これは、Enumに対する代数的データ型の主な利点です。

3.3. 製品タイプ

Productは、論理AND演算を表すデータ型です。 これは、いくつかの値の組み合わせです。

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));
    }
}

classacceptRequest()メソッドは、リクエストのタイプにパターンマッチングを使用し、リクエストのタイプに基づいて異なる応答を返します。

@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を使用すると、怠惰の概念を導入できます。つまり、オブジェクトに対して操作を実行するまで、オブジェクトは初期化されません。 interfaceLazyRequestとして宣言し、生成されたクラスを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で見つけることができます。