【问题标题】:Never instantiate objects in the constructor?永远不要在构造函数中实例化对象?
【发布时间】:2014-09-23 15:26:49
【问题描述】:

像下面这样在构造函数中实例化对象是不是很糟糕?

class Foo
{
    public function fooMethod() {
        return 'foo method';
    } 
}

class Too
{
    public function tooMethod() {
        return 'too method';
    } 
}

class Boo
{
    public $foo;
    public $too;

    public function __construct()
    {
        $this->foo = new Foo();
        $this->too = new Too();
    }
}

如果是这样,它有多糟糕?应该怎么做才合适?

【问题讨论】:

  • 如果你不这样做,你将如何在你的类中声明一个 foo 元素?
  • @Hanky웃Panky 通过构造函数传入。
  • 为什么会这样?!它是什么构造函数...
  • @WayneWhitty 这将要求调用者知道实现在内部使用FooBar 对象。这是一个可能应该隐藏的实现细节。这意味着您无法重新设计Boo,因此它使用NewFoo 而不是Foo

标签: php oop constructor


【解决方案1】:

这取决于您的项目规模。

在大型项目或长期项目上,应该稍微改变一下。

理想情况下,你会重构它实现Dependency Injection pattern,并可能使用工厂来实例化它。

一些示例代码:

interface FooInterface { function fooMethod(); } 
class Foo implements FooInterface { function fooMethod(){return 'Foo';} }

interface TooInterface { function tooMethod(); } 
class Too implements FooInterface { function tooMethod(){return 'Too';} }

class Boo
{
    public $foo;
    public $too;

    public function __construct(FooInterface $foo, TooInterface $boo)
    {
        $this->foo = $foo;
        $this->too = $boo;
    }
}

class BooFactory
{
    public static function create()
    {
        return new Boo(new Foo, new Too);
    }
}

【讨论】:

  • 感谢弗拉德的回答。您能否举例说明如何使用工厂完成此操作?谢谢!
  • 为您的示例添加。您应该阅读我链接到的文章,以便更好地理解依赖注入是什么。
  • 我能问一下——如果 Foo 需要依赖很多其他类怎么办? Foo的构造函数中要传递的列表会很长不是吗?例如,public function __construct(FooInterface $foo, TooInterface $boo, PooInterface $poo, etc, etc ,etc, etc, etc) - 你不觉得看起来太多了吗?还是正常?
  • @tealou ,你是要创造神物吗?
  • @sectus:什么是神物?
【解决方案2】:

在另一个类中手动实例化类会创建隐式依赖项,这很难维护 - 如果您需要更改 FooToo 类,您将很难检测到需要更改的内容。

因此,管理依赖项的更好方法是:

class Foo
{
    private $_bar;

    function __construct(Bar $bar)
    {
         $this->_bar = $bar;
    }
}

这样,你的对象依赖是明确的。这样做的另一个好处是,一些 PHP 框架(Laravel、Zend、Symfony)允许自动解决依赖关系。这意味着,您不必手动实例化您的对象,而只能通过某种工厂 - 就像这样(Laravel):

$foo = App::make('Foo');

App 工厂会使用一些反射魔法自动检测您的 Foo 类依赖项并适当地注入它们。其他框架也有类似的功能。

此外,OOP 中有一些通用原则,称为SOLID,它们有助于开发更好的 OOP 设计。其中之一 - D,代表 Dependency Inversion。这意味着,您应该避免 hard 依赖,就像在您的代码中一样。相反,FooBar 类都应该依赖于 interface,如下所示:

interface BarInterface
{
    function doBar();
}

class Bar implements BarInterface
{
    function doBar()
    {
        print "Doing BAR";
    }
}

class Foo
{
    /**
     * @var BarInterface
     */
    private $bar;

    function __construct(BarInterface $bar)
    {

        $this->bar = $bar;
    }
}

现在,如果您需要用其他东西更改 Bar 类,如果您的替代品也实现了 BarInterface,那么一切都不会松懈。

【讨论】:

  • 另外,虽然遵循SOLID 原则对于一个小项目来说似乎有点矫枉过正,但我​​仍然建议您这样做。制作一个好的面向对象设计并不是那么容易,并且需要大量的实践和经验。因此,遵循良好实践最初可能会减慢您的开发速度,但很快您就会发现维护和扩展您的项目比平时更顺利,并且得到了回报。此外,如果不对小型项目进行多次尝试,您将无法为大型项目做出良好的 OOP 设计。
  • @TwiStar:谢谢。请问 - 如果我不使用界面但我在Foo-function __construct($bar) 中这样做,有什么不同?
  • 使用接口可确保您正确使用依赖项。如果你输入一个接口 - 你知道注入的类有什么方法,以及它们应该做什么。但是$bar 做了什么?你一点头绪都没有。另外,请注意,最终您将不得不编写将被其他开发人员使用的代码,因此编写对任何使用它的人来说尽可能清晰的代码是一件好事。
【解决方案3】:

这取决于您的要求和课程。 假设每次调用 Foo/Too 的构造函数时,您都会对数据库执行大量查询以获取数据,在这种情况下,我会选择使用 lazy instantiation

当然,在构造函数上初始化你的属性是一个好习惯,但在现实生活中的表现可能是你的敌人。

例子:

class Boo {
   private $foo = null;
   private $too = null;
   public function __construct() {
      //Do something else
   }
   public function getFoo() {
      if (is_null($this->foo)) {
         $this->foo = new Foo();
      }
      return $this->foo;
   }
   public function getToo() {
      if (is_null($this->too)) {
         $this->too = new Too();
      }
      return $this->too;
   }
   public function aMethodThatUsesFoo() {
      $foo = $this->getFoo();
      $foo->fooMethod();
   }
   public function aMethodThatDoesntUsesFoo() {
      echo "Hello!, I don't need foo or too to execute this method";
   }
}

如果你只使用这个类来执行aMethodThatDoesntUsesFoo(),它永远不会调用Foo/Too的构造函数。

$boo = new Boo();
$boo->aMethodThatDoesntUsesFoo();

如果你只执行aMethodThatUsesFoo(),它只会实例化Foo

$boo = new Boo();
$boo->aMethodThatUsesFoo();

您也可以通过静态方法来做到这一点。

【讨论】:

  • 感谢您的回答。是的,请给我看一个延迟初始化的例子吗?
  • 如果Boo 是一个控制器,Foo 是一个从数据库中获取文章的模型,它需要数据库连接($connection)——那么我应该从哪里传递这个$connection? $boo = new Boo($connection);?但 $connection 与控制器无关。
  • @tealou 如果模型需要连接到数据库,我将使用Singleton 模式使其全局化,因此我不需要将其作为参数发送。我可以使用setConnection($connection) 之类的方法将这个Singleton 放入Factory 对象中,如果您需要使用其他数据库适配器(例如用于测试的SQLite),这将更改连接。好问题! ;)
  • 谢谢。但我被告知要避免单例模式......我曾尝试在我的 MVC 应用程序中使用 DI codereview.stackexchange.com/questions/58709/… 你能看一下,让我知道我这样做是对还是错?谢谢。
【解决方案4】:

本质上并不坏。

缺点是它降低了你的类的“可测试性”,仅仅是因为Boo 现在依赖于FooToo 的存在。

【讨论】:

  • 使用对象工厂是一个很好的解决方法,这意味着您可以模拟工厂返回模拟对象
  • @Mark Ba​​ker - 那么你如何使用对象工厂来做到这一点呢?谢谢!
  • @tealou:要回复某人,请输入 at 符号,输入前几个字母中间没有空格,然后按 Tab 以获取自动完成菜单。否则其他人可能看不到您的消息(Wayne 将被通知他回答下的所有内容,因为他拥有它,但除非明确通知,否则 Mark 不会)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-28
  • 1970-01-01
  • 1970-01-01
  • 2012-11-05
相关资源
最近更新 更多