Skip to content

Fluent Interfaces in Java: How to Create Readable and Expressive APIs

Reading Time: 4 minutes

Fluent interfaces are a term coined by Martin Fowler and Eric Evans. It’s a design approach in software development that aims to create an object-oriented API. The goal is to make the API more readable by making method calls more expressive and easier to understand.

 int sum = widgets.stream()
                  .filter(w -> w.getColor() == RED)
                  .mapToInt(w -> w.getWeight())
                  .sum();

Fluent interfaces rely on method chaining to simplify their usage for clients. One of the clearest examples of fluent interfaces is the Steam API introduced in Java 8:

Here, the intention is to convert a collection of widgets into a stream and operate on it. First, filtering widgets that are red in color, then extracting the weight value (an integer), and finally summing up these values and assigning them to the ‘sum’ variable.

Using an object with fluent interfaces typically involves three stages:

  1. Creating an element that initiates the message chaining.
  2. Chaining messages that form the object’s usage.
  3. Receiving the result from the fluent interface, which could be the construction of an object or, as seen in the example above, a function like sum.

Implementing Fluent Interfaces

Let’s consider a class called Checkout, which models a supermarket purchase. To create a checkout, you need a shopping cart, a payment method, and optionally a coupon and a loyalty card.

public class Checkout {
    private ShoppingCart cart;
    private PaymentMethod paymentMethod;
    private Coupon coupon;
    private LoyaltyCard loyaltyCard;

    public Checkout(ShoppingCart cart, PaymentMethod paymentMethod, Coupon coupon, LoyaltyCard loyaltyCard) {
        this.cart = cart;
        this.paymentMethod = paymentMethod;
        this.coupon = coupon;
        this.loyaltyCard = loyaltyCard;
    }

    // FUNCTIONALITY
}

In the first approach, we use a builder that implements the fluent API design to model the creation of a checkout:

// Fluent Interface implemented with basic Builder pattern
public class CheckoutFluent {
    private ShoppingCart cart;
    private PaymentMethod paymentMethod;
    private Coupon coupon;
    private LoyaltyCard loyaltyCard;

    private CheckoutFluent(){
    }

    public static CheckoutFluent getBuilderInstance(){
        return new CheckoutFluent();
    }   
    
    @Override
    public CheckoutFluent withShoppingCart(ShoppingCart cart) {
        this.cart= cart;
        return this;
    }
    @Override
    public CheckoutFluent withPaymentMethodCard(Card card) {
        this.paymentMethod= card;
        return this;
    }
    @Override
    public CheckoutFluent withPaymentMethodCash() {
        this.paymentMethod= new Cash();
        return this;
    }
    @Override
    public CheckoutFluent withValidCoupon(Coupon coupon) {
        this.coupon= coupon;
        return this;
    }
    @Override
    public CheckoutFluent withoutCoupons() {
        this.coupon= null;
        return this;
    }
    @Override
    public CheckoutFluent withLoyaltyCard(LoyaltyCard card) {
        this.loyaltyCard= card;
        return this;
    }
    @Override
    public CheckoutFluent withoutLoyaltyCard() {
        this.loyaltyCard= null;
        return this;
    }

    @Override
    public Checkout build() {
         return new Checkout(cart, 
                             paymentMethod,
                             coupon,
                             loyaltyCard
                           );
    }

}

However, a problem with this approach is that clients using this builder are not obligated to add the mandatory fields and can easily skip them.

public static void main( String[] args )
    {
        ShoppingCart cart= new ShoppingCart();
        LoyaltyCard loyaltyCard= new LoyaltyCard();
        Checkout checkout=  CheckoutFluent.getBuilderInstance()
                      .withShoppingCart(cart)
                      .build();
        // USE THE VAR checkout
    }
Example where skip phases in the build

To address this, let’s explore a second option that takes this into consideration.

Fluent Interfaces a more advanced approach

In this case multiple interfaces are created, each identifying a step in the creation of a Checkout.

Fluent Interface phases on construction

Each step is responsible for creating a part of the Checkout and exposes the interface for the next step, ultimately leading to the build phase, which returns the constructed Checkout object.

interface CheckoutFluentBuilder {

    interface SelectShoppingCartPhase{
      SelectPaymentMethodPhase withShoppingCart(ShoppingCart cart);
    }
    
    interface SelectPaymentMethodPhase{
       SelectCouponPhase withPaymentMethodCard(Card card);
       SelectCouponPhase withPaymentMethodCash();
    }
    
    interface SelectCouponPhase{
       SelectLoyaltyCardPhase withValidCoupon(Coupon coupon);
       SelectLoyaltyCardPhase withoutCoupons();
    }
    
    interface SelectLoyaltyCardPhase{
        BuildCheckoutPhase withLoyaltyCard(LoyaltyCard card);
        BuildCheckoutPhase withoutLoyaltyCard();
    }

    interface BuildCheckoutPhase{
        Checkout build();
    }
}

Implementing the interfaces

public class CheckoutFluent implements CheckoutFluentBuilder.SelectShoppingCartPhase,
                                     CheckoutFluentBuilder.SelectPaymentMethodPhase,
                                       CheckoutFluentBuilder.SelectCouponPhase,
                                       CheckoutFluentBuilder.SelectLoyaltyCardPhase,
                                       CheckoutFluentBuilder.BuildCheckoutPhase {
    private ShoppingCart cart;
    private PaymentMethod paymentMethod;
    private Optional<Coupon> coupon;
    private Optional<LoyaltyCard> loyaltyCard;

    private CheckoutFluent(){
    }

    public static CheckoutFluentBuilder.SelectShoppingCartPhase getBuilderInstance(){
        return new CheckoutFluent();
    }   
    
    @Override
    public SelectPaymentMethodPhase withShoppingCart(ShoppingCart cart) {
        this.cart= cart;
        return this;
    }
    @Override
    public SelectCouponPhase withPaymentMethodCard(Card card) {
        this.paymentMethod= card;
        return this;
    }
    @Override
    public SelectCouponPhase withPaymentMethodCash() {
        this.paymentMethod= new Cash();
        return this;
    }
    @Override
    public SelectLoyaltyCardPhase withValidCoupon(Coupon coupon) {
        this.coupon= Optional.of(coupon);
        return this;
    }
    @Override
    public SelectLoyaltyCardPhase withoutCoupons() {
        this.coupon= Optional.empty();
        return this;
    }
    @Override
    public BuildCheckoutPhase withLoyaltyCard(LoyaltyCard card) {
        this.loyaltyCard= Optional.of(card);
        return this;
    }
    @Override
    public BuildCheckoutPhase withoutLoyaltyCard() {
        this.loyaltyCard= Optional.empty();
        return this;
    }

    @Override
    public Checkout build() {
         return new Checkout(cart, 
                             paymentMethod,
                             coupon,
                             loyaltyCard
                           );
    }


}

The implementation of these interfaces follows a similar pattern to the first case, with the key difference being the return type of each fluent interface method. This leads us to the usage example:

    public static void main( String[] args )
    {
        ShoppingCart cart= new ShoppingCart();
        LoyaltyCard loyaltyCard= new LoyaltyCard();
        Checkout checkout=  CheckoutFluent.getBuilderInstance()
                      .withShoppingCart(cart)
                      .withPaymentMethodCash()
                      .withoutCoupons()
                      .withLoyaltyCard(loyaltyCard)
                      .build();
        // USE THE VAR checkout
    }

At line 6, the user can only choose to create a shopping cart; they cannot skip directly to the build step or any other step. This approach enforces a more structured and controlled creation process.

On the other hand, when a checkout is used, we can instantly see that it has a shopping cart, pays with cash, has no coupons, but does have a loyalty card. This way of expressing the object’s construction is particularly useful when creating customized tests. For instance, if we have test cases that change due to factors like a different payment method, such as a virtual wallet, we only need to add the method to the fluent interface for selecting the payment method.

The problems of fluent interfaces

One disadvantage is that the construction stages must be well-defined and reflected in the interfaces. It’s recommended that each interface has a single responsibility, adhering to the Single Responsibility principle of SOLID, in order to contain the explosion of methods of each

As the Checkout class gains more properties, the fluent interface methods become more extensive, potentially leading to code duplication.

Mocking objects implemented with fluent interfaces can be more challenging,

Personally I only use fluent interfaces in testing scenarios or as a builder.

A skilled developer knows how to limit the use of this tool to suitable cases. Even if you don’t use it in your day-to-day work, understanding how it works is valuable, as you’ll encounter various libraries that implement it.

Reference

Published inProgramming

Be First to Comment

Leave a Reply

Discover more from Samurai Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading