ABP中的依赖注入思想

  在充分理解整个ABP系统架构之前首先必须充分了解ABP中最重要的依赖注入思想,在后面会具体举出一些实例来帮助你充分了解ABP中的依赖注入思想,在了解这个之前我们首先来看看什么是依赖注入?来看看维基百科是怎样来介绍这个概念的。维基百科说:“依赖注入是一种软件设计模式,指一个或多个依赖(或服务)被注入,或通过引用传递,传入一个依赖对象(或客户端)并成为客户状态的一部分。模式通过自身的行为分离了客户依赖的创建,这允许程序设计是松耦合的,同时遵循依赖倒置和单一职责原则。与服务定位器模式直接进行对比,它允许客户了解他们用来查找依赖的机制。”

  如果你对容器的概念十分了解的话,那么你一定推崇使用依赖注入容器来管理依赖注入对象,如果不使用依赖注入技术,很难进行依赖管理、模块化开发和应用程序模块化。说了这么多使用依赖注入容器的好处,那么我们先来说一说传统开发方式存在哪些问题?

    一  传统开发方式

  在一个应用程序中,类之间相互依赖。假设在我们定义的应用层,我们有一个应用程序服务,使用仓储(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到数据库在,分析上面的代码的时候我们发现存在诸多的问题:

  A  PersonAppService通过IPersonRepository调用CreatePerson方法,所以这方法依赖于IPersonRepository接口,代替了PersonRepository具体类。但在PersonAppService的构造函数仍然依赖于PersonRepository。组件应该依赖于接口而不是实现。这就是所谓的依赖性倒置原则。

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

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

  D  有了这样的依赖,很难(或不可能)对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方法。但是,仍然存在一些问题:

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

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

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

    二 ABP中的实现

    既然有这些问题,那么我们来看看ABP中是怎么来解决这些问题的,这里主要有通过构造函数进行依赖注入和通过属性进行依赖注入的方式,还是通过上面的例子我们来分别说明。

  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);  

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

  A  创建一个PersonAppService变得困难。想想如果它有4个依赖,我们必须创建这四个依赖对象,并将它们传递到构造函数PersonAppService。

  B  从属类可能有其他依赖项(PersonRepository可能有依赖关系)。所以我们必须创建PersonAppService的所有依赖项,所有依赖项的依赖关系等等. .如此依赖关系使得我们创建一个对象变得过于复杂了。幸运的是依赖注入框架自动化管理依赖关系。  

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

        那么这里我们便来看看ABP框架中使用了什么依赖注入框架来管理这些彼此有依赖关系的依赖项。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单例模式,在整个生命周期内只存在一个唯一的实例,应用程序共享这个唯一的实例)。

  2.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);
        }
    }      

  在这里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实例是一个可选的依赖。几乎所有的依赖注入框架都支持属性注入模式。

  2.3  ABP中的依赖注入

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

  A 注册(Registering)

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

  B 常规注册(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命名你的服务类。如果你这样做,比如你将实现类定义为MyPersonAppServiceImpl它将不会为IPersonAppService自动注册(它需要自注册self-registration到DI框架),所以如果你想要你应该手动注册它。至于更深层次的原因你可以参考ABP源码中的实现来进行分析。

具体的解释我看了一下ABP的源码:
在ABP默认注册的类型中使用下面的代码
 //Transient
 context.IocManager.IocContainer.Register(
         Classes.FromAssembly(context.Assembly)
                 .IncludeNonPublicTypes()
                 .BasedOn<ITransientDependency>()
                 .If(type => !type.GetTypeInfo().IsGenericTypeDefinition)
                 .WithService.Self()
                 .WithService.DefaultInterfaces()
                 .LifestyleTransient()
                );
其中WithService.DefaultInterfaces()这个方法是使用Castle Winsor中的方法
里面有下面的注释:
   // 摘要:
   //     Uses all interfaces that have names matched by implementation type name. Matches
   //     Foo to IFoo, SuperFooExtended to IFoo and IFooExtended etc
        public BasedOnDescriptor DefaultInterfaces();  

    ABP按照约定注册程序集。所以你应该告诉ABP按照约定注册你的程序集。IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());这个Assembly.GetExecutingAssembly()得到一个对包括此代码的程序集的引用。你可以通过RegisterAssemblyByConvention方法注册其他程序集。在你实际的代码中你可以在继承自AbpModule的类下面通过的重写基类的Initialize方法来完成程序集中依赖接口的注入问题,例如:

 public class DcsDomainModule : AbpModule {

        public override void PreInitialize() {
            Configuration.Auditing.IsEnabledForAnonymousUsers = true;

            DcsLocalizationConfigurer.Configure(Configuration.Localization);
        }

        public override void Initialize() {
            IocManager.RegisterAssemblyByConvention(typeof(DcsDomainModule).GetAssembly());
        }
    }  

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

  C 帮助接口(Helper Interfaces)  

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

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

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

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

  如果之前描述的方法还是不足以应对你的情况,你可以使用Castle Windsor注册类和及依赖项。因此你将拥有Castle Windsor注册的所有能力,可以实现IWindsorInstaller接口进行注册,你可以在应用程序中创建一个实现IWindsorInstaller接口的类:

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

  在这里你将直接使用ABP中直接使用的Castle Windsor注册容器来注册任何你想直接进行注册的接口及相应实现。

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

  在ABP中作为最佳实践,你应该使用构造函数和属性注入去获取你的类的依赖。任何可能的地方,你都应该这样做。例如:

 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系统最适当的方式。

  F IIocResolver 和 IIocManager接口

  有时你可能需要直接创建你的依赖项,而不是构造函数和属性注入。应该尽可能避免这种情况,但它可能无法避免。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通过构造函数注入,然后用它来创建和释放对象。有几个解决方法的重载可以根据需要使用。Release方法用于释放组件(对象)。如果你是手动创建一个对象,调用Release方法释放对象非常重要。否则,你的应用程序会有内存泄漏问题。为了保证对象被释放,尽可能使用ResolveAsDisposable(就像上面的例子所示)。它会在using代码块结束的时候自动调用Release方法。

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

  上面介绍了整个ABP系统中的依赖注入的原理,后面所有的内容都将依赖于这部分内容,所以希望通过这些事例使得对整个依赖注入框架有一个更加清晰的认识。

  最后,点击这里返回整个ABP系列的主目录。

posted @ 2019-02-25 15:14  Hello——寻梦者!  阅读(1804)  评论(0编辑  收藏  举报