参数对象通过Builder-Pattern
首先,构建器模式是一种实例工厂,在调用 .build() 或类似构建器实例上的类似内容后,您会得到一个简单的 POJO。
因此构建器经常遵循这种语法:
SomeClass instance = new SomeClass.Builder<>(requiredArgument).optionalArgumentX(x).build();
这种模式通常与具体对象的构造函数的有限范围(private 或protected)齐头并进,但并不坚持这一点。
虽然 Timo 已经给出了一个示例,您可以在其中使用 Parameter Object 和 Builder 模式的组合,但编写一个收集之前已经被其他构建器捕获的参数的构建器可能会导致大量复制和粘贴代码(不要不要重复自己)。
因此,我提出了一个父构建器设置,您可能会感兴趣,特别是如果您将来可能需要扩展生成的参数对象。
这种可扩展构建器模式的核心是一个抽象的TestParam 类,它还定义了一个抽象构建器。
public abstract class TestParam<Z>
{
public static abstract class CommonBuilder<T extends CommonBuilder<T, Z>, Z>
{
protected final String a;
protected final String b;
protected final String c;
protected Z z = null;
public CommonBuilder(String a, String b, String c)
{
this.a = a;
this.b = b;
this.c = c;
}
public T withOptionalZ(Z z)
{
this.z = z;
return (T)this;
}
public abstract <T> T build();
}
protected final String name;
protected final String a;
protected final String b;
protected final String c;
protected Z z = null;
protected TestParam(String name, String a, String b, String c)
{
this.name = name;
this.a = a;
this.b = b;
this.c = c;
}
protected TestParam(String name, String a, String b, String c, Z z)
{
this.name = name;
this.a = a;
this.b = b;
this.c = c;
this.z = z;
}
public String getA()
{
return a;
}
public String getB()
{
return b;
}
public String getC()
{
return c;
}
protected abstract String getContent();
@Override
public String toString()
{
return name+"[A: " + a + ", B: " + b + ", C: " + c + (z != null ? ", Z: " + z.toString() : "") + getContent() +"]";
}
}
这个抽象类具有您示例中的所有公共参数(a、b 和 c)以及一个附加的可选参数 z,其类型可以通用传递。除了抽象定义之外,大多数东西都应该是直截了当的。通用构建器类型的定义是为了让我们实际上可以通过子构建器创建适当的子类。
子类(包括子构建器)现在可以如下所示:
public class TestParamA<D,E,Z> extends TestParam<Z>
{
public static class Builder<T extends TestParamA<D,E,Z>, B extends TestParamA.Builder<? extends TestParamA<D,E,Z>, ? extends B, D,E,Z>, D,E,Z> extends TestParam.CommonBuilder<TestParamA.Builder<?,?, D,E,Z>, Z>
{
protected D d;
protected E e;
public Builder(String a, String b, String c)
{
super(a, b, c);
}
public B withD(D d)
{
this.d = d;
return (B)this;
}
public B withE(E e)
{
this.e = e;
return (B)this;
}
@Override
public <T> T build()
{
TestParamA t = new TestParamA("TestParamA", a, b, c, z, d, e);
return (T)t;
}
}
protected final D d;
protected final E e;
protected TestParamA(String name, String a, String b, String c, Z z, D d, E e)
{
super(name, a, b, c, z);
this.d = d;
this.e = e;
}
public D getD()
{
return d;
}
public E getE()
{
return e;
}
@Override
protected String getContent()
{
return ", D: " + d + ", E: " + e;
}
}
除了泛型类型定义之外,这里大部分的东西都很简单:
Builder<T extends TestParamA<D,E,Z>,
B extends TestParamA.Builder<? extends TestParamA<D,E,Z>, ? extends B, D,E,Z>,
D,E,Z>
extends TestParam.CommonBuilder<TestParamA.Builder<?,?, D,E,Z>, Z>
-
T 是要通过构建器创建的对象的类型(TestParamA、TestParamB、...)
-
B 是创建参数对象的构建器的当前实例。这看起来相当复杂,但如果您使用来自父构建器的方法,则保证使用子构建器并且不会回退到父构建器。
-
D、E、Z是传递给builder的参数的实际类型
我不在这里发布TestParamB,因为它几乎与TestParamA 相同,只是它定义了构建器操作withF(...) 和withG(...) 而不是withD(...) 和withE(...),并且还打印了@987654348 @ 和 G 等效输出。
您现在有几个选项可以将构建器与方法声明结合使用。由于我不确定哪种方法最适合您,因此我创建了一个包含多个不同调用的小型测试用例:
public class Main
{
public static void main(String ... args)
{
TestParamA<D,E,?> a = new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()).build();
TestParamB<F,G,String> b = new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()).withOptionalZ("z").build();
TestParam<String> c = new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()).withOptionalZ("z").build();
TestParam d = new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()).build();
test(a);
test(b);
test(c);
test(d);
test(new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()));
test(new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()).withOptionalZ("z"));
testCommon(new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()).withOptionalZ("z"));
testCommon(new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()));
}
public static void test(TestParamA<?,?,?> testParam)
{
System.out.println("Test for ParamA: " + testParam.toString());
}
public static void test(TestParamB<?,?,?> testParam)
{
System.out.println("Test for ParamB: " + testParam.toString());
}
public static void test(TestParam<?> testParam)
{
System.out.println("Test for Param: " + testParam.toString());
}
public static void test(TestParamA.Builder<?,?,?,?,?> builder)
{
System.out.println("Test for BuilderA: " + builder.build().toString());
}
public static void test(TestParamB.Builder<?,?,?,?,?> builder)
{
System.out.println("Test for BuilderB: " + builder.build().toString());
}
public static void testCommon(TestParam.CommonBuilder<?,?> builder)
{
System.out.println("Test for CommonBuilder: " + builder.build().toString());
}
}
在运行这个测试类时,应该返回以下输出:
Test for ParamA: TestParamA[A: a, B: b, C: c, D: D, E: E]
Test for ParamB: TestParamB[A: a, B: b, C: c, Z: z, F: F, G: G]
Test for Param: TestParamA[A: a, B: b, C: c, Z: z, D: D, E: E]
Test for Param: TestParamB[A: a, B: b, C: c, F: F, G: G]
Test for BuilderA: TestParamA[A: a, B: b, C: c, D: D, E: E]
Test for BuilderB: TestParamB[A: a, B: b, C: c, Z: z, F: F, G: G]
Test for CommonBuilder: TestParamA[A: a, B: b, C: c, Z: z, D: D, E: E]
Test for CommonBuilder: TestParamB[A: a, B: b, C: c, F: F, G: G]
new D() 和使用new 创建的其他类只是简单的 POJO,它们在toString() 中返回它们的简单类名。
可以看出,每个调用的测试方法都包含通过相应的构建器创建的适当的子参数对象。对于更通用的方法,如test(TestParam<?> testParam) 或testCommon(...),您可能需要将参数对象强制转换为具体类,然后才能真正访问具体类独有的那些方法(getD(),...) - 但我猜你无论如何都熟悉这个概念。
缺点
- 与传统的构造函数调用相比,编写构建器会产生额外的开销
- 创建新实例还需要额外输入额外字符
优点
- 参数的灵活顺序可能。通常您不必记住参数的顺序,如果您处理超过 5 个以上的参数,这非常好。但是,必需的参数通常在构建器的构造函数中指定,因此需要固定的顺序,除非它们可以使用构建器方法指定。
- 支持对相关参数进行分组(如
.dimensions(int x, int y, int width, int height))
- 类型安全
- 可扩展性(如本文所示)
- 生成的类型可以用作
Parameter Objects,因此如果创建的对象遵循父子结构,则依赖于多态性
- 增加了可读性支持。尽管在这篇文章的 cmets 中有争论,但如果您在一个月后返回代码并且必须记住所有传递的参数是什么,构建者会增加可读性。构建器为参数添加了某种词汇语义。因此,可以通过适当地构造流畅的方法调用来提高可读性
何时(不)使用构建器
话虽如此,建设者很好,但也有开销。如果只有很少的参数或应该创建许多不同的独立类型,则不应使用它们,因为需要为每种类型设置构建器。在这里,第一种情况的简单 POJO 实例化和后一种情况的通用工厂模式是优越的 IMO。
如果您的方法需要尽可能灵活,并且您不需要依赖类型安全或提供一些内部类型提取机制(如 Camel 的类型转换器),请改用 Map<String, Object> 作为参数对象。 Camel 将这种方法用于其消息头。 Activiti BPMN 引擎也使用这种方法。 (由 AdamSkywalker 在此线程中解释)
如果您的场景数量有限且参数数量明确,请使用简单的方法重载(如 Chetan Kinger 所述)。
如果您随着时间的推移难以记住参数的确切顺序,那么将来可能会进行某种类扩展,或者如果您有一堆可选参数(甚至可能带有一些默认值),构建器会很好地出现.