理解控制反转(IoC)、依赖注入(DI)和引用方法

8
我正在学习依赖注入和控制反转,我觉得我开始明白了这是怎么工作的:
  • 类不应该关心自己依赖项的创建
  • 依赖应该通过构造函数或设置器方法传递给对象
  • DI容器可以创建带有所有所需依赖项的对象
如果上述内容都是正确的,那我是否不能再在我的对象中使用我所谓的“参考方法”了呢? 这里所说的参考方法是什么意思。假设我有两个家庭和家庭成员的模型。我发现创建引用与该模型相关的对象的方法非常有用。在下面的示例中,调用$family->members(),我可以快速访问所有家庭成员。但是,这意味着我的family对象正在实例化family_member类……这不会违反IoC的规则吗?
如果family_member类具有超出family类范围的依赖关系怎么办?非常感谢您的回答!
<?php

    class family
    {
        public $id;

        public function members()
        {
            // Return an array of family_member objects
        }
    }

    class family_member
    {
        public $family_id;
        public $first_name;
        public $last_name;
        public $age;
    }

我不确定 PHP 是否非常方便进行依赖注入。有一些依赖注入库,例如 Guice、Spring、Ninject 和 Structuted map 等。如果你看一下它们,你会更好地理解。 - DarthVader
@DarthVader:感谢您的评论。我认为人们正在使用PHP中的DI。我已经查看了一堆PHP DI容器(Symfony,Bucket,Twittee,Pimple)。我认为PHP 5.3使它更容易了。至于我的问题,我不确定它是否与PHP有关...您会如何在任何语言中处理这个问题? - Jonathan
3个回答

5
免责声明:我自己也在学习 DI。请谨慎接受我的回答。
依赖注入只涉及于注入依赖项。如果你的面向对象设计使得“家庭”对象有创建“成员”实例的责任,那么请让“家庭”对象创建“成员”,因为在这种情况下,“成员”不再被视为“家庭”的依赖项,而是一个职责。因此:
class Family
{
    /**
     * Constructor.
     * 
     * Since you have decided in your OO design phase that this
     * object should have the responsibility of creating members,
     * Member is no longer a dependency. MySQLi is, since you need
     * it to get the information to create the member. Inject it.
     *
     */
    public function __construct($id, MySQLi $mysqli)
    {
        $this->id = $id;
        $this->mysqli = $mysqli;
    }

    /**
     * Query the database for members data, instantiates them and
     * return them.
     *
     */
    public function getMembers()
    {
        // Do work using MySQLi
    }
}

但是如果你仔细想想,Family 是否真的应该负责创建 Member 更好的设计是有另一个对象,比如 FamilyMapper 来创建 Family 以及它的成员。就像这样:
class FamilyMapper
{
    /**
     * Constructor.
     * 
     * A better OO design, imho is using the DataMapper pattern.
     * The mapper's responsibility is instantiating Family,
     * which means it's going to have to connect to the database,
     * which makes MySQLi its dependency. So we inject it.
     *
     */
    public function __construct(MySQLi $mysqli)
    {
        $this->mysqli = $mysqli;
    }

    public function findByID($familyID)
    {
        // Query database for family and members data
        // Instantiate and return them
    }

}

class Family
{
    /**
     * Constructor.
     * 
     * Family is an object representing a Family and its members,
     * along with methods that *operate* on the data, so Member
     * in this OO design is a dependency. Inject it.
     *
     */
    public function __construct($id, MemberCollection $members)
    {
        $this->id;
        $this->members;
    }

    public function getMembers()
    {
        return $this->members;
    }
}

使用这种模式,您的领域对象以及它们的方法(可能包含业务逻辑)将与数据访问代码分离。这就是依赖注入的好处——它迫使您重新思考面向对象设计,从而最终得到更清晰的代码。
许多人认为使用依赖注入意味着不使用工厂等。这是错误的!依赖注入只涉及注入依赖项。您也可以将依赖注入用于工厂对象,通过将依赖项注入工厂而不是让工厂实例化自己的依赖项。
有用的链接:
  1. http://martinfowler.com/articles/injection.html
  2. 是否有关于依赖注入的好比喻?
  3. 如何向5岁的孩子解释依赖注入?

新增内容

再次提醒,以下内容需谨慎对待。

请注意「依赖注入」和「依赖注入容器」之间的区别。前者是一种简单的概念,通过注入依赖项而不是让对象自己创建依赖项(导致非常紧密的耦合)。我们可以从上面的例子中看到这一点。

后者是指处理依赖注入的框架/库,以便您无需进行手动注入。容器的责任是将依赖项连接起来,以免您需要进行繁琐的工作。其想法是您会「定义」一个依赖注入配置,该配置告诉容器 Foo 对象具有哪些依赖项以及如何注入它们。容器读取文档并为您执行注入。这就是 Pimple、SimpleDIC 等 DIC 库所实现的功能。

你可以将依赖注入容器与工厂进行比较,因为两者都是创建对象的创造性对象。虽然工厂经常是专业化的(例如,FamilyMemberFactory 创建 MemberInterface 实例),但依赖注入容器更加通用。一些人说使用依赖注入容器可以使你免于使用工厂,但你应该记住这意味着你必须创建和维护依赖注入配置文件,这可能需要数千行 XML/PHP 代码。
希望这可以帮助你。

@Jonathan 改编 Martin Fowler 的话,“领域模型是系统中真正的逻辑部分”。是的,这意味着所有你的应用程序所需完成的核心功能都属于领域模型。是的,它很大——它应该是你的应用程序中最大、最重要的部分。演示层和数据源层是另外两个重要的部分。 - rickchristie
@Jonathan - 如果你还没有阅读过马丁·福勒的《企业应用架构模式》,请务必阅读。这是一本经典之作。它会在第一章告诉你关于分层最重要的事情,你将理解为什么我们需要首先将应用程序分层。 - rickchristie
谢谢您的评论。我将去阅读那本书!不过我的问题稍微有些不对 - 我想问的是实体的角色从何处开始和结束?例如,最靠近底层的(值?)对象,在我的情况下通常是数据库表的表示。在上面的示例中,这些将是 familiesfamily_members。它们应该执行操作(比如DB访问),还是只是基本容器...带有简单的数据访问函数,例如 get_full_name() 返回 $first_name$last_name - Jonathan
2
@Jonathan - 很抱歉我不够熟悉来回答那个问题。但是如果我必须猜测的话,如果您使用数据映射器模式,域对象不应该意识到持久性(通常是数据库)层。这个想法是让域对象的责任集中在业务逻辑上,并有另一组类(数据映射器)处理持久性(选择、更新、删除方法)。这使得您的域对象与数据源层解耦,仅对数据库的更改会波及到数据映射器。此外,Gordon所说的也是正确的。 - rickchristie
再次感谢@rickchristie抽出时间向我解释这个问题。我认为由于您也在学习这个问题,所以您能够用我能理解的术语来简化它。也感谢其他人的回复,非常感激! - Jonathan
显示剩余6条评论

1

虽然你可以只使用依赖注入,但在我的看法中,尝试在保持可维护性的同时找到设计平衡是更明智的做法。

因此,依赖注入和工厂的结合将使你的生活更加轻松。

class family {
    protected $_members;

    public function __construct($members = array()) {
        $this->_members = array_filter($members, function($obj){
            return ($obj instanceof family_member);
        });
    }

    public function addMember() {
        $this->_members[] = $member = $this->_createMember();
        return $member;
    }

    protected function _createMember() {
        return new family_member;
    }
}

工厂模式与控制反转和依赖注入有些相似。虽然依赖注入可以使您的对象不必创建其依赖项,但工厂模式允许它创建依赖项的基本实现。最终,它仍然允许您在需要时覆盖对象依赖项:

class familyExtended extends family {
    protected function _createMember() {
        return new familyExtended_member;
    }
}

这在测试时特别有用:

class familyTestable extends family {
    protected function _createMember() {
        return new family_memberFake;
    }
}

依赖注入很棒,但它不能解决你所有的设计问题。当然,它们并不是可以互换的。工厂模式可以做到依赖注入无法实现的功能,反之亦然。


感谢@netcoder。这就是我现在的感受... DI在理论上很有道理,但我很难理解/看到它在我的应用程序中实际的样子。我开始觉得我在DI方面最大的问题是管理范围。例如,我可能需要创建一个新对象,该对象需要:1.一个新的依赖项实例和2.一个现有的依赖项实例。如果没有某种全局访问该DI容器的方式-这是不可能的。但此时,所有我的对象都依赖于DI容器...这正是我想避免的东西!啊!!! - Jonathan

0
问题在于 DI 的整个理念是,Family 不应该知道如何创建特定的 FamilyMember,例如,你应该有一个 IFamilyMember,而其实现可能因应用程序的每个模块/层而异。
应该像这样:
public function GetMembers()
{
    $members = array();

    foreach ( $member in $this->internalMemberList ){
       // get the current implementation of IFamilyMember
       $memberImpl = $diContainer->Resolve("IFamilyMember");

       $memberImpl->Name = $member->Name;
       //.... Not the best example....
    }

    return $members;
}

基本上,一切都归结为组件应该忽略它们的依赖关系的想法,在您的情况下,Family 依赖于 FamilyMember,因此任何 FamilyMember 实现都需要从 Family 本身中抽象出来。

对于 PHP 的特定情况,请考虑查看 Symfony,它大量使用 DI 模式,或者您可以考虑使用 Symfony 依赖注入框架,它是 Symfony 的一部分,但可以作为单独的组件使用。此外,Fabien Potencier 写了 一篇不错的文章

希望我能帮到您!


非常感谢您的输入!这很有道理...family不应该知道如何创建family_member。我的担忧是:几乎所有我的应用都依赖于数据库。这两个类都有各自的数据库表。因此,为了使这个工作正常,我的family类必须查找数据库中的这些记录,然后根据接口创建family_member。这对我来说似乎非常紧密耦合。我做错了吗?这样的快速引用是否不值得依赖?我还能怎么做? - Jonathan
还有一个跟进的问题,如果可以的话。在你的例子中$diContainer是一个全局可访问的变量吗?还是我需要将其设置为单例,或者需要将其作为依赖项传递给我创建的每个对象?看了一些开源DI容器,我真的无法弄清楚范围问题。好的,所以容器正在为我创建所有的对象...很酷...但是我如何从我的一个对象内部访问它呢?或者,再次说一遍,这完全是错误的方法,我不应该从对象内部访问它,因为我不应该在那里创建对象? - Jonathan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接