【问题标题】:Is doing a lot in constructors bad? [closed]在构造函数中做很多坏事吗? [关闭]
【发布时间】:2018-02-09 19:00:51
【问题描述】:

使所有字段final 通常是一个好主意,但有时我发现自己在构造函数中做了所有事情。最近我在构造函数中完成了一个实际上所有操作的类,包括读取属性文件和访问数据库。

一方面,这就是类的用途,它封装了读取的数据,我喜欢创建完全初始化的对象。构造函数一点也不复杂,因为它委托了大部分工作,所以看起来不错。

另一方面,感觉有点奇怪。此外,在大约 17:58 的 this talk 中,有充分的理由在构造函数中没有做太多工作。我想我可以通过传递适当的假人作为构造函数参数来消除这个问题。

问题仍然存在:在构造函数中做大量工作(甚至所有工作)是否不好?

【问题讨论】:

标签: java constructor


【解决方案1】:

我认为“在构造函数中工作”还可以......

...只要您不违反Single Responsibility Principle (SRP) 并坚持使用Dependency Injection (DI)

我最近一直在问自己这个问题。我发现的反对在构造函数中工作的动机是:

  • 这使得测试变得困难
    • 我见过的所有例子都是没有使用DI。实际工作并不是构造函数的错。
  • 您可能不需要构造函数计算的所有结果,这会浪费处理时间并且很难单独进行测试。
    • 这基本上违反了SRP,而不是构造函数的错误。
  • 旧的编译器在构造函数中抛出异常时遇到了问题,因此除了在构造函数中分配字段之外,您不应该做任何事情。
    • 我认为在编写新代码时考虑到编译器的历史缺陷并不是一个好主意。如果我们这样做,我们还不如取消 C++11 以及所有好的东西。

我的看法是……

...如果您的构造函数需要做一些工作以使其遵守Resource Acquisition Is Initialization (RAII) 并且该类不违反SRPDI 被正确使用;然后在构造函数中工作就是 A-Okay!如果您想阻止使用初始化完全失败的类对象,而不是依赖用户检查某些返回值,您甚至可以抛出异常。

【讨论】:

  • 显然,当我回答时,我的想法是用 C++ 设置的,哦,笨蛋。 nvm 仍然适用 ;)
  • “返回值”将是一个通过公共成员,因为不允许构造函数返回值。
  • @v.oddou 或通过参数传递(通过引用)给构造函数。但是如果构造函数确实是一个函数,那么同样的规则应该适用于所有其他函数......它开始看起来像一个函数式编程,如果你正在使用一种 FP 语言。
【解决方案2】:

这是一个非常开放的问题,所以我的回答会尽量笼统......

在构造函数中工作并不像几年前那样“糟糕”,当时异常处理不像今天那样普遍和发展。您会注意到 Google 技术讲座主要从测试的角度来看构造函数。构造函数在历史上一直非常难以调试,所以演讲者是正确的,在构造函数中做的越少越好。

话虽如此,您会注意到他还谈到了依赖注入/提供者模式,这种模式因使构造函数复杂化而臭名昭著。在这种情况下,最好只在构造函数中保留提供程序/DI 代码。同样,答案取决于您使用的模式以及您的代码如何“组合”在一起。

使用构造函数的全部意义创建一个可以立即使用的整洁对象;即new Student("David Titarenco", "Senior", 3.5)。没有必要这样做david.initialize(),因为这完全是愚蠢的。

这是我的一些生产代码,例如:

    Config Conf = new Config();
    Log.info("Loading server.conf");
    Conf.doConfig();

在上面的例子中,我决定不对构造函数做任何事情(它是空的),而是有一个 doConfig() 方法来完成所有的磁盘 i/o;我经常认为doConfig() 方法毫无意义,我应该在构造函数中做所有事情。 (毕竟我只检查了一次配置文件。)

我认为这完全取决于您的代码,您不应该认为在构造函数中放入“东西”是一件坏事。这就是构造函数的用途!有时我们会被 OOP(getThissetThatdoBark)迷住,而实际上类需要做的只是加载配置文件。在这种情况下,只需将所有内容都放在构造函数中并收工!

【讨论】:

  • 您的配置示例存在缺陷,因为它违反了 SRP。如果您可以将配置存储在 RDBMS 中而不是文件中呢? Config config = new FileConfigurationProvider('server.conf').getConfig(); ...收工吧!在构造函数中没有工作和更好的设计。
  • 啊,所有那些从未发生过的“假设”......不要过度设计你的代码。这就是重构的目的。
  • 对于较小的示例代码块,是的,收工,做任何事情。但在更大的生产代码库中,这些只是橱柜中的骨架。所以这个解决方案很好,直到一些同事开始在代码的随机部分调用'new Config()',导致一些磁盘 I/O 和多个配置对象实例。
【解决方案3】:

当我在构造函数中放入太多代码时,我遇到了以下问题:

  • 很难为该类的其他方法编写单元测试, 因为它想在构造函数中做很多事情,因此,我 必须设置很多有效的东西或至少模拟(数据库,文件, 无论如何)用于最简单的单元测试。
  • 很难为构造函数本身编写单元测试。反正, 将大量不同职责的代码合二为一 块甚至是一个坏主意。 (Single Responsibility Principle.)
  • 由于前面的原因,很难使用该类。例如, 它完全阻止了我实现一些延迟的初始化方法, 因为它在调用 构造函数。好的,我可以将惰性初始化方法写入 构造函数,不错。
  • 迟早我意识到重用一些代码部分是有意义的 它们被放置在构造函数中。嗯,当我第一次写 构造函数我还认为这些代码部分将仅用于 在那里,永远。
  • 当我想扩展该类并在或之前插入一些逻辑时 进入超级构造函数的逻辑,它根本不起作用,因为 在扩展类的构造函数中要做的第一件事是调用 超级的。

所以是的,在我看来,在构造函数中做很多事情是个坏主意。

通常我只是将一些字段初始化放入构造函数中,并创建一个 init 方法,以便在每个人都参与时调用。

【讨论】:

  • 我认为您的问题可能是由于违反了 SRP 而不是构造函数在实际工作中引起的。
  • 一般来说,“做很多事情”会显着增加违反 SRP 的机会。
  • 当然,但是做很多事情不是问题。问题是违反 SRP。
  • 你的每一个观点都是关于权衡的,并没有具体的好坏,但并不意味着构造函数中的工作是坏的。您认为代码只会在那里永远使用的观点应该是默认前提,直到被发现是错误的。在有理由之前,您不应该抽象。原因多种多样。可读性、可重用性等。但如果没有充分的理由,那就把它留在原处。大多数对象,尤其是值对象,应该是无状态的,这意味着所有工作都在构造函数中完成。这是很好的编码习惯。
  • 我想我要表达的主要观点是“面向未来”是一种不好的做法。以最简单的方式解决当前问题是一种很好的做法。因此,“猜测”某些东西如何被重用从根本上说不是一个好的做法。如果创建一个抽象(方法、类等)增加了可读性,或者你将“现在”使用的“已知”重用场景,那就去做吧。例如如果我看到一个 100 行的构造函数,我会说“这不可读,将其分解为逻辑步骤/私有方法”
【解决方案4】:

通常情况下,如果您的对象具有复杂的创建算法,您可以使用 Builder 或 Factory 来简化它。特别是如果要验证构建对象的先决条件。

一旦您开始使用 Builders 和 Factories 来构建您的对象,它们就可以验证前置条件和后置条件,并确保您的代码的客户端只能访问完全初始化的对象,而不是 半成品 一,你甚至可以使用时下流行的流畅界面来创建你的对象并让它看起来很酷;D

new EmailMessage()
    .from("demo@guilhermechapiewski.com")
    .to("destination@address.com")
    .withSubject("Fluent Mail API")
    .withBody("Demo message")
    .send();

显然这不是你的实际情况,因为它没有使用构建器,但它很像你可以构建的东西,以减少构造器的工作并让你的代码看起来更简单。

【讨论】:

    【解决方案5】:

    在我看来,有构造函数和析构函数是好的,但不要在其中做太多的工作。尤其是文件/数据库访问,除非它非常特定于类。你想让你的构造函数/析构函数保持轻量,让你的程序感觉流畅。有时你已经遇到了构造函数基本上完成所有工作的情况。有一种方法可以让事情变得更轻。概念/范式称为惰性评估。这个想法是接受输入并且什么都不做(例如在构造函数中),但在需要计算请求时使用输入。

    示例:假设您有一个类可以读取文件、解析文件并告诉您文件中所有数字的总和等信息。您可以在构造函数中完成这一切。使用惰性求值,您只需打开文件,并拥有一个 getTotalSum() 函数。调用时,它将进行解析并为您提供结果。这样,您还可以使用 getBestFit() 来获得最佳拟合线。有时您不想获得最佳拟合,而对于某些输入,您会这样做。这样用户就不会在用户决定做什么之前等待构造函数进行计算。

    另一个例子:假设您有一个加载 20 张图像的视图。但是只显示了 5 个,并且构造函数需要一个图像数组来显示。您可以将它们全部加载到构造函数中,但从用户的角度来看,这在开始时会感觉很慢。或者您可以一次加载 1 张“正在加载”图片并加载 1 张图片。并且当用户滚动时,会根据显示/需要加载更多图像。

    当然,第一个问题是您在以后发现错误,例如无效图片,而不是构造函数。您可以随时为自己执行简单的检查,以在一定程度上预先验证输入(例如检查密码是否正确)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-12-09
      • 2013-08-26
      • 1970-01-01
      • 2014-07-29
      • 2014-06-02
      • 2011-12-19
      • 2010-11-04
      • 2011-02-25
      相关资源
      最近更新 更多