有没有理由在构造函数中完成所有对象的工作?


49

首先,我说这不是我的代码也不是我的同事的代码。几年前,当我们的公司规模较小时,我们有一些我们需要做的项目,但我们没有能力,因此将它们外包了。现在,我一般都不会反对外包或承包商,但是他们产生的代码库是大量的WTF。话虽这么说,它确实(大部分)有效,所以我认为它在我所见过的外包项目中排名前10%。

随着我们公司的成长,我们尝试将更多的内部开发工作带入公司。这个特殊的项目落在我的腿上,所以我一直在检查,清理,添加测试等。

我看到有一种模式经常重复出现,而且看起来如此令人恐惧,以至于我想知道是否存在某种原因,而我只是看不到它。模式是一个没有公共方法或成员的对象,只是一个公共构造函数,它可以完成对象的所有工作。

例如,(如果很重要,代码是Java的,但是我希望这是一个更普遍的问题):

public class Foo {
  private int bar;
  private String baz;

  public Foo(File f) {
    execute(f);
  }

  private void execute(File f) {
     // FTP the file to some hardcoded location, 
     // or parse the file and commit to the database, or whatever
  }
}

如果您想知道,这种类型的代码通常以以下方式调用:

for(File f : someListOfFiles) {
   new Foo(f);
}

现在,我很久以前就被告知,在循环中实例化对象通常不是一个好主意,而构造函数应该做最少的工作。查看此代码,似乎最好删除构造函数并创建execute一个公共静态方法。

我确实问承包商为什么要这样做,我得到的答复是“我们可以根据需要进行更改”。这真的没有帮助。

无论如何,是否有任何理由以任何编程语言来做类似的事情,或者这仅仅是向Daily WTF提交的另一份文件?


18
在我看来,它是由了解public static void main(string[] args)并听说过对象的开发人员编写的,然后尝试将它们融合在一起。
安迪·亨特

如果您再也不会被打来,那么您必须在那做。许多现代框架(例如,依赖注入)都需要制作Bean,设置属性并调用THEN,然后您才能使用这些繁重的工作。

4
相关问题:在类的构造函数中执行类的主要工作是否有问题?。但是,这有点不同,因为创建的实例将在以后使用。
sleske 2012年


我并不是真的想将其扩展为一个完整的答案,但是我只想说:在某些情况下,可以说这是可以接受的。这离它很远。在这里实例化一个对象,而实际上并不需要该对象。与此相反,如果要从XML文件中创建某种数据结构,则从构造函数中启动解析并不是完全糟糕的。实际上,在允许重载构造函数的语言中,作为维护者,我什么都不杀;)
back2dos 2012年

Answers:


60

好的,在列表中:

我很久以前就被告知循环中的实例化对象通常是一个坏主意

没有使用过的任何语言。

在C语言中,最好预先声明变量,但这与您所说的不同。如果在循环上方声明对象并重用它们,可能会快一些,但是在许多语言中,这种提高速度是没有意义的(可能还有一些编译器会为您进行优化:))。

通常,如果您需要一个循环中的对象,请创建一个。

建设者应该做最少的工作

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

是否有任何理由以任何编程语言来做类似的事情,或者这仅仅是向Daily WTF提交的另一份文件?

这是向每日WTF提交的。当然,使用代码可以做的更糟。问题是作者对什么是类以及如何使用它们有很大的误解。具体来说,这是我认为这段代码有问题的地方:

  • 滥用类:基本上,类的行为就像一个函数。正如您提到的那样,应将其替换为静态函数,或者应仅在调用它的类中实现该函数。取决于它的功能和使用位置。
  • 性能开销:根据语言的不同,创建对象可能比调用函数要慢。
  • 一般的困惑:程序员如何使用此代码通常会使他们感到困惑。没有看到它的使用,没人会知道作者打算如何使用相关代码。

感谢您的答复。关于“实例化循环中的对象”,这是许多静态分析工具所警告的,我也从一些大学教授那里听说过。当然,这可能是90年代后性能影响更大的原因。当然,如果您需要它,可以这样做,但是我很惊讶听到相反的观点。感谢您开阔视野。
凯恩

8
在循环中实例化对象是一种次要的代码味道;您应该查看一下并询问“我是否真的需要多次实例化此对象?”。如果要使用相同的数据实例化同一个对象并告诉它执行相同的操作,则可以通过实例化一次然后再重复执行具有对象状态可能需要的任何增量更改的指令来节省周期(并避免内存浪费)。 。但是,例如,如果要从数据库或其他输入中读取数据流并创建一系列对象来保存该数据,则必须实例化。
KeithS 2012年

3
简短版本:不要像使用函数那样使用对象实例化。
艾里克·雷彭

27

对我来说,当您让对象在构造函数中执行大量工作时,这有点令人惊讶,因此我建议您反对它(最不惊奇的原则)。

一个很好的例子

我不确定这种用法是否是反模式,但是在C#中我已经看到了类似的代码(有些示例):

using (ImpersonationContext administrativeContext = new ImpersonationContext(ADMIN_USER))
{
    // Perform actions here under the administrator's security context
}
// We're back to "normal" privileges here

在这种情况下,ImpersonationContext该类将在构造函数和Dispose方法中完成所有工作。它利用了C#using语句的优势,C#开发人员已经很好地理解了C#语句,从而保证了using即使在发生异常的情况下,一旦我们不在该语句之外,构造器中发生的设置也会被回退。这可能是我所见过的最佳用法(即,构造函数中的“工作”),尽管我仍然不确定我是否会认为它是“好”。在这种情况下,它是不言自明的,实用的和简洁的。

良好且相似的模式的一个示例是命令模式。您肯定不会在其中的构造函数中执行逻辑,但是在整个类等效于方法调用(包括调用方法所需的参数)的意义上,这是相似的。这样,方法调用信息可以序列化或传递以供以后使用,这非常有用。也许您的承包商正在尝试类似的尝试,尽管我对此表示高度怀疑。

对您的示例的评论:

我会说这是一个非常明确的反模式。它没有增加任何内容,与预期不符,并且在构造函数中执行过长的处理(构造函数中的FTP?实际上是WTF,尽管我想人们已经知道以这种方式调用Web服务)。

我一直在努力寻找报价,但是Grady Booch的书“ Object Solutions”有一个轶事,讲述了一群C开发人员向C ++过渡的故事。显然,他们已经接受了培训,并被管理层告知他们绝对必须做“面向对象”的事情。为了弄清楚问题出在哪儿,作者使用了代码度量工具,发现每个类的平均方法数恰好是1,并且是短语“ Do It”的变体。我必须说,我以前从未遇到过这个特殊问题,但是显然这可能是人们被迫创建面向对象的代码的征兆,而他们并没有真正理解如何做以及为什么做。


1
您的好例子是“ 资源分配是初始化”的实例。这是C ++中的推荐模式,因为它使编写异常安全代码更加容易。我不知道这是否会延续到C#中。(在这种情况下,分配的“资源”是管理特权。)
zwol 2012年

@Zack我从来没有用这些术语想到过它,即使我已经知道C ++中的RAII。在C#世界中,它被称为Disposable模式,强烈建议将具有(通常是不受管理的)资源在其生命周期后释放的任何东西使用。此示例的主要区别在于,通常不会在构造函数中执行昂贵的运算符。例如,仍然需要在构造函数之后调用SqlConnection的Open()方法。
丹尼尔·B

14

没有。

如果所有工作都在构造函数中完成,则意味着一开始就不需要该对象。承包商很可能只是将对象用于模块化。在这种情况下,使用静态方法的非实例化类将获得相同的好处,而无需创建任何多余的对象实例。

只有一种情况可以接受在对象构造函数中完成所有工作。那就是当对象的目的是专门代表长期唯一身份而不是执行工作时。在这种情况下,除了执行有意义的工作以外,还会出于其他原因保留对象。例如,一个单例实例。


7

我当然已经创建了许多类,这些类在计算构造函数中需要做很多工作,但是对象是不可变的,因此它可以通过属性使计算结果可用,然后再不做任何其他事情。这是行动中的正常不变性。

我认为这很奇怪,是将具有副作用的代码放入构造函数中。构建对象并将其扔掉而不访问属性或方法看起来很奇怪(但至少在这种情况下,很明显其他事情正在发生)。

如果将函数移至公共方法,然后注入类的实例,然后使for循环重复调用该方法,则效果会更好。


4

有没有理由在构造函数中完成所有对象的工作?

没有。

  • 构造函数不应有任何副作用。
    • 除了私有字段初始化外,任何其他事情都应视为副作用。
    • 具有副作用的构造函数破坏了单一职责原则(SRP),并且与面向对象编程(OOP)的精神背道而驰。
  • 构造函数应该轻巧,并且永不失败。
    • 例如,当我在构造函数中看到try-catch块时,我总是发抖。构造函数不应引发异常或记录错误。

一个人可以合理地质疑这些准则,然后说:“但是我不遵守这些规则,我的代码可以正常工作!” 为此,我会回答:“那可能是正确的,直到事实并非如此。”

  • 构造函数内部的异常和错误是非常意外的。除非被告知这样做,否则将来的程序员将不会倾向于将这些构造函数调用包含在防御性代码中。
  • 如果生产中有任何失败,则可能难以解析生成的堆栈跟踪。堆栈跟踪的顶部可能指向构造函数调用,但是在构造函数中发生了很多事情,并且可能没有指向失败的实际LOC。
    • 在这种情况下,我已经解析了许多.NET堆栈跟踪。

1
“除非告知他们这样做,否则未来的程序员将不会倾向于将这些构造函数调用与防御性代码一起使用。” - 好点子。
格雷厄姆

2

在我看来,这样做的人正在试图破解Java的局限性,Java的局限性是不允许以一流公民的身份传递功能。每当需要传递函数时,都必须使用类似方法将其包装在类中apply类似或类似。

因此,我猜他只是使用了快捷方式,而直接将类用作函数替换。每当您要执行功能时,都只需实例化该类。

绝对不是一个好的模式,至少是因为我们在这里试图弄清楚它,但是我想它可能是执行类似于Java中的函数编程的最不冗长的方法。


3
我敢肯定,这个特定的程序员从未听过“函数式编程”一词。
凯恩2012年

我不认为程序员正在尝试创建闭包抽象。Java等效项将需要一种抽象方法(可能的计划)多态性。这里没有证据。
凯文·A·诺德

1

对我而言,经验法则是除了初始化成员变量外,不对构造函数执行任何操作。这样做的第一个原因是它有助于遵循SRP原理,因为通常漫长的初始化过程表明类做了比应做的事情更多的事情,并且初始化应在类外部进行。第二个原因是这种方式仅传递必要的参数,因此您创建的耦合代码更少。具有复杂初始化的构造函数通常需要仅用于构造另一个对象的参数,而原始类却不使用。


1

在这里,我将与潮流背道而驰-在语言中,所有内容都必须在某个类中,并且您不能拥有静态类,因此您可能会遇到需要隔离一些功能的情况放在某个地方,不适合其他任何东西。您可以选择一个实用程序类,它涵盖各种无关的功能,或者基本上只做一件事。

如果只有这样一点,那么您的静态类就是您的特定类。因此,您可以有一个完成所有工作的构造函数,或者是一个可以完成所有工作的单一函数。在构造函数中执行所有操作的优点是它只发生一次,它使您可以使用私有字段,属性和方法,而不必担心它们会被重用或线程化。如果您有构造函数/方法,则可能很想让参数传递给单个方法,但是如果您使用的私有字段或属性会引入潜在的线程问题。

即使有了实用程序类,大多数人也不会想到拥有嵌套的静态类(根据语言的不同,这可能是不可能的)。

基本上,这是在语言允许的范围内将行为与系统其余部分隔离的相对可理解的方式,同时仍允许您尽可能多地利用语言。

坦白说,我会像您的承包商那样回答很多,将它转换成两个电话只是一个小小的调整,没有什么可以推荐一个电话了。有什么差异/辩解可能是假想的,而不是实际的,为什么要在无所谓且可能没有那么多决定而只是默认动作的情况下为该决定辩护?


1

在语言中,有一些常见的模式,它们的函数和类非常相似,例如Python,人们基本上将一个对象用作具有太多副作用或返回多个命名对象的函数。

在这种情况下,大部分(如果不是全部)工作都可以在构造函数中完成,因为对象本身只是单个工作包的包装,并且没有充分的理由将其填充到单独的函数中。就像是

var parser = XMLParser()
var x = parser.parse(string)
string a = x.LookupTag(s)

真的没有比

 var x = ParsedXML(string)
 string a = x.LookupTag(s)

可以直接调用该对象以增强提示的副作用,而不是直接使副作用完全消失,这可以为您提供更好的可见性。如果我要对某个对象产生不良影响,则可以完成所有工作,然后再通过该对象,这将对我造成严重影响并对其造成损害。与一次将多个此类对象传递给函数相比,以这种方式标识对象并隔离调用更为清晰。

x.UpdateView(v)

您可以通过对象上的访问器获得多个返回值。所有工作都已完成,但是我希望所有内容都在上下文中,并且我真的不想将多个引用传递到我的函数中。

var x = new ComputeStatistics(inputFile)
int max = x.MaxOptions;
int mean = x.AverageOption;
int sd = x.StandardDeviation;
int k = x.Kurtosis;
...

因此,作为Python / C#联合程序员,这似乎对我来说并不奇怪。


我需要澄清的是,给定的示例仍然具有误导性。在没有访问器且没有返回值的示例中,这可能是残留的。也许原始提供的调试返回了什么。
乔恩·杰伊·奥伯马克2014年

1

想到一种情况,构造器/析构函数完成所有工作:

锁 通常,您在锁的整个生命周期内都不会与之交互。C#的使用体系结构非常适合处理此类情况,而无需显式引用析构函数。但是,并非所有锁都以这种方式实现。

我还写了一些仅用于构造函数的代码来修补运行时库错误。(实际上,修改代码可能会导致其他问题。)由于没有必要撤消该补丁程序,因此没有析构函数。


-1

我同意AndyBursh的观点。似乎有些知识不足的问题。

但是,重要的是要提到诸如NHibernate之类的框架,这些框架只要求您在构造函数中编码(在创建映射类时)。但这不是框架的限制,而是您所需要的。说到反射,构造函数是唯一将始终存在的方法(尽管它可能是私有的),如果必须动态调用类的方法,这可能会有所帮助。


-4

“很久以前,我就被告知循环中的实例化对象通常是一个坏主意”

是的,也许您是在回想为什么我们需要StringBuilder。

Java有两个流派。

建造者被第一次提拔一个,它教我们重用(因为共享=身份对效率)。但是,从2000年初开始,我开始读到用Java创建对象非常便宜(与C ++相对),而GC却是如此高效,以至于重用毫无用处,唯一重要的是程序员的生产力。为了提高工作效率,他必须编写可管理的代码,这意味着模块化(也就是缩小范围)。所以,

这不好

byte[] array = new byte[kilos or megas]
for_loop:
  use(array)

很好

for_loop:
  byte[] array = new byte[kilos or megas]
  use(array)

当forum.java.sun存在时,我问了这个问题,人们同意他们更愿意将数组声明为尽可能窄。

现代的Java程序员会毫不犹豫地声明一个新类或创建一个对象。鼓励他这样做。Java提供了这样做的所有理由,其思想是“一切都是对象”和廉价的创造。

此外,现代企业编程风格是,当您需要执行某个动作时,可以创建一个特殊的类(甚至更好的框架)来执行该简单的动作。好的Java IDE,在这里有帮助。它们会像呼吸一样自动产生大量垃圾。

因此,我认为您的对象缺少Builder或Factory模式。通过构造函数直接创建实例并不恰当。建议为您的每个对象提供特殊的工厂类。

现在,当函数式编程开始起作用时,创建对象变得更加简单。您将在呼吸的每一步上创建对象,甚至不会注意到这一点。因此,我希望您的代码在新时代将变得更加“高效”

“不要在构造函数中执行任何操作。仅用于初始化”

就个人而言,我认为指南中没有任何理由。我认为这只是另一种方法。仅当您可以共享代码时,才需要分解代码。


3
该解释结构不清晰,难以理解。您只是到处都是带有可争辩的断言,而断言与上下文没有任何联系。
新亚历山大

真正有争议的陈述是“构造函数应实例化对象的字段,并进行其他任何必要的初始化以使对象可以使用”。告诉最受欢迎的贡献者。
2013年
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.