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

     

  • 相关阅读:
    Linux产生coredump文件(core)
    shell脚本每五分钟执行一次可执行程序(nohup)
    VIM快捷操作
    日期正则表达式
    istringstream字符串流对象
    json和pickle模块
    sys模块
    random模块
    time模块
    python的模块
  • 原文地址:https://www.cnblogs.com/Lau7/p/8005884.html
Copyright © 2020-2023  润新知