Fluent-API: Creating Easier, More Intuitive Code With a Fluent API
In software development, writing clean and readable code is a top priority for developers. Not only does it make maintenance and collaboration more manageable, but it also enhances the overall value of the codebase. Using a Fluent API is a powerful design approach that promotes clean and expressive code.
In this article, we will delve into the concept of a Fluent API and explore how it can transform your code into something that is easier to read and more intuitive to use. A Fluent API allows developers to build code that resembles natural language, making it highly readable and self-explanatory.
But what exactly is a Fluent API? It is an interface or set of methods carefully designed to provide a smooth and fluid coding experience. By leveraging method chaining and an intuitive naming convention, a Fluent API allows you to write code that reads like a sentence, with each method call seamlessly flowing into the next.
The benefits of a Fluent API are twofold. First, it improves the readability of your code by eliminating unnecessary syntax noise and making the intent of the code crystal clear. Second, it enhances the developer experience by providing a more intuitive and discoverable API surface. It reduces the learning curve for new team members and makes the codebase more maintainable in the long run.
This article will explore various techniques and best practices for designing and implementing a Fluent API. We will discuss leveraging method chaining, appropriate naming conventions, and meaningful return types to create a fluent and expressive API that adds value to your codebase.
Whether you are developing a library, a framework, or an application, adopting a Fluent API design approach can significantly improve the quality and maintainability of your code. By the end of this article, you will have a solid understanding of what a Fluent API is, why it is valuable, and how you can start implementing it in your projects.
What Is a Fluent-API?
When we speak in the context of software engineering, a fluent API is an object-oriented API whose design is largely based on method chaining.
This concept, created in 2005 by Eric Evans and Martin Fowler, aims to increase code readability by creating a domain-specific language (DSL).
In practice, creating a fluent API means developing an API in which it is unnecessary to memorize the next steps or methods, allowing for a natural and continuous sequence as if it were a menu of options.
This natural cadence works similarly to a restaurant or even a fast-food chain in that as you are putting together a dish, the options vary according to your choices. If, for example, you choose a chicken sandwich, the sides will be suggested considering the chosen dish and so on.
Fluent API in Java Context
In the Java world, we can think of two famous examples of this type of implementation.
The first is the JOOQ framework, a project led by Lukas Eder, which facilitates communication between Java and relational databases. JOOQ's most significant difference is that it is data-oriented, which helps avoid and/or reduce the impedance problem, or loss, associated with relational and object-oriented.
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();
Another example is in non-relational databases, i.e., NoSQL, within the specifications of the enterprise Java world. Among them is Jakarta EE, which is the first specification of its kind and which became Jakarta NoSQL under the umbrella of the Eclipse Foundation.
The purpose of this specification is to ensure smooth communication between Java and NoSQL databases.
DocumentQuery query = select().from("Person").where(eq(Document.of("_id", id))).build();
Optional<Person> person = documentTemplate.singleResult(query);
System.out.println("Entity found: " + person);
Generally speaking, a fluent API is divided into three parts:
1. The final object or result: The fluent API resembles the builder pattern, but the most powerful dynamic is coupled with DSL. In both, the result tends to be an instance representing the result of a process or a new entity.
2. The options: In this case, these are the collections of interfaces or classes that will serve as "our interactive menu." From an action, the idea is that show only the options available for the next step, following an intuitive sequence.
3. The result: After all this process, the answer may or may not result in an instance either for an entity, strategy, etc. The critical point is that the result must be valid.
Fluid API in Practice
To demonstrate some of this concept, we'll create a sandwich order with the expected result of an order with the corresponding purchase price. The flow will be as shown below.
Of course, there are several ways to implement this fluent API functionality, but we opted for a short version.
As we already mentioned, the three parts of an API — object, options, and result — we will start with the order that the "Order" interface will represent. A highlight is that this interface has some interfaces which will be responsible for demonstrating our options.
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);
}
The result of this API will be our order class. It will contain the sandwich, the drink, and their respective quantities.
A Quick Add-on Before We Go Back to the Tutorial
One point that we won't focus on in this article, but that is worth mentioning, is related to the representation of money.
When it comes to numerical operations, it is ideal to use BigDecimal. That's because, following references like the Java Effective book and the blog When Make a Type, we understand that complex types need a unique type. This reasoning, coupled with the pragmatism of "don't repeat yourself" (DRY, or don't repeat yourself), the result is the use of the Java specification for money: The 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;
//...
}
The last step in the journey is API implementation. It will be responsible for the "ugly" part of the code, making the API look beautiful.
The price list will be placed directly in the code since we don't use a database or another data reference, and we intend to make the example as simple as possible. But it is worth stressing that, in a natural environment, this information would be in a database or a service.
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);
}
}
The result is an API that will return the request to us straightforwardly and intuitively.
Checkout checkout = Order.bread(Bread.PLAIN)
.size(Size.SMALL)
.meat()
.quantity(2)
.softDrink(2);
How Is Fluent-API Different From Other Patterns?
It is widespread to have a comparison between two API standards, which are Builder and Fluent-API. The reason is that they both use methods in sequence for the process of creating an instance.
However, Fluent-API is "tied with a DSL," and it forces an easy path to do so. But to make these differences even more evident, we've separated highlights for each of these patterns:
1. The Builder Pattern
- It tends to be much easier to implement;
- It is not clear which construction methods are needed;
- The vast majority of problems will happen at runtime;
- Some tools and frameworks create it automatically;
- It needs more robust validation in the build method to check which mandatory methods were not invoked.
2. The Fluent-API
- It is important that, for each method, there is validation and throws the error if the parameter is invalid, remembers the fast fail premise;
- It must return a valid object at the end of the process.
And now, is it easier to understand the similarities and differences between patterns?
That is our introduction to the concept of fluent API. As with all solutions, there is no "silver bullet," as great complexity is often not justified for the entire process.
Our colleague Rafael Ponte often says, "The easier it is to use the API for the client, the more difficult it will be to design and implement it for the developer."
It is an excellent tool that helps in creating a failsafe for you and other users. You can see more about it on Sou Java's Github.