(译)ABP之依赖注入
原文地址:https://aspnetboilerplate.com/Pages/Documents/Dependency-Injection
- 什么是依赖注入
- 传统方式的问题
- 解决方案
- 构造函数注入
- 属性注入
- 依赖注入框架
- ABP依赖注入基础设施
- 注册依赖项
- 常用注册
- 帮助接口
- 自定义/直接注册
- 使用IocManager
- 解析
- 构造函数&属性注入
- IIocResolver, IIocManager and IScopedIocResolver
- 附加部分
- IShouldInitialize 接口
- ASP.NET MVC & ASP.NET Web API 集成
- ASP.NET Core 集成
- 最后备注
什么是依赖注入
如果你已经知道了依赖注入的概念、构造函数和属性注入模式,那你可以跳到下一节。
维基百科说:“依赖注入是一种软件设计模式,它注入一个或多个依赖项(或服务),或者通过引用传递到一个依赖对象(或客户端),并成为客户端状态的一部分。这种设计模式使得客户端的依赖项的创建与其行为本身分离开来,这让程序设计松耦合、遵循依赖反转和单一职责原则,这与服务定位器模式形成了一个鲜明的对比,服务定位器模式允许客户端清楚它用来寻找依赖的系统”。
如果不使用依赖注入技术,我们很难管理依赖关系及开发一个模块化和结构良好的应用程序。
传统方式的问题
在一个应用程序中,类彼此依赖,假设我们有一个使用 repository 插入entities到数据库的application service ,在这种情况下,这个application service是依赖于repository这个类的。如下面的例子:
public class PersonAppService { private IPersonRepository _personRepository; public PersonAppService() { _personRepository = new PersonRepository(); } public void CreatePerson(string name, int age) { var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); } }
PersonAppService 使用PersonRepository 插入一个 Person 到数据库里,这代码的问题是:
- PersonAppService在CreatePerson方法里使用了IPersonRepository的引用,所以这个方法依赖于IPersonRepository,而不是依赖于PersonRepository,但是,在PersonAppService的构造函数中依然依赖于PersonRepository。组件应该依赖于接口而不是实现类。这就是依赖反转原则。
- 如果PersonAppService创建PersonRepository本身,这样就变成了依赖IPersonRepository接口具体的实现类了,同时使得它不能用IPersonRepository接口的其它实现类,这样,将接口与实现类分离就毫无意义了。强依赖关系使得代码基础紧密耦合,且可重用性较低。
- 我们在以后可能需要改变PersonRepository的创建,比如说,我们可能想把它创建成一个单例(单一共享一个实例,而不是每次用时都创建一个对象),或者我们可能想创建多个IPersonRepository接口的实现类,而我们是根据条件来创建其中某个实现类的实例。在这种情况下,我们就得修改所有依赖于IPersonRepository接口的类。
- 有了这样的依赖关系,我们是很难(或者不可能)对PersonAppService进行单元测试。
为了解决这样的问题,工厂模式可以起到作用,因此,创建repository类是抽象的。如下面代码:
public class PersonAppService { private IPersonRepository _personRepository; public PersonAppService() { _personRepository = PersonRepositoryFactory.Create(); } public void CreatePerson(string name, int age) { var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); } }
PersonRepositoryFactory 是用来创建和返回一个IPersonRepository对象的静态类,这就是服务定位器模式。因为PersonAppService 不知道如何创建IPersonRepository实现类对象,而且它与PersonRepository 的实现无关,所以创建的问题被解决了,但是这样还是存在一些问题:
- 这次,PersonAppService 依赖于PersonRepositoryFactory ,这个相当来说还可以接受,但是这样还是一种强依赖关系。
- 为每一个repository 或它的依赖类写一个工厂类或方法是很乏味的。
- 这样可测试性还是不好,因为很难让PersonAppService 去使用一些模拟实现IPersonRepository的对象。
解决方案
对于依赖其它的类,还有一些最佳实践(模式)。
构造函数注入模式
上面的例子可以重写成下面这样:
public class PersonAppService { private IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; } public void CreatePerson(string name, int age) { var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); } }
这就是构造函数注入。现在,PersonAppService 不知道那个类实现了IPersonRepository,而且不知道如何去创建实例的。谁需要用到PersonAppService ,首先要创建一个IPersonRepository实现类对象,并把它传递给PersonAppService 的构造函数,如下所示:
var repository = new PersonRepository(); var personService = new PersonAppService(repository); personService.CreatePerson("Yunus Emre", 19);
构造函数注入是使类独立于创建依赖对象的一种完美的解决方式,但是,上面的代码还是有一些问题:
- 创建一个PersonAppService 对象变得更难了。试想一下,如果PersonAppService 依赖于4个类,那我们就必须创建这4个依赖类的对象,并把它们传递给PersonAppService 的构造函数。
- 依赖类可能也有它们自己的依赖类(这里,PersonRepository可能还依赖其它的类),因此,我们必须创建所有PersonAppService 所依赖的对象,以及这些依赖对象所依赖类的对象,如此等等。这样,我们甚至连一个对象都创建不了了,因为这依赖关系它复杂了。
幸运的是,有依赖注入框架去自动管理这些依赖关系。
属性注入模式
构造函数注入模式是一种解决类依赖问题的完美方式,这样,在不能提供类的依赖项的情况下,你就不能创建这个类的实例,它也是明确声明这个类需求是如何正确地工作的一种强有力的方式。
但是,在有些情况下,这个类即使没有它所依赖的另一个类,它也要能正常运行, 对于诸如日志记录这样横切关注点的需求是很正常的。一个类可以在没有提供日志记录对象的情况下能正常运行,但是如果你提供了一个日志记录对象,它就能记录日志了。在这种情况下,你可以将依赖项定义为一个公共属性,而不是将它们放在构造函数中。试想一下,我们想要在PersonAppService里写日志,我们可以像下面所示的代码一样来重写这个类:
public class PersonAppService { public ILogger Logger { get; set; } private IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; Logger = NullLogger.Instance; } public void CreatePerson(string name, int age) { Logger.Debug("Inserting a new person to database with name = " + name); var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); Logger.Debug("Successfully inserted!"); } }
NullLogger.Instance是一个实现了ILogger接口的单例对象,但事实上它里面什么也没有(不记录日志,它实现ILogger接口的方法是空方法体),所以,现在,如果在创建PersonAppService 对象后,设置Logger,PersonAppService 就可以写日志了,如下所示:
var personService = new PersonAppService(new PersonRepository()); personService.Logger = new Log4NetLogger(); personService.CreatePerson("Yunus Emre", 19);
假设Log4NetLogger 实现ILogger,而且使用Log4Net类库来写日志,因此,PersonAppService 事实上是可以写日志的。如果我们不设置Logger,它就不能写日志,所以,我们可以说ILogger是PersonAppService的可选依赖项。
几乎所有依赖注入框架都支持属性注入模式。
依赖注入框架
网上有很多可以自动解析依赖关系的依赖注入框架,它可以创建具有所有依赖项的对象(并递归依赖项的依赖项),所以,你可以用构造函数&属性注入模式来写你的类,DI框架为你处理剩下的事!在一个好的应用程序里,你的类是独立的,甚至独立于DI框架。在你整个应用程序里,你要写几行代码或类来声明与DI框架交互。
ABP使用了Castle Windsor 依赖注入框架,这是一个最成熟的依赖注入框架之一,网络上还有其它一些DI框架,如Unity, Ninject, StructureMap, Autofac等等。
在依赖注入框架里,你首先得注册你的接口或类至依赖注入框架里,然后你就可以解析(创建)对象。在Castle Windsor里,会有像下面这样的代码:
var container = new WindsorContainer(); container.Register( Component.For<IPersonRepository>().ImplementedBy<PersonRepository>().LifestyleTransient(), Component.For<IPersonAppService>().ImplementedBy<PersonAppService>().LifestyleTransient() ); var personService = container.Resolve<IPersonAppService>(); personService.CreatePerson("Yunus Emre", 19);
我们先创建WindsorContainer对象,然后使用注册了PersonRepository 和PersonAppService的接口,再然后,我们让container去创建IPersonAppService的对象,container创建带有依赖项的PersonAppService 对象并返回。也许在这个简单的例子里,我们不能明显地看出使用DI框架的优势,但是想想,在一个真实的企业应用程序里,你往往是有许多的类及依赖关系。当然,注册依赖项是在创建和使用这些对象之外的地方,而且你在应用程序启动时只注册一次。
注意我们上面也声明对象的生命周期为transient,这意味着无论我们什么时候解析这些类型的对象时,都会创建一个新的实例。生命周期有很多种(比如单例)。
ABP依赖注入基础设施层
当你按照最佳实践和一些惯例来写你的应用程序,ABP几乎没有显露出使用了依赖注入框架痕迹。
注册依赖项
在ABP中,有几种不同的方式注册你的类到依赖注入系统里,大多数时候,用常规注册方法就足够了。
常规注册
ABP自动注册所有Repositories, Domain Services, Application Services,MVC Controllers and Web API Controllers。比如,你可能有一个IPersonAppService 接口和实现这个接口的PersonAppService类:
public interface IPersonAppService : IApplicationService { //... } public class PersonAppService : IPersonAppService { //... }
ABP自动注册它,因为它实现了IApplicationService 接口(它只是一个口接口)。它注册为transient (每次使用都创建一个实例)。当你注入(使用构造函数注入)IPersonAppService接口给一个类时,自动会创建一个PersonAppService对象,并把它传递到构造函数里。
在这里命名约定是很重要的。比如你可以把PersonAppService的名字改为MyPersonAppService或其他包含了“PersonAppService”为后缀的名字,因为IPersonAppService 有这个后缀,但是你不可以把你的service类命名为PeopleService,如果你这样做的话,它就不能自动注册为IPersonAppService (它注册到了DI框架里,但是以其本身注册的,而不是注册为接口),所以,如果你想这样做,你应该手动注册。
ABP可以通过常规方式来注册程序集,你可以告诉ABP用常规方式去注册你的程序集,这实现起来非常容易:
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
Assembly.GetExecutingAssembly() 取得包含这代码的程序集引用,你可以传递其它程序集到RegisterAssemblyByConvention方法,当你的模块初始化完成时,这些基本就完成了。想了解更多可以去看ABP模块系统。
通过实现IConventionalRegisterer接口,和在你的类里调用IocManager.AddConventionalRegisterer方法,你可以写你自己的常规注册类,你应该把它放在你的模块里的pre-initialize 方法里。
帮助接口
你可能想注册一个不符合常规注册规则的特定的类,ABP提供了ITransientDependency 和ISingletonDependency接口作为一种快捷方式,例如:
public interface IPersonManager { //... } public class MyPersonManager : IPersonManager, ISingletonDependency { //... }
这样,你就可以轻易地注册MyPersonManager类。当你需要注册IPersonManager时,使用MyPersonManager。注意依赖被声明为单例,因此,创建MyPersonManager的单例,并且是把这同一个对象传递给所有需要它的类里,它只在第一次使用时创建,然后在应用程序的生命周期里都一直使用同一个实例。
自定义/直接注册
对于你的情况,如果常规注册不足以应付,你可以使用 IocManager 或Castle Windsor去注册你的类和依赖项。
使用IocManager
你可以使用IocManager 去注册依赖关系(通常是写在你的模块定义类的PreInitialize 方法里)
IocManager.Register<IMyService, MyService>(DependencyLifeStyle.Transient);
使用Castle Windsor API
你可以使用IIocManager.IocContainer 属性去访问Castle Windsor容器去注册依赖关系。比如:
IocManager.IocContainer.Register(Classes.FromThisAssembly().BasedOn<IMySpecialInterface>().LifestylePerThread().WithServiceSelf());
更多信息,可以参考Windsor文档
解析
注册通知IOC(控制反转)容器(有名DI框架)关于你的类、它们的依赖关系和生命周期,在你应用程序的其它地方需要使用IOC容器去创建对象时,ASP.NET提供了一些解析依赖关系的选项。
构造函数&属性注入
作为最佳实践,你可以使用构造函数和属性注入去取得依赖关系,你应该在任何地方都按这个方式来做。例如:
public class PersonAppService { public ILogger Logger { get; set; } private IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; Logger = NullLogger.Instance; } public void CreatePerson(string name, int age) { Logger.Debug("Inserting a new person to database with name = " + name); var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); Logger.Debug("Successfully inserted!"); } }
IPersonRepository从构造函数注入,ILogger通过公用属性注入。这样,你的代码对依赖注入系统毫无所知了,这是使用DI系统最合适的方式。
IIocResolver, IIocManager and IScopedIocResolver
你可能需要直接创建你的依赖项,而不是通过构造函数&属性注入,如果可能的话,应该尽量避免这样的情况,但是有时是无法避免的。ABP提供一些服务使得注入和使用都变得容易。比如:
public class MySampleClass : ITransientDependency { private readonly IIocResolver _iocResolver; public MySampleClass(IIocResolver iocResolver) { _iocResolver = iocResolver; } public void DoIt() { //Resolving, using and releasing manually var personService1 = _iocResolver.Resolve<PersonAppService>(); personService1.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" }); _iocResolver.Release(personService1); //Resolving and using in a safe way using (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>()) { personService2.Object.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" }); } } }
在应用程序里的示例类MySampleClass,通过构造函数注入IIcResolver ,并且使用它来解析和释放对象,如果需要,Resolve有一些重载方法可供使用,Release 方法被用来释放组件(对象),如果你手动创建了一个对象,调用Release方法是很关键的,否则,你的应用程序可能会有内存泄露的问题。为了确保释放了对象,无论什么地方尽可能使用ResolveAsDisposable (如上面例子中所示的),它在using块结束时自动调用Release方法。
IIocResolver (和IIocManager)也有一个CreateScope扩展方法(定义在Abp.Dependency命名空间下)来确保所有已创建的对象被安全地释放,如:
using (var scope = _iocResolver.CreateScope()) { var simpleObj1 = scope.Resolve<SimpleService1>(); var simpleObj2 = scope.Resolve<SimpleService2>(); //... }
在using块结束时,所有已创建的对象都自动被移除,使用IScopedIocResolver scope也是可注入的。你可以注入这个接口和解析依赖关系。当你的类释放后,所有已解析的依赖项都将被释放,但是要谨慎地使用它;比如,你的类的生命周期很长(如你的类是单例),而且你使用它创建太多的对象,然后这些对象将一直保留在内存里,直到你的类被释放。
如果你想直接用IOC容器(Castle Windsor)去解析依赖关系,你可以通过构造函数注入IIocManager,并使用IIocManager.IocContainer属性。如果在静态上下文里或不能注入IIocManager,在最后你还可以使用单例对象IocManager.Instance,但是,这样你的代码将变得不易测试。
附加部分
IShouldInitialize 接口
一些类在第一次使用前,需要先初始化,IShouldInitialize 有一个 Initialize()方法。如果你实现了它,然后在创建你的类的对象(使用前)后,会自动调用你的Initialize()方法。当然,你应该通过注入/解析对象来完成这个功能。
ASP.NET MVC & ASP.NET Web API 集成
我们必须调用依赖注入系统来解析依赖图中的根对象,在ASP.NET MVC 应用程序里,它通常是一个Controller类。我们也可以在控制器里使用构造函数注入和属性注入模式,当一个请求到达我们的程序里时,使用IOC容器来创建这个控制器,并递归解析所有依赖关系。所以,这个谁来做?这是ABP通过扩展ASP.NET MVC的默认控制器工厂来自动完成的。同此,对于ASP.NET Web API 也是一样的。你可以不用关心对象的创建和释放。
ASP.NET Core集成
ASP.NET Core使用Microsoft.Extensions.DependencyInjection 包内嵌了依赖注入系统,在ASP.NET Core里,ABP使用Castle.Windsor.MsDependencyInjection包集成它的依赖注入系统,所以,你可以不用考虑这些。
最后备注
只要你遵循ABP的规则和使用上面的结构,ABP简化和自动使用依赖注入,大部分情况下,这就足够使用了,但是如果你需要,你可以直接使用Castle Windsor的强大功能来执行任何任务(如自定义注册、注入钩子、拦截器等等)。