Bulider Design Pattern为具有大量参数的方法制作通用方法
我有一个接口Itest
, ClassA
和ClassB
正在实现这个接口。 testA
和testB
分别是这些类中的方法。
testA(String a, String b, String c, D d, E e) testB(String a, String b, String c, F f, G g)
这里D
, E
, F
, G
是自定义数据类型(与数据库相关) 。 我简化了方法实际上他们有更多的参数。
我需要在Itest
接口的testAB
中创建一个generics方法,并在两个类中实现它而不是拥有自己的方法。
testAB(String a, String b, String c, D d, E e, F f, G g)
由于参数的数量越多,通用方法testAB
对于用户testAB
将是痛苦的,因为他必须传递如此多的null
值。
-
这是Bulider Design Pattern的用例吗?
-
如果是,如何使用这种设计模式实现这一目标?
是的,你可以使用构建器模式。 您希望将包含信息的对象传递给您的方法。
可以使用内部构建器创建这些对象,而字段可以包含默认值。 这是一个小例子,看起来像这样。
ParamTestA paramA = new ParamTestA<>.Builder(a, b, c).setD(d).setE(e).build(); testA(paramA);
感觉就像你想做错事:
首先,您需要一个在您的界面中具有大量通用参数的方法
interface ITest { void test(String a, D d, E e, F f, G g) }
这是错误的,因为您的界面与实现细节紧密结合
如果您将尝试从方法参数的差异中抽象出来
interface ITest { void test(String a, Map params); }
你会得到你想要的东西,但你会失去通用类型检查。
无论如何,我会推荐你这个变种,因为你负责将参数传递给你的方法。
看起来您的核心要求是您不希望客户端在不需要时传递其他参数。 您可以使用普通的旧方法overloading
来解决您的问题:
改变您的ITest
界面以获得一个名为test
方法
public interface ITest { public void test(String a,String b,String c,D d,E e,F f,G g); }
更改A
如下:
public class A implements ITest { //this is an overload - v1 public void test(String a,String b,String c,D d,E e) { //dispatch the call to the overriden method test(a,b,c,d,e,null,null); } //this is an overload - v2 public void test(String a,String b,String c,E e,F f) { //dispatch the call to the overriden method test(a,b,c,null,null,e,f); } @Override //this is an overriden method - v3 public void test(String a,String b,String c,D d,E e,F f,G g) { if(d!=null && e!=null) { //use a,b,c,d,e and do something } if(f!=null && g!=null) { //use a,b,c,f,g and do something } } }
现在,客户端代码可以调用他们想要的任何重载forms,而无需传递null
。 您的重载方法将简单地将调用分派给一个公共方法(这为您提供了代码重用的优势):
classAObj.test("1","2","3",new D(),new E());//calls overloaded method - v1 classAObj.test("1","2","3",new F(),new G());//calls overloaded method - v2 classAObj.test("1","2","3",new D(),new E(),new F(),new G());//calls overriden method - v3
请注意客户端代码在不需要时不必担心传递其他参数。 还要注意客户端调用的外观。 也可以在B
进行类似的改变。
1.您可以选择将ITest
作为抽象类。 这将允许您使其中的test
方法具有protected
访问说明符。 想要protected
访问说明符的原因是限制客户端类无法访问该方法,而是始终通过重载的表单。 这是一个附加function,如果您目前仍在使用interface
,可以考虑在将来实施。
2.您还可以利用Generics
来避免在每次引入新对象类型时编写新类,但从其他答案中可以看出,这很容易使代码在很大程度上复杂化。 您还可以将重载方法添加到ITest
接口,使其成为合同的一部分。 但是,我故意将这些部分从我的答案中删除,因为问题的关键可以通过使用overloading
来解决。
3. Builder
模式是一种创造模式。 在这种特殊情况下,这是一种矫枉过正,因为D
, E
, F
和G
是域对象。 A类和B
类在真正意义上并不真正依赖于它们,而是将它们用作数据源。
参数对象通过Builder-Pattern
首先,构建器模式是一种实例工厂,在调用.build()
或类似于构建器实例的类似之后,您将得到一个简单的POJO。
因此,构建器通常遵循这种语法:
SomeClass instance = new SomeClass.Builder<>(requiredArgument).optionalArgumentX(x).build();
这种模式通常与具体对象的构造函数的有限范围( private
或protected
)密切相关,但并不坚持这一点。
虽然Timo已经给出了一个可以使用Parameter Object
和Builder
模式组合的示例,但编写一个构建器来收集其他构建器之前已经捕获的参数可能会导致大量的复制和粘贴代码(不要重复自己)。
因此,我提出了一个父级构建器设置,您可能会感兴趣,特别是如果您将来可能需要扩展生成的参数对象。
此可扩展构建器模式的核心是一个抽象的TestParam
类,它还定义了一个抽象构建器。
public abstract class TestParam { public static abstract class CommonBuilder, 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 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 extends TestParam { public static class Builder, B extends TestParamA.Builder extends TestParamA, ? extends B, D,E,Z>, D,E,Z> extends TestParam.CommonBuilder, 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 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; } }
除了generics类型定义之外,大多数东西都非常简单:
Builder, B extends TestParamA.Builder extends TestParamA, ? extends B, D,E,Z>, D,E,Z> extends TestParam.CommonBuilder, Z>
-
T
是通过构建器创建的对象的类型(TestParamA
,TestParamB
,…) -
B
是构建参数对象的构建器的当前实例。 这看起来相当复杂,但保证使用子构建器,如果使用父构建器中的方法,则不会回退到父构建器。 -
D
,E
,Z
是传递给构建器的实际参数类型
我不在这里发布TestParamB
,因为它几乎与TestParamA
相同,只是它定义了使用withF(...)
和withG(...)
而不是withD(...)
和withE(...)
构建器操作withE(...)
还可以打印F
和G
等效输出。
您现在有几个选项可以将构建器与方法声明结合使用。 由于我不确定哪种方法最适合您,因此我创建了一个包含多个不同调用的小型测试用例:
public class Main { public static void main(String ... args) { TestParamA a = new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()).build(); TestParamB b = new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()).withOptionalZ("z").build(); TestParam 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()
,…)的访问权限之前将参数对象testCommon(...)
转换为具体类。具体的课程 – 但我想你无论如何都熟悉这个概念。
缺点
- 与传统的构造函数调用相比,编写构建器会产生额外的开销
- 创建新实例还需要额外填写额外字符的成本
PROS
- 灵活的参数顺序可能。 通常你不必记住参数的顺序,如果你处理5个以上的参数,这是非常好的。 但是,必需参数通常在构建器的构造函数中指定,因此需要固定顺序,除非可以使用builder-methods指定它们。
- 支持相关参数的分组(如
.dimensions(int x, int y, int width, int height)
) - 输入安全
- 可扩展性(如本文所示)
- 生成的类型可以用作
Parameter Objects
,因此如果创建的对象遵循父子结构,则依赖于多态 - 增加可读性支持。 虽然在这篇文章的评论中有争议,但是如果你在几个月之后回到代码并且必须记住所有这些参数传递的内容,那么构建器会提高可读性。 构建者为参数添加某种词法语义。 因此,通过适当地构造流畅的方法调用,可以提高可读性
何时(不)使用Builders
话虽这么说,建设者很好,但也带来了开销。 如果只有少数参数或者应创建许多不同的独立类型,则不应使用它们,因为需要为每种类型设置构建器。 这里针对第一种情况的简单POJO实例化和针对后一种情况的一般工厂模式是优越的IMO。
如果您的方法需要尽可能灵活,并且您不需要依赖类型安全性或提供一些内部类型提取机制(如Camel的类型转换器),请使用Map
作为参数对象。 Camel将此方法用于其邮件头。 Activiti BPMN引擎也使用这种方法。 (AdamSkywalker在这篇post中解释)
如果您的场景数量有限且参数数量明显,请使用简单的方法重载(如Chetan Kinger所述)。
如果你很难记住参数随时间的确切顺序,将来可能会出现某种类扩展,或者如果你有一堆可选参数(甚至可能有一些默认值),那么构建器很好。