2.1 ABP公共结构 - 依赖注入

如果你已经了解依赖注入的概念、构造函数和属性注入模式,你可以跳过这一节。

维基百科:“依赖注入是一种软件设计模式,指一个或多个依赖(或服务)被注入,或通过引用传递,传入一个依赖对象(或客户端)并成为客户状态的一部分。该模式通过自身的行为分离了客户依赖的创建,这允许程序设计是松耦合的,同时遵循依赖倒置和单一职责原则。与服务定位器模式直接进行对比,它允许客户了解他们用来查找依赖的机制。”

如果不使用依赖注入技术,很难进行依赖管理、模块化开发和应用程序模块化。

2.1.1 传统方式的问题

在一个应用程序中,类之间相互依赖。假设我们有一个应用程序服务,使用仓储(repository)类插入实体到数据库。在这种情况下,应用程序服务类依赖于仓储(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通过IPersonRepository调用CreatePerson方法,所以这方法依赖于IPersonRepository,代替了PersonRepository具体类。但PersonAppService(的构造函数)仍然依赖于PersonRepository。组件应该依赖于接口而不是实现。这就是所谓的依赖性倒置原则。

  • 如果PersonAppService亲自创建PersonRepository,这变得依赖到了一个特定的IPersonRepository接口实现,且不能使用另一个实现进行工作。因此,从实现分离接口变得无意义,硬依赖使得代码基于紧耦合和低重用。硬依赖(hard-dependency)使得代码紧密耦合和较低的可重用。

  • 我们可能需要在未来改变创建PersonRepository的方式。即,我们可能想让它创建为单例(单一共享实例而不是为每个使用创建一个对象)。或者我们可能想要创建多个类实现IPersonRepository并根据条件创建对象。在这种情况下,我们需要修改所有依赖于IPersonRepository的类。

  • 有了这样的依赖,很难(或不可能)对PersonAppService进行单元测试。

为了克服这些问题,可以使用工厂模式。因此,创建的仓储类是抽象的。看下面的代码:

 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的实现的对象,这个对象取决于PersonRepositoryFactory的Create方法。但是,仍然存在一些问题:

  • 此时,PersonAppService取决于PersonRepositoryFactory。这是更容易接受,但仍有一个硬依赖(hard-dependency)。

  • 为每个库或每个依赖项乏味的写一个工厂类/方法。

  • 测试性依然不好,由于很难使得PersonAppService使用mock实现IPersonRepository。

2.1.2 解决方案

有一些最佳实践(模式)用于类依赖。

1. 构造函数注入(Constructor injection)

重写上面的例子,如下所示:

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变得困难。想想如果它有4个依赖,我们必须创建这四个依赖对象,并将它们传递到构造函数PersonAppService。

  • 从属类可能有其他依赖项(在这里,PersonRepository可能有依赖关系)。所以,我们必须创建PersonAppService的所有依赖项,所有依赖项的依赖关系等等. .如此,依赖关系使得我们创建一个对象变得过于复杂了。

幸运的是,依赖注入框架能够自动化管理依赖关系。

2. 属性注入(Property injection)

采用构造函数的注入模式是一个完美的提供类的依赖关系的方式。通过这种方式,只有提供了依赖你才能创建类的实例。同时这也是一个强大的方式显式地声明,类需要什么样的依赖才能正确的工作。

但是,在有些情况下,该类依赖于另一个类,但也可以没有它。这通常是适用于横切关注点(如日志记录)。一个类可以没有工作日志,但它可以写日志如果你提供一个日志对象。在这种情况下,你可以定义依赖为公共属性,而不是让他们放在构造函数。想想,如果我们想在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可以写日志了,如果你为PersonAppService实例设置了Logger,如下面:

Var personService = new PersonAppService(new PersonRepository());
    personService.Logger = new Log4NetLogger();
    personService.CreatePerson("Yunus Emre", 19);

假设Log4NetLogger实现ILogger实例,使得我们可以使用Log4Net库写日志。因此,PersonAppService可以写日志。如果我们不设置Logger,PersonAppService就不写日志。因此,我们可以说PersonAppService ILogger实例是一个可选的依赖。

几乎所有的依赖注入框架都支持属性注入模式。

3. 依赖注入框架

有许多依赖注入框架,都可以自动解决依赖关系。他们可以创建所有依赖项(递归地依赖和依赖关系)。所以你只需要依赖注入模式写类和类构造函数&属性,其他的交给DI框架处理!在良好的应用程序中,类甚至独立于DI框架。整个应用程序只会有几行代码或类,显示的与DI框架交互。

ABP的依赖注入基于 Castle Windsor框架。Castle Windsor最成熟的DI框架之一。还有很多这样的框架,如Unity,Ninject,StructureMap,Autofac等等。

在使用一个依赖注入框架时,首先注册你的接口/类到依赖注入框架中,然后你就可以resolve一个对象。在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及它们的接口。然后我们要求容器创建一个IPersonAppService实例。它创建PersonAppService对象及其依赖项并返回。在这个简单的示例中,使用DI框架也许不是那么简洁,但想象下,在实际的企业应用程序中你会有很多类和依赖关系。当然,注册的依赖项只在程序启动的某个地方创建一次。

请注意,我们只是将对象声明为临时对象(transient)。这意味着每当我们创建这些类型的一个对象时,就会创建一个新的实例。在这里会有许多不同的生命周期(如:Singletion单例模式)。

4. ABP依赖注入的基础结构

在编写应用程序时遵循最佳实践和一些约定,ABP几乎让依赖注入框架使用变得无形。

注册(Registering)

在ABP中,有很多种不同的方法来注册你的类到依赖注入系统。大部分时间,常规方法就足够了。

常规注册(Conventional registrations)

按照约定,ABP自动注册所有 Repositories, Domain Services, Application Services, MVC 控制器和Web API控制器。例如,你可能有一个IPersonAppService 接口和实现类PersonAppService:

public interface IPersonAppService : IApplicationService
{
    //...
}

public class PersonAppService : IPersonAppService
{
    //...
}

ABP会自动注册它,因为它实现IApplicationService接口(它只是一个空的接口)。它会被注册为transient (每次使用都创建实例)。当你注入(使用构造函数注入)IPersonAppService接口成一个类,PersonAppService对象会被自动创建并传递给构造函数。

注意:命名约定在这里非常重要。例如你可以将名字PersonAppService改为 MyPersonAppService或另一个包含“PersonAppService”后缀的名称,由于IPersonAppService包含这个后缀。但是你可以不遵循PeopleService命名你的服务类。如果你这样做,它将不会为IPersonAppService自动注册(它需要自注册(self-registration)到DI框架,而不是接口),所以,如果你想要你应该手动注册它。

ABP按照约定注册程序集。所以,你应该告诉ABP按照约定注册你的程序集。这很容易:

IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());

Assembly.GetExecutingAssembly()得到一个对包括此代码的程序集的引用。你可以通过RegisterAssemblyByConvention方法注册其他程序集。这同在你的模块初始化(AbpModule.Initialize())时完成。请查看ABP的模块系统获得更多信息。

你可以通过实现 IConventionalRegisterer 接口和调用 IocManager.AddConventionalRegisterer 方法编写自己的约定注册类。你应该将它添加到模块的pre-initialize方法中。

帮助接口(Helper interfaces)

你可以注册一个特定的类,不遵循传统的约定制度规则。ABP提供了 ITransientDependency和ISingletonDependency 接口的快速实现方法。例如:

public interface IPersonManager
{
    //...
}

public class MyPersonManager : IPersonManager, ISingletonDependency
{
    //...
}

以这种方式,你可以很容易地注册MyPersonManager。当需要注入IPersonManager时,MyPersonManager会被使用。注意,依赖被声明为单例。因此,创建的MyPersonManager同一个对象被传递给所有需要的类。只是在第一次使用时创建,那么应用程序的整生命周期使用的是同一实例。

自定义/直接 注册(Custom/Direct registration)

如果之前描述的方法还是不足以应对你的情况,你可以使用 IocManager或者Castle Windsor 来注册你自己的类。

使用IocManager

你可以使用 IocManager 来注册依赖关系(这通常是在模块的PreInitialize方法中实现):

IocManager.Register<IMyService, MyService>(DependencyLifeStyle.Transient);
使用Castle Windsor API

你也可以使用 IIocManager.IocContainer 属性来访问Windsor容器并且注册依赖关系。如下所示:

IocManager.IocContainer.Register(Classes.FromThisAssembly().BasedOn<IMySpecialInterface>().LifestylePerThread().WithServiceSelf());

更多信息,请阅读Windsor文档

这里没有删除之前实现IWindsorInstaller接口的翻译,因为我感觉这个非常有用。

也可以实现IWindsorInstaller接口进行注册。你可以在应用程序中创建一个实现IWindsorInstaller接口的类:

public class MyInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly().BasedOn<IMySpecialInterface>().LifestylePerThread().WithServiceSelf());
    }
}

Abp自动发现和执行这个类。最后,你可以通过使用IIocManager.IocContainer属性得到WindsorContainer。

解析(Resolving)

注册通知IOC(控制反转)容器关于你的类,它们的依赖项和生命周期。在你的应用程序需要使用IOC容器创建对象时,ASP.NET提供了一些方法解决依赖关系。

构造函数 & 属性注入(Constructor & Property Injection)

作为最佳实践,应该使用构造函数和属性注入去获取类的依赖。例子:

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以及IScopedIocResolver接口

有时可能需要直接创建所需的依赖项,而不是构造函数和属性注入。(应该尽可能避免这种情况)。Abp提供一些服务使得这样的注入很容易实现。例子:

public class MySampleClass : ITransientDependency
{
    private readonly IIocResolver _iocResolver;

    public MySampleClass(IIocResolver iocResolver)
    {
        _iocResolver = iocResolver;
    }

    public void DoIt()
    {
        //解析, 使用并手动释放
        var personService1 = _iocResolver.Resolve<PersonAppService>();
        personService1.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" });
        _iocResolver.Release(personService1);

        //解析并使用using语法糖来释放资源
        using (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>())
        {
            personService2.Object.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" });
        }
    }
}

MySampleClass是一个应用程序的示例类。IIcResolver通过构造函数注入,然后用它来创建和释放对象。有几个解决方法的重载可以根据需要使用。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 接口来实现上述操作。你能注入该接口并解析依赖关系。当你使用的类被释放后,所有被解析的依赖资源也会被自动释放掉。但是,请小心使用它;例如:如果该类的生命周期很长(如单例模式),并且需要解析很多对象,那么它们会一直停留在内存中,直到该类被释放掉。

如果你想直接使用IOC容器(Castle Windsor)来处理依赖关系项,可以通过构造函数注入 IIocManager并使用它IIocManager.IocContainer 属性。如果你是在一个静态上下文或不能注入IIocManager,还有最后一个方法,你可以使用单例对象IocManager.Instance,你可以在任何地方获取到,它无处不在。但是,在这种情况下你的代码将变得不容易测试。

3.1.3 其他

1. IShouldInitialize 接口

有些类在第一次使用前需要初始化。IShouldInitialize有Initialize()方法。如果你实现它,那么你的Initialize()方法自动会被自动调用在创建对象之后(在使用之前)。当然,为了使用这个特性,你应该注入/创建此对象。

2. ASP.NET MVC & ASP.NET Web API 集成

当然,我们必须调用依赖注入系统处理依赖关系图的根对象。在一个ASP.NET MVC应用程序,通常是一个控制器类。我们可以使用构造函数注入模式注入控制器。当一个请求来到我们的应用程序中,控制器和所有依赖项被IOC容器递归创建。所以,谁做了这些?这是被Abp扩展的ASP.NET MVC默认控制器工厂自动完成的。ASP.NET Web API 也是相似的。你不用关心对象的创建和释放。

3. ASP.NET Core 集成

ASP.NET Core已经内置了依赖注入:Microsoft.Extensions.DependencyInjection。在ASP.NET Core中ABP使用Castle.Windsor.MsDependencyInjection实现了依赖注入。所以你不需要考虑它。

4. 最后说明(Last notes)

Abp简化并自动使用依赖注入,只要你遵守规则和使用上面的结构。大多数时候这样就够了。但是如果不能满足你的需求,你可以直接使用Castle Windsor的所有能力来执行任何任务(如自定义注册,注入钩子,拦截器等等)。