构造函数应该有多复杂


18

我正在与我的同事讨论一个构造函数可以完成多少工作。我有一个类B,它内部需要另一个对象A。对象A是类B需要完成其工作的几个成员之一。它的所有公共方法都依赖于内部对象A。有关对象A的信息存储在DB中,因此我尝试通过在构造函数中的DB上进行查找来进行验证和获取。我的同事指出,除了捕获构造函数参数外,构造函数不应做太多工作。由于如果使用构造函数的输入未找到对象A,所有公共方法都将失败,因此我认为与其允许创建实例并在以后失败,不如让它早于构造器。

别人怎么想?如果这有任何区别,我正在使用C#。

阅读是否有理由在构造函数中完成对象的所有工作?我想知道通过转到DB来获取对象A是“使该对象准备好使用所需的任何其他初始化”的一部分,因为如果用户将错误的值传递给构造函数,我将无法使用其任何公共方法。

构造函数应实例化对象的字段,并进行其他任何必要的初始化,以使对象可以立即使用。这通常意味着构造函数很小,但是在某些情况下这将需要大量的工作。


8
我不同意。看一下依赖性反转原理,最好将对象A在其构造函数中以有效状态传递给对象B。
Jimmy Hoffa 2014年

5
您在问可以做什么?多少应该做些什么呢?还是应该处理多少具体情况?这是三个完全不同的问题。
Bobson,2014年

1
我同意吉米·霍法的建议,着眼于依赖注入(或“依赖倒置”)。那是我阅读说明的第一个想法,因为听起来您已经到了一半。您已经将功能分为B类正在使用的A类。我无法谈及您的情况,但是我通常发现,使用依赖注入模式的重构代码使类更简单/更不纠结。
Wily博士的学徒

1
Bobson,我将标题更改为“ should”。我在这里要求最佳实践。
泰恩·金

1
@JimmyHoffa:也许您应该将评论扩展为答案。
凯文·克莱恩

Answers:


22

您的问题由两个完全独立的部分组成:

我应该从构造函数中引发异常,还是应该让方法失败?

显然,这是快速失败原则的应用。与必须找出方法失败的原因相比,使构造函数失败更容易调试。例如,您可能已经从代码的其他部分获得了已经创建的实例,并且在调用方法时遇到错误。显然对象创建错误吗?没有。

至于“在try / catch中包装呼叫”问题。例外是例外。如果知道某些代码将引发异常,则无需将代码包装在try / catch中,而是在执行可能引发异常的代码之前验证该代码的参数。异常仅是为了确保系统不会进入无效状态。如果知道某些输入参数可能导致无效状态,请确保这些参数永远不会发生。这样,您只需要在逻辑上可以处理异常的地方进行尝试/捕获,通常就在系统边界上。

我可以从构造函数访问“系统的其他部分,例如DB”。

我认为这违背了最小惊讶原则。没有多少人会期望构造函数访问数据库。所以不,您不应该这样做。


2
对此更合适的解决方案通常是添加一个“ open”类型的方法,该方法是免费的,但是在“ open” /“ connect” / etc(所有实际初始化发生的位置)之前不能使用。当您将来想要对对象进行部分构造时,构造函数的失败会导致各种问题,这是一种有用的功能。
Jimmy Hoffa 2014年

@JimmyHoffa我没有看到构造方法和构造方法之间的区别。
欣快2014年

4
您可能不会,但很多人会。考虑一下创建连接对象时,构造函数是否将其连接?如果未连接,该对象是否可用?在这两个问题中,答案都是“否”,因为连接对象的部分构造会有所帮助,因此您可以在打开连接之前调整设置和设置。这是这种情况的常用方法。我仍然认为,对于对象A具有资源依赖性但开放方法仍然比让构造函数执行危险工作的方案而言,构造函数注入是正确的方法。
Jimmy Hoffa 2014年

@JimmyHoffa但这是连接类的特定要求,而不是OOP设计的一般规则。实际上,我会称其为例外情况,而不是规则。您基本上是在谈论构建器模式。
欣快2014年

2
这不是必需条件,而是设计决定。他们本可以使构造函数采用连接字符串,并在构造时立即进行连接。他们决定不太多,因为它是有用的,以便能够创建对象,然后再稍微弄明白连接字符串或其他位和绒球,并进行连接
吉米·霍法

19

啊。

构造函数应该做的尽可能少-问题是构造函数中的异常行为很尴尬。很少有程序员知道什么是正确的行为(包括继承方案),并且强迫用户尝试/捕获每个实例都非常痛苦。

这有两个部分,构造函数中和使用构造函数。这在构造函数中很尴尬,因为发生异常时您不能在那里做很多事情。您不能返回其他类型。您不能返回null。您基本上可以抛出异常,返回损坏的对象(不良的构造函数),或(最佳情况)用合理的默认值替换某些内部零件。使用构造函数很麻烦,因为这样会抛出您的实例化(以及所有派生类型的实例化!)。然后,您将无法在成员初始化程序中使用它们。您最终会使用异常的逻辑来查看“我的创建成功了吗?”,这超出了由各处try / catch引起的可读性(和脆弱性)。

如果您的构造函数正在执行不重要的事情,或者可以合理预期失败的事情,请考虑使用静态Create方法(使用非公共构造函数)。它具有更多的可用性,更易于调试,并且在成功时仍可以为您提供完整的实例。


2
复杂的构建场景正是为什么存在您在此处提到的创建模式的原因。这是解决这些问题的好方法。我考虑过在.NET中Connection对象如何CreateCommand为您服务,以便您知道该命令已正确连接。
Jimmy Hoffa 2014年

2
为此,我要补充一点,数据库是一个严重的外部依赖,假设您想在将来某个时候切换数据库(或模拟一个对象以进行测试),将这种代码移至Factory类(或类似IService的东西)提供者)似乎是一种更清洁的方法
Jason Sperske 2014年

4
您可以详细说明“构造函数中的异常行为很尴尬”吗?如果要使用Create,就不需要像将包装对构造函数的调用一样,将其包装在try catch中吗?我觉得Create只是构造函数的另一个名称。也许我没有得到正确的答案?
Taein Kim

1
必须尝试捕获从构造函数冒出来的异常,然后对这些参数+1。与曾经说过“在构造函数中没有工作”的人一起工作过,这也会产生一些奇怪的模式。我看过的代码中,每个对象不仅具有构造函数,而且还具有某种形式的Initialize(),在哪里呢?该对象只有在被调用之前才真正有效。然后您需要调用两件事:ctor和Initialize()。完成设置对象的工作的问题不会消失-C#可能会提供与C ++不同的解决方案。工厂很好!
J特拉纳2014年

1
@jtrana-是的,初始化不好。构造函数初始化为一些合理的默认值,或者使用工厂或create方法对其进行构造并返回可用实例。
Telastyn 2014年

1

构造函数-方法复杂度的平衡确实是关于良好阶梯讨论的问题。通常使用空的构造函数Class() {},以及Init(..) {..}用于更复杂事物的方法。在要考虑的参数中:

  • 类序列化的可能性
  • 使用与构造函数分离的Init方法,您经常需要Class x = new Class(); x.Init(..);多次重复同一序列,部分是在单元测试中。但是,效果不太好,无法清晰阅读。有时,我只是将它们放在一行代码中。
  • 当单元测试的目标只是类初始化测试时,最好具有自己的Init,并通过中间的Asserts来逐个完成更复杂的动作。

我认为该init方法的优势在于处理故障要容易得多。特别地,该init方法的返回值可以指示初始化是否成功,并且该方法可以确保对象始终处于良好状态。
pqnet
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.