依赖性覆盖而不是依赖注入?

时间:2013-10-21 21:09:17

标签: php oop dependency-injection

我将在问题的简短版本前面提出长问题:

问题的简短版本

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

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');这样繁琐的东西实例化新对象似乎很尴尬。特别是因为您需要对抽象工厂类进行编码,然后实际工厂连接所需对象的依赖项。

3 个答案:

答案 0 :(得分:4)

你的问题很好,我真的很喜欢人们质疑那些由于单元测试和可维护性原因而常识的东西。 (无论这些你是一个糟糕的程序员 - 如果你不做其他人 - 主题,它总是 总是 关于单元测试和可维护性)。所以你在这里问正确的问题:DI 真的是否支持单元测试和可维护性,如果是,如何?并预测它:如果正确使用它会...

关于分解

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

但是,与往常一样,它需要付出代价:分解也会增加复杂性,正如您在DI方面所说的那样。它还有意义吗?是的,因为:

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

基本上,它是抽象的。它具有影响:根据您构建的系统的固有复杂性(或某天可能达到的复杂性),您需要选择分解程度和实现它的努力。

分解DI

特别关于DI:根据上述,存在足够小的系统,其中DI的增加的复杂性不能证明降低的感知复杂性。不幸的是,网络上的每一个教程都涉及其中一个不支持理解整个模糊内容的教程。

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

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

少一点哲学

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

虽然系统在不断发展,但总有一些部分尚未开发出来。在大多数情况下,指定行为远比实现行为少得多。没有DI,只要没有开发门就无法开发你的房子,因为没有什么可以实例化的。使用DI,您无需关心:您只需使用界面设计您的房屋,就可以使用模拟器为这些界面编写测试以及您的罚款:您的房屋可以工作,甚至不存在门窗。

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

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

答案 1 :(得分: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实例,那么您说您不想要全局状态。否则你说你确实想要这个。

  • 紧耦合

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

  • 非明显,单一责任原则违规

一个班级应该只有一个改变的理由。课程应该只用于单一目的。 这意味着,Foo类具有多个职责 - 它还负责依赖关系管理。

如何正确完成这项工作?

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

因为它没有与特定的适配器紧密耦合,从那时起它就很容易进行单元测试,或者更换适配器(Say,MySQL与其他东西)

覆盖默认参数怎么样?

如果你覆盖默认objects,那么你做错了,这表明你的班级做得太多了。

构造函数的基本目的是初始化类的状态。如果初始化状态,然后通过依赖setter方法改变该状态,那么end up with broken encapsulation ,表明一个对象应该完全控制其状态和实现

返回您的代码示例

让我们看看你的代码示例。

   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:依赖注入的核心点是对象可以共享相同的实例

还有一件事,

Service Locators / IoC容器负责对象存储。它们只是存储/检索对象,例如$pdo

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

那样, 它们不是依赖注入的“组成部分”,它们利用它。

就是这样。

答案 2 :(得分:1)

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

使用您的示例: Roof物体需要俯仰角。默认角度取决于您的房屋的位置(平顶不能与10'的雪一起工作)通过新的/更改业务规则。现在,您的House需要计算传入Roof的角度。您可以通过传递位置(House当前仅需要计算角度或创建“默认”位置以传递Roof构造函数)来执行此操作。无论哪种方式,构造函数现在都必须做一些工作来创建默认屋顶。

任何依赖项都会发生这种情况,一旦其中一个需要确定/计算某些内容,那么您的对象必须知道它的依赖关系以及如何制作它们。它不应该做的事情。

这并不一定会发生在每种情况下,在某些情况下,您可以放弃您所建议的内容。但是你冒了风险。

尝试让人们“更容易”让您的设计变得不灵活和困难,因为代码需要更改。