在理解依赖注入时遇到问题

Posted

技术标签:

【中文标题】在理解依赖注入时遇到问题【英文标题】:Having issues understanding Dependency Injection 【发布时间】:2011-07-26 17:32:51 【问题描述】:

我正在构建一个小项目,试图尽可能多地自学基础知识,这对我来说意味着不使用预制框架(如 Jeff once put it,“不要重新发明***,除非您打算进一步了解车轮”[强调我的])并遵循测试驱动开发的原则。

在我的探索中,我最近遇到了依赖注入的概念,这对 TDD 来说似乎很重要。我的问题是我无法完全理解它。到目前为止,我的理解是,它或多或少相当于“让调用者传递类/方法它可能需要的任何其他类,而不是让它们自己创建它们。”

我尝试使用 DI 解决两个示例问题。我在这些重构方面走在正确的轨道上吗?

数据库连接

我打算只使用一个单例来处理数据库,因为我目前不希望使用多个数据库。最初,我的模型看起来像这样:

class Post   
  private $id;  
  private $body;  

  public static function getPostById($id)   
    $db = Database::getDB();  
    $db->query("SELECT...");  
    //etc.  
    return new Post($id, $body);
    

  public function edit($newBody)   
    $db = Database::getDB();  
    $db->query("UPDATE...");  
    //etc.  
    
  

使用 DI,我认为它看起来更像这样:

class Post   
  private $db; // new member

  private $id;  
  private $body;  

  public static function getPostById($id, $db)  // new parameter   
    $db->query("SELECT...");  // uses parameter
    //etc.  
    return new Post($db, $id, $body);
    

  public function edit($id, $newBody)    
    $this->db->query("UPDATE...");  // uses member
    //etc.  
    
 

我仍然可以使用单例,并在应用程序设置中指定凭据,但我只需要从控制器传递它(控制器无论如何都是不可单元测试的):

Post::getPostById(123, Database::getDB);

模型调用模型

以具有查看次数的帖子为例。由于确定视图是否为新视图的逻辑并不特定于 Post 对象,因此它只是其自身对象的静态方法。 Post 对象会调用它:

class Post 
  //...

  public function addView() 
    if (PageView::registerView("post", $this->id) 
     $db = Database::getDB();
     $db->query("UPDATE..");
     $this->viewCount++;
   

使用 DI,我认为它看起来更像这样:

class Post 
  private $db;
  //...

  public function addView($viewRegistry) 
    if ($viewRegistry->registerView("post", $this->id, $this->db) 
     $this->db->query("UPDATE..");
     $this->viewCount++;
   

这会将来自控制器的调用更改为:

$post->addView(new PageView());

这意味着实例化一个只有静态方法的类的新实例,这对我来说很糟糕(我认为在某些语言中是不可能的,但在这里可行,因为 php 不允许类本身是静态的)。

在这种情况下,我们只深入一层,因此让控制器实例化一切似乎是可行的(尽管 PageView 类通过 Post 的成员变量间接获取其 DB 连接),但它似乎可以得到如果您必须调用需要类的方法,而该类需要需要类的类,则该方法很笨拙。我想这可能只是意味着这也是一种代码味道。

我是在正确的轨道上,还是我完全误解了 DI?非常感谢任何批评和建议。

【问题讨论】:

【参考方案1】:

是的。看起来你有正确的想法。您会看到,当您实施 DI 时,您的所有依赖项都将浮动到“顶部”。将所有内容都放在顶部可以轻松模拟必要的测试对象。

拥有一个需要一个需要一个类的类并不是一件坏事。你在那里描述的是你的对象图。这对于 DI 来说是正常的。让我们以 House 对象为例。它依赖于厨房; Kitchen 依赖于 Sink; Sink 依赖于 Faucet 等。 House 的实例化看起来类似于new House(new Kitchen(new Sink(new Faucet())))。这有助于执行单一职责原则。 (顺便说一句,您应该在工厂或建筑商之类的地方进行此实例化工作,以进一步执行单一职责原则。)

Misko Hevery 撰写了大量有关 DI 的文章。他的blog 是一个很好的资源。他也是pointed out some of the common flaws(构造函数做真正的工作,挖掘合作者,脆弱的全局状态和单例,类做的太多了)有警告标志来发现它们和修复它们的方法。有时间值得一试。

【讨论】:

如果我在一个我被要求从事的项目中看到你的 House 实例化,我想我会害怕。并不是说这样做是错误的,只是看起来……“大”。您在哪里画线并称其为“顶部”?哪些对象可以使用new,哪些对象不能使用?如果你总是需要传入你的依赖项,那么你的前端控制器不会有一个构造函数,它的参数可以嵌套 20 个构造函数吗?线路在哪里? 它并没有第一眼看上去那么糟糕。您可能永远不会拥有一个具有大量依赖链的巨型对象。如果你这样做,你可能有一个关系不正确的对象。该规则有一些例外(如列表和字符串)。 This post 谈论“injectables”和“newables”以确定您可以在哪些上使用 new。至于实例化所有对象,这是计算机可以做的相当乏味的事情,这就是可以使用 IoC 容器而不是工厂的地方。 也许在众议院的例子中应用合成会更好。如果厨房有 Sink,您可以在类中构建对象。无需传递。【参考方案2】:

依赖注入是关于注入。您需要一些解决方案来注入外部对象。

传统的方法是:

构造函数注入__construnctor($dependecy) $this->_object = $dependency setter 注入 setObject($dependency) $this->_object = $dependency gettter 注入 getObject() return $this->_dependency 并重载此方法,例如。来自测试中的存根或模拟。

您也可以混合使用以上所有内容,这取决于您的需要。

避免静态调用。我个人的规则是仅在调用某些函数时才使用静态,例如My::strpos() 或者在处理单例或注册表时(应该限制在最低限度,因为全局状态是邪恶的)。

当您的应用具有良好的依赖容器时,您将很少需要静态方法。

看看其他dependency injection + [php] topics on SO。

评论后编辑:

容器

不同的框架以不同的方式处理容器。通常这是一个对象,它包含您需要的对象的实例,因此您不必每次都实例化新对象。您可以使用这样的容器注册任何对象,然后在需要时随时访问它。

容器可以在启动时实例化您需要的所有资源,或者在访问时延迟加载资源(更好的解决方案)。

例如,考虑:

Zend Application Resource Plugins Symfony Dependency Injection Container

另一个很好的参考:

http://martinfowler.com/articles/injection.html

【讨论】:

所以我在正确的轨道上?我做的重构基本上都是构造函数注入吧? Gordon 评论了一些摆脱静态函数的方法,例如创建 PostFinder 对象。您能否详细说明“依赖容器”的含义?我认为这与我尚未研究的所有 DI 框架有关。听起来这将是一些巨大的、全球性的单身事物​​……这不可能,因为你说那很糟糕。你能指出一些依赖容器的简单例子吗?【参考方案3】:

它肯定会朝着正确的方向发展,但你不应该止步于此。

DI 的重点是消除类之间的强耦合,以便更轻松地替换单个组件。这将允许更好的可测试性,因为您可以使用 Mocks 和 Stub 更轻松地替换依赖项。而且,一旦您的代码经过测试,更改和维护起来就会容易得多。

因此,您还应该删除代码中产生强烈耦合气味的其他方面,例如删除静态方法和单例以及任何其他全局变量。

有关更多信息,请参阅

How is testing the registry pattern or singleton hard in PHP? http://gooh.posterous.com/singletons-in-php http://kore-nordmann.de/blog/0103_static_considered_harmful.html http://fabien.potencier.org/article/11/what-is-dependency-injection

编辑:其他几个答案建议使用 DI 容器,我觉得有必要强调您确实需要 DI 容器来执行 DI。上面给出的最后一个链接中的第二篇博文讨论了这一点。

【讨论】:

谢谢。关于静态方法,您是说开发人员应该永远使用静态方法吗? (似乎有点极端)。您链接的文章似乎不适用于我的示例,因为更新版本传递了该类的实例(PHP 没有静态类,只有静态方法)。因此,被调用的代码不依赖于具体的类,只是传递的对象有一个registerView() 方法。这似乎很解耦,因为对象可以很容易地被模拟——例如class PageViewTest public function registerView() return True; $post->addView(new PageViewTest()); @Agent 示例中没有静态变量,所以我对此没有意见:) 我会说将它们减少到最低限度。您从静态中获得的收益很少。速度影响可以忽略不计。 @Gordon:在问题中有静态。您可能需要重新阅读它(提示:PageView::registerView)。当方法对实例成员没有用处时,静态对我来说非常有意义。例如,Post::getPostById($id) 之类的内容没有理由链接到实例。其他人说得很好,基本上说静态方法在确定性时很好(即在给定相同输入时总是返回相同的结果)。我看不出有什么理由与之争论。 DI 示例确实使静态函数看起来是动态的,所以也许这证明了你的观点。 @Agent 抱歉,例如我的意思是您在评论中显示的代码。不,PageView:: 或 Post:: 根本不能很好地使用静态,因为它们将类名硬编码到您的代码中,因此您不能轻易更改它们。至于 Post::getPostById,我也不同意。它确实在 Db 实例上运行。这里的问题是它不应该放在 Post 上,而是放在 PostFinder 或 PostMapper 类 IMO 上。对于输入 Y,函数应该返回 X 始终为真。 @Gordon:所以你会把它重构成class PostFinder public function __construct($dbConn) ... public function findById($id) $dbConn->query("..."); .. ?我认为这将是我更难内化的事情之一——单一职责原则。让像“Post”这样的类来处理所有与 Post 相关的事情对我来说更直观,但更好的做法是将它们全部分离为相关但又独立的类,处理他们自己的小专业领域,对吗?跨度> 【参考方案4】:

回答您的问题:是的,您在正确的轨道上。为您提供更多详细信息:这是我发现的与 DI 相关的最佳帖子之一:

http://www.potstuck.com/2009/01/08/php-dependency-injection

您将了解什么是容器: $book = Container::makeBook();

关于第二个示例:在您的方法 addView 中,我会尽量避免传递对象 $viewRegistry,我会在控制器中检查外部条件。

【讨论】:

以上是关于在理解依赖注入时遇到问题的主要内容,如果未能解决你的问题,请参考以下文章

Angular 2 - 如何将依赖项注入到自定义类中,该类不是组件并且不作为服务公开

@InjectMocks注入依赖遇到的坑

将 TranslateService 注入拦截器时的 Angular 循环依赖

OpenIddict 的依赖注入错误

依赖注入与控制反转

spring依赖注入报错