10

我将用问题的简短版本作为长问题的序言:

问题的简短版本

允许对象实例化它自己的依赖项,然后提供构造函数参数(或设置方法)来简单地覆盖默认实例化有什么问题?

class House
{
   protected $door;
   protected $window;
   protected $roof;

   public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null)
   {
      $this->door   = ($door)   ? $door   : new Door;
      $this->window = ($window) ? $window : new Window;
      $this->roof   = ($roof)   ? $roof   : new Roof;
   }
}

长版问题

我对这个问题的动机是依赖注入需要你跳过箍只是为了给一个对象它需要的东西。IoC 容器、工厂、服务定位器……所有这些都引入了许多额外的类和抽象,使您的应用程序的 API 变得复杂,我认为,在许多情况下,测试同样困难。

一个对象实际上知道它需要哪些依赖项才能正常运行,这难道不是合乎逻辑的吗???

如果依赖注入的两个主要动机是代码可重用性和单元可测试性,那么能够用存根或其他对象覆盖默认实例化就可以了。

同时,如果您需要将 House 类添加到您的应用程序中,您只需要编写 House 类,而不是在其之上的工厂和/或 DI 容器。此外,任何使用房屋的客户端代码都可以只包含房屋,并且不需要从上面的某个地方获得房屋工厂或抽象服务定位器。一切都变得非常简单,没有中间人代码,并且仅在需要时才实例化。

如果一个对象有依赖关系,它应该能够自己加载它们,同时提供一种机制让这些依赖关系在需要时被重载,我是否完全不合时宜?

例子

#index.php (front controller)

$db = new PDO(...);
$cache = new Cache($dbGateway);
$session = new Session($dbGateway);
$router = new Router;

$router::route('/some/route', function() use ($db, $cache, $session) 
{   
   $controller = new SomeController($db, $cache, $session);
   $controller->doSomeAction();
});



#SomeController.php

class SomeController
{
   protected $db;
   protected $cache;
   protected $session;

   public function __construct(PDO $db, ICache $cache, ISession $session)
   {
      $this->db = $db;
      $this->cache = $cache;
      $this->session = $session;
   }

   public function doSomeAction()
   {
      $user = new \Domain\User;
      $userData = new \Data\User($this->db);

      $user->setName('Derp');
      $userData->save($user);
   }
}

现在,在一个具有许多不同模型/数据类和控制器的非常大的应用程序中,我觉得必须通过每个控制器(不需要它)传递 DB 对象只是为了将它提供给每个数据映射器(这将需要它),有点臭。

而且通过控制器传递一个服务定位器或DI容器,只是为了定位数据库然后每次将它交给数据映射器,也似乎有点臭。

将工厂或抽象工厂传递给控制器​​也是如此,然后不得不通过一些$this->factory->make('\Data\User');看起来很笨拙的东西来实例化新对象。特别是因为您需要编写抽象工厂类,然后是为您想要的对象连接依赖关系的实际工厂。

4

3 回答 3

5

您的问题问得很好,我真的很喜欢人们出于“单元测试和可维护性”的原因而质疑常识的东西(无论这些中的哪一个你是一个糟糕的程序员,如果你不这样做-it -topics,它总是关于单元测试和可维护性)。所以你在这里问了正确的问题:DI 是否真的支持单元测试和可维护性,如果支持,如何支持?并预测它:如果使用得当,它确实......

关于分解

依赖注入 (DI) 和控制反转 (IoC) 是增强 OOP 的封装和关注点分离的核心概念的机制。因此,要回答这个问题,必须争论为什么封装和关注点分离是很酷的事情。两者都是分解的核心机制:封装(是的,我们有模块)和关注点分离(我们有模块在某种程度上是有意义的)。关于这个主题可以写很多,但是,就目前而言,说它是关于降低复杂性就足够了。系统分解允许您将系统(无论有多大)分解为人脑能够管理的块。虽然有点哲学,但这真的很重要:如果没有人脑的限制,整个可维护性主题就不会' 没那么重要。好的,让我们说:分解是减少将系统的复杂性感知为我们可以管理的块。

但是,与往常一样,它是有代价的:分解也会增加复杂性,正如您在 DI 中所说的那样。那么它仍然有意义吗?是的,因为:

人为增加的复杂性与系统固有的复杂性无关。

基本上就是这样,在抽象层面上。它有一些含义:您需要根据您正在构建的系统的固有复杂性(或它可能达到的复杂性)来选择分解程度和实现它所花费的努力。

DI分解

特别是关于 DI:根据上述内容,有足够小的系统,其中 DI 增加的复杂性并不能证明降低的感知复杂性是合理的。而且,不幸的是,网络上的每个教程都涉及其中一个,它们不支持理解整个 fuzz 是关于什么的。

然而,大多数(或许多,至少)现实生活中的项目都达到了一定程度的内在复杂性,因此对额外分解的投资是值得的,因为感知复杂性的降低加速了后续开发并减少了错误。依赖注入是这样做的技术之一:

DI 支持分离What(接口)和How(实现):如果只是关于玻璃门,我同意:如果这对一个人的大脑来说太过分了,那么他或她可能不应该成为一名程序员。但现实生活中的事情要复杂得多:DI 让你专注于真正重要的事情:作为一个房子,我不在乎我的门,只要我能依赖它可以关闭和打开的事实。也许现在没有任何门存在?在这一点上,您根本不需要关心。在注册容器中的组件时,您可以再次关注:我想要在我的房子里有什么门?你不再需要关心门或房子本身:它们很好,你已经知道了。您已经分离了关注点:定义事物如何组合在一起(组件)和实际将它们组合在一起(容器)。就这样,据我所知,我的经验。这听起来很笨拙,但在现实生活中,这是一项了不起的成就。

少一点哲学

为了让它再次落地,有一些更实际的优势:

在系统不断发展的过程中,总有一些部分尚未开发。在大多数情况下,指定一个行为远比实现它要少得多。没有 DI,只要没有开发门,你就无法开发你的房子,因为没有任何东西可以实例化。使用 DI,您不必在意:您设计您的房子,只使用接口,您为这些接口编写带有模拟的测试,并且您的罚款:您的房子可以工作,甚至没有门窗。

您可能知道以下内容:您已经为某事工作了几天(比如说玻璃门)并且您很自豪。六个月后——在此期间你学到了很多东西——你再看一遍,那是废话。你把它扔掉。没有 DI,你需要改变你的房子,因为它使用你刚刚废弃的类。使用 DI,您的房子不会改变。它可能位于它自己的程序集中:您甚至不需要重新编译房屋程序集,它没有被触及。在复杂的情况下,这是一个巨大的优势。

还有更多,但也许考虑到所有这些,当您下次阅读 DI 时,更容易想象 DI 的好处......

于 2013-10-22T08:53:41.883 回答
3

虽然另一个答案很好,但我会尝试从实际的角度来解决这个问题。

想象一下,您有一个内容管理系统,您可以根据需要调整其配置。假设,此配置存储在数据库中。从那时起,这意味着您应该实例化以下内容:

$dsn = '....';
$pdo = new PDO($dsn, $params);

$config_adapter = new MySQL_Config_Adapter($pdo);

$config_manager = new Config_Manager($config_adapter);
// $config_manager is ready to be used

现在,让我们看看如果我们允许一个类实例化它自己的依赖会发生什么

class Foo
{
    public function __construct($config = null)
    {
         if ($config !== null) {
             global $pdo;

             $config_adapter = new MySQL_Config_Adapter($pdo);

             $config_manager = new Config_Manager($config_adapter);

             $this->config = $config_manager;
        } else {
             // Ok, it was injected
             $this->config = $config;
        }
    }
}

这里有3个明显的问题:

  • 全局状态

因此,您基本上决定是否希望它具有全局状态。如果你提供一个$config实例,那么你就是说你不想要一个全局状态。否则,您是在说您确实想要这个。

  • 紧耦合

那么,如果您决定从 切换MySQLMongoDB,甚至直接切换file-based PHP-array到存储CMS's配置怎么办?然后你必须重写很多代码,负责依赖初始化。

  • 不明显的单一责任原则违反

一个类应该只有一个改变的理由。一个类应该只服务于单一目的。这意味着,这个Foo类有不止一个职责——它还负责依赖管理。

这应该如何正确完成?

public function __construct(IConfig $config)
{
    $this->config = $config;
}

因为它没有与特定的适配器紧密耦合,从那时起,对它进行单元测试或替换适配器(比如,用别的东西来替换 MySQL)就变得很容易了

覆盖默认参数怎么样?

如果您覆盖 default objects,那么您做错了什么,这表明您的班级做得太多。

构造函数的一个基本目的是初始化一个类的状态。如果您初始化了状态,然后您通过依赖项设置器方法更改该状态,那么您最终会破坏封装,这表明 一个对象应该完全控制其状态和实现

回到您的代码示例

让我们看一下您的代码示例。

   public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null)
   {
      $this->door   = ($door)   ? $door   : new Door;
      $this->window = ($window) ? $window : new Window;
      $this->roof   = ($roof)   ? $roof   : new Roof;
   }

你在这里说的是这样的:如果没有提供某些参数,则从全局范围导入该参数的实例。这里的问题是你House知道你的依赖来自哪里,而它应该完全不知道这些信息。

现在让我们提出一些现实世界的场景问题:

  • 如果你想改变你的门的颜色怎么办?
  • 如果您想更改窗口的大小怎么办。
  • 如果您想为另一所房子使用相同的门,但窗户尺寸不同怎么办?

如果您要坚持编写代码的方式,那么您最终会出现大量代码重复。考虑到“纯”DI,这将很简单:

$door = new Door();
$door->setColor('black');

$window = new Window();
$window->setSize(500, 500);

$a_house = new House($door, $window, $roof);

// As I said, I want house2 to have the same door, but different window size
$window->setSize(1000, 1000);

$b_house = new House($door, $window, $roof);

AGAIN:依赖注入的核心点是对象可以共享相同的实例

还有一件事,

服务定位器/IoC 容器负责对象存储。它们只是存储/检索对象,例如$pdo.

工厂只是抽象一个类的实例。

因此,它们不是依赖注入的“一部分”,而是利用它。

而已。

于 2013-10-22T23:20:34.267 回答
2

当您的依赖项也具有必须指定的依赖项时,会出现这样的问题。然后你的构造函数需要知道如何构造它的依赖,然后你的构造函数开始变得非常复杂。

使用您的示例:Roof对象需要一个俯仰角。默认角度取决于业务规则的新/更改,取决于您房屋的位置(平屋顶在 10 英尺的积雪下效果不佳)。所以现在你House需要计算要传入的角度Roof。您可以通过传递位置来做到这一点(House目前只需要计算角度,或者它正在创建一个“默认”位置以传递给Roof构造函数)。无论哪种方式,构造函数现在都必须做一些工作来创建默认屋顶。

这可能发生在您的任何依赖项上,一旦其中一个需要确定/计算某些东西,那么您的对象必须知道它的依赖项以及如何制作它们。它不应该做的事情。

这不一定在每种情况下都会发生,在某些情况下,您可以逃避您的建议。然而,你是在​​冒险。

试图让人们的事情“更容易”可能会导致您的设计变得不灵活和困难,因为需要更改代码。

于 2013-10-22T14:20:43.667 回答