Method Builder With Lombok @Builder
In this tutorial, we are going to explore the various possibilities of generating method builders with Lombok's @Builder annotation to improve usability.
Join the DZone community and get the full member experience.
Join For FreeOverview
In this tutorial, we are going to explore the possibilities of generating method builders with Lombok's @Builder annotation. The aim is to improve usability by providing a flexible way of calling a given method even if it has a lot of parameters.
@Builder on Simple Methods
How to provide a flexible usage for methods is a general topic that might take multiple inputs. Take a look at the following example:
void method(@NotNull String firstParam, @NotNull String secondParam,
String thirdParam, String fourthParam,
Long fifthParam, @NotNull Object sixthParam) {
...
}
If the parameters that are not marked as not null are optional, the method might accept all the following calls:
method("A", "B", null, null, null, new Object());
method("A", "B", "C", null, 2L, "D");
method("A", "B", null, null, 3L, this);
...
This example already shows some problematic points such as:
- The caller should know which parameter is which (e.g. in order to change the first call to provide a
Long
too, the caller must knowLong
is expected to be the fifth parameter). - Inputs must be set in a given order.
- Names of the input parameters are not transparent.
In the meantime, from the provider's perspective, providing methods with fewer parameters would mean massive overloading of the method name, such as:
void method(@NotNull String firstParam, @NotNull String secondParam, @NotNull Object sixthParam);
void method(@NotNull String firstParam, @NotNull String secondParam, String thirdParam, @NotNull Object sixthParam);
void method(@NotNull String firstParam, @NotNull String secondParam, String thirdParam, String fourthParam, @NotNull Object sixthParam);
void method(@NotNull String firstParam, @NotNull String secondParam, String thirdParam, String fourthParam, Long fifthParam, @NotNull Object sixthParam);
...
To achieve better usability and avoid boilerplate code, method builders could be introduced. Project Lombok already provides an annotation in order to make the use of builders simple. The example method above could be annotated in the following way:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call")
void method(@NotNull String firstParam, @NotNull String secondParam,
String thirdParam, String fourthParam,
Long fifthParam, @NotNull Object sixthParam) {
...
}
Thus, calling the method would look like:
methodBuilder()
.firstParam("A")
.secondParam("B")
.sixthParam(new Object())
.call();
methodBuilder()
.firstParam("A")
.secondParam("B")
.thirdParam("C")
.fifthParam(2L)
.sixthParam("D")
.call();
methodBuilder()
.firstParam("A")
.secondParam("B")
.fifthParam(3L)
.sixthParam(this)
.call();
In this way, the method call is much easier to understand and change later. Some remarks:
- By default, a builder method (method to obtain a builder instance) on a static method is going to be itself a static method.
- By default, the
call()
method will have the same throw signature as the original method.
Default Values
In many cases, it can be really helpful to define default values for the input parameters. Unlike some other languages, Java does not have a language element to support this need. Therefore, in most cases, this is reached via method overloading, having structures like:
method() { method("Hello"); }
method(String a) { method(a, "builder"); }
method(String a, String b) { method(a, b, "world!"); }
method(String a, String b, String c) { ... acutal logic here ... }
While using Lombok builders, a builder class is going to be generated within the target class. This builder class:
- has the same number of properties and arguments as the method.
- has setters for its arguments.
It is also possible to define the class manually, which gives the possibility to define default values for the parameters. In this way, the method above would look like this:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call", builderClassName = "MethodBuilder")
method(String a, String b, String c) {
... acutal logic here ...
}
private class MethodBuilder {
private String a = "Hello";
private String b = "builder";
private String c = "world!";
}
With this addition, if the caller does not specify a parameter, the default value defined in the builder class is going to be used.
Note: In this case, we do not have to declare all the input parameters of the method in the class. If an input parameter of the method is not present in the class, Lombok will generate an additional property accordingly.
Typed Methods
It is a common need to define the return type of a given method through one of the inputs, such as:
public <T> T read(byte[] content, Class<T> type) {...}
In this case, the builder class will also be a typed class, but the builder method will create an instance without a bounded type. Take a look at the following example:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call", builderClassName = "MethodBuilder")
public <T> T read(byte[] content, Class<T> type) {...}
In this case, the methodBuilder
method is going to create an instance of MethodBuilder
without bounded type parameters. This leads to the fact that the following code will not compile (as would be required by Class<T>
, and is provided by Class<String>
):
methodBuilder()
.content(new byte[]{})
.type(String.class)
.call();
This can be resolved by casting the input of type
and use it as:
methodBuilder()
.content(new byte[]{})
.type((Class)String.class)
.call();
It will compile, but there is another aspect to mention: the return type of call method is not going to be String in this case, but still unbound T. Therefore, the client has to cast the return type like this:
String result = (String)methodBuilder()
.content(new byte[]{})
.type((Class)String.class)
.call();
This solution works, but it also requires the caller to cast both the input and the result. As the original motivation is to provide a caller-friendly way to invoke the methods, it is recommended to consider one of the two following options.
Override the Builder Method
As stated above, the root of the problem is that the builder method creates an instance of the builder class without a specific type parameter. It is still possible to define the builder method in the class and create an instance of the builder class with the desired type:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call", builderClassName = "MethodBuilder")
public <T extends Collection> T read(final byte[] content, final Class<T> type) {...}
public <T extends Collection> MethodBuilder<T> methodBuilder(final Class<T> type) {
return new MethodBuilder<T>().type(type);
}
public class MethodBuilder<T extends Collection> {
private Class<T> type;
public MethodBuilder<T> type(Class<T> type) { this.type = type; return this; }
public T call() { return read(content, type); }
}
In this case, the caller does not have to cast anytime and the call looks like this:
List result = methodBuilder(List.class)
.content(new byte[]{})
.call();
Casting in the Setter
It is also possible to cast the builder instance within the setter of the type parameter:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call", builderClassName = "MethodBuilder")
public <T extends Collection> T read(final byte[] content, final Class<T> type) {...}
public class MethodBuilder<T extends Collection> {
private Class<T> type;
public <L extends Collection> MethodBuilder<L> type(final Class<L> type) {
this.type = (Class)type;
return (MethodBuilder<L>) this;
}
public T call() { return read(content, type); }
}
Using this way, there is no need to define the builder method manually, and from the caller's perspective, the type parameter is handed over just as any other parameter.
Conclusion
Using @Builder on methods can bring the following advantages:
- More flexibility on the caller's side
- Default input values without method overloads
- Improved readability of the method calls
- Allowing similar calls via the same builder instance
In the meantime, is also worth mentioning that in some cases, using method builders can bring unnecessary complexity on the provider's side. Various examples for method builders are available on GitHub.
Opinions expressed by DZone contributors are their own.
Comments