IoC的全称是Inversion of Control,中文叫控制反转。要理解控制反转,可以看看非控制反转的一个例子。
public class MPGMovieLister { public Movie[] GetMPG() { var finder = new ListMovieFinder(); var allMovies = finder.FindAll(); return allMovies.Where(m => m.Name.EndsWith(".MPG")).ToArray(); } } public class ListMovieFinder { public ListFindAll() { return new List { new Movie { Name = "Die Hard.wmv" }, new Movie { Name = "My Name is John.MPG" } }; } }
上面的例子中,类MPGMovieLister的作用是列出所有的mpg类型的电影,其中调用了类ListMovieFinder类的方法FindAll()来获取所有的电影。
这段代码看起来还不错,已经符合当前的需求了。
假如,这个时候,movie的列表获取不是直接创建一个list获取,而要求从某个文本文件读取,或者是数据库获取,又或者从web service中获取,我们怎么办?
第一步,再实现一个类, 比如FileMovieFinder,来实现从文本文件中读取Movie列表,再把MPGMovieLister中的这行代码,
var finder = new ListMovieFinder();
替换成
var finder = new FileMovieFinder();
那么这行代码就又能够符合要求了。
新的MPGMovieLister代码是这个样子:
public class MPGMovieLister { public Movie[] GetMPG() { var finder = new FileMovieFinder(); var allMovies = finder.FindAll(); return allMovies.Where(m => m.Name.EndsWith(".MPG")).ToArray(); } }
如果底层--获取数据的方式不确定,或者经常更改,MPGMovieLister的代码岂不是要频繁改动?
MPGMovieLister的功能都是依赖着具体的类,ListMovieFinder,FileMovieFinder。当需求发生变化的时候,就会导致MPGMovieLister的代码也要做相应的改动。
也就是说,MPGMovieLister直接依赖于ListMovieFinder和FileMovieFinder了。
跳出来看,MPGMovieLister的功能只是负责从列表中找出MPG的movie, 至于movie从什么地方来的,不是MPGMovieLister的职责,它也不需要关心。
而解耦合的方法就是”依赖于抽象,而不是依赖于具体”.
(这个例子非常类似于我们的做开发时候的持久层(数据层)和业务逻辑层,其实业务逻辑层也不关心数据是如何提供的,所以业务逻辑层也应当与持久层解耦合。)
实际解决之后的代码:
public class MPGMovieLister { public Movie[] GetMPG() { var finder = MovieFinderFactory.GetFinder(); var allMovies = finder.FindAll(); return allMovies.Where(m => m.Name.EndsWith(".MPG")).ToArray(); } } public class MovieFinderFactory { public static IMovieFinder GetFinder() { return new FileMovieFinder(); } } public interface IMovieFinder { ListFindAll() }
这里MPGMovieLister就依赖于IMovieFinder接口(依赖抽象), 实际运行时候的实例化由MovieFinderFactory来提供。这样,不同的Movie数据源只需要一个实现IMovieFinder 的类就可以了,不会对MPGMovieLister产生任何影响。
到这里,实际上已经完成了IoC, 控制权最初取决于MPGMovieLister中是如何实例化MovieFinder 的,现在它已经交出控制权,交由外部来提供具体实例对象了。
这里的MovieFinderFactory就已经是一个简陋的IoC容器功能了。
IoC这种解决依赖的方法是面向对象方法的使用。现实世界中,这种方法无处不在。
比如,汽车不会强依赖于某个品牌的轮胎,任何公司生产的轮胎,只要符合汽车的接口,就可以装在这个汽车上使用。
还有电脑的USB接口,只要符合USB标准的外设,就都能够接上电脑使用。
解除依赖不仅让代码结构看起来更加合理,其带来的另一个好处是,各个部分可以单独的做单元测试,使得单元测试能够更加容易的进行。这个对于一些复杂度高的项目,对于保证项目的稳定性和可用性非常有意义。
真正的IoC容器比上面的MovieFinderFactory自然要好用和适用的多。下一篇文章将会介绍一个非常棒的IoC框架Autofac.
我们回顾一下之前的代码:
//这个类的作用是筛选出MPG类型的电影 public class MPGMovieLister:IMovieFinder { public Movie[] GetMPG() { var finder = MovieFinderFactory.GetFinder();//这里调用工厂类获取具体的实例,得到一个电影列表 var allMovies = finder.FindAll(); return allMovies.Where(m => m.Name.EndsWith(".MPG")).ToArray(); } } public class MovieFinderFactory { public static IMovieFinder GetFinder() { return new ListMovieFinder(); } } public class ListMovieFinder :IMovieFinder { public ListFindAll() { return new List { new Movie { Name = "Die Hard.wmv" }, new Movie { Name = "My Name is John.MPG" } }; } } public interface IMovieFinder { List FindAll() }
这里MPGMovieLister已经不和具体的MovieFinder耦合了,而是依赖于MovieFinderFactory工厂类提供的IMovieFinder接口的具体实现来取Movie数据。
所以工厂类只要返回不同的实现IMovieFinder的实例,就能够让MovieLister从列表,文本,数据库,web service …… 中获取数据。
在应用Autofac替换MovieFinderFactory之前,我们先从代码中去掉MovieFinderFactory, 改动之后的代码是这样:
public class MPGMovieLister { private readonly IMovieFinder _movieFinder; //增加了构造函数,参数是IMovieFinder对象 public MPGMovieLister(IMovieFinder movieFinder) { _movieFinder = movieFinder; } public Movie[] GetMPG() { var allMovies = _movieFinder.FindAll(); return allMovies.Where(m => m.Name.EndsWith(".MPG")).ToArray(); } } public interface IMovieFinder { ListFindAll() }
我们去掉了工厂类MovieFinderFactory, 改造了MPGMovieLister, 添加了一个构造函数, 构造函数要求使用MPGMovieLister时,需要提供一个IMovieFinder的实例。
应用Autofac改造上面的代码。
第一步: 从Nuget中添加Autofac引用
第二步:
* 创建一个ContainerBuilder对象(ContainerBuilder从字面的意思就是用来创建Container(容器)的,而Conainter就是我们从中取各种我们需要对象的地方)
* 注册我们后面将从容器中取出对象的类型。
代码是这样:
var builder = new ContainerBuilder();// builder.RegisterType().AsImplementedInterfaces();//注册ListMovieFinder类型,这里的AsImplementedInterfaces表示以接口的形式注册 builder.RegisterType ();//注册MPGMovieLister类型
* 创建容器
_container = builder.Build();
第三步: 在程序中使用 _container容器:
var lister = _container.Resolve(); foreach (var movie in lister.GetMPG()) { Console.WriteLine(movie.Name); }
理解一下Autofac为我们在背后做了什么:
首先,我们注册了类型ListMovieFinder和MPGMovieLister,这样容器就能够知道如何创建这两种类型的实例了。(类其实是创建对象的模板,当我们把模板注册给Autofac, 它就会遵循这个模板为我们提供实例)
后面的代码中,我们调用Resolve方法,取出一个MPGMovieLister的实例。
_container.Resolve();
这里还有一个需要解释的,对于MPGMovieLister类型,我们为Autofac提供了类型, 但是当Autofac创建MPGMovieLister的实例, 调用它的构造函数的时候,却遇到了问题:
它的构造函数需要提供一个IMovieFinder的实例作为参数的, 聪明的Autofac要在自己的容器里找找,看看没有有办法提供一个IMovieFinder的实例。
这个时候Autofac会发现我们注册过ListMovieFinder, 并且通过AsImplementedInterfaces()方法,指明了就是为接口IMovieFinder提供实例的。
所以Autofac会创建一个ListMovieFinder的实例,作为创建MPGMovieLister时,提供给构造函数的参数。
builder.RegisterType().AsImplementedInterfaces();
上面的例子中,我们的类ListMovieFinder实现了IMovieFinder接口, 实际运行中,是由它来提供数据。
假如这个时候,我们要从数据库中获取数据,怎么办?
非常简单,创建一个类DBMovieFinder继承IMovieFinder接口, 然后注册给Autofac就可以了。 这样程序就非常容易的切换到从数据库中取数据了。
注册相关改动的代码是这样的:
var builder = new ContainerBuilder(); builder.RegisterType().AsImplementedInterfaces(); //这里注册了DBMovieFinder, 这个类继承IMovieFinder接口。因为它也使用了AsImplementedInterfaces,它会覆盖ListMovieFinder的注册。 builder.RegisterType ().AsImplementedInterfaces(); builder.RegisterType (); _container = builder.Build();
常见的程序架构大概是: UI层, 业务逻辑层, 持久层(数据层)。
我们可以使用Autofac作为不同层之间的中间人,让UI层依赖于业务逻辑层的抽象接口,业务逻辑层依赖于持久层的接口,而实际运行过程中的实例都由Auotfac来提供。
这样我们就能够解除不同层之间的依赖,将所有的注册类型的操作在一个核心函数或者核心类中实现,那么只要修改这个函数或者类,就能够非常方便的让它们之间的依赖关系发生变化。
比如, 在一个大的项目中,持久层和业务逻辑层是并行开发的,而且是不同团队开发,这个时候业务逻辑开发团队的人在没有持久层代码的情况下,如何开始呢?
我们只要定义好持久层的接口, 业务逻辑团队再写一些Stub类(桩类)来实现这些接口,让这些Stub类来替换真正的持久层,所要做的就只是简单的把这些Stub类型注册到Autofac中就可以了。同时做业务逻辑层的单元测试也非常容易了。
拿上面的例子来说,就是这个样子:
var builder = new ContainerBuilder(); if(IsLive)//如果是正式环境中,使用DBMovieFinder, 从数据库中获取数据 { builder.RegisterType().AsImplementedInterfaces(); } else//在开发环境中,先用Stub类ListMovieFinder替代 { builder.RegisterType ().AsImplementedInterfaces(); } builder.RegisterType (); _container = builder.Build();
同样的,UI层和业务逻辑层也可以运用同样的思路。
从上面的例子可以看出,使用IoC对于复杂的项目来说,非常有意义,能够为我们搭建一个好的开发层次。
同时,在使用过程中,还能够发现Autofac有以下优点:
1. 可以使用C#代码来完成注册配置,非常方便而且便于调试。(使用xml配置,往往容易出现格式不对,或者其它问题,非常难于调试和排错)
2. 非常聪明,能够自动装配(发现构造函数需要的必须参数的时候,会自己想办法解决)
相关源代码下载: AutofactDemo.zip
我们回顾一下之前的代码:
//这个类的作用是筛选出MPG类型的电影 public class MPGMovieLister:IMovieFinder { public Movie[] GetMPG() { var finder = MovieFinderFactory.GetFinder();//这里调用工厂类获取具体的实例,得到一个电影列表 var allMovies = finder.FindAll(); return allMovies.Where(m => m.Name.EndsWith(".MPG")).ToArray(); } } public class MovieFinderFactory { public static IMovieFinder GetFinder() { return new ListMovieFinder(); } } public class ListMovieFinder :IMovieFinder { public ListFindAll() { return new List { new Movie { Name = "Die Hard.wmv" }, new Movie { Name = "My Name is John.MPG" } }; } } public interface IMovieFinder { List FindAll() }
这里MPGMovieLister已经不和具体的MovieFinder耦合了,而是依赖于MovieFinderFactory工厂类提供的IMovieFinder接口的具体实现来取Movie数据。
所以工厂类只要返回不同的实现IMovieFinder的实例,就能够让MovieLister从列表,文本,数据库,web service …… 中获取数据。
在应用Autofac替换MovieFinderFactory之前,我们先从代码中去掉MovieFinderFactory, 改动之后的代码是这样:
public class MPGMovieLister { private readonly IMovieFinder _movieFinder; //增加了构造函数,参数是IMovieFinder对象 public MPGMovieLister(IMovieFinder movieFinder) { _movieFinder = movieFinder; } public Movie[] GetMPG() { var allMovies = _movieFinder.FindAll(); return allMovies.Where(m => m.Name.EndsWith(".MPG")).ToArray(); } } public interface IMovieFinder { ListFindAll() }
我们去掉了工厂类MovieFinderFactory, 改造了MPGMovieLister, 添加了一个构造函数, 构造函数要求使用MPGMovieLister时,需要提供一个IMovieFinder的实例。
应用Autofac改造上面的代码。
第一步: 从Nuget中添加Autofac引用
第二步:
* 创建一个ContainerBuilder对象(ContainerBuilder从字面的意思就是用来创建Container(容器)的,而Conainter就是我们从中取各种我们需要对象的地方)
* 注册我们后面将从容器中取出对象的类型。
代码是这样:
var builder = new ContainerBuilder();// builder.RegisterType().AsImplementedInterfaces();//注册ListMovieFinder类型,这里的AsImplementedInterfaces表示以接口的形式注册 builder.RegisterType ();//注册MPGMovieLister类型
* 创建容器
_container = builder.Build();
第三步: 在程序中使用 _container容器:
var lister = _container.Resolve(); foreach (var movie in lister.GetMPG()) { Console.WriteLine(movie.Name); }
理解一下Autofac为我们在背后做了什么:
首先,我们注册了类型ListMovieFinder和MPGMovieLister,这样容器就能够知道如何创建这两种类型的实例了。(类其实是创建对象的模板,当我们把模板注册给Autofac, 它就会遵循这个模板为我们提供实例)
后面的代码中,我们调用Resolve方法,取出一个MPGMovieLister的实例。
_container.Resolve();
这里还有一个需要解释的,对于MPGMovieLister类型,我们为Autofac提供了类型, 但是当Autofac创建MPGMovieLister的实例, 调用它的构造函数的时候,却遇到了问题:
它的构造函数需要提供一个IMovieFinder的实例作为参数的, 聪明的Autofac要在自己的容器里找找,看看没有有办法提供一个IMovieFinder的实例。
这个时候Autofac会发现我们注册过ListMovieFinder, 并且通过AsImplementedInterfaces()方法,指明了就是为接口IMovieFinder提供实例的。
所以Autofac会创建一个ListMovieFinder的实例,作为创建MPGMovieLister时,提供给构造函数的参数。
builder.RegisterType().AsImplementedInterfaces();
上面的例子中,我们的类ListMovieFinder实现了IMovieFinder接口, 实际运行中,是由它来提供数据。
假如这个时候,我们要从数据库中获取数据,怎么办?
非常简单,创建一个类DBMovieFinder继承IMovieFinder接口, 然后注册给Autofac就可以了。 这样程序就非常容易的切换到从数据库中取数据了。
注册相关改动的代码是这样的:
var builder = new ContainerBuilder(); builder.RegisterType().AsImplementedInterfaces(); //这里注册了DBMovieFinder, 这个类继承IMovieFinder接口。因为它也使用了AsImplementedInterfaces,它会覆盖ListMovieFinder的注册。 builder.RegisterType ().AsImplementedInterfaces(); builder.RegisterType (); _container = builder.Build();
常见的程序架构大概是: UI层, 业务逻辑层, 持久层(数据层)。
我们可以使用Autofac作为不同层之间的中间人,让UI层依赖于业务逻辑层的抽象接口,业务逻辑层依赖于持久层的接口,而实际运行过程中的实例都由Auotfac来提供。
这样我们就能够解除不同层之间的依赖,将所有的注册类型的操作在一个核心函数或者核心类中实现,那么只要修改这个函数或者类,就能够非常方便的让它们之间的依赖关系发生变化。
比如, 在一个大的项目中,持久层和业务逻辑层是并行开发的,而且是不同团队开发,这个时候业务逻辑开发团队的人在没有持久层代码的情况下,如何开始呢?
我们只要定义好持久层的接口, 业务逻辑团队再写一些Stub类(桩类)来实现这些接口,让这些Stub类来替换真正的持久层,所要做的就只是简单的把这些Stub类型注册到Autofac中就可以了。同时做业务逻辑层的单元测试也非常容易了。
拿上面的例子来说,就是这个样子:
var builder = new ContainerBuilder(); if(IsLive)//如果是正式环境中,使用DBMovieFinder, 从数据库中获取数据 { builder.RegisterType().AsImplementedInterfaces(); } else//在开发环境中,先用Stub类ListMovieFinder替代 { builder.RegisterType ().AsImplementedInterfaces(); } builder.RegisterType (); _container = builder.Build();
同样的,UI层和业务逻辑层也可以运用同样的思路。
从上面的例子可以看出,使用IoC对于复杂的项目来说,非常有意义,能够为我们搭建一个好的开发层次。
同时,在使用过程中,还能够发现Autofac有以下优点:
1. 可以使用C#代码来完成注册配置,非常方便而且便于调试。(使用xml配置,往往容易出现格式不对,或者其它问题,非常难于调试和排错)
2. 非常聪明,能够自动装配(发现构造函数需要的必须参数的时候,会自己想办法解决)
相关源代码下载: AutofactDemo.zip
较复杂的应用程序都是由多个项目组织成的,项目可以划分成程序集(Assemblies)和宿主(Hosts),也就是应用程序的入口。
Assemblies 通常是常见的类库项目,包括可以重用的功能和方便测试,通常包括下面的组件:
这些项目通常不应该直接依赖于下面的组件:
为了分离这些逻辑,我们可以定义一些接口,然后通过配置代码将具体实现关联起来,例如日志记录我们可以定义一个接口ILog,生产环境下我们可以把它改成用Apache log4net或者企业类库的日志记录模块都可以。由于这是接口定义和实现分离的,我们可以在不同环境下使用不同的实现,只需要通过配置修改就可以而不要重新编译代码。
Hosts代表应用程的入口,有下面这些形式:
Host负责构建应用程环境(上下文),并把它传递给应用程序的入口,在IOC容器方面来说,通过配置容器中的应用程序组件,获取Shell类并运行。通常Host项目都很小,主要完成两个方面的工作:配置容器和调用Shell.Run()。
用Autofac的Host的伪代码类似于
var builder = new ContainerBuilder();
builder.Register(new ConfigurationSettingsReader());
using (var container = builder.Build())
{
var shell = container.Resolve
shell.Execute();
}
上述代码中new ConfigurationSettingsReader()就是autofac从配置文件中读取相关的组件配置,一般使用XML文件进行配置,autofac的xml配置文档可以看XmlConfiguration,使用配置文件也有缺点:
不是强类型的,编译器无法发现错误,没有智能提示
配置文件会变得越来越大
维护多个配置文件比较困难
文件文件不适合用于复杂的环境
上述缺点我们可以通过.NET代码块封装相关的配置细节,在XML文件中只保留粗粒度的配置,Autofac可以通过Module进行配置块的封装,具体可以参考文档StructuringWithModules。
我这里取个例子:
public class LoggingModule : Module
{
public Mode Mode { get; set; }
public static string EventLogName = "网站通行证";
public static string EventLogSource = "应用程序";
public LoggingModule()
{
Mode = NCASService.Mode.Diagnostics;
}
protected override void Load(ContainerBuilder builder)
{
// configure logging
var logger = GetLoggerForWindows(Mode);
builder.RegisterInstance(logger);
builder.RegisterInstance(logger.Get("DefaultLog"));
base.Load(builder);
}
static INamedProvider
{ // configuring different logging based on our mode
switch (mode)
{
case Mode.Release:
// write all informational and higher events to indows event log
LoggingStack.UseEventLog(EventLogName, EventLogSource)
.Filter(LogLevel.Info, LogLevel.Max);
// dump all warning and higher messages to rolling text log
LoggingStack.UseRollingLog(@"logs\errorlog.txt", 100.Kb(), 10)
.Filter(LogLevel.Warn, LogLevel.Fatal);
break;
case Mode.Diagnostics:
// dump all messages to daily log
LoggingStack.UseDailyLog(@"log.txt");
break;
case Mode.Debug:
// Visual studio would get these messages
return TraceLog.Provider;
default:
throw new ArgumentOutOfRangeException("mode");
}
return LoggingStack.GetLogProvider();
}
上述是把我们的日志模块的配置用代码进行配置,我们的XML配置文件中的配置就会变得很简单:
使用模块组织不同程序集中的组件注册到容器里,模块我一般需要配置以下内容:
通过Autofac的Module分解项目组件间的复杂关系。
Autofac是一个Ioc框架,最大的特点应该是可以不用配置文件,直接用C#代码来注册。
Autofac同时也提供了对于Asp.net MVC的扩展。
这里是Autofac的文档,介绍如何在MVC项目中集成使用:
protected void Application_Start() { var builder = new ContainerBuilder(); builder.RegisterControllers(typeof(MvcApplication).Assembly); var container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); // Other MVC setup...
解释一下里面的过程:
1. 首先创建一个ContainerBuilder(它会稍后为我们提供一个容器,我们可以从中取出我们所需的对象实例)
2. 注册当前Assembly中的所有Controllers到Builder,这样Builder就获取了当前MVC项目中的所有Controller类型
3. 创建容器
4. 用AutofacDependencyResolver替换MVC默认的DependencyResolver
OK. 到这里, 前面的还容易理解,最后一个DependencyResolver做了什么? 要了解DependencyResolver, 需要先知道ControllerFactory
MVC请求的过程,就是根据请求的URL找到匹配的Route, Route解析出来对应的Controller的名字, 然后根据名字,找到对应的Controller类型,然后实例化一个Controller的对象响应请求。
上面粗体标出的,就是DefaultControllerFactory做的事情,也就是它的方法CreateController方法:
public virtual IController CreateController(RequestContext requestContext, string controllerName)
CreateController方法又主要依赖于GetControllerType和GetControllerInstance方法:
protected internal virtual Type GetControllerType(RequestContext requestContext, string controllerName) protected internal virtual IController GetControllerInstance(RequestContext requestContext, Type controllerType)
看到这么多的virtual方法,是不是很激动? 这个不是明显是让我们继承和override的嘛。假如我们override这里的GetControllerInstance方法,然后根据这里的controllerType从Ioc容器中获取这个ControllerType的实例,不就万事OK了吗?
没错,继承和重写GetControllerInstance方法的确可以实现Controller的Ioc, 然后在Application_Start()里面,使用下面这行代码,替换掉DefaultControllerFactory.
ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory(_container));
其实DefaultControllerFactory中的GetControllerInstance,调用了IDependencyResolver接口定义的方法GetService获取实例。通过继承这个接口,替换原有的DependencyResolver, 对于MVC原有的改动更小。所以使用通过继承IDependencyResolver 接口的方式来实现更好. 这就是开篇我们看到的:
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
IDependencyResolver借口
public interface IDependencyResolver { object GetService(Type serviceType); IEnumerableGetServices(Type serviceType); }
想看看AutofacDependencyResolver的源代码是如何根据Type从容器中获取实例的, 可以到这里
Asp.net MVC中的Area的使用,应该大家比较熟悉了。它是用来解决大的项目,多人开发的情况的。area实现的原理,是通过命名空间来区分即使名字相同的Controller的。可是无论如何,area都是在一个project里面的.
如何能够分离area到不同的project中呢? 这里的GetControllerType方法就是很好的突破口。下次探讨一下如何实现area分离到不同的project中,方便较大的项目。这也是Orchard CMS模块实现的原理。