Skip to content

stawirej/fluent-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

95 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Java 8+ words substitution functions for building fluent api

Java CI with Maven License

Practical application of higher-order functions and function currying for building fluent api in Java

TL;DR

Build a fluent api in Java that mimic natural language.

From

medicalCenter.register(patient, headache, doctor, date);

to

medicalCenter.register(patient).with(HEADACHE).to(doctor).at(date);

or create builders (limiting boilerplate and without libraries like lombok)

User user = User.with(Name.from("John"))
                .with(Surname.from("Doe"))
                .with(Login.from("johndoe"))
                .with(Password.from("sosecretpassword"))
                .with(Email.from("[email protected]"));

by decorating existing methods or constructors with fluent-api library functions

public WithFunction<ToFunction<AtConsumer<Instant>, Doctor>, Reason> register(Patient patient) {

    return reason -> doctor -> date -> register(patient, reason, doctor, date);
}
public static WithFunction<WithFunction<WithFunction<WithFunction<User, Email>, Password>, Login>, Surname> with(Name name) {

    return surname -> login -> password -> email -> new User(name, surname, login, password, email);
}

Dependencies

Maven

<dependency>
    <groupId>io.github.stawirej</groupId>
    <artifactId>fluent-api</artifactId>
    <version>0.1.0</version>
</dependency>

Gradle

implementation group: 'io.github.stawirej', name: 'fluent-api', version: "0.1.0"

Introduction

Situation

When we create library or framework we expose methods with set of parameters as a public API. For methods with a small amount of parameters we might not have an issue with that. Especially when we provide decent documentation. We use them and experience shows we get used to their look.

Complication

We struggle to introduce fluent api approach. It might rise some challenge for new or existing code. For covering trivial functionality connected with creating objects we always can use a builder pattern. We start to struggle when comes to create fluent api of more complicated behavior, or we just simple don’t care.


Clean code is simple and direct. Clean code reads like well-written prose.
Clean code never obscures the designer’s intent but rather is full of crisp abstractions and straightforward lines of control.

— Grady Booch author of Object Oriented Analysis and Design with Applications

Stop for a moment and try to think about part of above quote: "… well-written prose."
Since the day I’ve read the Grady Booch quote, I feel some kind of itching and discomfort in my brain each time
I can’t read code as well written english sentence. Often we encounter methods like:

library.lend(book, reader);

Due to decent naming we clearly see this code realize book lending functionality. However, try to read that code at loud. You will get something like: "Library lend book reader". Doesn’t sound as "well-written prose"? It doesn’t even sound as a proper sentence.

Key question

How to build a fluent API in Java that mimic natural language?

Answer

Missing parts of the speech

Terry Bozzio was asked once why he need such a big drum kit. "To play music, I need notes!" he said. Paraphrasing: "To create fluent API, we need words!"

We know a concept of having class names (things) as nouns and verbs for methods (behaviours). It worked pretty well so far. However, we face an obstacle when we try to build fluent API with limited parts of speech.

To solve it, we need to introduce missing part of english sentences as e.g. prepositions. Having all verbs, nouns and prepositions in place we can build API which sounds like "well-written prose".

Let’s add preposition to our example.

library.lend(book).to(reader);

Now we have correct english sentence: "Library lend book to reader"!

One way to introduce this concept into Java code in practical way is using higher-order functions and function currying.

Higher-order functions

Higher-order functions consists of functions that either takes a function as a parameter or returns a function.

Well known higher-order functions are e.g. filter() or map().

     List<Apple> goldenDelicious = apples
                .stream()
                .filter(isGoldenDelicious)
                .collect(toList());

Where isGoldenDelicious represents a predicate (function that returns true or false):

Predicate<? super Apple> isGoldenDelicious =
        apple -> apple.cultivar().equals(Cultivar.GOLDEN_DELICIOUS);

We will no longer rely on methods returning "this" as e.g. for builder pattern approach or other non-functional
interface implementations for having fluent api from user perspective. Higher-order functions allows us to implement fluent API as a "decorator" for existing multi-parameter methods. Additionally, we will manage to build fluent API for more complex behaviors than object creation.

Function currying

In mathematics and computer science, currying is the technique of translating the evaluation of a function that
takes multiple arguments into evaluating a sequence of functions, each with a single argument (wiki).

Let’s transform function with two arguments to sequence of functions with a single argument.

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

In "old", anonymous class approach we would get function currying as:

Function<Integer, Function<Integer, Integer>> add = new Function<Integer, Function<Integer, Integer>>() {
            @Override
            public Function<Integer, Integer> apply(Integer a) {

                return new Function<Integer, Integer>() {
                    @Override
                    public Integer apply(Integer b) {

                        return a + b;
                    }
                };
            }
        };

Let’s make it more concise using lambda.

 Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;

We would like to apply this technique. Having one-argument functions and "curry" them enable creating fluent api. This allows having functions which will represent single "words" in our fluent API sentence.

Decorator

We have public method for registering a patient for the medical visit.

public void register(Patient patient,
                     Reason reason,
                     Doctor doctor,
                     Instant date) {
                         // Method body
}
Tip
We could simplify this api by usage of Parameter Object,
but for the better understanding of described concept we won’t.

Standard usage will look like.

medicalCenter.register(patient, headache, doctor, date);

Using higher-order functions and function currying we will "decorate" this method. This will create a fluent api which mimic natural language.

medicalCenter.register(patient).with(HEADACHE).to(doctor).at(date);

Decorating

Decorate register method.

public WithFunction<ToFunction<AtConsumer<Instant>, Doctor>, Reason> register(Patient patient) {

    return reason -> doctor -> date -> register(patient, reason, doctor, date);
}

Hide old register method by using `private' accessor.

private void register(Patient patient,
                      Reason reason,
                      Doctor doctor,
                      Instant date) {
                          // Method body
}

Decoration steps

Declaring a nested functions can be confusing at first sight. After second look, the procedure is quite simple. Let’s examine declaration steps:

  • Start from left original method parameter. In our case it is "patient".

  • Declare starting, entry fluent interface method:

register(Patient patient) {...}
  • Create return function for second parameter ("reason").

WithFunction<..., Reason> register(Patient patient) {...}
  • Create return function for third parameter ("doctor").

WithFunction<ToFunction<..., Doctor>, Reason> register(Patient patient) {...}
  • Create return function for last parameter ("date").

WithFunction<ToFunction<AtConsumer<Instant>, Doctor>, Reason> register(Patient patient) {...}
  • Call original method with all parameters inside our decorator

WithFunction<ToFunction<AtConsumer<Instant>, Doctor>, Reason> register(Patient patient) {

    return reason -> doctor -> date -> register(patient, reason, doctor, date);
}

Builder

Decoration idea from previous paragraph can be applied to create simple builder.

The procedure is the same as above, just the base method is constructor. The procedure is the same as above, just the base method is constructor.

Standard builder

public final class User {

    private final Name name;
    private final Surname surname;
    private final Login login;
    private final Password password;
    private final Email email;

    private User(Name name,
                 Surname surname,
                 Login login,
                 Password password,
                 Email email) {

        this.name = name;
        this.surname = surname;
        this.login = login;
        this.password = password;
        this.email = email;
    }

    public static UserBuilder user() {
        return new UserBuilder();
    }

    public Name name() {
        return name;
    }

    public Surname surname() {
        return surname;
    }

    public Login login() {
        return login;
    }

    public Password password() {
        return password;
    }

    public Email email() {
        return email;
    }

    public static class UserBuilder {

        private Name name;
        private Surname surname;
        private Login login;
        private Password password;
        private Email email;

        private UserBuilder() {
        }

        public UserBuilder withName(Name name) {
            this.name = name;
            return this;
        }

        public UserBuilder withSurname(Surname surname) {
            this.surname = surname;
            return this;
        }

        public UserBuilder withLogin(Login login) {
            this.login = login;
            return this;
        }

        public UserBuilder withPassword(Password password) {
            this.password = password;
            return this;
        }

        public UserBuilder withEmail(Email email) {
            this.email = email;
            return this;
        }

        public User build() {

            requireNonNull(name, "name cannot be null");
            requireNonNull(surname, "surname cannot be null");
            requireNonNull(login, "login cannot be null");
            requireNonNull(password, "password cannot be null");
            requireNonNull(email, "email cannot be null");

            return new User(name, surname, login, password, email);
        }
    }
}

Usage

 User user = user().withName(Name.from("John"))
                   .withSurname(Surname.from("Doe"))
                   .withLogin(Login.from("johndoe"))
                   .withPassword(Password.from("sosecretpassword"))
                   .withEmail(Email.from("[email protected]"))
                   .build();

Decorated constructor builder

Replace the standard builder with a decorated constructor builder.

public final class User {

    private final Name name;
    private final Surname surname;
    private final Login login;
    private final Password password;
    private final Email email;

    private User(Name name,
                 Surname surname,
                 Login login,
                 Password password,
                 Email email) {

        requireNonNull(name, "name cannot be null");
        requireNonNull(surname, "surname cannot be null");
        requireNonNull(login, "login cannot be null");
        requireNonNull(password, "password cannot be null");
        requireNonNull(email, "email cannot be null");

        this.name = name;
        this.surname = surname;
        this.login = login;
        this.password = password;
        this.email = email;
    }

    public static WithFunction<WithFunction<WithFunction<WithFunction<User, Email>, Password>, Login>, Surname> with(Name name) {

        return surname -> login -> password -> email -> new User(name, surname, login, password, email);
    }

    public Name name() {
        return name;
    }

    public Surname surname() {
        return surname;
    }

    public Login login() {
        return login;
    }

    public Password password() {
        return password;
    }

    public Email email() {
        return email;
    }
}

Usage

User user = User.with(Name.from("John"))
                .with(Surname.from("Doe"))
                .with(Login.from("johndoe"))
                .with(Password.from("sosecretpassword"))
                .with(Email.from("[email protected]"));

Fluent builder for java records

  • It is even more concise and readable than the previous example

public record User(Name name,
                   Surname surname,
                   Login login,
                   Password password,
                   Email email) {

    public User {

        requireNonNull(name, "name cannot be null");
        requireNonNull(surname, "surname cannot be null");
        requireNonNull(login, "login cannot be null");
        requireNonNull(password, "password cannot be null");
        requireNonNull(email, "email cannot be null");

    }

    public static WithFunction<WithFunction<WithFunction<WithFunction<User, Email>, Password>, Login>, Surname> with(Name name) {

        return surname -> login -> password -> email -> new User(name, surname, login, password, email);
    }
}

Usage

User user = User.with(Name.from("John"))
                        .with(Surname.from("Doe"))
                        .with(Login.from("johndoe"))
                        .with(Password.from("sosecretpassword"))
                        .with(Email.from("[email protected]"));

Primitive Obsession

  • Modeling using primitive obsession has impact on functional builder usage.

  • Building with primitive types require remembering constructor parameters order.

  • Creating below User record with functional builder, will not give you verbose parameter type name, but only required type information with one letter variable (e.g. with(String t)).

public record User(String name,
                   String surname,
                   String login,
                   String password,
                   String email) {

    public User {
        requireNonNull(name, "name cannot be null");
        requireNonNull(surname, "surname cannot be null");
        requireNonNull(login, "login cannot be null");
        requireNonNull(password, "password cannot be null");
        requireNonNull(email, "email cannot be null");
    }

    public WithFunction<WithFunction<WithFunction<WithFunction<User, String>, String>, String>, String> with(String name) {

        return surname -> login -> password -> email -> new User(name, surname, login, password, email);
    }
}
 User user = User.with("John")
                        .with("Doe")
                        .with("johndoe")
                        .with("sosecretpassword")
                        .with("[email protected]");

Library limitations

  • Unable to create builders with optional paths/parameters.

  • For records unable to hide the constructor.

  • Confusing when not using Value Objects and leveraging static language features.

    • E.g. which String means what in with(String t)?

    • Not an issue when using strongly typed Value Objects.

  • Possible personal style preferences issues:

    • Multiple nested functions in builder declaration.

    • General method names in builder (with(…​)) without parameter name (e.g. withName(…​)).

    • No explicit build() method.

Functions

  • Library provides two types of functions:

    • <WORD>Consumer (e.g. WithConsumer<T>) - function that accepts a single input argument, returns no result and substitutes <WORD>.

    • <WORD>Function (e.g. WithFunction<R, T>) - function that accepts one argument, produces a result and substitutes <WORD>.

  • Available functions

Customization

What if fluent-api library does not provide functional interfaces for my specific domain?

Use auxiliary project fluent-api-generator to generate version of fluent-api library tailored to your needs.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages