当第一次遇到控制反转(IoC)时,它可能非常令人困惑。

这是怎么一回事?它解决了哪个问题?什么时候使用合适,什么时候不合适?


当前回答

控制反转(IoC)模式是关于提供任何类型的回调,它“实现”和/或控制反应,而不是直接执行自己(换句话说,反转和/或将控制重定向到外部处理器/控制器)。依赖注入(DI)模式是IoC模式的一个更具体的版本,它完全是从代码中删除依赖项。

每个DI实现都可以被视为IoC,但不应该称之为IoC。因为实现依赖注入比回调更困难(不要使用通用术语“IoC”来降低产品的价值)。

例如DI,假设您的应用程序有一个文本编辑器组件,并且您希望提供拼写检查。你的标准代码应该是这样的:

public class TextEditor {

    private SpellChecker checker;

    public TextEditor() {
        this.checker = new SpellChecker();
    }
}

我们在这里所做的工作在TextEditor和SpellChecker之间创建了依赖关系。在IoC场景中,我们会这样做:

public class TextEditor {

    private IocSpellChecker checker;

    public TextEditor(IocSpellChecker checker) {
        this.checker = checker;
    }
}

在第一个代码示例中,我们正在实例化SpellChecker(this.checker=new SpellCheckr();),这意味着TextEditor类直接依赖于SpellChecker类。

在第二个代码示例中,我们通过在TextEditor的构造函数签名中使用SpellChecker依赖类(而不是在类中初始化依赖项)来创建抽象。这允许我们调用依赖项,然后将其传递给TextEditor类,如下所示:

SpellChecker sc = new SpellChecker(); // dependency
TextEditor textEditor = new TextEditor(sc);

现在,创建TextEditor类的客户端可以控制使用哪个SpellChecker实现,因为我们正在将依赖项注入TextEditor签名中。


注意,就像IoC是许多其他模式的基础一样,上面的示例只是依赖注入类型中的一种,例如:

构造函数注入。IocSpellChecker的实例将自动传递给构造函数,或手动传递给构造函数。沉淀剂注入。IocSpellChecker的实例将通过setter方法或公共属性传递。服务查找和/或服务定位器其中TextEditor将向已知的提供者请求IocSpellChecker类型的全局使用的实例(服务)(这可能不存储所述实例,而是一次又一次地询问提供者)。

其他回答

我在这里找到了一个非常清楚的例子,它解释了“控制是如何颠倒的”。

经典代码(无依赖注入)

以下是不使用DI的代码大致工作原理:

应用程序需要Foo(例如控制器),因此:应用程序创建Foo应用程序调用FooFoo需要Bar(例如服务),因此:Foo创建BarFoo调用BarBar需要Bim(服务、存储库…),因此:条形图创建Bim酒吧有点事

使用依赖注入

以下是使用DI的代码大致工作原理:

应用程序需要Foo,需要Bar,需要Bim,因此:应用程序创建Bim应用程序创建Bar并赋予它Bim应用程序创建Foo并给它Bar应用程序调用FooFoo调用Bar酒吧有点事

依赖项的控制是从一个被调用到另一个调用的。

它解决了什么问题?

依赖注入使得可以很容易地与注入类的不同实现进行交换。在单元测试时,您可以注入一个虚拟实现,这使测试更加容易。

例如:假设您的应用程序将用户上传的文件存储在Google Drive中,使用DI,您的控制器代码可能如下所示:

class SomeController
{
    private $storage;

    function __construct(StorageServiceInterface $storage)
    {
        $this->storage = $storage;
    }

    public function myFunction () 
    {
        return $this->storage->getFile($fileName);
    }
}

class GoogleDriveService implements StorageServiceInterface
{
    public function authenticate($user) {}
    public function putFile($file) {}
    public function getFile($file) {}
}

当你的需求发生变化时,比如说,你被要求使用Dropbox而不是GoogleDrive。您只需要为StorageServiceInterface编写一个dropbox实现。只要Dropbox实现符合StorageServiceInterface,就不必对控制器进行任何更改。

测试时,您可以使用虚拟实现为StorageServiceInterface创建模拟,其中所有方法都返回null(或根据测试要求的任何预定义值)。

相反,如果您有一个控制器类来构造具有如下新关键字的存储对象:

class SomeController
{
    private $storage;

    function __construct()
    {
        $this->storage = new GoogleDriveService();
    }

    public function myFunction () 
    {
        return $this->storage->getFile($fileName);
    }
}

当您想要使用Dropbox实现进行更改时,必须替换构建新GoogleDriveService对象的所有行,并使用DropboxService。此外,在测试SomeController类时,构造函数总是期望GoogleDriveService类,并触发该类的实际方法。

什么时候合适,什么时候不合适?在我看来,当您认为类有(或可能有)替代实现时,您可以使用DI。

假设你是一个物体。然后你去餐馆:

没有IoC:你要求“苹果”,当你要求更多时,你总是得到苹果。

与IoC:你可以要求“水果”。每次上桌你都可以得到不同的水果。例如,苹果、橙子或西瓜。

所以,很明显,当你喜欢品种时,IoC是首选。

但我认为你必须非常小心。如果你过度使用这种模式,你会做出非常复杂的设计,甚至更复杂的代码。

就像这个例子中的TextEditor一样:如果你只有一个拼写检查器,那么可能真的没有必要使用IoC?除非你需要写单元测试之类的。。。

无论如何:要讲道理。设计模式是很好的实践,但不是圣经。不要把它粘在任何地方。

控制反转是用于解耦系统中的组件和层的模式。该模式是通过在构建组件时将依赖项注入组件来实现的。这些依赖性通常作为接口提供,用于进一步去耦和支持可测试性。IoC/DI容器(如Castle Windsor、Unity)是可用于提供IoC的工具(库)。这些工具提供了超越简单依赖管理的扩展功能,包括生存期、AOP/Interception、策略等。a.减轻组件对管理其依赖性的责任。b.提供在不同环境中交换依赖实现的能力。c.允许通过模仿依赖关系来测试组件。d.提供在整个应用程序中共享资源的机制。a.进行测试驱动开发时至关重要。如果没有IoC,很难测试,因为被测组件与系统的其他部分高度耦合。b.开发模块化系统时至关重要。模块化系统是一种无需重新编译即可更换组件的系统。c.如果有许多跨领域的问题需要解决,尤其是在企业应用程序中,则至关重要。

“IoC”这个首字母缩略词和它所代表的名字似乎最让人困惑的是,这个名字太迷人了——几乎是一个喧嚣的名字。

我们真的需要一个名字来描述过程式编程和事件驱动编程之间的区别吗?好吧,如果我们需要的话,但我们是否需要选择一个全新的“比生活更大”的名字,它让人困惑而不是解决问题?