Перейти к основному содержимому

Введение в Deriv4J

· 6 мин. чтения

1. Введение

Derive4J — это процессор аннотаций, реализующий различные функциональные концепции в Java 8.

В этом руководстве мы познакомим вас с 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 делятся на две основные категории:

  • сумма
  • товар

Алгебраические типы данных присутствуют по умолчанию во многих языках, таких как Haskell и Scala.

3.2. Тип суммы

Сумма — это тип данных, представляющий логическую операцию ИЛИ. Это означает, что это может быть что-то одно, но не то и другое одновременно. Проще говоря, тип суммы — это набор различных падежей. Название «сумма» происходит от того факта, что общее количество различных значений равно общему количеству наблюдений.

Enum ближе всего к типу суммы в Java. Enum имеет набор возможных значений, но может иметь только одно из них за раз. Однако мы не можем связать какие-либо дополнительные данные с Enum в Java, что является основным преимуществом алгебраических типов данных перед Enum .

3.3. Тип продукта

Продукт — это тип данных, представляющий логическую операцию И. Это сочетание нескольких значений.

Класс в Java можно рассматривать как тип продукта. Типы продуктов определяются комбинацией их полей в целом.

Дополнительную информацию о АТД мы можем найти в этой статье в Википедии .

3.4. Применение

Один из наиболее часто используемых алгебраических типов данных — « Либо». Либо можно рассматривать как более сложный необязательный параметр, который можно использовать, когда есть возможность пропустить значения или операция может привести к исключению.

Нам нужно аннотировать абстрактный класс или интерфейс хотя бы одним абстрактным методом, который будет использоваться Derive4J для создания структуры нашего АТД.

Чтобы создать тип данных Both в Derive4J , нам нужно создать интерфейс :

@Data
interface Either<A, B> {
<X> X match(Function<A, X> left, Function<B, X> right);
}

Наш интерфейс снабжен аннотацией @Data , что позволит Derive4J сгенерировать для нас правильный код. Сгенерированный код содержит фабричные методы, ленивые конструкторы и различные другие методы.

По умолчанию сгенерированный код получает имя аннотированного класса , но во множественном числе. Но есть возможность настроить это через параметр inClass .

Теперь мы можем использовать сгенерированный код для создания АТД либо и убедиться, что он работает правильно:

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. Сопоставление с образцом

Одной из возможностей, доступных благодаря использованию алгебраических типов данных, является сопоставление с образцом.

Сопоставление с образцом — это механизм проверки значения по образцу. По сути, сопоставление с образцом — это более мощный оператор switch , но без ограничений на тип сопоставления или требование, чтобы шаблоны были постоянными. Для получения дополнительной информации мы можем проверить эту статью в Википедии о сопоставлении с образцом .

Чтобы использовать сопоставление с образцом, мы создадим класс, который будет моделировать HTTP-запрос. Пользователи смогут использовать один из указанных HTTP-методов:

  • ПОЛУЧИТЬ
  • ПОЧТА
  • УДАЛИТЬ
  • ПОМЕЩАТЬ

Давайте смоделируем наш класс запросов как ADT в Derive4J, начиная с интерфейса HTTPRequest :

@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 (обратите внимание на форму множественного числа) теперь позволит нам выполнять сопоставление шаблонов на основе типа запроса.

Для этой цели мы создадим очень простой класс 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));
}
}

Метод 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 позволяет нам ввести понятие лени, означающее, что наши объекты не будут инициализированы до тех пор, пока мы не выполним над ними операцию. Давайте объявим интерфейс как 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.