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" };
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;
}
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;
}
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();
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()
);
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.
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
}
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)
]
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()
);
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;
}
}
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<>());
}
}
(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()))
);
The code looks much cleaner and declarative. It is more similar to our pseudo-code in the beginning.
Conclusion
Applying chaining method (mimic the Lombok @Builder) allows us to customize the number of arguments without writing too many constructor.
Using
Function<Entity, Entity> builder
allows us to omit the keywordnew
No more
new
in code:new Class()
,new Class<Entity>()
,new Class<>()
or evenClass.<Entity>init()
. Thebuilder
has the ability to resolve the generic typeEntity
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 exampleColumn.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.
- My Excel library: https://github.com/nambach/ExcelUtil
- SpringFu (from Spring team): https://github.com/spring-projects-experimental/spring-fu/tree/main/jafu
Top comments (0)