说明:本文是Ninject依赖注入说明文档的一部分,原文地址:https://github.com/ninject/ninject/wiki/Dependency-Injection-By-Hand。Ninject是一款开源的依赖注入容器,在VS中可通过Nuget安装。
所谓依赖注入(Dependency Injection),是指从类的外部把依赖关系“注入”到类的内部,这一“注入”的过程一般是通过用接口作为一个“占位”实现的,使得具体类的依赖转化为对接口的依赖,符合IOC(控制反转)原则。如此,获得注入的类只需要了解所要依赖的“契约”即可,而不必了解具体的实现类,从而实现解耦。本文讲述的是如何实现构造函数注入,它是实现依赖注入的一种重要方式。
首先通过一个简单案例研究一下依赖注入的思想。假设你正在开发一个游戏,场景是勇士们为荣誉而战。首先,我们需要设计一款武器来武装我们的勇士:
class Sword
{
public void Hit(string target)
{
Console.WriteLine($"把 {target} 砍成了两半儿!");
}
}
然后,我创建一个代表勇士的类。为了攻击敌人,勇士需要一个 Attack() 方法,当此方法被调用时,它会使用 Sword 来攻击敌人。
class Warrior
{
readonly Sword sword;
public Warrior()
{
this.sword = new Sword();
}
public void Attack(string target)
{
this.sword.Hit(target);
}
}
现在,我可以创建一个勇士来参加战斗了!
class Program
{
public static void Main()
{
var warrior = new Warrior();
warrior.Attack("邪恶的敌人");
}
}
我们能猜得到,这会在控制台上输出“把 邪恶的敌人 砍成了两半儿!”。这样看起来运行的不错,但是,如果我们想给我们的勇士配备另一种武器呢?由于 Sword 是在 Warrior 类的构造函数里创建的,为了改变武器,我们不得不修改 Warrior 类的实现。
当一个类依赖于另一个具体类时,也称为与那个类紧密地耦合。在这个例子中,Warrior 类与 Sword 类紧密地耦合在一起了。当两个类紧密地耦合时,若不修改二者的实现方法,我们是无法替换二者的。为了避免类的紧密耦合,我们可以通过接口来构造一个中间层。下面来创建一个代表武器的接口。
interface IWeapon
{
Void Hit(string target);
}
那么,我们的 Sword 类就可以实现这个接口了;
class Sword : IWeapon
{
public void Hit(string target)
{
Console.WriteLine($"把 {target} 砍成了两半儿!");
}
}
这样,我们就可以修改 Warrior 类了:
class Warrior
{
readonly IWeapon weapon;
public Warrior()
{
this.weapon = new Sword();
}
public void Attack(string target)
{
this.weapon.Hit(target);
}
}
现在我们就可以为 Warrior 配置不同的武器了。但是等一下! Sword 类仍然是在 Warrior 类的构造函数里创建的,那么我们仍然要修改 Warrior 的实现方式才能改变武器,Warrior 仍然与 Sword 耦合在一起。
幸运的是,有一种简单的解决方法:相比于在构造函数内部创建 Sword,我们可以把 Sword 暴露给构造函数的参数:
class Warrior
{
readonly IWeapon weapon;
public Warrior(IWeapon weapon)
{
this.weapon = weapon;
}
public void Attack(string target)
{
this.weapon.Hit(target);
}
}
于是,我们可以通过 Warrior 的构造函数来注入 Sword 对象。这就是依赖注入的一个案例(具体来说,这种方式叫“构造函数注入”)。我们先创建另一种武器:
class Cannon : IWeapon
{
public void Hit(string target)
{
Console.WriteLine($"把 {target} 轰成了渣渣!");
}
}
现在,我们就可以组件一支勇士军队了:
class Program
{
public static void Main()
{
var warrior1 = new Warrior(new Sword());
var warrior2 = new Warrior(new Cannon());
warrior1.Attack("邪恶的敌人");
warrior2.Attack("邪恶的敌人");
}
}
控制台的输出结果为:
把 邪恶的敌人 砍成了两半儿!
把 邪恶的敌人 轰成了渣渣!
这种方法就叫做“手动依赖注入”,因为每次你想创建一个 Warrior 时,你就必须首先创建一个 IWeapon 接口的实现然后把它传递给 Warrior 的构造函数。既然现在我们在不修改 Warrior 类的实现时,就能改变它使用的武器,那么,Warrior 类就可以和 Sword 类位于不同的程序集中——实际上,我们可以创建任意新武器而不必事先了解 Warrior 类的源代码!
对于小项目来说,手动依赖注入是一种高效的策略。但是随着应用程序的规模和复杂度的增加,把所有的对象连接起来就变得越来越繁琐了。当依赖项具有自己的依赖项时会发生什么? 当你想在给定依赖项的每个实例前添加装饰器(例如缓存,跟踪日志,审计等)时会发生什么? 你应该去编写能够为软件增加真正价值的代码,而不是将大部分时间用于创建和连接对象,这就是Ninject之类的依赖注入库/框架可以提供帮助的地方。关于Ninject这方面可继续阅读Dependency Injection With Ninject。