什么是依赖注入
依赖注入就是把本来应该在程序中有的依赖在外部注入到程序之中。
假定有接口A和A的实现B,那么就会执行这一段代码A a=new B();这个时候必然会产生一定的依赖,然而出现接口的就是为了解决依赖的,但是这么做还是会产生耦合,我们就可以使用依赖注入的方式来实现解耦。
传统方式:
1 public class PersonAppService 2 { 3 private IPersonRepository _personRepository; 4 5 public PersonAppService() 6 { 7 _personRepository = new PersonRepository(); 8 } 9 10 public void CreatePerson(string name, int age) 11 { 12 var person = new Person { Name = name, Age = age }; 13 _personRepository.Insert(person); 14 } 15 }
PersonAppService使用PersonRepository插入Person到数据库。这段代码的问题:
- PersonAppService通过IPersonRepository调用CreatePerson方法,所以这方法依赖于IPersonRepository,代替了PersonRepository具体类。但PersonAppService(的构造函数)仍然依赖于PersonRepository。组件应该依赖于接口而不是实现。这就是所谓的依赖性倒置原则。
- 如果PersonAppService创建PersonRepository本身,它成为依赖IPersonRepository接口的具体实现,不能使用另一个实现。因此,此方式的将接口与实现分离变得毫无意义。硬依赖(hard-dependency)使得代码紧密耦合和较低的可重用。
- 我们可能需要在未来改变创建PersonRepository的方式。即,我们可能想让它创建为单例(单一共享实例而不是为每个使用创建一个对象)。或者我们可能想要创建多个类实现IPersonRepository并根据条件创建对象。在这种情况下,我们需要修改所有依赖于IPersonRepository的类。
- 有了这样的依赖,很难(或不可能)对PersonAppService进行单元测试。
为了克服这些问题,可以使用工厂模式。因此,创建的仓储类是抽象的。看下面的代码:
1 public class PersonAppService 2 { 3 private IPersonRepository _personRepository; 4 5 public PersonAppService() 6 { 7 _personRepository = PersonRepositoryFactory.Create(); 8 } 9 10 public void CreatePerson(string name, int age) 11 { 12 var person = new Person { Name = name, Age = age }; 13 _personRepository.Insert(person); 14 } 15 }
PersonRepositoryFactory是一个静态类,创建并返回一个IPersonRepository。这就是所谓的服务定位器模式。以上依赖问题得到解决,因为PersonAppService不需要创建一个IPersonRepository的实现的对象,这个对象取决于PersonRepositoryFactory的Create方法。但是,仍然存在一些问题:
- 此时,PersonAppService取决于PersonRepositoryFactory。这个已经是比较好的方法,但还是有一个硬依赖(hard-dependency)。
- 为每个库或每个依赖项乏味的写一个工厂类/方法。
- 测试性依然不好,由于很难使得PersonAppService使用mock实现IPersonRepository。
-
解决方案
有一些最佳实践(模式)用于类依赖。
构造函数注入
重写上面的例子,如下所示:
1 public class PersonAppService 2 { 3 private IPersonRepository _personRepository; 4 5 public PersonAppService(IPersonRepository personRepository) 6 { 7 _personRepository = personRepository; 8 } 9 10 public void CreatePerson(string name, int age) 11 { 12 var person = new Person { Name = name, Age = age }; 13 _personRepository.Insert(person); 14 } 15 }
这被称为构造函数注入。现在,PersonAppService不知道哪些类实现IPersonRepository以及如何创建它。谁需要使用PersonAppService,首先创建一个IPersonRepository PersonAppService并将其传递给构造函数,如下所示:
-
var repository = new PersonRepository(); var personService = new PersonAppService(repository); personService.CreatePerson("Yunus Emre", 19);
构造函数注入是一个完美的方法,使一个类独立创建依赖对象。但是,上面的代码有一些问题:
- 创建一个PersonAppService变得困难。想想如果它有4个依赖,我们必须创建这四个依赖对象,并将它们传递到构造函数PersonAppService。
- 从属类可能有其他依赖项(在这里,PersonRepository可能有依赖关系)。所以,我们必须创建PersonAppService的所有依赖项,所有依赖项的依赖关系等等. .如此,依赖关系使得我们创建一个对象变得过于复杂了。
幸运的是,依赖注入框架自动化管理依赖关系。
-
属性注入
构造函数注入模式是一个完美的提供类的依赖关系的方式。通过这种方式,您不用创建类的实例,而不会产生依赖项。
但是,在某些情况下,一些类依赖别的类实现某些非必须的功能(这通常适用于切面如日志记录)。一个类实现了一些业务功能,但是我们还希望他实现日志功能。但是我们的日志实现模型并不确定。在这种情况下,我们可以定义依赖为公共属性,而不是让他们放在构造函数。如果我们想在PersonAppService实现日志功能。我们可以重写类如下:
1 public class PersonAppService 2 { 3 public ILogger Logger { get; set; } 4 5 private IPersonRepository _personRepository; 6 7 public PersonAppService(IPersonRepository personRepository) 8 { 9 _personRepository = personRepository; 10 Logger = NullLogger.Instance;//是一个单例对象,实现了ILogger接口,但实际上什么都没做 11 } 12 13 public void CreatePerson(string name, int age) 14 { 15 Logger.Debug("Inserting a new person to database with name = " + name); 16 var person = new Person { Name = name, Age = age }; 17 _personRepository.Insert(person); 18 Logger.Debug("Successfully inserted!"); 19 } 20 }
要想实现日志功能,我们只需要为PersonAppService实例设置了Logger,如下面:
var personService = new PersonAppService(new PersonRepository()); personService.Logger = new Log4NetLogger(); personService.CreatePerson("Yunus Emre", 19);
Log4NetLogger实现ILogger,使得我们的PersonAppService可以写日志。
如果我们不设置Logger,PersonAppService就不写日志,只是执行了个空函数。因此,我们可以说ILogger实例是PersonAppService 的一个可选的依赖。
几乎所有的依赖注入框架都支持属性注入模式
依赖注入框架
有许多依赖注入框架,都可以自动解决依赖关系。他们可以创建所有依赖项(递归地依赖和依赖关系)。所以你只需要根据注入模式写类和类构造函数&属性,其他的交给DI框架处理!
目前主流的框架有Autofac、Castle Windsor、Unity,Ninject,StructureMap框架等。