1、IoC/DI简介
IoC 即 Inversion of Control,DI 即 Dependency Injection,前一个中文含义为控制反转,后一个译为依赖注入,可以理解成一种编程模式,详细的说明可参见大牛Martin Fowler的强文 ,借用Hollywood的名言:Don't call us, we'll call you,意即你呆着别动,到时我会找你。控制反转的核心是控制权的转移,从原有的应用程序转移到框架如IoC容器,从而实现模块间的解耦。
2、Unity是什么?
Unity是微软Patterns & Practices团队所开发的一个轻量级的,并且可扩展的依赖注入(Dependency Injection)容器,它支持常用的三种依赖注入方式:构造器注入(Constructor Injection)、属性注入(Property Injection),以及方法调用注入(Method Call Injection)。现在Unity最新的版本的3.5版,可以在微软的开源站点:下载最新的发布版本和文档。
它有助于构建松耦合的应用程序和为开发者提供以下便利:
- 简化对象的创建,特别在分层对象结构和依赖的情形下;
- 它支持需求的抽象化,这允许开发人员在运行时或在配置文件中指定依赖,简化横切关注点(crosscutting concerns)的管理;
- 它通过把组件配置推给容器来决定,增加了灵活性;
- 服务定位能力; 这使客户端能够存储或缓存容器;
- 轻松构建松耦合结构的程序,从而让整个程序框架变得清晰和易于维护。
3、如何使用Unity?
下面我们用一个简单的例子来演示如何使用Ioc框架:Unity。我们的大多数应用程序都是由两个或是更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要获取与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。使用Unity就是要解决这个这个问题。
Martin Fowler在那篇著名的文章《Inversion of Control Containers and the Dependency Injection pattern》中将具体依赖注入划分为三种形式,即构造器注入、属性(设置)注入和接口注入,习惯将其划分为一种(类型)匹配和三种注入:
- 类型匹配(Type Matching):虽然我们通过接口(或者抽象类)来进行服务调用,但是服务本身还是实现在某个具体的服务类型中,这就需要某个类型注册机制来解决服务接口和服务实现类型之间的匹配关系;
- 构造器注入(Constructor Injection):IoC容器会智能选择选择和调用适合的构造函数以创建依赖的对象。如果被选择的构造函数具有相应的参数,IoC容器在调用构造函数之前解析注册的依赖关系并自行获得相应参数对象;
- 属性注入(Property Injection):如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后,IoC容器会自动初始化该属性;
- 方法注入(Method Injection):如果被依赖对象需要调用某个方法进行相应的初始化,在该对象创建之后,IoC容器会自动调用该方法。
依赖翻转的核心原则:
1、高层模块不应该依赖底层模块,两个都应该依赖抽象(抽象类或接口);
2、抽象不应该依赖细节,细节应该依赖抽象。
下面我采用“属性注入(Property Injection)”的方式演示如何使用Unity框架。
3.1、定义接口类
一个简单日志记录接口类,用于记录请求的报文。
public interface ILogger { TupleGetLogContent(AtomRequest request, AtomResponse response = null); void Log(Tuple logInfo); }
3.2、定义接口的实现类
定义2个接口的实现类,分别用来实现对请求的响应的报文进行日志记录。
internal class LogRequest : ILogger{ public TupleGetLogContent(AtomRequest request, AtomResponse response = null) { var reqType = request.GetType().Name; switch (reqType) { case "AtomQueryRequest": { var req = (AtomQueryRequest) request; return Tuple.Create(request.CollectionId, string.Format("流水号:{0} 交易类型:{1}", req.TransNo, req.TransType)); } case "AtomSaleRequest": { var req = (AtomSaleRequest) request; return Tuple.Create(req.CollectionId, string.Format("流水号:{0} 订单号:{1} 金额:{2}", req.TransNo, req.OrderNo, req.LocalAmount)); } default: { throw new CheckRequestException("无效的交易类型:".Contact(reqType)); } } } public void Log(Tuple logInfo) { LogManager.InfoRequest(logInfo.Item1, logInfo.Item2); }}
3.3、定义容器并注册接口及接口实现类之间的映射关系
项目引用:Microsoft.Practices.Unity.dll
internal class ServiceContainer{ // 核心容器类型的定义,设置为静态的,公开的,可为其它任何类型调用。 public static UnityContainer RtpContainer; // 初始化容器并在容器中注册项目程序中所有的依赖关系 static ServiceContainer() { RtpContainer = new UnityContainer(); // 每次调用,容器都会生成一个新的对象实例 RtpContainer.RegisterType(); // 注册为单例,任何时候调用都使用同一个对象实例 RtpContainer.RegisterType ("Request", new ContainerControlledLifetimeManager()); RtpContainer.RegisterType ("Response", new ContainerControlledLifetimeManager()); }}
什么时候注册为单例,我的个人标准为:实现类,比如:LogRequest类,没有静态成员或者静态成员没有并发写的可能都可以用,使用单例可以减少频繁创建对象可能造成的开销。在注册类型时,如果要注册为单例模式,额外传入一个:new ContainerControlledLifetimeManager() 参数即可,表示创建对象的生命周期由容器来控制。另外如果一个接口由多个实现类,如上面的LogRequest和LogResponse都实现了ILogger接口。这样在注册map映射关系时,需要额外使用一个name参数(比如上面的“Request”,“Response”)来唯一标识map关系。使用XML也可以实现依赖关系的注册,但我更倾向于使用:约定优于配置(convention over configuration)的原则,也称作按约定编程,是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。
3.4、依赖注入
internal abstract class AbstractRequest{ static AbstractRequest() { JsConfig.EmitCamelCaseNames = true; JsConfig.IncludeNullValues = true; } [Dependency("Request")] public ILogger Logger { get; set; } [Dependency] public IRequestCheck RequestCheck { get; set; } protected virtual void CheckRequest(AtomRequest request) { if (null == request) { throw new ArgumentNullException("request", "请求实体不能为NULL"); } }}
上面的代码我采用了“属性注入(Property Injection)”的方式注入了一个Logger属性,并且Dependency属性类的name参数为:Request,标识当AbstractRequest的实现类被实例化时,IoC容器自动初始化该Logger属性为一个LogRequest对象实例。
3.5、使用注入的属性
internal class ProcessAtomSaleRequest : AbstractRequest, IAtomRequest{ [Dependency("AtomSale")] public MessageProviderFactory MessageProviderFactory { get; set; } public object Execute(AtomRequest request) { CheckRequest(request); // 使用AbstractRequest类中注入的属性Logger,ProcessAtomSaleRequest被实例化时Logger属性自动被初始化为LogRequest实例对象 Logger.Log(Logger.GetLogContent(request)); }}
3.6、使用容器来解析并创建对象实例
public class AtomSale : AtomTransaction{ protected override object ProcessRequest(AtomRequest request) { return ((IAtomRequest)ServiceContainer.RtpContainer.Resolve(typeof(IAtomRequest), "AtomSale")).Execute(request); }}
ServiceContainer.RtpContainer就是我们前面定义的静态的、公共的容器类(类型为:UnityContainer),在第一次被调用时初始化。Resolve方法通过指定抽象类型及对应的name属性来确定唯一映射关系并创建对象,最后执行对象的Execute方法。
总结:
使用IoC框架后,使用相同架构模型的应用,其高层抽象可以完全移植,只须关注实现类的业务细节即可,业务逻辑修改时也只须改动实现的部分,这样就实现了抽象层和实现层的分离,同样对象创建职责也做了转移。系统解耦或松耦合也就顺理成章了。如果所有代码都揉合在一起,任何代码的修改都可能对其它代码造成影响,如果没能细致和全面的回归测试,线上故障也难免发生。
帮助到您了吗?
打赏作者(支付宝):