ABP之依赖注入

维基百科中说“依赖注入是一种软件设计模式,在这种模式中,一种或多种依赖项(或服务)被注入,或者通过引用传递到一个依赖对象(或客户端),并成为客户端状态的一部分,这种模式将客户端依赖的创建和其自身行为分离开来,允许程序设计松散耦合,
遵循依赖注入和单一责任原则。它直接对比了服务定位模式,该模式允许客户了解其用来查找依赖项的系统”。

在不使用依赖注入技术的情况下,很难去管理依赖项和去开发一个模块化并且结构良好的应用程序。

传统方法中存在的问题

在应用程序中,类与类之间是互相依赖,假设我们现在有一个使用仓储库将实体插入数据库的应用程序服务。这种情形下,应用程序服务类依赖于仓储库类,看下面的例子:

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的类。
  • 有了这样的依赖关系,对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的创建是怎么实现的,并且它和PersonRepository 的实现是独立的。但是仍然有一些问题:

PersonRepositoryFactory是一个静态类,它创建并返回IPersonRepository.这称为服务器定位模式。创建问题被解决了,因为PersonAppService 不知道IPersonRepository的创建是怎么实现的,并且它和PersonRepository 的实现是独立的。但是仍然有一些问题:

  • 这个时候,PersonAppService 依赖于PersonRepositoryFactory,这个更容易被接收,但是仍然是一个硬依赖
  • 为每个存储库或每个依赖项编写工厂类/方法是很乏味的。
  • 同样,这也不容易测试,因为很难让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("John Doe", 32);

构造函数注入是使类独立于依赖对象的创建的一种很好的方法,但是上面的代码有一些问题:

  • 创建一个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("John Doe", 32);

假设Log4NetLogger实现了ILogge,并且它使用Log4Net 库写日志,这样PersonAppService 可以真正的写日志。如果我们没有设置Logger ,它不会写日志。我们可以说ILogger是PersonAppService的一个可选择的依赖。
大多数依赖注入框架都支持属性注入模式。

依赖注入框架

有许多依赖注入框架可以自动解决依赖项。他们可以递归的创建所有依赖项,以及依赖项的依赖项的对象。

仅仅是简单的使用构造函数和属性注入模式编写类,依赖注入框架会做剩下的事情,在一个好的应用程序中,你的类是独立的,甚至独立于依赖注入框架。在整个应用程序中,只有几行代码或类显式地与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("John Doe", 32);

首先,我们创建了WindsorContainer,并且使用其接口注册了PersonRepository 和 PersonAppService。我们使用这个容器窗口了IPersonAppService。它用依赖关系创建了具体的类PersonAppService,然后返回它。在这个简单的例子中,可能使用DI框架的优势不明显。然而在一个真正的企业项目中,你可能有多个类和依赖项。依赖项的注册和对象的使用时分开的,并且在应用程序启动时只创建一次。

注意,我们还将对象的生命周期设置为瞬态的。这意味着无论何时解析这些类型的对象,都会创建一个新实例。有许多不同的生命周期,例如单例。

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,因为它有相同的后缀。但是,如果不能没有后缀,比如“PeopleService。如果您这样做,它不会自动注册到IPersonAppService。相反,它使用自注册(而不是接口)注册到DI框架。在这种情况下,您可以手动注册它。

ABP可以按照约定注册程序集:

IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());

Assembly.GetExecutingAssembly() 获取对包含此代码的程序集的引用。我们可以将其他程序集传递给RegisterAssemblyByConvention方法。这通常是在初始化模块时完成的。

我们可以通过实现IConventionalRegisterer接口,然后调用IocManager.AddConventionalRegisterer方法来编写自己的常规注册类。我们应该将其添加到模块的预初始化方法中。

辅助接口Helper Interfaces

您可能想注册一个不符合常规注册规则的特定类。ABP提供ITransientDependency、IPerWebRequestDependency和ISingletonDependency接口作为快捷方式。例如:

public interface IPersonManager
{
    //...
}

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

通过这种方式,WOMEN 可以轻松注册MyPersonManager。当需要注入IPersonManager时,将使用MyPersonManager类。注意,依赖项声明为单例。创建MyPersonManager的一个实例,并将相同的对象传递给所有需要的类。它在第一次使用时实例化,然后在应用程序的整个生命周期中使用。

注意:IPerWebRequestDependency只能在web层中使用。

Custom/Direct Registration

如果传统的注册不能满足您的需要,您可以使用IocManager或Castle Windsor来注册您的类和依赖项

使用IocManager

你可以使用IocManager来注册依赖项(通常在模块定义类的预初始化方法中):

IocManager.Register<IMyService, MyService>(DependencyLifeStyle.Transient);

使用Castle Windsor API

 

您可以使用IocManager.IocContainer属性访问Castle Windsor容器并注册依赖项。例子:

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

解决依赖关系

注册通知IOC(控制反转)容器(也称为DI框架)关于类、它们的依赖关系和生命周期。在应用程序的某个地方,您需要使用IOC容器创建对象。ASPNET提供了一些解决依赖关系的选项。

构造函数和属性注入

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

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()
    {
        //Resolving, using and releasing manually
        var personService1 = _iocResolver.Resolve<PersonAppService>();
        personService1.CreatePerson(new CreatePersonInput { Name = "John", Surname = "Doe" });
        _iocResolver.Release(personService1);

        //Resolving and using in a safe way
        using (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>())
        {
            personService2.Object.CreatePerson(new CreatePersonInput { Name = "John", Surname = "Doe" });
        }
    }
}

MySampleClass是应用程序示例类。它被构造器注入IIocResolver,并使用它解析和释放对象。解析方法有一些重载,可以根据需要使用。Release方法用于释放组件(对象)。如果我们手工解析一个对象,调用Release是非常重要的。否则,应用程序可能有内存泄漏。为了确保释放对象,尽可能使用ResolveAsDisposable(如上面的示例所示)。Release在using块的末尾自动调用。

IIocResolver(和IIocManager)还具有CreateScope扩展方法(在app. 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。实例无处不在。然而,在这种情况下,代码并不容易测试。

扩展知识

IShouldInitialize接口

有些类在第一次使用之前需要初始化。IShouldInitialize有一个Initialize()方法。如果实现了它,那么在创建对象之后(在使用对象之前)会自动调用Initialize()方法。需要注入/解析对象才能使用该特性。

ASP.NET MVC和ASP.NET Web API集成

我们必须调用依赖注入系统来解析依赖关系图中的根对象。在ASPNETMVC应用程序中,它通常是一个控制器类。我们还可以在控制器中使用构造器和属性注入模式。当请求到达应用程序时,使用IOC容器创建控制器,并递归地解析所有依赖项。这是怎么发生的?这一切都是由ABP通过扩展ASP.NET MVC的默认控制器工厂自动完成的。对于ASP.NETWeb API也是如此。我们不必担心创建和处理对象。

ASP.NET Core 集成

ASPNETCore已经有一个内置的依赖注入系统,带有Microsoft.Extensions.DependencyInjection包.ABP使用Castle.Windsor.MsDependencyInjection包将其依赖注入系统集成到ASP.Net Core.、

最后:

只要遵循规则并使用上面的结构,ABP就可以简化和自动化依赖项注入。大多数时候不需要我们做什么。如果需要,可以直接使用Castle Windsor的原始功能执行许多任务,比如定制注册、注入挂钩、拦截器等等。

posted @ 2019-04-02 13:37  yixuan.han  阅读(373)  评论(0编辑  收藏  举报