具有大参数或Java bean getter / setter方法的Java构造函数

我无法确定哪种方法更适合创建具有大量字段(10+)(所有必需的)getter / setter的构造函数方法的对象。 构造函数至少要强制设置所有字段。 Java Bean更容易看到正在设置哪些变量而不是巨大的列表。 构建器模式似乎不合适,因为所有字段都是必需的,构建器要求您将所有必需参数放在构建器构造函数中。

感谢:D

更好的方法(imho)是使用某种构建器:

MyClass a = new MyClassBuilder().blah("blah").foo("foo").doStuff().toMyClass(); 

其中MyClass仍然是不可变的,但具有比具有10个参数的构造函数更可读的创建。

这也称为流畅的界面 。 Josh Bloch在Effective Java中提到了这一点。

我的第一个想法是检查你的封装模型是否正确。 拥有10个以上的必填字段听起来相当多,在这种情况下拥有更细粒度的组件可能更有意义吗?

这些字段/参数中的一些是否相关? 它们可以组合成有意义的对象(例如, x-coordinatey-coordinate组合成Point对象等)

史蒂夫麦康奈尔在其着作“完整的代码”中指出,任何程序都不应超过最多6个,也许7个参数。 这些陈述中的大多数不仅仅是他的观点,而是以研究为后盾,例如与代码结构相关的错误率。

罗伯特·马丁的清洁代码甚至更进一步:他建议使用1或2个参数,而3则已经被认为是“代码味道”。 就个人而言,我认为清洁代码在某些地方有点极端,但总的来说它提出了一些好的论据。

“一大堆参数”(无论多少参数)都是“厨房水槽”设计,具有大量的后遗症和很少的结构。 它还使维护更加困难和容易出错; 至少,它使代码难以阅读。

所有这些都有充分的理由考虑减少参数的数量。 其他答案提供了一些实用的建议。

我建议你在这种情况下考虑构建器模式 。 您可以保证获得有效的对象,而无需拥有大量参数。

OP是更新以拒绝构建器模式,但它似乎是基于误解。 Builder模式存在的事实不会删除所有参数的强制执行。

考虑以下对象:

  public class SomeImmutableObject { private String requiredParam1; private String requiredParam2; //etc. private SomeImmutableObject() { //cannot be instantiated outside the class } public static class Builder { private SomeImmutableObject instance; public Builder() { instance = new SomeImmutableObject(); public Builder setParameter1(String value) { instance.requiredParam1 = value; return this; } //etc for each parameter. public SomeImmutableObject build() { if (instance.requiredParam1 == null || instance.requiredParam2 == null /*etc*/) throw new IllegalStateException("All required parameters were not supplied."); return instance; } } } 

请注意,通过将字段包设为私有并将构建器放在同一个包中,您可以完成基本相同的操作。

如果由于某种原因你不能这样做,你仍然可以使用10个参数的构造函数,然后让Builder成为调用该构造函数的唯一东西,这样它就是一个更容易使用的API。

因此,对于所有声明的要求,Builder模式工作得很好。 需要所有10个参数这一事实并不会取消构建器模式的资格。 如果模式不满足其他需要,请详细说明。

编辑:OP添加了一个评论(很久以前,但我刚刚在这个问题上得到了一个upvote,所以我现在才看到它)有一个有趣的问题:你如何在以后的某个时间点validation原语?

有几种解决这个问题的方法,包括一个保护布尔值,但我首选的方法是使用Double对象,如下所示:

  private Double doubleForPrimitive; public Builder setDouble(double d) { doubleForPrimitive = d; } public SomeImmutableObject build() { if(doubleForPrimitive != null) { instance.doubleParam = doubleForPrimitive; } else { throw new IllegalArgumentExcepion("The parameter double was not provided"); } //etc. } 

应该注意的是,如果你需要真正的线程安全不变性,将不可变对象的所有字段都作为final,这需要更多的样板(将变量存储在构建器中并将它们传递给不可变对象的私有构造函数),但是从客户端代码的角度来看,这种方法仍然很简洁。

您可以考虑使用构建器模式 ,构建器确保所有字段至少设置为合理的默认值。 请参阅实施链接,但最终会看到如下所示的电话:

 Widget widge = new Widget.Builder(). manufacturer("333").serialNumber("54321").build(); 

考虑这种情况,这两种模式很有用:

  • 从福勒的“重构”中引入参数对象 。
  • 用 Joshua Kerievsky的书“Refactoring to Patterns” 替换构造函数和创建方法 。

构造函数的十个参数很多。 我会认真考虑是否所有这些都是必需的,而且其中一些组合成逻辑对象是没有意义的。 如果确实有十个不同的必需数据,那么构造函数应该包含十个字段。

恕我直言,您应该根据构造函数中的业务逻辑传递对象有效所需的所有内容。

如果参数列表很长,您可以创建一个包含参数的对象并传递它。

我会像这样实现构建器模式:

 package so1632058; public class MyClass { private final String param1; private final String param2; MyClass(Builder builder) { this.param1 = builder.param1; this.param2 = builder.param2; } public String getParam1() { return param1; } public String getParam2() { return param2; } @SuppressWarnings("hiding") public static final class Builder { String param1; String param2; public Builder param1(String param1) { this.param1 = param1; return this; } public Builder param2(String param2) { this.param2 = param2; return this; } public MyClass toMyClass() { return new MyClass(this); } } } 

然后使用以下代码来使用它:

 package so1632058; public class Main { public static void main(String[] args) { MyClass.Builder builder = new MyClass.Builder(); builder.param1("p1").param2("p2"); MyClass instance = builder.toMyClass(); instance.toString(); } } 

一些说明:

  • 没有多种方法的方法。
  • 可以在MyClass构造函数中完成附加检查。
  • 我在整个包的范围内使构造函数可见,以避免“合成访问”警告。
  • 对于构建器的实例字段也是如此。
  • 构建MyClass实例的唯一方法是通过Builder

真的取决于具体的课程。 它应该是不可变的吗? 它是一个没有任何行为的简单值对象吗? 您是要将此值对象映射到Web服务参数还是关系数据库? 你要序列化吗? (其中一些东西需要一个默认的构造函数)。 你能谈谈这个对象吗?

是否有可能需要较少参数的类的变体,或者只有一个,它有十个属性?

该对象是不可变的吗?

就个人而言,我没有看到大型构造函数有任何问题,特别是如果只有一个构造函数,并且所有属性都是最终的。

如果所有参数实际上都是强制性的,那么我认为没有理由不使用构造函数。 但是,如果情况并非如此,那么使用构建器似乎是最好的方法。
在我看来,仅仅依赖于setter是最糟糕的解决方案,因为没有什么可以强制执行所有强制属性的设置。 当然,如果您使用的是Spring Framework的bean连接或其他类似的解决方案,那么Java bean就可以完全正常,因为您可以在初始化后检查所有内容已经设置完毕。

毫无疑问,这是一个设计问题。 你必须轻松权衡可读性。 十个arg构造函数更容易,但可能或可能不是更易读/可维护。 它还有较少的方法调用来调用和调用调用堆栈。 通过setter设置十个不同的值更具可读性和显性。 它不一定“更容易”(尽管你可以两种方式争论),并添加更多的方法调用来打开和关闭调用堆栈。

但是这里还有一些要考虑的问题。 即使使用十个参数构造函数,您也可以让程序员选择传入null,空白,false或零(取决于对象或原语),这可能是您想要的也可能不是。 控制它的唯一方法是在构造函数中抛出exception。 这是你真正想做的吗? 这是你期望的行为吗?

当然,通过setter分别设置每个变量,您可能无法知道何时构建对象或未构建对象。 这是上面讨论的Builder模式有用的地方。 让它创建对象,设置值,然后validation所有设置。 如果缺少某些东西,因为程序员决定不传递某些东西,那么你就会受到保护。 你的class级不必做超过预期的事情。 (毕竟,考虑一下有朝一日可能会使用你的课程是很重要的。尽管世界上有很多伟大的Javadoc,但他们可能无法理解你的意图。)

最后,我会问你是否需要违约? 因为如果某些东西可以默认,那么你可以在类级别将它们设置为默认值,或者在构造函数中将它们设置为默认值(取决于你的编程理想,你觉得它更具体,并协助你对象的行为) 。 然后,您可以“预设”某些字段,只需要通过手动设置器或通过构建器通过构建器覆盖它们。

同样,你必须自己决定这些事情。 但可能最重要的是考虑可读性而不是效率,以使代码更易于维护,并创建API和行为,以后程序员将无法滥用。 无论您使用什么,都要在设计中预见滥用保护。

IMO构造函数在创建对象时不会形成良好的API,尤其是当参数的数量很大且它们属于同一类型时。

 new Person(String, String, String, String); // what the?? this is what may // show up in IDEs 

它实际上意味着人物(名字,姓氏,屏幕名称,密码,(仅举例))

正如cletus提到的那样,带链接的Builder模式很好。 构建器的另一个优点是, 如果对象是不可变的,构建器可以返回相同的对象 (在这种情况下,您可以拥有一个包含15个args的包私有构造函数,只有构建器知道)。 构建器还可以返回它们构建的对象的任何子类型

您可以采取的另一种方法是考虑使用内部DSL。 但是,只有在构建配置,查询等对象时才有意义。看看内部DSL在您的情况下是否有意义。

我们的项目遇到了类似的问题。 我们必须从家庭网关(我们的产品)获取某些值。 它支持基于http的请求 – 响应,基于查询的XML协议。 但是创建用于在UI中发送的Request对象是繁琐的,通过使用适当的参数和filter等设置Request obejcts。

最初我们的请求对象如下所示:

 Request r = new Request("SET"); r.setAction("add"); // modify, add, delete r.setModuleName("NPWIFI"): r.addParameter(new Param("wifiAclMac", "aa:bb:cc:dd:ee:ff")); r.addParameter(new Param("wifiCommit", "commit")); r.setActionParam("wifiAclRowStatus") r.addFilter(new Filter(Type.EQUALS, "wifiInterface", "wl0")); r.addFilter(new Filter(Type.EQUALS, "wifiAclMac", "yourgateway")); Resonse r = gwSession.sendRequest(r); 

所以我们将它改成了一个内部DSL,它具有类似SQL的感觉,但只是程序化的

 Query q = DQL.add("wifiAclMac", "wifiCommit").into("NPWIFI").values ("aa:bb:cc:dd:ee:ff", "commit").withAp("wifiAclRowStatus") .where("wifiInterface").is("wl0") .and("wifiAclMac").is("aa:bb:cc:dd:ee:ff").end(); 

DQL“查询构建器”对象通过validation完成了所有构建,并且certificate使用起来非常方便。

构建器和DSL是一种优雅而强大的创建和构建对象的方法,但请查看在您的案例中有意义的内容。

这在摘要中很难回答。 真正需要做的是查看这十个参数并查看它们是什么。 我认为这些是关键问题:

  • 他们中的一些可以组合成更高级别的“价值”对象吗? 例如,变量X和Y可以组合成Point。 我们在货物路线选择程序中有很多这样的情况,其中所有字段都被建模为原始字符串。 引入一些更高级别的概念确实有助于使其可读。
  • 某些参数可以“默认”为某些值吗?
  • 它们真的是独立的,正交的概念吗? 我曾经在许多系统上工作,从未见过这是真的。 如果没有,可以考虑一下这些。

我会避免使用大量参数的构造函数。 构造函数中包含大量参数的类可能很难处理。 想象一下,如果你有一个inheritanceheirarchy与子类,每个子类在其构造函数中有许多参数。 如果需要更改某些顶级类的参数,将会有多少工作量。

我会将一个接口传递给你的构造函数,你可以在不破坏代码的情况下进行更改,或者使用Javabeans方法并且没有arg构造函数。

您的字段可以组合成中间对象吗? 例如,如果您传入10个描述人物的字段,则创建一个PersonInfo对象以传递该数据。 我个人更喜欢在实例化对象时传入所有必填字段。 这样你就不会得到一个不可避免地被滥用的半生不熟的物品。

如果实际需要所有参数,并且您没有使用Spring来实例化bean,那么我肯定会使用带有10个参数的构造函数。 事实上,所有参数实际上都是必需的。

如果你做Spring(可能偶尔)创建bean,或者你真的不喜欢在构造bean的方法中有很多临时变量,你也可以使用getter和setter。 如果您还实现了一个validation方法来检查对象是否正确,那么您应该没有问题。

validation方法效果最好但是如果你一直使用它; 也许有一个Validatable接口。 如果它是编程风格的一部分,它非常适合工作流程。 如果只有一个或两个类使用这个范例,那么它可能是不值得的。 无论如何,总有一天你会忘记调用validate()。

如果你没有使用validation器方法并且不喜欢构造函数(虽然我不知道为什么,这就是它的用途),你总是可以回到其他人提到的构建器。

物有所值; 如果你有10个必需的参数,并且你不能逻辑地对它们进行分组,你可能只是在一个类中混合了许多不同的概念; 在这种情况下,对您的设计进行良好,严格的审视并进行一些重构可能会更好。