小编在之前面试某家企业时,面试官提出了一个问题,虽然按经验,说出了解决方法,面试官也表示此方法可以实现,且提出了另一种方案,就是“依赖注入”。
面试官的题目是这样子的:如果现在公司有个程序,假设是关于动物园的程序,有些许动物(动物库A)已经加到了动物园里了,里面的动物都完成了动物该有的行为,然后程序已经测试通过,没有问题并已经发布。现在问题是,动物园里要添加新的动物种类,请问,如何不修改动物库A的代码的情况下,将新的动物物种添加到动物园里(题目大概就是这个意思,无非就是说,原来程序不允许修改,只能新增加别的类库,并完成原来的那个类库中的行为,差不多就是这个意思的啦!)。
小编给的方案是,使用反射,好了,这个不是今天的话题,不做详细方案介绍。而面试官提出的方案就是----依赖注入。
面试回来后,总结面试中遇到的问题,其中就对依赖注入进行了一下研究,还是收获很多,以下就是研究这个依赖注入的DEMO。
先看解决方案架构,是个控制台程序
主程序是Zoo,BLL是执行动物的行为,动物库A就是LocalAnimal,其实就是我们熟知的三层架构。有过一定经验的人都应该知道,普通的三层架构中,UI(Zoo)依赖于->BLL依赖于->DAL(LocalAnimal),而这样的设计会导致每一层之间存在很强的耦合关系。点到为止,不详细说了,不是此篇的重点。
我们先看LocalAnimal
先是IAnimal这个接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LocalAnimal
{
/// <summary>
/// 动物接口类
/// </summary>
public interface IAnimal
{
void Voice(); //叫声
}
}
接口很简单,里面就是动物应该所拥有的行为,此处定义了叫声
OK,现在看看Animal文件夹下的动物们
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LocalAnimal.Animal
{
/// <summary>
/// 白鳍豚
/// </summary>
public class White_Flag_Dolphin:IAnimal
{
public void Voice()
{
Console.WriteLine("吱吱吱");
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LocalAnimal.Animal
{
/// <summary>
/// 熊猫
/// </summary>
public class Panda:IAnimal
{
public void Voice()
{
Console.WriteLine("嚯嚯嚯");
}
}
}
每种动物都实现了动物接口的叫声方法,其中熊猫的叫声是“嚯嚯嚯”(小编也没见过熊猫,不知道怎么叫的,随便写的),白鳍豚的叫声是“吱吱吱”(大概是这样吧)
OK,现在为止,这些动物都是存在的了,但是要放到动物园里,并且让他们开心的叫起来啊!
先看看业务(BLL,动物叫)里的代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LocalAnimal;
using LocalAnimal.Animal;
namespace BLL
{
public class AnimalVoice
{
IAnimal _iAnimal = null;
public AnimalVoice(IAnimal iAnimal)
{
this._iAnimal = iAnimal; //构造注入
}
public void Voice()
{
_iAnimal.Voice();
}
}
}
此处就是重点了,实现依赖注入的方法之一是构造注入,也就是使用构造函数进行依赖项注入。
看UI(Zoo)中的代码
using System.Text;
using LocalAnimal;
using LocalAnimal.Animal;
using BLL;
namespace Zoo
{
class Program
{
static void Main(string[] args)
{
//动物园里引进了熊猫
//IAnimal iAnimal = new Panda();
//AnimalVoice animalVoice = new AnimalVoice(iAnimal);
//animalVoice.Voice();
//动物园里引进了白鳍豚
IAnimal iAnimal = new White_Flag_Dolphin();
AnimalVoice animalVoice = new AnimalVoice(iAnimal);
animalVoice.Voice();
Console.ReadKey();
}
}
}
执行结果
到这里,我们可以看到,无论我们将来需要动物园中增加多少动物,只需要在动物库(LocalAnimal)中增加该动物,并且在Zoo中实例化该动物,就能开始完成动物的业务(叫),而业务层(BLL)中的代码完全不需要改变。
OK,开始完成本篇开始面试官给我出的面试题。
如题描述,如果LocalAnimal中的代码已经完成编码并关闭,已经不允许在里面做任何的改动,则此时,如果动物园要引入新的动物怎么办呢?没问题,接下来,可以看到面向接口开发以及使用依赖注入解耦的优点。
我们新建一个类库,取名为ForeignAnimal(相当于引入了新的.dll,这也是面试官想看到,我们新增加了一个扩展文件,而不需要修改它的LocalAnimal),如下图
ForeignAnimal中引入了LocalAnimal,其目的是为了实现LocalAnimal中的IAnimal接口,以完成对BLL中的依赖注入,看袋鼠(Kangaroo类)的代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LocalAnimal;
namespace ForeignAnimal.NewAnimal
{
/// <summary>
/// 袋鼠
/// </summary>
public class Kangaroo : IAnimal
{
public void Voice()
{
Console.WriteLine("咚咚咚");
}
}
}
与Panda和White_Flag_Dolphin中的代码完全一致,没有任何不同。那现在Zoo中要开始引入袋鼠了,此时,先在Zoo中引入ForeignAnimal,如下图
引入 ForeignAnimal的动态库后,Zoo开始引进袋鼠,如下代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LocalAnimal;
using LocalAnimal.Animal;
using BLL;
using ForeignAnimal.NewAnimal;
namespace Zoo
{
class Program
{
static void Main(string[] args)
{
//动物园里引进了熊猫
//IAnimal iAnimal = new Panda();
//AnimalVoice animalVoice = new AnimalVoice(iAnimal);
//animalVoice.Voice();
//动物园里引进了白鳍豚
//IAnimal iAnimal = new White_Flag_Dolphin();
//AnimalVoice animalVoice = new AnimalVoice(iAnimal);
//animalVoice.Voice();
//动物园里引进了袋鼠
IAnimal iAnimal = new Kangaroo();
AnimalVoice animalVoice = new AnimalVoice(iAnimal);
animalVoice.Voice();
Console.ReadKey();
}
}
}
执行结果
执行结果,也没问题,并且也可以看到,我们完全没有动过业务层(BLL)的代码。
到这里,就完成面试官提出的问题,我在完全没有修改LocalAnimal的情况下,新增了ForeignAnimal,并且将ForeignAnimal中的动物成功引进到了Zoo中,这就叫--依赖注入。
现在,如果我们看一下代码,我们只依赖于业务访问层中数据访问层的抽象,而业务访问层是使用的是数据访问层实现的接口。因此,我们遵循了更高层次对象和更低层次对象都依赖于抽象的原则,抽象是更高层次对象和更低层次对象之间的契约。
现在,有同学又要说了,“不行啊!我能不能也不修改Zoo中的代码呢?”,答案是可以的,此时需要用到反射,代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LocalAnimal;
using LocalAnimal.Animal;
using BLL;
using ForeignAnimal.NewAnimal;
using System.Reflection;
namespace Zoo
{
class Program
{
static void Main(string[] args)
{
//动物园里引进了熊猫
//IAnimal iAnimal = new Panda();
//AnimalVoice animalVoice = new AnimalVoice(iAnimal);
//animalVoice.Voice();
//动物园里引进了白鳍豚
//IAnimal iAnimal = new White_Flag_Dolphin();
//AnimalVoice animalVoice = new AnimalVoice(iAnimal);
//animalVoice.Voice();
//动物园里引进了袋鼠
//IAnimal iAnimal = new Kangaroo();
//AnimalVoice animalVoice = new AnimalVoice(iAnimal);
//animalVoice.Voice();
Assembly assembly = Assembly.LoadFrom("ForeignAnimal.dll"); //如果是需要LocalAnimal,则此处字符串改成 LocalAnimal.dll 即可
IAnimal iAnimal = (IAnimal)assembly.CreateInstance("ForeignAnimal.NewAnimal.Kangaroo", true);
AnimalVoice animalVoice = new AnimalVoice(iAnimal);
animalVoice.Voice();
Console.ReadKey();
}
}
}
执行结果
结果也没问题。
如下图,红框中的字符串均可写入到配置文件中,这样的话,无论新增了多少动态库、类,只需要在配置文件中做新增修改就可以,Zoo中的代码无需再修改。
总结
依赖注入的优点:
1、传统的代码,每个对象负责管理与自己需要依赖的对象,导致如果需要切换依赖对象的实现类时,需要修改多处地方。同时,过度耦合也使得对象难以进行单元测试。
2、依赖注入把对象的创造交给外部去管理,很好的解决了代码紧耦合(tight couple)的问题,是一种让代码实现松耦合(loose couple)的机制。
3、松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试。
依赖注入的缺点:
使用依赖注入时,往往会配合反射使用,这在一定程度上影响程序的性能。