Making the Java Fluent API More Flexible
Want to learn more about improving the flexibility of the Java fluent API? Check out this tutorial to learn how in Java, Groovy, and Kotlin.
Join the DZone community and get the full member experience.
Join For FreeWith the appearance of Java 8 lambdas, and even before that, a lot of APIs appeared that used the fluent API pattern (that started as a pattern explained in this post from Martin Fowler). To name a few of these API instances in Java includes:
- Java Stream API
- Java Time API
- JPA Query Builder
- Lombok builders
There are a lot of articles about it on DZone and other sources:
- Design a Fluent API in Java
- API Design with Java 8
- Nested Fluent Builders with Java 8
- The Java Fluent API Designer Crash Course
While this pattern is quite fine, there are two cases when it becomes less fluent: conditional stages and stage reuse.
Java Case
Let's investigate the problem with using fluent builders. To stay on the bleeding edge, let’s use the experimental Lombok’s SuperBuilder annotation processor, which appeared in the version 1.18.2, as this builder fits the scenario described here. Note that this code generator is not supported by IntelliJ IDEA yet, so you will likely see a lot of red code if you open the project there, but the code works and could be compiled on Maven.
To start, let’s define a domain model.
@Data
public class Session {
private final String sessionId;
private final String user;
}
@SuperBuilder
public abstract class Base {
@Getter
@Builder.Default
private long created = System.currentTimeMillis();
@Getter
private String createdBy;
@Getter
private String createdSessionId;
}
@SuperBuilder
public class Person extends Base {
@Getter
private String name;
@Getter
private int age;
@Getter
@Singular
private Set<String> occupations;
}
Now, let’s build the object in the normal case:
public Person get(Session session) {
return Person.builder()
.name("test")
.createdBy(session.getUser())
.createdSessionId(session.getSessionId())
.age(42)
.occupation("test")
.build();
}
In the example above, there might be a case when the session object is null in case of anonymous users. The naïve handling of this case might look like the following:
public Person getDefaultValue(Session session) {
return Person.builder()
.name("test")
.createdBy(session == null ? null : session.getUser())
.createdSessionId(session == null ? null : session.getSessionId())
.age(42)
.occupation("test")
.build();
}
However, in this example, we create additional coupling between objects. The code now depends on knowledge of what is the default value for security fields. Currently, it is the null, but in the future, the default might change to the string “anonymous”
and our code will break. Thus, it would be better to skip this phase completely, as shown below:
public Person getOptional(Session session) {
Person.PersonBuilder<?, ?> b = Person.builder()
.name("test");
if (session != null) {
b = b
.createdBy(session.getUser())
.createdSessionId(session.getSessionId());
}
return b
.age(42)
.occupation("test")
.build();
}
Suddenly, our code is much less beautiful and fluent. Also, as we work with fields in the base class and we need to use the same check elsewhere, we could abstract it to the utility method:
public class SecurityUtil {
@SuppressWarnings("unchecked")
public static <B extends Base.BaseBuilder<?, ?>> B supplySecurityInfo(B builder, Session session) {
return session == null ? builder : (B) builder
.createdBy(session.getUser())
.createdSessionId(session.getSessionId());
}
}
And, to reuse it:
public Person getReuseWrap(Session session) {
return SecurityUtil.supplySecurityInfo(Person.builder()
.name("test")
, session)
.age(42)
.occupation("test")
.build();
}
Or, like the following:
public Person getReuseVariable(Session session) {
Person.PersonBuilder<?, ?> b = Person.builder()
.name("test");
b = SecurityUtil.supplySecurityInfo(b, session);
return b.age(42)
.occupation("test")
.build();
}
The code is a bit better, but the rhythm of invocations is still broken in both cases, and it does not feel fluent. We have only one reused stage, but consider the case when there are several of them. The code could grow quite hairy.
Introducing the Process Method
To solve these problems, we could have a simple <T> T process(Function<Builder,T>)
method, that just invokes a function passed as an argument with builder reference. In this example, we extend the Lombok-generated builder, as shown below:
public class PersonBuilderExt extends Person.PersonBuilder<Person, PersonBuilderExt> {
public <T> T process(Function<PersonBuilderExt, T> function) {
return function.apply(this);
}
public PersonBuilderExt() {
}
protected PersonBuilderExt self() {
return this;
}
public Person build() {
return new Person(this);
}
}
Then, we could implement the two scenarios above much more fluently for optional stages.
public static PersonBuilderExt personBuilderExt() {return new PersonBuilderExt();}
public Person getOptionalProcess(Session session) {
return personBuilderExt()
.name("test")
.process(b -> session == null ? b : b
.createdBy(session.getUser())
.createdSessionId(session.getSessionId())
)
.age(42)
.occupation("test")
.build();
}
And, stage resue:
public Person getReuseProcess(Session session) {
return personBuilderExt()
.name("test")
.process(b -> SecurityUtil.supplySecurityInfo(b, session))
.age(42)
.occupation("test")
.build();
}
The rhythm still breaks a bit, but it happens on a local level, rather than on a global level. After we invoke our special actions, the processing continues as normal. And, there are no user-visible assignments there. The need for such a custom stage processing happens quite often when working with complex APIs, like the JPA Query Builder. The lack of such a simple method sometimes leads to very complicated and hairy code.
Kotlin Case
If you use some other JVM language that has extension methods, like Groovy or Kotlin, then you could add such method to the API yourself. For Kotlin, we just need to define the method as an extension:
fun <B : BaseBuilder<*, *>?, T> B.process(ext: (b: B) -> T): T = ext(this)
And then, we could use it for optional and reuse cases:
fun getOptional(session: Session?): Person {
return Person.builder()
.name("test")
.process { b ->
if (session == null) b
else b.createdBy(session.user)
.createdSessionId(session.sessionId)
}
.age(42)
.occupation("test")
.build()
}
fun getReuse(session: Session?): Person {
return Person.builder()
.name("test")
.process { SecurityUtil.supplySecurityInfo(it, session) }
.age(42)
.occupation("test")
.build()
}
Note, if your process
method just invoke function like in this example, you do not have to write own method, it is possible to directly use the standard Kotlin's let
method like the following:
fun getReuseLet(session: Session?): Person {
return Person.builder()
.name("test")
.let { b -> SecurityUtil.supplySecurityInfo(b, session) }
.age(42)
.occupation("test")
.build()
}
We could even go a step further, if our method deserves it, and declare the method as a custom processing stage:
fun <B : BaseBuilder<*, *>?> B.supplySecurityInfo(session: Session?): B =
SecurityUtil.supplySecurityInfo(this, session)
And, we could use it directly in a bit nicer code:
fun getCustomStage(session: Session?): Person {
return Person.builder()
.name("test")
.supplySecurityInfo(session)
.age(42)
.occupation("test")
.build()
}
Note: Kotlin resolves these extension methods at compile time to normal static method calls.
Groovy Case
The Groovy case is more complicated, as there are currently some problems with type inference and compiling the code from IDEA. The supplied code is working fine with Maven, but it fails in IntelliJ IDEA if we use the @CompileStatic
annotation. First, we define an extension method (I'm using Java, as Groovy complains on generic bounds here):
public class BaseBuilderExtensions {
public static <B extends Base.BaseBuilder<?, ?>, T> T process(B builder, Function<B, T> body) {
return body.apply(builder);
}
}
Then, we need to register extensions in the file META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule
, like the following. The older versions before Groovy version 2.5.0 used META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
, but such usage is not completely compatible with Java 11, so the path to the file has changed, while the old path is still supported for compatibility:
moduleName = builder-extension
moduleVersion = 1.0
extensionClasses = samples.BaseBuilderExtensions, samples.SecurityUtil
Note: We register our Java class SecurityUtil
as an extension as well, as it follows the pattern of Groovy extension methods. Then, we could use our new methods for optional processing:
Person getOptionalProcess(Session session) {
return Person.builder()
.name("test")
.process { b -> session == null ? b : b
.createdBy(session.getUser())
.createdSessionId(session.getSessionId())
}
.age(42)
.occupation("test")
.build()
}
For reusing code:
Person getReuseProcess(Session session) {
return Person.builder()
.name("test")
.process { SecurityUtil.supplySecurityInfo(it, session) }
.age(42)
.occupation("test")
.build()
}
And, even for custom stage, the method comes from the SecurityUtil
class:
Person getCustomStage(Session session) {
return Person.builder()
.name("test")
.supplySecurityInfo(session)
.age(42)
.occupation("test")
.build()
}
The code looks as nice as the Kotlin code, but some additional efforts are required due to the dynamic nature of Groovy.
Conclusion
This pattern is very simple to implement, but it could add a great deal of usability in case of complex scenarios. If you are designing a fluent API, I would suggest adding such a method to your API. It would not harm anything, but it could make the user code look a bit nicer.
If you are using Kotlin, Groovy, or some other JVM language with extension methods, you do not need to wait until the API designer will add such a method to their fluent API (.NET had extension methods from the start, and the custom stage pattern is used there quite often). In languages with extension methods, it is possible to start using this pattern right now with custom extension methods when it is needed.
For Java projects, we still have to wait for API designers to support this pattern, as it is not always easy to plug own method into API. If you are fluent API designer yourself, please consider adding a single simple method like <T> T process(Funtion<Builder,T>)
to the API. This could help users with more flexible usage of your API.
The code for this article is available on GitHub.
Opinions expressed by DZone contributors are their own.
Comments