审校 | 梁策 孙淑娟
概述
我们将研究如何通过Lombok的@Builder注解让含有多个入参的方法更容易被调用,从而提高可用性。
使用@Builder的简单方法
怎样才能更灵活方便地调用多入参方法呢?请看下面的例子:
void method(@NotNull String firstParam, @NotNull String secondParam,
String thirdParam, String fourthParam,
Long fifthParam, @NotNull Object sixthParam) {
}
如果非空参数是可选的,那么有以下几种调用方式:
method("A", "B", null, null, null, new Object());
method("A", "B", "C", null, 2L, "D");
method("A", "B", null, null, 3L, this);
这个例子暴露了一些问题,比如:
- 调用者必须知道修改哪个参数(例如,第一个调用方法如果改为第二个,调用者必须知道要修改第五个为Long类型参数)。
- 入参顺序不可改变。
- 需要传入的参数名称不可见。
从提供者的角度来看,如果要提供参数较少的方法则需要大量重载,如下:
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);
为了提高可用性且避免重复代码,我们可以使用方法构建器。Lombok项目已经提供了一个注解,它可以使生成构建器变得更简单。因此,上面的例子可以改造为以下方式:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call")
void method(@NotNull String firstParam, @NotNull String secondParam,
String thirdParam, String fourthParam,
Long fifthParam, @NotNull Object sixthParam) {
}
然后就可以这样调用方法:
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();
这样一来,方法的可读性及灵活性都提高了。
- 默认情况下,一个静态方法的builder方法(获得构建器实例的方法)就是一个静态方法。
- 默认情况下,call()方法将抛出原方法的异常。
默认值
在许多情况下,需要去为方法的入参指定默认值,而Java与其他一些语言不同,它没有语义来支持这一需求。因此,在大多数情况下,它都是通过方法重载来实现的,如下:
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 }
当使用Lombok builder时,会在目标类中生成一个构建器类。这个构建器类:
- 拥有与方法相同数量的属性和参数。
- 有参数的set方法。
该类也可以手动定义,这样就可以为参数定义默认值。上面的方法也就可以写成:
@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!";
}
有了这个补充,如果调用者没有指定一个参数,那么将使用构建器类中定义的默认值。
注意:在这种情况下,我们不需要在类中声明该方法的所有入参,Lombok会自动配置。
泛型方法
我们会有通过入参去定义返回类型的需求,例如:
public <T> T read(byte[] content, Class<T> type) { }
在这种情况下,builder类将是一个泛型类,但是builder方法将创建一个没有约束类型的实例。请看下面的例子:
@Builder(builderMethodName = "methodBuilder", buildMethodName = "call", builderClassName = "MethodBuilder")
public <T> T read(byte[] content, Class<T> type) { }
在这种情况下,methodBuilder方法将创建一个没有约束类型的MethodBuilder实例。这导致下面的代码不会被编译(入参定义为Class,传入Class):
methodBuilder()
.content(new byte[]{})
.type(String.class)
.call();
上述问题可以通过强制转换类型来解决:
methodBuilder()
.content(new byte[]{})
.type((Class)String.class)
.call();
这样,它就可以通过编译。但是这么使用,调用方法后返回的类型不是String,而是未知类型的泛型T。
String result = (String)methodBuilder()
.content(new byte[]{})
.type((Class)String.class)
.call();
那么,我们需要再将输出结果进行强制转换。而我们最初的目的是提供一种方便调用者的方法,因此建议考虑下面的两种解决方案之一。
覆盖Builder方法
如上所述,问题的根源在于builder方法在没有传入特定类型参数的情况下,创建了builder类的实例。所以我们可以在类中重载构建器方法,并实例化一个指定类型的构建器类:
@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); }
}
在这种情况下,调用者不需要进行强制类型转换,调用过程如下:
List result = methodBuilder(List.class)
.content(new byte[]{})
.call();
在setter方法中强制转换
也可以在类型参数set方法中强转构建器实例类型:
@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); }
}
使用这种方式就不需要手动定义构建器方法。从调用者的角度来看,类型参数就像其他参数一样直接传入。
结论
在方法上使用@Builder可以带来以下优势:
- 提高了调用者的灵活性
- 在不写重载方法的前提下,可以定义参数的默认值
- 提高了方法调用的可读性
- 可以通过同一个builder实例进行类似的调用
需要注意的是,在某些情况下,使用方法构建器会给提供者带来不必要的复杂情况。各类方法构建器的demo可参考GitHub上的信息。
译者介绍
翟珂,51CTO社区编辑,目前在杭州从事软件研发工作,做过电商、征信等方面的系统,享受分享知识的过程,充实自己的生活。
原文Method Builder With Lombok @Builder,作者:Daniel Buza