(译)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自动注册所有RepositoriesDomain ServicesApplication 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的强大功能来执行任何任务(如自定义注册、注入钩子、拦截器等等)。

 

posted @ 2017-12-08 16:33  Eric-Lau  阅读(1319)  评论(0编辑  收藏  举报