Introdução ao Derive4J
1. Introdução
O Derive4J é um processador de anotação que permite vários conceitos funcionais no Java 8.
Neste tutorial, apresentaremos Derive4J e os conceitos mais importantes habilitados pela estrutura:
-
Tipos de dados algébricos
-
Correspondência de padrão estrutural
-
Preguiça de primeira classe
2. Dependência do Maven
Para usar Derive4J, precisamos incluir odependency de nosso projeto:
org.derive4j
derive4j
1.1.0
true
3. Tipos de dados algébricos
3.1. Descrição
Os tipos de dados algébricos (ADTs) são um tipo de tipo composto - são combinações de outros tipos ou genéricos.
Os ADTs geralmente se enquadram em duas categorias principais:
-
sum
-
produtos
Os tipos de dados algébricos estão presentes por padrão em vários idiomas, como Haskell e Scala.
3.2. Tipo de soma
Sum is the data type representing the logical OR operation. Isso significa que pode ser uma coisa ou outra, mas não as duas. Simplesmente falando, o tipo soma é um conjunto de casos diferentes. O nome "soma" vem do fato de que o número total de valores distintos é o número total de casos.
Enum é a coisa mais próxima do tipo soma em Java. Enum tem um conjunto de valores possíveis, mas pode ter apenas um deles por vez. No entanto,we can’t associate any additional data with Enum in Java, which is the main advantage of algebraic data types over Enum.
3.3. Tipo de Produto
Product is the data type representing the logical AND operation. é a combinação de vários valores.
Class em Java pode ser considerado um tipo de produto. Os tipos de produtos são definidos pela combinação de seus campos.
Podemos encontrar mais informações sobre ADTsin this Wikipedia article.
3.4. Uso
Um dos tipos de dados algébricos comumente usados éEither. Podemos pensar emEither como umOptional mais sofisticado que pode ser usado quando há a possibilidade de valores ausentes ou a operação pode resultar em um exceção.
Precisamos anotar umabstract class ouinterface com pelo menos um método abstrato que será usado por Derive4J para gerar a estrutura de nosso ADT.
Para criar o tipo de dadosEither em Derive4J, precisamos criar uminterface:
Nossointerface é anotado com@Data, o que permitirá que Derive4J gere o código apropriado para nós. O código gerado contém métodos de fábrica, construtores preguiçosos e vários outros métodos.
Por padrão, o código gerado obtém o nome doclass anotado, mas no plural. Mas, existe a possibilidade de configurar isso através do parâmetroinClass.
Agora, podemos usar o código gerado para criar o ADTEither e verificar se ele está funcionando corretamente:
public void testEitherIsCreatedFromRight() {
Either either = Eithers.right("Okay");
Optional leftOptional = Eithers.getLeft(either);
Optional rightOptional = Eithers.getRight(either);
Assertions.assertThat(leftOptional).isEmpty();
Assertions.assertThat(rightOptional).hasValue("Okay");
}
Também podemos usar o métodomatch() gerado para executar uma função, dependendo de qual lado deEither está presente:
public void testEitherIsMatchedWithRight() {
Either either = Eithers.right("Okay");
Function leftFunction = Mockito.mock(Function.class);
Function 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. Correspondência de padrões
Um dos recursos habilitados pelo uso de tipos de dados algébricos épattern matching.
Pattern matching is the mechanism for checking a value against a pattern. Basicamente, a correspondência de padrões é uma instruçãoswitch mais poderosa, mas sem limitações no tipo de correspondência ou o requisito de que os padrões sejam constantes. Para obter mais informações, podemos verificarthis Wikipedia article on pattern matching.
Para usar a correspondência de padrões, criaremos uma classe que modelará a solicitação HTTP. Os usuários poderão usar um dos métodos HTTP fornecidos:
-
GET
-
POST
-
EXCLUIR
-
PUT
Vamos modelar nossa classe de solicitação como um ADT em Derive4J, começando com a interfaceHTTPRequest:
@Data
interface HTTPRequest {
interface Cases{
R GET(String path);
R POST(String path);
R PUT(String path);
R DELETE(String path);
}
R match(Cases method);
}
A classe gerada,HttpRequests (observe a forma plural), agora nos permitirá realizar a correspondência de padrões com base no tipo de solicitação.
Para este propósito, criaremos um sclassHTTPServer muito simples que responderá comStatus diferentes dependendo do tipo de solicitação.
Primeiro, vamos criar um sclassHTTPResponse imples que servirá como uma resposta de nosso servidor para nosso cliente:
public class HTTPResponse {
int statusCode;
String responseBody;
public HTTPResponse(int statusCode, String responseBody) {
this.statusCode = statusCode;
this.responseBody = responseBody;
}
}
Em seguida, podemos criar o servidor que usará a correspondência de padrões para enviar a resposta adequada:
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));
}
}
O métodoacceptRequest() de nossoclass usa correspondência de padrões no tipo de solicitação e retornará respostas diferentes com base no tipo de solicitação:
@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. Preguiça de Primeira Classe
O Derive4J nos permite introduzir o conceito de preguiça, o que significa que nossos objetos não serão inicializados até executarmos uma operação neles. Vamos declararinterface comoLazyRequest e configurar a classe gerada para ser nomeadaLazyRequestImpl:
@Data(value = @Derive(
inClass = "{ClassName}Impl",
make = {Make.lazyConstructor, Make.constructors}
))
public interface LazyRequest {
interface Cases{
R GET(String path);
R POST(String path, String body);
R PUT(String path, String body);
R DELETE(String path);
}
R match(LazyRequest.Cases method);
}
Agora podemos verificar se o construtor lento gerado está funcionando como deveria:
@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 {
@Override
public LazyRequest get() {
return LazyRequestImpl.GET("http://test.com/get");
}
}
Podemos encontrar mais informações sobre preguiça de primeira classe e exemplosin the Scala documentation.
6. Conclusão
Neste tutorial, introduzimos a biblioteca Derive4J e a usamos para implementar alguns conceitos funcionais, como Algebraic Data Types e correspondência de padrões, que normalmente não estão disponíveis em Java.
Podemos encontrar mais informações sobre a biblioteca emin the official Derive4J documentation.
Como sempre, todos os exemplos de código podem ser encontradosover on GitHub.