21

我回答了一个问题(链接),我在另一个类的构造函数中使用了新对象的创建,这里是示例:

class Person {
  public $mother_language;

  function __construct(){ // just to initialize $mother_language
    $this->mother_language = new Language('English');
}

我得到了用户“Matija”(他的个人资料)的评论,他写道:你永远不应该在对象构造函数中实例化一个新对象,依赖关系应该从外部推送,所以任何使用这个类的人都知道这个类依赖什么!

一般来说,我可以同意这一点,我理解他的观点。

但是,我以前经常这样做,例如:

  • 作为私有属性,其他类为我提供了无需复制代码即可解决的功能,例如,我可以创建一个ArrayAccess对象列表(类实现接口),并且此类将用于另一个具有此类列表的类对象,
  • 一些类使用例如DateTime对象,
  • 如果我include(或自动加载)依赖类,一个应该没有错误的问题,
  • 因为依赖对象可能非常大,将它们全部传递给类构造函数可能会很长而且不清楚,例如

    $color = new TColor('red'); // do we really need these lines?
    $vin_number = new TVinNumber('xxx');
    $production_date = new TDate(...);
    ...
    $my_car = new TCar($color, $vin_number, $production_date, ...............);
    
  • 因为我是在 Pascal 中“出生”的,然后在 Delphi 中,我从那里就有了一些习惯。在 Delphi(以及 FreePascal 作为其竞争对手)中,这种做法非常常见。例如,有一个TStrings类处理字符串数组,并存储它们,它不使用arrays 而是另一个类,TList,它提供了一些有用的方法,而TStrings只是某种接口。该TList对象是私有声明的,除了TStrings.

  • (不重要,但出于某种原因)通常我是使用我的课程的人。

请解释一下,避免在构造函数中创建对象真的很重要吗?

我已经阅读了这个讨论,但仍然不清楚。

4

7 回答 7

12

是的,确实如此。然后,您就清楚了构建对象需要什么。传入的大量依赖对象是一种代码异味,可能您的类做得太多,应该分解成多个较小的类。

如果你想测试你的代码,传入依赖对象的主要优势就来了。在您的示例中,我不能使用假语言类。我必须使用实际的类来测试 Person。我现在无法控制 Language 的行为以确保 Person 正常工作。

这篇文章有助于解释为什么这是一件坏事以及它导致的潜在问题。http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

更新

抛开测试不谈,传入依赖对象也会使你的代码更加明确、灵活和可扩展。引用我链接到的博客文章:

当协作者构造与初始化混合时,它表明只有一种配置类的方法,它关闭了可能可用的重用机会。

在您的示例中,您只能创建以“英语”为语言的人。但是当你想创造一个说“法语”的人时呢?我无法定义。

至于创建对象并将它们传入,这就是Factory模式http://www.oodesign.com/factory-pattern.html的全部目的。它将创建依赖项并为您注入它们。因此,您可以向它询问将以您想要的方式初始化的对象。Person 对象不必决定它需要是什么。

于 2013-04-09T12:39:45.597 回答
7

原来的评论对我来说似乎很愚蠢。

没有理由害怕管理资源的类。事实上,类提供的封装非常适合这项任务。

如果你限制自己只从外部推入资源,那么什么来管理这些资源?意大利面条程序代码?哎呀!

尽量避免相信您在互联网上阅读的所有内容。

于 2013-04-09T12:32:52.737 回答
4

这更像是一个可测试性问题而不是正确/错误的事情,如果您在构造函数上创建一个对象,您将永远无法独立测试您的类。

如果您的类需要对构造函数进行大量注入,则始终可以使用工厂方法来注入这些对象。

这样,您将可以自由地模拟任何注入的对象来真正测试您自己的类。

<?php

class Person {
  public $mother_language;

  // We ask for a known interface we don't mind the implementation
  function __construct(Language_Interface $mother_language) 
  {
    $this->mother_language = $mother_language
  }

  static function factory()
  {
    return new Person(new Language('English'));
  }
}

$person = Person::factory();
$person = new Person(new Language('Spanish'); // Here you are free to inject your mocked object implementing the Language_Interface
于 2013-04-09T12:42:05.607 回答
3

我认为这取决于情况。例如,如果您查看 Doctrine 2,建议您始终将您的值设置为 null 并在构造函数中设置您的值。这是因为当从数据库中检索到类时,Doctrine 2 会跳过实例化类。

在其中实例化一个新的 DateTime 或 ArrayCollection 是完全可以接受的,您甚至可以添加默认的关系对象。我个人喜欢注入你的依赖,让你的代码很容易测试和修改,但有些事情在你的构造函数中实例化更有意义。

于 2013-04-09T12:36:23.083 回答
2

这些答案中的大多数似乎都是合理的。但我总是做你该做的。如果我正在测试它,那么我要么有一个设置器,要么只是将它设置为某个东西(看到你的“mother_language”是公开的)。

但我个人认为这取决于你在做什么。从我看到的其他人发布的代码来看,如果您的类实例化对象,则构造函数应始终采用一些参数。

如果我想创建一个实例化另一个对象的对象,而该内部对象实例化另一个对象,依此类推怎么办?看来我手上会有很多东西。

对我来说,这句话似乎是在说,“不要使用 camelCase,使用 under_score”。我认为你应该以任何适合你的方式去做。毕竟,您确实有自己的测试方式,不是吗?

于 2013-04-09T13:15:47.500 回答
2

如果您想设置默认值,但要提高灵活性/可测试性,为什么不这样做呢?

class Person
{
    protected $mother_language;

    function __construct(Language $language = null)
    {
         $this->mother_language = $language ?: new Language('English');
    }
}

有些人可能仍然不喜欢它,但它一种改进。

于 2015-10-14T18:28:28.557 回答
0

在构造函数中构造新对象并没有错。这两种方法都有优点。通过构造函数参数接收所有构造函数依赖项确实使您的类更加灵活,并且这种灵活性在为您的类编写自动化测试时尤其有用。但这也产生了一层间接性。这也使得类在一般用例中使用起来不太方便,因为要考虑更多的构造函数参数。这也使得准确地知道类的行为变得更加困难,因为您不确切知道将传递什么。它使类变得不那么简单。

通过参数获取所有构造函数依赖项是使用称为“控制反转”的一种方式。我同意熟悉这项技术并能够使用它很重要,但推广从根本上说是优越的,但恰恰相反。控制反转是邪恶的,但有时它是必要的邪恶。有人会说这总是必要的。我不同意这一点,但即使我同意,我会说这意味着它始终是必要的邪恶。控制反转使您的代码以上述方式变得不那么简单。当人们争论支持控制反转时,他们真正想说的是,“使用代码是不好的”。我并不是说人们是故意这么说的,因为如果他们这样做了,他们可能不会宣传它。一世' 我说这些概念以不明显的方式在说同样的事情,希望通过意识到他们在说这个,有些人会重新考虑它。我在我的文章中概述了这种说法的原因“使用代码是不好的”

于 2020-05-01T20:45:17.800 回答