«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 обычно делятся на две основные категории:
-
sum product
Алгебраические типы данных присутствуют по умолчанию во многих языках, таких как Haskell и Scala.
3.2. Sum Type
Sum — это тип данных, представляющий операцию логического ИЛИ. Это означает, что это может быть что-то одно, но не то и другое одновременно. Проще говоря, тип суммы — это набор различных падежей. Название «сумма» происходит от того факта, что общее количество различных значений равно общему количеству наблюдений.
Enum ближе всего к типу суммы в Java. Enum имеет набор возможных значений, но может иметь только одно из них за раз. Однако мы не можем связать с Enum какие-либо дополнительные данные в Java, что является основным преимуществом алгебраических типов данных перед Enum.
3.3. Тип продукта
Продукт — это тип данных, представляющий логическую операцию И. Это сочетание нескольких значений.
Класс в Java можно рассматривать как тип продукта. Типы продуктов определяются комбинацией их полей в целом.
Дополнительную информацию о АТД можно найти в этой статье Википедии.
3.4. Использование
Одним из наиболее часто используемых алгебраических типов данных является Both. Либо можно рассматривать как более сложный необязательный параметр, который можно использовать, когда есть возможность пропустить значения или операция может привести к исключению.
Нам нужно аннотировать абстрактный класс или интерфейс хотя бы одним абстрактным методом, который будет использоваться 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-методов:
-
GET POST DELETE PUT
Давайте смоделируем наш класс запроса как 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));
}
}
«
@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());
}
«Метод acceptRequest() нашего класса использует сопоставление с образцом для типа запроса и будет возвращать разные ответы в зависимости от типа запроса:
5. Лень первого класса
@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);
}
Derive4J позволяет нам представить концепцию лени, что означает, что наши объекты не будут инициализированы, пока мы не выполним над ними операцию. Давайте объявим интерфейс как LazyRequest и настроим сгенерированный класс с именем LazyRequestImpl:
@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.