繁体   English   中英

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

[英]Java constructor with large arguments or Java bean getter/setter approach

我无法确定哪种方法更适合创建具有大量字段(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,所以我现在才看到它),并提出了一个有趣的问题:如何在以后的某个时间点验证原语?

有几种方法可以解决这个问题,包括一个保护布尔值,但我首选的方法是使用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();

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

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

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

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

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

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或零(取决于对象或原语),这可能是您想要的也可能不是。 控制它的唯一方法是在构造函数中抛出异常。 这是你真正想做的吗? 这是你期望的行为吗?

当然,通过setter分别设置每个变量,您可能无法知道何时构建对象或未构建对象。 这是上面讨论的Builder模式有用的地方。 让它创建对象,设置值,然后验证所有设置。 如果缺少某些东西,因为程序员决定不传递内容,那么你就会受到保护。 你的班级不必做比预期更多的事情。 (毕竟,考虑一下有朝一日可能会使用你的课程是很重要的。尽管世界上有很多伟大的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对象是单调乏味的,可以使用适当的参数和过滤器等设置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“查询构建器”对象通过验证完成了所有构建,并且证明使用起来非常方便。

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

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

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

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM