为什么不“在对象构造函数中实例化一个新对象”?

时间:2013-04-09 12:26:34

标签: php oop

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

class Person {
  public $mother_language;

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

我收到了用户" Matija"的评论。 (his profile)并且他写道:你永远不应该在对象consturctor中实例化一个新对象,应该从外部推送依赖项,所以使用这个类的任何人都知道这个类依赖于什么!

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

但是,我过去经常这样做,例如:

  • 作为私有属性,其他类给我功能,我可以解决不复制代码,例如我可以创建一个列表(实现ArrayAccess接口的类)),这个类将在另一个中使用class,有这样的对象列表,
  • 某些类使用例如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类处理字符串数组,并且存储它们不会使用array,而是另一个类TList,它提供了一些有用的方法,而{ {1}}只是某种界面。 TStrings对象是私有声明的,并且无法从外部访问,但TList的getter和setter。

  • (不重要,但有些原因)通常我是使用我班级的人。

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

我已经阅读了this discussion,但仍然不清楚。

7 个答案:

答案 0 :(得分:12)

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

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

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

更新

除了测试之外,传入依赖对象还可以使您的代码更加明确,灵活和可扩展。引用我链接到的博客文章:

  

当协作者构造与初始化混合在一起时,它表明只有一种方法可以配置该类,从而关闭原本可用的重用机会。

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

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

答案 1 :(得分:7)

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

没有理由害怕管理资源的。实际上,类提供的封装对于此任务来说是 perfecty apt

如果你限制自己只是从外部推动资源,那么应该管理这些资源?意大利面程序代码?糟糕!

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

答案 2 :(得分: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

答案 3 :(得分:3)

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

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

答案 4 :(得分:2)

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

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

如果我想创建一个实例化另一个对象的对象,该内部对象实例化另一个对象,等等,该怎么办?这似乎让我手上有很多东西。

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

答案 5 :(得分:2)

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

class Person
{
    protected $mother_language;

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

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

答案 6 :(得分:0)

在构造函数内部构造新对象没有错。两种方法都有优点。通过构造函数参数获取所有构造函数依赖确实使您的类更加灵活,尤其是在为您的类编写自动化测试时,这种灵活性特别有用。但这还会创建一个间接层。这也使该类在一般用例中使用起来不太方便,因为要考虑的构造函数参数更多。这也使得很难确切地知道该类的行为,因为您不确切知道将要传递的内容。这使该类变得不那么简单。

通过参数获取所有构造函数的依赖关系是使用一种称为“控制反转”的方法。我同意,熟悉这项技术并能够使用它很重要,但是从根本上讲,提升是完全相反的。控制权的颠倒是邪恶的,但有时却是必然的邪恶。有人会说这总是必要的。我不同意这一点,但是即使我同意,我也要说这始终是必不可少的邪恶。控制反转使您的代码以上述方式不那么直接。当人们主张控制反转时,他们真正的意思是“使用代码是不好的”。我并不是说人们故意在说这话,因为如果这样做的话,他们可能不会推广它。我说的是这些概念用相同的方式表达不同的意思,并希望通过意识到他们在说这些,一些人会重新考虑它。我在我的文章"Using code is bad"中概述了提出这一要求的原因。