依赖注入

什么是依赖注入?

  如果你已经知道依赖注入,构造函数和属性注入模式,可以直接跳到下一部分。

  维基百科说:“依赖注入是一种软件设计模式,一个或多个依赖项(或服务)被注入或通过引用传递到一个依赖对象,并且成为客户端状态的一部分。这种模式把客户端依赖项的创建从它自己的行为中分离出来,允许程序设计成松耦合的,遵循依赖倒置和单一职责的原则。和服务定位器模式相比,它允许客户端知道他们使用的系统查找依赖项。”

  不使用依赖注入技术,很难管理依赖项和发布一个结构良好的应用。

传统方法的问题

  在一个应用里,类之间相互依赖。假设我们有一个使用仓储插入实体到数据的应用服务,在这种情况下,这个应用服务类依赖仓储类。如下示例:

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

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

  • 创建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对象后设置记录器,它就可以写日志了:

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

  假定Log4NetLogger实现了ILogger,且使用Log4Net类库写日志。从而,PersonAppService实际上可以写日志了。如果我们没有设置记录器,它就不写日志。所以,我们可以说ILogger是PersonAppService一个可选的依赖项。
  几乎所有的依赖注入框架都支持属性注入模式。

依赖注入框架

  有许多可以自动解析依赖项的依赖注入框架。他们可以创建对象以及其所有依赖项(可以递归依赖的依赖)。所以,只要使用构造函数、属性注入模式写类,DI框架会处理剩下的工作!在一个好的应用里,类甚至独立于DI框架。在整个应用里,只有几行或几个类显示的与DI框架交互。

  ABP使用Castle Windsor作为依赖注入框架。它是最成熟的框架之一。还有许多其他的框架,如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", );

  

  我们首先创建WindsorContainer。然后使用接口注册PersonRepository和PersonAppService。我们让容器创建一个IPersonAppService。它创建PersonAppService及其依赖项并返回。在这个简单例子里,使用DI框架的优势并不是很明显,但是设想一下在一个真实的企业应用里将会有许多的类和依赖项。当然,注册依赖项只在应用启动的时候注册一次,创建和使用对象可能会在其他的地方。

  注意,我们把对象的生命周期生命为短暂的。意味着无论什么时候我们需要类型的一个对象时,一个新的实例将会被创建。还有许多其他不同的生命周期(如单例)。

ABP依赖注入基础设施

  当你遵循最佳实践和一些约定编写应用的时候,ABP几乎使依赖注入框架的使用无感知的。

注册依赖项

  在ABP中有不同的方式注册类到依赖注入系统。大多数时候,常规的注册就足够了。

常规注册

  ABP默认会自动注册所有的仓储、领域服务、应用服务、MVC控制器和Web API控制器。例如,你有一个IPersonAppService接口和一个它的实现类PersonAppService:

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

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

  命名约定在这里是非常重要的。例如,你可以把PersonAppService的名称改为MyPersonAppService或者其他包含'PersonAppService'后缀的名字,因为IPersonAppService有这个后缀。但是不可以把服务命名为PeopleService。如果这样做了,就不会自动注册IPersonAppService了(它使用自注册方式注册到DI框架,不使用接口),所以,如果想要注册的话可以手动注册。

  ABP可以根据约定注册程序集。可以让ABP根据约定注册程序集。这是相当简单的:

IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());

  Assembly.GetExeutingAssembly()获取包含这些代码的程序集引用。你可以传递其他程序集到RegisterAssemblyByConvention方法。这些通常在模块初始化的时候执行。可参见ABP模块系统了解更多。

  可以编写自定义的约定注册类,需要实现IConventionalRegisterer接口,并且在类里调用IocManager.AddConventionalRegiisterer方法。需要添加到模块的preinitialize方法中。

帮助接口

  你可能想注册一个特定的类,但这个类不符合约定注册规则。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(Inversion of control)容器(a.k.a DI框架)知道有哪些类、依赖项和生命周期。在应用的一些地方,需要使用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和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,作用范围也是可以注入的。可以注入这个接口用来解析所有依赖项。当类被释放的时候,所有解析的依赖项也会被释放。但是,需要小心使用;例如,如果有个类有很长的生命(比方是单例)并且解析了大量的对象,所有的这些类都会存在内存中,直到这个类被释放。

  如果想直接使用IOC容器(Castle Windsor)解析依赖项,可以使用构造函数注入IIocManager并使用IIocManager.IocContainer属性。如果在一个静态上下文或者不能注入IIocManager,作为最后的选择,可以使用单例对象IocManager.Instance。但是,这样的话代码就不易测试了。

额外部分

IShouldInitialize 接口

  一些类需要在首次使用的时候初始化。IShouldInitialize有一个Initialize()方法。如果实现了它,当创建对象后(在使用前)Initialize()方法会自动调用。当然,可以注入或解析这个对象以便使用这个特征。

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

  我们必须调用依赖注入系统来解决在依赖图中的根对象。在ASP.NET MVC应用里,它通常为一个控制器类。我们可以在控制器里使用构造函数和属性注入模式。当请求到达应用时,控制器使用IOC容器创建,所有的依赖递归解析。所以,谁做这些呢?这些由ABP通过扩展ASP.NET MVC的默认控制器工厂自动完成。同样,ASP.NET Web API也是这样的。不用关心创建和释放对象。

ASP.NET Core集成

  ASP.NET Core 已经有一个内置的依赖注入系统,在Microsoft.Extensions.DependencyInjection包里。ABP使用Castle.Windsor.MsDependencyInjection包集成和ASP.NET Core的依赖注入系统。所以,不需要关心它。

最后建议

  ABP简化且自动使用依赖入住,只要遵循规则并使用上述的结构。大多数时候不需要更多的依赖注入。但是,如果需要,可以直接使用Castle Windsor的力量执行任何任务(如自定义注册,注入钩子,拦截等等)。

返回主目录

ABP官方文档翻译 2.1 依赖注入的更多相关文章

  1. 0.0 ABP官方文档翻译目录

    一直想学习ABP,但囿于工作比较忙,没有合适的契机,当然最重要的还是自己懒.不知不觉从毕业到参加工作七年了,没留下点儿什么,总感觉很遗憾,所以今天终于卯足劲鼓起勇气开始写博客.有些事能做的很好,但要跟 ...

  2. ABP官方文档翻译 2.5 设置管理

    设置管理 介绍 关于 ISettingStore 定义设置 设置范围 重写设置定义 获取设置值 服务端 客户端 更改设置 关于缓存 介绍 每个应用都需要存储设置,并且在应用的某些地方需要使用这些设置. ...

  3. ABP官方文档翻译 8.2 SignalR集成

    SignalR集成 介绍 安装 服务器端 客户端 建立连接 內建特征 通知 在线客户端 PascalCase与CamelCase对比 你的SignalR代码 介绍 ABP中的Abp.Web.Signa ...

  4. ABP官方文档翻译 7.2 Hangfire集成

    Hangfire集成 介绍 ASP.NET Core集成 ASP.NET MVC 5.x集成 面板授权 介绍 Hangfire是一个综合的后台job管理器.你可以 把它集成到ABP,用来取代默认的后台 ...

  5. ABP官方文档翻译 7.1 后台Jobs和Workers

    后台Jobs和Workers 介绍 后台Jobs 关于Job持久化 创建后台Job 在队列中添加一个新Job 默认的后台Job管理器 后台Job存储 配置 禁用Job执行 异常处理 Hangfire集 ...

  6. ABP官方文档翻译 6.3 本地化

    本地化 介绍 应用程序语言 本地化源 XML文件 注册XML本地化源 JSON文件 注册JSON本地化源 资源文件 自定义源 当前语言是如何决定的 ASP.NET Core ASP.NET MVC 5 ...

  7. ABP官方文档翻译 6.1.1 MVC控制器

    ASP.NET MVC控制器 介绍 AbpController基类 本地化 其他 过滤器 异常处理和结果包装 审计日志 验证 授权 工作单元 介绍 ABP通过Abp.Web.Mvc nuget包集成到 ...

  8. ABP官方文档翻译 5.1 Web API控制器

    ASP.NET Web API控制器 介绍 AbpApiController基类 本地化 其他 过滤器 审计日志 授权 反伪造过滤器 工作单元 结果包装和异常处理 结果缓存 校验 模型绑定器 介绍 A ...

  9. ABP官方文档翻译 4.5 特征管理

    特征管理 介绍 关于IFeatureValueStore 特征类型 Boolean特征 Value特征 定义特征 基本特征属性 其他特征属性 特征层级 检查特征 使用RequiresFeature特性 ...

随机推荐

  1. IOS 开发之--获取真机的deviceToeken

    获取真机的devicetoken的方法: #pragma mark 注册APNs成功并上报DeviceToken - (void)application:(UIApplication *)applic ...

  2. laravel 添加 404 页面

    1)使用 laravel 抛出 404 头很简单 abort(404); 还可以添加描述 abort(404, '404 File Not Fund'); 2)如果想自定义 404 页面模版,直接添加 ...

  3. [JAVA]基于微信公众平台开放接口编写的sdk

    最近在研究微信公众平台提供的公众服务号,以及提供的开放接口. 写了一个相对来说比较简单的基于java的微信sdk,目前实现的功能没有覆盖所有接口. 有兴趣的话,大家可以在这个基础上进行改进和完善,这样 ...

  4. 1:TwoSum(如果两个和为某个数,找出这俩数的位置)

    package leetcode; import java.util.HashMap; import java.util.Map; /** * @author mercy *Example: *Giv ...

  5. Go语言中的一些函数

    1.并行 通过使用goroutine和channel,go语言可以很好地支持并发,但是在我的电脑上是默认只使用一个核执行,要使用多核,在代码前面加入 import("runtime" ...

  6. 深入分析Cocos2d-x 2.0中的“纹理”

    对CCImage的绘制是通过CCTexture2D来实现的(OPENGL es)通过纹理绘制到某个面. (本文中所提到的方法在cocos2d2.0中部分有调整,请应用时候具体察看源码)1. 首先来了解 ...

  7. Code Forces 645A Amity Assessment

    A. Amity Assessment time limit per test2 seconds memory limit per test256 megabytes inputstandard in ...

  8. input即时————模糊匹配(纯html+jquery简单实现)

    <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <m ...

  9. 购物车 cookie session

    0-服务器识别用户的目的:服务器存有不同用户的信息,而对这些信息,服务器自身.网站开发管理者.网站访问者会对其读写: 1-暂且存入服务器数据库,购物车分为2种表:购物车入车表和购物车下单表: 2-单个 ...

  10. post 传递参数中包含 html 代码解决办法,js加密,.net解密

    今天遇到一个问题,就是用post方式传递参数,程序在vs中完美调试,但是在iis中,就无法运行了,显示传递的参数获取不到,报错了,查看浏览器请求情况,错误500,服务器内部错误,当时第一想法是接收方式 ...