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 provideLongThe caller must have known, tooLongExpected 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 defaultcall()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