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

我正在构建一个小项目,试图尽可能多地自学基础知识,这对我来说意味着不使用预制框架(正如Jeff曾经说过的那样,“不要重新发明轮子,除非你打算学习更多关于轮子的知识”[强调我的]),并遵循测试驱动开发的原则。

在我的探索中,我最近遇到了依赖注入的概念,这似乎对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 对象为例。它依赖于厨房;厨房依赖于水槽;水槽依赖于水龙头等。众议院的实例化看起来像 .这有助于执行单一责任原则。(顺便说一句,您应该在工厂或构建器之类的地方执行此实例化工作,以进一步执行单一责任原则。new House(new Kitchen(new Sink(new Faucet())))

Misko Hevery撰写了大量关于DI的文章。他的博客是一个很好的资源。他还指出了一些常见的缺陷(构造器做实际工作,挖掘协作者,脆弱的全球状态和单例,类做太多),并警告标志来发现它们和修复它们的方法。值得一游。


答案 2

Dependency injection is about injecting. You need some solution to inject the external object.

The traditional approaches are:

  • constructor injection __construnctor($dependecy) {$this->_object = $dependency}
  • setter injection setObject($dependency) {$this->_object = $dependency}
  • gettter injection and oveloading this method eg. from stub or mock in the tests.getObject() {return $this->_dependency}

You may also mix all the above, depends what you need.

Avoid static calls. My personal rule is use static only when you call some functions, e.g. or when dealing with singletons or registry (which should be limited to minimum, because global state is evil).My::strpos()

You will rarely need static methods when your app has a good dependency container.

Take a look at the other dependency injection + [php] topics on SO.

Edit after comment:

The container

Different frameworks handle the container in different way. Generally this is an object, which holds the instances of objects you need, so you don't have to instantiate new object each time. You may register any object with such a container, and then access it anytime you need.

The container may instantiate all the resources you need at boot time, or lazy load the resource when accessed (better solution).

As an example, consider:

Another great reference:


推荐