在设计课程时,您通常需要决定:
我见过几个API和框架,它们使用上述之一甚至是不一致的方法,这种方法因类而异。您对该主题有何看法和最佳实践?
答案 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")
)<强>设置器强>
优点:
缺点:
<强>生成器强>
优点:
缺点:
静态工厂方法
优点:
缺点:
答案 1 :(得分:3)
除了最边缘的边缘情况之外,其他所有地方都是“完整的”。
对我来说非常简单,构造函数的 point 是设置对象以便它可以使用。显然,不可变对象是最好的,但是某些类型的对象通常也可以在以后更改状态。但是,要避免的一件事是,可以在一个点上构建对象,但在使用之前必须在其上调用init()
或setup()
方法。它令人生气,令人困惑 - 如果在其上调用“真实”方法是非法的,那么构建一个对象有什么意义呢?
在我看来,要求“全面”建设没有实质性的缺点;在能够使用该对象之前,调用者必须将所有必需的参数编组在一起,并且使它们在构造函数中这样做会消除整个类的错误。很高兴知道,如果你传递了一个对象的实例,它将有效使用,而不是具有一些时间和/或状态依赖性。如果有的话,这也可以让代码更清楚地通过在一个清晰的地方定义所有依赖项来准确地调用代码。
事实上,我没有看到的唯一具体论据是循环依赖;如果类A
需要B
,反之亦然,则必须通过setter方法完成这些关系中的一个或另一个。这是否代表良好的设计是留给读者的练习。 : - )
答案 2 :(得分:2)
我想说完整的构造函数是可行的,不仅仅是为了一致性(正如你所说,在创建对象后保证对象的完全初始化),而且因为它在必须使用时简化了工作依赖注入机制。
答案 3 :(得分:2)
我有一个我自己使用的规则列表:
因此,这是对具有重载构造函数的混合方法的投票。但对我来说最重要的是构造对象应该在构造之后有效或者抛出异常,否则它太容易出错而不允许“未完全初始化”或无效对象。
答案 4 :(得分:1)
我的想法是:
答案 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和构造函数之间修改了文件系统,那么参数是否“坏”可能会改变。所以你别无选择,只能事后检查。这仍然比两阶段建设更好。
如果您必须对对象本身进行两阶段构建,那么我认为最好将其隐藏在工厂方法中。假设您的语言允许某种“空”来表示错误,那就是。