MOL是一个喜欢喝酒的人,MOL一直认为酒是人类一个很伟大的发明。追溯到遥远的母系氏族社会,我们的祖先就已经将剩余的野果储存起来,放置一段时间后,就酿成了香甜的甘醴。
我们想一下,原始的祖先们想要喝酒,他们会怎么办?首先采一堆野果,然后放在一个瓦罐中,等几个月,就可以喝到酒了。
这是不是类似于我们写代码的时候直接new一个对象出来?采一堆野果(输出构造函数的参数),放到一个瓦罐中(分配内存),等几个月(CPU工作时间),就可以喝到酒了(得到实例化对象)。从“想要”到“得到”中间所有的过程都是自己亲力亲为。
进入了封建社会后,出现了很多商铺、酒坊。如果MOL想要喝酒,就不用自己采野果酿酒了,直接到咸亨酒店,把9个大钱排开放在柜台上,说:“温两碗酒,要一碟茴香豆”,自有酒保把酒端上来。喝酒的时候,还可以和店小二讨论关于茴字有几种写法的问题。
我们再来想一下,这是不是就是工厂模式的过程?MOL想喝酒了,只需要找咸亨酒店(工厂)表明自己的需求(需要实例化的对象),工厂就会把相应的对象实例化并输出,而MOL完全不用关心酒是怎么做出来的(实例化过程)。
到了21世纪,MOL喝酒也不用去咸亨酒店了,直接在网上订购就可以了,MOL甚至不用关心是哪个工厂做的酒,只需要表达“我要喝酒”的强烈愿望就可以了。
那么,我们在写程序的时候是否也可以做到这样?只需要说“我需要一个IuserDal对象”,然后随着一声惊雷,这个对象就呱呱坠地了。听起来是不是很神奇?但这并不是天方夜谭,接下来的内容将带你完成这个看似神奇的过程。
很多80后90后的程序员一定看过86版的西游记。作为影片中的男一号(貌似这个电影中的男一号很多),唐僧这个角色性格鲜明。比如他经常会对一些化妆成为美女的妖怪自我介绍:“贫僧从东土大唐而来,前往西天拜佛求经。”;又比如他经常会说:“施主莫怕,我这几个徒弟相貌虽丑陋点,但心地善良”。可是大家有没有注意过,在86版的西游记中,唐僧这个角色是由3个演员组团塑造的。
这里不方便写出这3位演员的真实名讳,暂且以演员A、演员B、演员C来说明吧。我们以唐僧把孙猴子从五行山下救出的那一集来举例说明。
在电影开拍的时候,导演基本上是不会想到扮演唐僧的演员换了一茬又一茬,所以演员A在表演把孙猴子从五行山下救出来的时候,他会说:“揭了压帖矣,你出来么。”
用程序表示就是:
演员A actor=new 演员A(); Actor.Say("揭了压帖矣,你出来么。");
事实往往就是这么不可预料,当到拍唐僧救孙猴子的那一集时,演员A已经离开了剧组,替代演员A的是演员B,所以这句台词应该由演员B来说。
用程序表示就是:
演员B actor=new 演员B(); Actor.Say("揭了压帖矣,你出来么。");
这样写程序肯定是不对的。角色(Actor)和演员之间造成了强依赖,当演员变换时,就需要重新修改程序、重新编译。
我们完全可以使用一个接口来规范演员,这个接口是这样的:
public Interface IActor { void Say(); }
也就是说,只有实现了IActor接口的对象才可以充当演员的角色。借助工厂模式,代码如下:
IActor actor=Factory.CreateInstance("演员B的DLL所在的路径","演员B的DLL的全路径");
问题解决了,电影可以接着往下拍了!
但好景不长,演员B也离开了剧组,演员C替代了他的位置,程序不得不修改为:
IActor actor=Factory.CreateInstance("演员C的DLL所在的路径","演员C的DLL的全路径");
前面提到,工厂方法里的参数可以写在配置文件中,因此直接修改配置文件就可以了。
但这还不够完美,完美的过程是下面这样的。
编剧说:第13集中的唐僧有一个武打片断。
导演说:没问题,你不用关心演员的事情,你只需要确定“角色”就可以了。
演员XXX说:导演,我有特殊技能,我会跳远。
导演说:好的,你实现了动作演员的接口,等到动作演员需要出场的时候我叫你。
这样一来,编剧不知道演员的存在,而演员也不认识编剧。演员和编剧之间通过导演来调配,需要演员的时候,把演员叫出来上场(实例化),不需要演员的时候就可以把他辞退(销毁)。
用程序来描述就是:
private IActor actor{get;set;}; public void Display() { This.actor.Say("揭了压帖矣,你出来么。"); }
有人可能疑惑了:就声明一个接口类型的对象,都没有实例化,就可以直接使用了?答案是肯定的。
声明接口类型的对象,这是编剧做的事情。表示“我需要IActor类型的演员”。
程序运行到This.actor.Say("揭了压帖矣,你出来么。")的时候,导演发现Actor还没有实例化,赶紧找一个演员补上。也就是说,编剧只需要表达需求就可以了,剩下的工作完全是由导演来完成。
原本需要编剧来创建一个演员,现在由导演来“现找”一个演员来完成。这样把原来的过程进行颠倒就是“控制反转”(Inverse Of Control,IOC)。
编剧本来是依赖具体的演员,而现在变成了依赖于抽象的导演“注入”一个演员(导演先找到一个IActor对象,然后再提供给编剧),这就是依赖注入(Dependence Injection,DI)。
这就是大家经常会听到的控制反转和依赖注入。这两个概念比较抽象、难于理解且易混淆的,MOL不要求大家能快速地说出这两个概念到底是什么意思,但是一定要理解这种“注入”的思想。
了解了IOC的思想后,就需要介绍一下常见的IOC框架。
常见的IOC框架有Autofac、Castle Windsor、Unitl、Spring.NET、MEF等。在这些框架中,Autofac在IOC方面的表现是非常良好的,但MOL挑选的是Spring.NET来讲解的,这是为什么呢?
Spring.NET是开源的,这一点就足以吸引大量开源爱好者的眼球了。开源意味着程序员可以很方便地看到源代码,很容易地了解它的工作原理,并且还可以和很多志同道合的程序员交流、探讨。
Spring.NET是从Java移植到.NET平台的。如果一个项目由.NET部分和Java部分组成,那么应尽量使用双方都熟悉的框架,这样会给沟通和维护带来很大的方便,包括以后还会提到的NHibernate,都是这样。
Spring.NET不仅有IOC的功能,还集成了很多其他好用的功能,如AOP(面向切片的编程)等。除此之外,我们还能通过使用Spring.NET来体验一下Java程序员是怎么思考的,这对.NET程序员的帮助很大。
综合考虑,因此MOL选择Spring.NET来进行讲解。
前面已经说过了,Spring.NET有很多功能,还有很多好处,那么Spring.NET到底该怎么使用呢?
Spring.NET中包含有很多概念,MOL挑一些我们能用得着的概念简单说一下。
IOC容器是Spring.NET在IOC方面的一个“灵魂”概念,容器就相当于剧组中的“导演”,它负责生成一个对象给调用者(编导)。
IOC目标对象就相当于演员,编导不关心演员是谁,那就只能由导演来实例化演员了。导演(IOC容器)会从配置文件中找到对应的Object节点进行实例化。
在具体的代码中,就需要在配置文件中描述这两个角色(IOC容器、IOC目标对象)。配置文件可以是App.config或web.config。在配置文件中其配置如下:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/> <section name="objects" type="Spring.Context.Support.DefaultSection Handler, Spring.Core"/> </sectionGroup> </configSections> </configuration>
我们可以很清晰地看到,sectionGroup节点中描述了两个角色,分别是context和objects,context就是IOC容器,而objects就是IOC目标对象。
当然,只有这些描述还是不够的,还需要描述具体的IOC目标对象的类型。注意,IOC目标对象的描述一定要在容器中进行,而容器一定要放在spring节点中,例如下面的配置:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/> <section name="objects" type="Spring.Context.Support.DefaultSection Handler, Spring.Core"/> </sectionGroup> </configSections> <spring> <context> <object id="userDALRef" type=" Elands.JinCard.DAL.userDal" /> </context> </spring> </configuration>
上面粗体显示的配置描述了我们要在容器中配置一个userDal的对象,这个对象由context容器来管理。
是不是so easy?
配置完成以后,代码中应该如何使用呢?
首先需要有一个容器对象,这个容器对象将管理配置文件中的所有IOC目标对象,获取容器对象的方法很简单:
IApplicationContext ctx = ContextRegistry.GetContext();
IApplicationContext接口类型是容器类型的父类,在Spring.NET中有很多的容器类型,这里就不展开说了。
有了容器对象,就可以通过容器来获取IOC目标对象了。获取userDal的方法如下:
IuserDAL userDal= (IuserDAL)ctx.GetObject("userDALRef");
其中,userDALRef就是配置文件中描述的object节点中的Id属性。
这样就可以使用IuserDAL对象了。
经过MOL的讲解,大家似乎已经知道了怎么使用Spring.NET解耦。
李冲冲:虽然通过容器来获取对象这种写法和我们以前的代码风格不一样,但还是比较好接受的。你以前说可以只声明,不实例化就可以使用IuserDAL对象,但上面的代码还是在声明之外,需要再做一下获取的步骤吗?
MOL:我们的目标是只声明IuserDAL,其他的啥都不做,这也是接下来要讲的内容。只要理解了上面讲的获取对象的方法,下面的内容就很好理解了。
通过前面的讲解我们了解到,获取一个对象,需要以下两个步骤:
(1)获取容器。
(2)通过容器来获取接口对象。
我们可以通过单例设计模式来获取容器,保证获取容器的代码不会重复。但是获取接口对象就不能幸免了,必须写大量的获取接口的代码,例如:
IuserDAL 对象A= (IuserDAL)ctx.GetObject("接口A"); IuserDAL 对象B = (IuserDAL)ctx.GetObject("接口B "); IuserDAL 对象C = (IuserDAL)ctx.GetObject("接口C ");
在写WinForm代码的时候,这样的重复是不可避免的。但是“晋商卡”项目是一个Web应用,那么这个事情就变得简单了。
Spring.NET简直就是一个神器,它包含了许多功能。接下来要使用的MVC组件,也是其功能之一。
大家都知道,MVC项目中关于MVC的定义是在Global.asax这个文件中的,如果使用微软自带的MVC,定义是这样的:
public class MvcApplication : System.Web.HttpApplication
而MOL更愿意使用Spring.NET提供的MVC(至于原因,之后就明白了),需要把MVC的定义修改为:
public class MvcApplication : Spring.Web.Mvc.SpringMvcApplication
其他的代码不需要做任何调整。
那么为什么使用Spring.NET提供的MVC呢?使用Spring.NET的MVC可以不用关心容器是什么样的,也不用管它存放在哪里;不用去手动写代码获取IOC目标对象了。这样一来,就不会出现大量重复性的代码,真正地实现了:声明接口对象就可以直接使用。
例如,现在有一个BLL层需要调用IuserDAL对象,这个BLL层如下:
public class userBll { //定义一个IuserDAL类型的对象 Public IuserDAL userDal{get;set;} Public AddUser(userModel input) { //调用IuserDAL的Add()方法 userDal.Add(input); } }
在这段代码里,我们没有写任何获取IuserDAL对象的代码,那么UserDAL对象是怎么来的呢?请看配置文件:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.MvcContextHandler, Spring.Web.Mvc3" /> <section name="parsers" type="Spring.Context.Support.NamespaceParsers SectionHandler, Spring.Core" /> </sectionGroup> </configSections> <spring> <context> <object type="Elands.JinCard.BLL.userBLL, Elands.JinCard.BLL" singleton= "false" > <property name="IuserDAL" ref="IuserDALREF" /> </object> <object id="IuserDALREF" type="Elands.JinCard.DAL.userDAL,Elands.JinCard. DAL" singleton="false" ></object> </context> </spring> </configuration>
在这个配置文件中,描述了userBLL中需要一个叫IuserDAL的对象,而IuserDAL对象是引用id="IuserDALREF"的object节点的。
找到<object id="IuserDALREF"…>这个节点,就可以确定IuserDAL是要用userDAL这个类来实例化,然后容器就会按照相应的类型去实例化该对象。
所有这一切过程,都是在Spring.NET的MVC组件中完成的。细心的读者一定看出来了,我们在定义容器的时候使用的已经是Spring.Web.Mvc3了。
经过这样的配置,就达到了“只配置,不写代码”的目的。
很多读者觉得这些配置好乱,看得太晕了,还是new来得简单一些。其实学Java的人刚开始也是这样的,被各种各样的配置弄的晕头转向,但看得多了,也就习惯了。一般的同学在被虐一个月之后,基本上都觉得这种写法真是太爽了!大家自己再把MOL讲过的代码输入一遍,在输入代码的时候一定会遇到缺少引用的问题,所以在创建好解决方案之后,一定要把DLL引用到项目中。DLL路径是:源代码\第3章\简单三层\Elands.JinCard\Library\Spring。