DEV Community

nambach
nambach

Posted on

A new declarative way to construct objects in Java

Declarative programming brings many huge benefits: ease in maintainability, scalability, readability. Java is designed heavily in OOP, an imperative paradigm, which seems impossible to be declarative. However, I will show how to achieve declarative programming in Java in this article, starting with objects creation.

Introduction

Initializing objects in Java seems to be a trivia topic. Actually, there are more things we can talk about that.

C# - the longtime rival of Java - allows something like this (source).

class Cat
{
    public int Age { get; set; }
    public string Name { get; set; }
}
...
Cat cat = new Cat { Age = 10, Name = "Fluffy" };
Enter fullscreen mode Exit fullscreen mode

This syntax looks just like declaring a JSON object. What I want to mention here is that the above code is really declarative (the field names are specified along with the values) and flexible (we can arbitrarily omit any fields).

Meanwhile, Java makes it more strict. In this article, I will introduce a solution using chaining methods and lambda expression that can unlock the same ability for Java.

Existing approaches

1. Using Constructor

The traditional way to create new objects is using constructor. This may end up with too many constructors with unclear context in our codebase.

public Book(String isbn, String title) {
    this.isbn = isbn;
    this.title = title;
}

public Book(String isbn, String title, String author) {
    this.isbn = isbn;
    this.title = title;
    this.author = author;
}

public Book(String isbn, String title, String author, String subCategory, Category category) {
    this.isbn = isbn;
    this.title = title;
    this.author = author;
    this.subCategory = subCategory;
    this.category = category;
}
Enter fullscreen mode Exit fullscreen mode

To solve this problem, we can replace with static creation methods.

public static Book init(String isbn, String title) {
    Book book = new Book();
    book.isbn = isbn;
    book.title = title;
    return book;
}

public static Book fullInit(String isbn, String title, String author, String subCategory, Category category) {
    Book book = new Book();
    book.isbn = isbn;
    book.title = title;
    book.author = author;
    book.subCategory = subCategory;
    book.category = category;
    return book;
}
Enter fullscreen mode Exit fullscreen mode

2. Using Lombok @Builder

Eventually, constructors and static methods still lack the flexibility in customizing the number of arguments. To solve this problem, we can use the annotation @Builder of Lombok. This will allow us to omit any arguments as we want when initializing an object.

Book book = Book.builder()
    .title("Sapiens: A Brief History of Humankind")
    .author("Yuval Noah Harari")
    .subCategory("History")
    .build();
Enter fullscreen mode Exit fullscreen mode

You may wonder "Why makes it complicated? An empty constructor with some setters would just be fine". Let's think about situations that we need all in one row as below.

List<Book> nonFictions = Arrays.asList(
    Book.builder().title("Sapiens: A Brief History of Humankind").author("Yuval Noah Harari").build(),
    Book.builder().title("The Defining Decade").author("Meg Jay").build(),
    Book.builder().title("The State of Affairs").author("Esther Perel").build()
);
Enter fullscreen mode Exit fullscreen mode

3. Generic Typed Parameter

Still, there is one more issue with annotation @Builder, that is the case the object has generic type parameters.

Let's have a look at this example. We want to export a list of books in table format.

Book Table

We define a class Column<T> to map corresponding fields in the object.

public class Column<T> {
    String title; // required
    String fieldName; // optional
    Function<T, ?> customExtractor; // optional
}
Enter fullscreen mode Exit fullscreen mode

This will result in a list of Column that define the table we want to export.

Table<Book> = [
  Column("Book ID", "isbn"),
  Column("Name", "title"),
  Column("Category", book -> book.category.name)
]
Enter fullscreen mode Exit fullscreen mode

The above pseudo-code describes a table of Book. The first column is called "Book ID" with values taken from field isbn. The second column is called "Name" with values taken from title. The last column is "Category" with a function used to extract the target values.

Below is the corresponding Java code using Lombok @Builder.

List<Column<Book>> bookTable = Arrays.asList(
    Column.<Book>builder().title("Book ID").fieldName("isbn").build(),
    Column.<Book>builder().title("Name").fieldName("title").build(),
    Column.<Book>builder().title("Category").customExtractor(book -> book.getCategory().getName()).build()
);
Enter fullscreen mode Exit fullscreen mode

Notice that each time invoking .builder(), we always have to add <Book>, or else the compiler will fail to resolve the generic type T.

Solution: Function as a Builder

Now I will introduce the solution.

We won't use the Lombok @Builder. Instead, we will modify the class Column to mimic its chaining behavior.

(Chaining method: a method return the object invoking it - return this)

public class Column<T> {
    String title;
    String fieldName;
    Function<T, ?> customExtractor;

    public Column<T> title(String title) {
        this.title = title;
        return this;
    }

    public Column<T> fieldName(String fieldName) {
        this.fieldName = fieldName;
        return this;
    }

    public Column<T> customValue(Function<T, ?> customExtractor) {
        this.customExtractor = customExtractor;
        return this;
    }
}
Enter fullscreen mode Exit fullscreen mode

To let the compiler be able to resolve type T at runtime, we will use a static creation method having one argument Function<Column<T>, Column<T>>. This is the key concept of the solution.

public class Column<T> {
    ...

    public static <T> Column<T> add(Function<Column<T>, Column<T>> builder) {
        return builder.apply(new Column<>());
    }
}
Enter fullscreen mode Exit fullscreen mode

(We can replace Function<Column<T>, Column<T>> with UnaryOperator<Column<T>> for the brevity)

Function builder will transform the empty object into a fully created object with all fields valued. Our final code will look like this.

List<Column<Book>> bookTables = Arrays.asList(
    add(c -> c.title("Book ID").fieldName("isbn")),
    add(c -> c.title("Name").fieldName("title")),
    add(c -> c.title("Category").customValue(book -> book.getCategory().getName()))
);
Enter fullscreen mode Exit fullscreen mode

The code looks much cleaner and declarative. It is more similar to our pseudo-code in the beginning.

Conclusion

  1. Applying chaining method (mimic the Lombok @Builder) allows us to customize the number of arguments without writing too many constructor.

  2. Using Function<Entity, Entity> builder allows us to omit the keyword new

  • No more new in code: new Class(), new Class<Entity>(), new Class<>() or even Class.<Entity>init(). The builder has the ability to resolve the generic type Entity at runtime.

  • Thanks to lambda expression, we make our code much shorter with only 6 characters when creating a new object: o -> o.... Of course, we can use another variable name, but it is ok to let it be concise like that. The usage context has already been provided via the name of the invoking method (as the above example Column.add(c -> c...))

External resources

If you are interested in this pattern, here are some projects you can have a look to see how this pattern is applied.

Top comments (0)