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接口的一个具体实现,这样就造成可能不会和其他实现一起工作。因此,从实现中分离接口就会变得毫无意义。硬依赖使得代码基变得紧耦合,可复用性降低。
  • 在未来我们可能需要改变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的实现。但是,仍然有下面这些问题:

  • 这次,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("Yunus Emre", 19);

构造函数注入是使类独立于依赖对象创建的一种完美方式,但是,上面的代码存在一些问题:

  • 创建一个PersonAppService变得更加困难。试想如果它有4个依赖,那么我们必须创建这4个依赖的对象,然后把它们传入PersonAppService的构造函数中。
  • 依赖的类可能有其它的依赖(这里,PersonRepository可能有依赖)。因此,我们必须创建PersonAppService的所有依赖,依赖的所有依赖等等。这样的话,我们甚至可能不再创建单一对象,因为依赖图太复杂了。

幸运的是,ABP有依赖注入框架自动管理依赖。

属性注入模式

构造函数注入是提供一个类的依赖的完美模式。用这种方式,你可以不需要提供依赖就能创建一个类的实例,它也是显示声明该类需要满足什么要求才能正确工作的强大方式。

但在某些情况下,该类依赖于其他的类而且其他的类没有它也能工作。这对于关注度分离(比如日志记录)来说经常是成立的。一个类可以离开logging工作,但如果提供了logger,那它就能记录日志。这种情况下,你可以定义将依赖定义为公共的属性而不是在构造函数中获得这些依赖。试想如果我们要在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的接口注册了它们,再然后我们要求容器创建一个IPersonAppService。容器使用依赖创建了PersonAppService并返回,也许在这个简单的例子中使用DI框架的优势不是很明显,但是想象一下你在一个真实的企业应用中会有很多类和依赖。当然,也会在别的地方使用对象来注册依赖,这个在应用启动时只会做一次。

注意,我们也将对象的生命周期声明为transient。这意味着,无论何时解析这些类型的一个对象,都会创建一个新的实例。当然还有很多不同的生命周期(像singleton)。

ABP中的依赖注入基础设施###

当你通过下面的最佳实践和一些惯例编写你的应用时,ABP几乎让使用DI框架变得不可见了。

注册

在ABP中,将你的类注册到DI系统有几种不同的方式。大多数情况下,按照惯例注册已经足够了。

惯例注册

ABP会按照惯例自动注册所有的仓储,领域服务,应用服务,MVC控制器和Web API控制器。比如,你可能有一个IPersonAppService接口和一个实现了该接口的PersonAppService类:

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

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

因为它实现了IApplicationService接口(只是一个空接口),所以ABP会自动注册它,并注册为transient(每次使用创建一个实例)。当你使用构造函数注入IPersonAppService接口到一个类中时,一个PersonAppService对象会自动地创建并传入该类的构造函数中。

命名规范在ABP中非常重要。比如,你可以将PersonAppService更名为MyPersonAppService或是其他包含了“PersonAppService”后缀的名字,因为IPersonAppService接口有这个后缀。但你不能将它命名为PeopleService。如果你没有按照这种命名规范来操作的话,那么IPersonAppService不会自动地注册(但是它已经以自注册的方式注入到DI框架,而不是接口方式),因此如果你想要以接口方式注册的话,那么你应该手动注册。

ABP按照惯例注册程序集。因此,你应该按照惯例告诉ABP注册你的程序集。这个相当简单:

IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());

Assembly.GetExecutingAssembly()会获得包含这句代码的程序集的引用。你也可以将其他的程序集传入RegisterAssemblyByConvention 方法中。这个操作通常在你的模块初始化的时候完成的。查看《模块系统》博文获得更多信息。

通过实现IConventionalRegister接口和调用IocManager.AddConventionalRegister方法,你可以用你的类编写你自己的惯例注册类。你要做的就是在模块的PreInitialize方法中加入它。

帮助接口

你可能想要注册一个特殊的类,但是它不符合惯例注册的原则。为此,ABP提供了ITransientDependencyISingletonDependency接口。比如:

public interface IPersonManager
{
    //...
}

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

用这种方式,你可以轻松地注册MyPersonManager。当需要注入IPersonManager的时候,就会使用MyPersonManager。注意依赖声明为Singleton。这样,MyPersonManager的单例就被创建了,并且相同的对象也被传入到所有的类中。只有在第一次使用时才会创建,以后再整个应用的生命周期都会使用相同的实例。

自定义/直接注册

如果之前描述的方法还不能满足你,那么你可以直接使用Castle Windsor来注册你的类和依赖。这样,你就在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会自动找到并执行这个类。最后,可以使用IIocManager.IocContainer属性到达WindsorContainer。获取更多Windsor信息,请查看官方文档

解析

注册会将你的类,类的依赖和生命周期通知给IOC(控制反转)容器。接下来,你需要在应用中的某些地方使用IOC容器创建对象。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系统最合适的方式。

IIocResolver和IIocManager

有时,你可能必须要直接解析依赖而不是通过构造函数和属性注入。这种情况要尽可能地避免,但这种情况也是有可能的。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 (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>())
        {
            personService2.Object.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" });
        }
    }
}

在以上例子中的MySampleClass通过构造函数注入IIocResolver并用它来解析和释放对象。Resolve方法有许多重载可供使用。Release方法用来释放组件(对象)。调用Release来手动解析一个对象是很关键的,否则,应用会有内存泄漏问题。为了确保释放对象,要尽可能使用ResolveAsDisPosable(如例子中演示的那样)。在using块的末尾会自动地调用Release。

如果你想要直接使用IOC容器(Castle Windor)来解析依赖,那么你可以构造函数注入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也是如此。你不必关心创建和释放对象的事情。

最后提示

只要你遵循规则并使用上面的结构,ABP就能简化并自动化依赖注入的使用。大多数情况下,这些已经够用了。但是,如果你需要的话,你可以直接使用所有Castle Windsor的能力来执行任何任务(如自定义注册,注入钩子,拦截器等等)。

posted @ 2015-12-16 22:19  tkbSimplest  阅读(25191)  评论(13编辑  收藏  举报