In this tutorial, we explore the possibilities of using Lombok’s @Builder annotation generation method Builder to improve usability.
An overview
In this tutorial, we will explore the possibility of generating method builders with Lombok @Builder annotations. The goal is to improve availability by providing a flexible way to call a given method, even if it has many arguments.
@Simple method-based builder
How to provide flexible usage for methods is a common topic that may accept multiple inputs. Consider the following example:
void method(@NotNull String firstParam, @NotNull String secondParam,
String thirdParam, String fourthParam,
Long fifthParam, @NotNull Object sixthParam) {
...
}
Copy the code
If arguments not marked as not NULL are optional, the method may accept all of 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); .Copy the code
This example already shows some problem points, such as:
- The caller should know which argument is which (for example, in order to change the first call to provide
Long
The caller must have known, tooLong
Expected to be the fifth parameter). - The inputs must be set in the given order.
- The name of the input parameter is opaque.
Also, from the provider’s point of view, providing methods with fewer parameters means a lot of overloading of method names, for example:
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); .Copy the code
For better usability and to avoid boilerplate code, you can introduce a method builder. The Lombok project has provided an annotation to simplify the use of the builder. The sample method above can be commented 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) {
...
}
Copy the code
Therefore, calling this method will look something 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();
Copy the code
This way, method calls are easier to understand and modify later. Some comments:
- By default, a builder method on a static method (the method that gets the builder instance) is itself a static method.
- By default
call()
The method will have the same throw signature as the original method.
The default value
In many cases, it is useful to define default values for input parameters. Unlike some other languages, Java does not have language elements that support this requirement. So, in most cases, this is done through method overloading, structured as follows:
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 ... }Copy the code
When Lombok Builders are used, a builder class is generated in the target class. The generator class:
- Has the same number of attributes and parameters as a method.
- Set for its parameters.
Classes can also be defined manually, which provides the possibility of defining default values for parameters. Thus, the above method looks 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!" ; }Copy the code
With this addition, if the caller does not specify parameters, the default values defined in the Builder class are used.
Note: In this case, we don’t have to declare all the input parameters of the method in the class. If the input parameter to the method is not in the class, Lombok generates an additional attribute accordingly.
Typing method
An input is usually required to define the return type of a given method, for example:
public <T> T read(byte[] content, Class<T> type) {... }Copy the code
In this case, the Builder class will also be a typed class, but the Builder method will create an instance without bounded types. Consider the following example:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call", builderClassName = "MethodBuilder") public <T> T read(byte[] content, Class<T> type) {... }Copy the code
The methodBuilder method in this case will create a methodBuilder with no bounded type parameters. This causes the following code to fail to compile (this is Class
, provided by Class
):
methodBuilder()
.content(new byte[]{})
.type(String.class)
.call();
Copy the code
This can be done by taking type and using it as:
methodBuilder()
.content(new byte[]{})
.type((Class)String.class)
.call();
Copy the code
It compiles, but there’s another aspect to mention: in this case, the return type of the call method will not be String, but it will still be an unbound T. Therefore, the client must cast the return type like this:
String result = (String)methodBuilder()
.content(new byte[]{})
.type((Class)String.class)
.call();
Copy the code
This solution is possible, but it also requires the caller to transform both the input and the result. Because the original motivation was to provide a caller-friendly way to invoke these methods, it is recommended to consider one of the following two options.
Override generator method
As mentioned above, the root of the problem is that the Builder method creates an instance of the Builder class without specifying a type parameter. You can still define builder methods in classes and create instances of builder classes with the required types:
@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); }
}
Copy the code
In this case, instead of casting at any time, the caller calls something like this:
List result = methodBuilder(List.class)
.content(new byte[]{})
.call();
Copy the code
Cast in the setter
You can also cast generator instances in setters for type arguments:
@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); }
}
Copy the code
In this way, there is no need to define the Builder method manually, and the type parameter is passed just like any other parameter from the caller’s point of view.
conclusion
Using @Builder in a method can provide the following benefits:
- More flexibility on the part of callers
- There is no default input value for method overloading
- Improved readability of method calls
- Allows similar calls from the same generator instance
It is also worth mentioning that in some cases using method builders can introduce unnecessary complexity to providers
Thanks for reading