Sabemos que, em um projeto de software, nada substitui uma boa documentação. Porém também é necessário ter atenção ao quão intuitivo é o código escrito. Afinal de contas, quanto mais simples e mais natural o código for, melhor será a sua experiência para os usuários. E é aqui que entra o conceito de Fluent API (API fluida).
Na simples “regra de programação”, na qual tudo aquilo que temos que lembrar esqueceremos, uma API que te “força” a lembrar é uma prova de falha crucial.
Por isso que, neste artigo, faremos uma introdução ao tema e vamos mostrar como criar uma API fluida a partir do conceito de Fluent API.
O que é uma Fluent API, ou API fluente?
Quando falamos no contexto de engenharia de software, uma fluent API é uma API orientada a objetos cujo design se baseia amplamente no encadeamento de métodos.
Esse conceito, que foi criado em 2005 por Eric Evans e Martin Fowler, tem por objetivo aumentar a legibilidade do código criando uma linguagem específica de domínio (DSL).
Na prática, criar uma API fluente significa desenvolver uma API em que não é necessário memorizar os próximos passos ou métodos, permitindo que haja uma sequência natural e contínua como se fosse um menu de opções.
Essa cadência natural funciona de forma semelhante a um restaurante ou mesmo uma rede fast food em que à medida que você está montando um prato, as opções variam de acordo com as escolhas que você for fazendo. Se, por exemplo, você optar por um sanduíche de frango, os acompanhamentos serão sugeridos considerando o prato escolhido e assim por diante.
Fluent API no contexto Java
No mundo Java, podemos pensar em dois exemplos famosos deste tipo de implementação.
O primeiro deles é o framework JOOQ, projeto liderado por Lukas Eder e que tem o objetivo de facilitar a comunicação entre Java e bancos de dados relacionais. O maior diferencial do JOOQ é ser orientado a dados, o que ajuda a evitar e/ou diminuir o problema de impedância, ou perda, associada ao relacional e orientado a objetos.
Query query = create.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
.from(BOOK)
.join(AUTHOR)
.on(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
.where(BOOK.PUBLISHED_IN.eq(1948));
String sql = query.getSQL();
List<Object> bindValues = query.getBindValues();
Outro exemplo está nos bancos de dados não relacionais, ou seja, NoSQL, dentro das especificações do mundo Java corporativo. Entre eles, está o Jakarta EE, que é a primeira especificação deste tipo e que, sob a tutela da Eclipse Foundation, se tornou o Jakarta NoSQL.
O objetivo desta especificação é garantir uma comunicação fácil entre Java e bancos de dados NoSQL.
DocumentQuery query = select().from("Person").where(eq(Document.of("_id", id))).build();
Optional<Person> person = documentTemplate.singleResult(query);
System.out.println("Entity found: " + person);
De modo geral, uma Fluent API é dividida em três partes:
- O objeto ou resultado final: no geral, o fluent API tem uma semelhança ao padrão builder, porém, a maior dinâmica está acoplado com a DSL. Nos dois, o resultado final tende a ser uma instância tanto para representar o resultado de um processo ou uma nova entidade.
- As opções: no caso, são as coleções de interfaces ou classes que servirão como o “nosso menu interativo”. A ideia é que a partir de uma ação, mostre apenas as opções disponíveis do próximo passo, seguindo uma sequência intuitiva.
- O resultado: após todo esse processo, a resposta pode ou não resultar em uma instância seja para uma entidade, processo etc. O ponto importante é que o resultado precisa ser um resultado válido.
Fluent API na prática
Para demonstrar um pouco desse conceito, criaremos um pedido de sanduíche com o resultado esperado de um pedido com o respectivo preço da compra. O fluxo será o mostrado a seguir.
Certamente, existem diversas maneiras de implementar essa funcionalidade de Fluent API, porém optamos por uma versão extremamente simples.
Como já mencionamos as três partes de uma API – objeto, opções e resultado -, começaremos com o pedido que será representado pela interface “Order”. Um ponto de destaque é que essa interface possui algumas interfaces, que serão responsáveis por demonstrar as nossas opções.
public interface Order {
interface SizeOrder {
StyleOrder size(Size size);
}
interface StyleOrder {
StyleQuantityOrder vegan();
StyleQuantityOrder meat();
}
interface StyleQuantityOrder extends DrinksOrder {
DrinksOrder quantity(int quantity);
}
interface DrinksOrder {
Checkout softDrink(int quantity);
Checkout cocktail(int quantity);
Checkout softDrink();
Checkout cocktail();
Checkout noBeveragesThanks();
}
static SizeOrder bread(Bread bread) {
Objects.requireNonNull(bread, "Bread is required o the order");
return new OrderFluent(bread);
}
}
O resultado dessa API será a nossa classe do pedido. É nela que conterá o sanduíche, a bebida e as suas respectivas quantidades.
Um rápido complemento antes de voltarmos ao tutorial
Um ponto que não focaremos neste artigo, mas que vale a pena mencionar, está relacionado à representação do dinheiro.
Quando se trata de operações numéricas, o ideal é usarmos BigDecimal. Isso porque, seguindo referências como o livro Java Effective e o blog When Make a Type, entendemos que tipos complexos precisam de um tipo exclusivo. Esse raciocínio, atrelado ao pragmatismo de “não se repita” (DRY, ou don’t repeat yourself), o resultado é a utilização da especificação Java para dinheiro: a Money API.
import javax.money.MonetaryAmount;
import java.util.Optional;
public class Checkout {
private final Sandwich sandwich;
private final int quantity;
private final Drink drink;
private final int drinkQuantity;
private final MonetaryAmount total;
//...
}
A última etapa da jornada é a implementação da API. Ela será responsável pela parte “feia” do código, fazendo com que a API seja bonita.
A tabela de preços será colocada diretamente no código, já que não usamos banco de dados, ou outra referência de dados, e nosso intuito é tornar o exemplo o mais simples possível. Mas vale reforçar que, em um ambiente real, essa informação estaria em um banco de dados ou em um serviço.
import javax.money.MonetaryAmount;
import java.util.Objects;
class OrderFluent implements Order.SizeOrder, Order.StyleOrder, Order.StyleQuantityOrder, Order.DrinksOrder {
private final PricingTables pricingTables = PricingTables.INSTANCE;
private final Bread bread;
private Size size;
private Sandwich sandwich;
private int quantity;
private Drink drink;
private int drinkQuantity;
OrderFluent(Bread bread) {
this.bread = bread;
}
@Override
public Order.StyleOrder size(Size size) {
Objects.requireNonNull(size, "Size is required");
this.size = size;
return this;
}
@Override
public Order.StyleQuantityOrder vegan() {
createSandwich(SandwichStyle.VEGAN);
return this;
}
@Override
public Order.StyleQuantityOrder meat() {
createSandwich(SandwichStyle.MEAT);
return this;
}
@Override
public Order.DrinksOrder quantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("You must request at least one sandwich");
}
this.quantity = quantity;
return this;
}
@Override
public Checkout softDrink(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("You must request at least one sandwich");
}
this.drinkQuantity = quantity;
this.drink = new Drink(DrinkType.SOFT_DRINK, pricingTables.getPrice(DrinkType.SOFT_DRINK));
return checkout();
}
@Override
public Checkout cocktail(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("You must request at least one sandwich");
}
this.drinkQuantity = quantity;
this.drink = new Drink(DrinkType.COCKTAIL, pricingTables.getPrice(DrinkType.COCKTAIL));
return checkout();
}
@Override
public Checkout softDrink() {
return softDrink(1);
}
@Override
public Checkout cocktail() {
return cocktail(1);
}
@Override
public Checkout noBeveragesThanks() {
return checkout();
}
private Checkout checkout() {
MonetaryAmount total = sandwich.getPrice().multiply(quantity);
if (drink != null) {
MonetaryAmount drinkTotal = drink.getPrice().multiply(drinkQuantity);
total = total.add(drinkTotal);
}
return new Checkout(sandwich, quantity, drink, drinkQuantity, total);
}
private void createSandwich(SandwichStyle style) {
MonetaryAmount breadPrice = pricingTables.getPrice(this.bread);
MonetaryAmount sizePrice = pricingTables.getPrice(this.size);
MonetaryAmount stylePrice = pricingTables.getPrice(SandwichStyle.VEGAN);
MonetaryAmount total = breadPrice.add(sizePrice).add(stylePrice);
this.sandwich = new Sandwich(style, this.bread, this.size, total);
}
}
Implementação da API fluente
O resultado é uma API que nos retornará o pedido de forma bastante simples e intuitiva.
Checkout checkout = Order.bread(Bread.PLAIN)
.size(Size.SMALL)
.meat()
.quantity(2)
.softDrink(2);
Este é o resultado do código a partir de uma API mais fluente.
O que a fluent API tem de diferente em relação a outros padrões de API?
É muito comum existir a comparação entre dois padrões de API, que são o Builder e o Fluent API. O motivo é que ambos usam métodos em sequência para o processo de criação de uma instância.
No entanto, o Fluent API está “amarrado com um DSL“, além de forçar um caminho fácil para isso. Mas para tornar essas diferenças ainda mais evidentes, separamos pontos de destaque para cada um desses padrões:
Padrão Builder:
- Tende a ser muito mais fácil para a implementação;
- Não deixa claro quais métodos de construção são necessários;
- A grande maioria dos problemas acontecerão em tempo de execução;
- Existem ferramentas e frameworks que o criam de forma automática;
- Necessita de uma validação mais forte no método de construção para verificar quais métodos obrigatórios não foram invocados.
Fluent API:
- Tende a ter uma implementação mais complexa, principalmente com o maior número de opções no caminho.
- Força que o usuário desenvolvedor siga o fluxo, além de ser mais visível quais campos são opcionais.
- Caso o caminho não seja seguido, existe a possibilidade do código nem compilar.
Builder e API Fluente:
- É importante que, para cada método, haja a validação e lance o erro caso parâmetro seja inválido, lembre-se da premissa de falhe rápido;
- É necessário que retorne um objeto válido no final do processo.
E agora, ficou mais fácil de compreender as semelhanças e diferenças entre os padrões de API?
Fluent API: código fácil e intuitivo com uma API fluente
Esta foi a nossa introdução ao conceito de API fluente ou fluida. Como todas as soluções, não existe uma “bala de prata”, já que há uma grande complexidade que, muitas vezes, não se justifica para todo o processo.
O nosso colega Rafael Ponte costuma falar que “quanto mais fácil for usar a API para o cliente, mais difícil será desenhá-la e implementá-la para profissionais de desenvolvimento”.
O Fluent API é uma excelente ferramenta que auxilia na criação à prova de falhas para você e outros usuários. Você pode ver mais sobre o assunto no Github da Sou Java.
Convidamos você a conhecer os projetos da Zup, em especial nossos produtos open source. Inclusive, um deles – o Charles CD – utiliza Java e pode ser um excelente ponto de partida para você aplicar um pouco do que falamos neste artigo 😉