构造函数:完全成熟还是最小化?

时间:2009-12-22 09:09:58

标签: language-agnostic oop constructor

在设计课程时,您通常需要决定:

  • 提供一个“完整”构造函数,它将所有必填字段的初始值作为参数:使用笨拙但保证完全初始化和有效的对象
  • 仅为所有必填字段提供“默认”构造函数和访问器:有时可能很方便,但不保证在调用某些关键方法之前所有成员都已正确初始化。
  • 一种混合方法(更多代码,更多工作,可以“消除”未完全初始化的“问题)

我见过几个API和框架,它们使用上述之一甚至是不一致的方法,这种方法因类而异。您对该主题有何看法和最佳实践?

11 个答案:

答案 0 :(得分:7)

简短的回答是,在调用构造函数后,对象应该完全初始化。

这应该是默认方法,对用户来说意外最少。在某些情况下,您的运行时,框架或其他技术限制会阻止默认方法。

在某些情况下,构建器模式有助于在无法使用简单构造函数时支持案例。这种方法处于中间位置,允许用户调用setter进行初始化,并且仍然只能使用完全初始化的对象。

静态工厂方法适用于对象构造函数需要比构造函数更灵活但构建器太复杂而无法实现的情况。

构造

x = new X(a, b);

设置器:

x = new X();
x.setA(a);
x.setB(b);

生成器:

builder = new Builder();
builder.setA(a);
builder.setB(b);
x = builder.build();

静态工厂方法:

x = X.newX(a, b);

所有四种方法都会产生一个类X的实例。

构造

优点:

  • 简单

缺点:

  • 由于约束可能无法实现
  • 可能需要太多构造函数参数(如果语言支持,则命名参数和默认值可以在这里提供帮助:new X(a = "a", b = "c")

<强>设置器

优点:

  • 中等复杂度
  • 可以由框架
  • 强制执行

缺点:

  • 实例可能未完全初始化

<强>生成器

优点:

  • 最灵活的方法
  • 可以重用实例(在内部使用单例,flyweights和缓存)

缺点:

  • 实施最复杂的
  • 可能会产生无效初始化对象的运行时异常
  • 构建器对象实例的开销

静态工厂方法

优点:

  • 中等复杂度
  • 可以重用实例(在内部使用单例,flyweights和缓存)

缺点:

  • 比构造函数
  • 更复杂的实现和使用

答案 1 :(得分:3)

除了最边缘的边缘情况之外,其他所有地方都是“完整的”。

对我来说非常简单,构造函数的 point 是设置对象以便它可以使用。显然,不可变对象是最好的,但是某些类型的对象通常也可以在以后更改状态。但是,要避免的一件事是,可以在一个点上构建对象,但在使用之前必须在其上调用init()setup()方法。它令人生气,令人困惑 - 如果在其上调用“真实”方法是非法的,那么构建一个对象有什么意义呢?

在我看来,要求“全面”建设没有实质性的缺点;在能够使用该对象之前,调用者必须将所有必需的参数编组在一起,并且使它们在构造函数中这样做会消除整个类的错误。很高兴知道,如果你传递了一个对象的实例,它有效使用,而不是具有一些时间和/或状态依赖性。如果有的话,这也可以让代码更清楚地通过在一个清晰的地方定义所有依赖项来准确地调用代码。

事实上,我没有看到的唯一具体论据是循环依赖;如果类A需要B,反之亦然,则必须通过setter方法完成这些关系中的一个或另一个。这是否代表良好的设计是留给读者的练习。 : - )

答案 2 :(得分:2)

我想说完整的构造函数是可行的,不仅仅是为了一致性(正如你所说,在创建对象后保证对象的完全初始化),而且因为它在必须使用时简化了工作依赖注入机制。

答案 3 :(得分:2)

我有一个我自己使用的规则列表:

  • 构造后的实例应该是有效的 - 因此我的构造函数具有最小参数将具有尽可能少的基本要求;
  • 应该有一个构造函数允许尽可能多地初始化参数;
  • 其他具有合理参数子集的构造函数。

因此,这是对具有重载构造函数的混合方法的投票。但对我来说最重要的是构造对象应该在构造之后有效或者抛出异常,否则它太容易出错而不允许“未完全初始化”或无效对象。

答案 4 :(得分:1)

我的想法是:

  • 超过4-5个构造函数参数往往令人困惑,难以阅读且无法理解;
  • 不可变对象应该优先于可变对象。这通常需要大型构造函数;
  • 如果需要,创建“构建器”对象(使用流畅的接口),这些对象是可变的,但最后会吐出不可变对象;
  • 如果您的语言支持它,匿名对象(如Javascript中)是为构造函数(或任何函数)提供许多参数的好方法。

答案 5 :(得分:1)

这是一个非常通用的问题,所以我只能给你一个非常通用的答案。编写内部代码和将作为其他开发人员的API发布的代码之间存在巨大差异。

我认为对于后一种情况(我相信你感兴趣的是)我会采用一种非常小的方法。如果你需要提供大量的价值观,我甚至会争辩说你甚至可能试图在一个单一的课程中坚持过多的功能。

但是如果用户最有可能每次都自定义所有值,那么一定要强制他们提供值。如果有很多合理的值,请确保使用这些默认值。

最后,如果您的类经常被用户子类化,您可能希望完全避免构造函数参数。 (从PHP背景说起,我们没有方法重载)。

答案 6 :(得分:1)

没有。 (你不应该使用其中一个 - 你应该使用一个,另一个,或混合方法:)

问题是你何时应该使用它们?

1)是否有一些明确定义的用例?或者有数百万?

示例A:始终需要初始化某些字段的Address对象。使用完整的构造函数。

示例B:需要将某些字段初始化的Address对象,其他字段是可选的。使用混合方法。

示例C:具有数十亿个参数的图形对象,可以设置也可以不设置,并且每个使用它的程序都以不同的方式调用。对某些必填字段(如果有)使用完整构造函数,但主要依靠方法/属性来设置字段。

答案 7 :(得分:1)

Qt Software的人们对这些类型的问题进行了很好的思考:http://qt.gitorious.org/qt/pages/ApiDesignPrinciples

答案 8 :(得分:0)

在使用之前始终完全初始化,而不要求类的用户做任何事情,除了构造和使用对象。

如果性能分析指示,则仅将完全初始化延迟到正常成员函数的第一次调用。

仅使用两阶段初始化(即用户必须构造,然后必须在使用真实对象之前调用自定义Init()函数),这是实现初始化的唯一方法。

如果您允许代码的用户创建不完整的对象并需要一定的方法调用序列来使对象安全使用,那么您只是为将来构建错误。

答案 9 :(得分:0)

“使用构造函数填充数据字段”比“默认构造函数+手动将数据分配给字段”更快

但请始终牢记一个方法(也是构造函数)不能超过6-7个参数。

答案 10 :(得分:0)

完整构造函数,除非出于某种原因,您试图避免在代码库中使用异常。

我不打算讨论和反对禁止所有异常,但是例如在C ++中,如果你没有在任何地方使用新的,那么你就不会避免所有异常,所以特殊情况不会适用。

回到更多与语言无关的领域,你可能会争辩说“异常只针对例外情况”,而且“这个构造函数失败也不例外”。我也不打算支持或反对这一论点,但它给你留下了两个选择:

1)如果构造失败,则在对象上设置“failure”标志,并且调用者必须检查它(显式地,或者在其他根据是否设置的行为不同的函数中检查)。 C ++文件流使用带有文件名的构造函数执行此操作。这对来电者来说是一种麻烦,但比两阶段建设更令人讨厌。

2)有一个帮助对象来执行可能失败但不应该是异常的操作,并让调用者使用它。也就是说,替换:

MyObj(a,b,c); // might throw

MyArgs args(a,b,c);
// optionally, if caller doesn't want an exception
if (!args.ok()) handle_the_error();
MyObj(args);

然后在带有MyArgs的构造函数中,MyObj也可以调用args.ok(),如果不是,则抛出异常。一个类可以提供构造函数(或者如果您的语言不允许多个构造函数,则为工厂方法),并让调用者决定。

从根本上说,如果你想避免异常,那么调用者将不得不在某处手动检查是否成功。就个人而言,我认为如果失败的原因相当于不好的论点,那么最好事先检查一下。显然文件流不能这样做,因为如果在check和构造函数之间修改了文件系统,那么参数是否“坏”可能会改变。所以你别无选择,只能事后检查。这仍然比两阶段建设更好。

如果您必须对对象本身进行两阶段构建,那么我认为最好将其隐藏在工厂方法中。假设您的语言允许某种“空”来表示错误,那就是。