刘朋:接口层就长这个样子啊,好像也没什么用嘛。
鹏辉:如果没有接口层的话,我们是这样定义的:
CustomerDAL customerDAL=new CustomerDAL();
有了接口层以后,我们的定义是这样的:
IDAL customerDAL=new CUstomerDAL();
在写法上的差别并不是太大,而且增加了接口层还会增加开发量。接口层的意义也就是在设计层面吧。
冲冲:你讲的接口层的目的是阻断BLL和DAL层之间的关联,但我们在写代码的时候还是会去new一个DAL对象,好像也没有达到目的啊。
MOL:你们提的问题,也是我们接下来要探讨的知识。其实,冲冲的疑问是比较关键的,只要把new的问题解决了,大家也就自然了解了接口层其实并不是鸡肋。
为了不使用new来实例化一个DAL,那么我们会考虑使用工厂模式来达到这个效果。
工厂模式算是一种比较常见的设计模式(Design Pattern),工厂模式的目的是不用new来创建一个实例。接下来用“演绎法”来描述一下工厂模式。
前面已经说过,加入接口层以后,实例化一个对象可能是这样:
IDAL customerDAL=new CUstomerDAL();
我们不想用new的方式来创建实例,而希望是这样:
IDAL customerDAL=工厂.创建(实例类型);
第一反应,就是把new的操作放到工厂中去执行。那么工厂就需要根据传入不同的实例类型来“制造”出不同的对象实例。最简单的方式是使用switch…case来输出对应的对象实例。例如:
public static object GetInstences(string className) { switch (className) { case "userDal": return new userDal(); case "orderDal": return new orderDal(); case "pageDal": return new pageDal(); case "customerDal": return new customerDal(); default: return null; } }
这样就不用再显式地去new一个对象出来了,这种写法就是传说中的“简单工厂”,因为它足够简单,所以会带来很多问题。
首先,这种写法是违反DRY(Do not Repeater Yourself)的,我们可以看到大量重复的代码(return new…)。
其次,这种写法需要穷举项目中所有需要new的类名。这是一个不小的工作量,而且很有可能会遗漏掉某些类。如果直接使用简单工厂来代替new其实并没有用,反而会带来额外的工作。
那么就没有其他的办法了吗?
在.NET的机制里,有一种技术叫反射。反射可以动态地加载DLL,并实例化类(class)。例如:
//只有在当前解决方案里添加了该dll的引用后才可以使用Load Assembly objDALAss = Assembly.LoadFrom(@"E:\Project\Elands.JinCard.DAL. dll"); //Elands.JinCard.DAL.userDAL类的全路径 Type t = objAss.GetType("Elands.JinCard.DAL.userDAL"); //动态生成类StringUtil的实例 IuserDal obj = System.Activator.CreateInstance(t) as IuserDal;
上面的代码就是反射的一个基本应用,使用反射的基本步骤如下。
(1)找到dll所在的路径。
(2)找到class/interface的全路径。
(3)实例化class/interface。
这意味着不需要再去判断输入的类型,也就不需要出现大量重复的代码,最后完成的工厂代码如下:
public class DALSimpleFactory { public static Object GetInstences(string assemblyName, string typeName) { return Assembly.Load(assemblyName).CreateInstance(typeName); } }
是不是看起来干净许多了?
接下来再来创建一个userDAL,用来说明这个工厂代码如何使用:
IuserDAL userDal=DALSimpleFactory.GetInstences("Elands.JinCard.DAL", "Elands. JinCard.DAL." + "userDal") as IuserDAL;
OK,到这里为止工厂模式的使用就已经讲述完了。
鹏辉:用工厂模式的写法确实是没有new了,但是也不可避免地要写入硬编码,比如要实例化userDAL的时候,必须写入userDAL的路径,并且写入userDAL的全路径,这样和直接new有什么本质的区别呢?
MOL:new属于静态编译,也就是在编译网站的时候userDAL就会被编译放到网站中。而反射生成userDAL属于动态编译,生成网站的时候不会被编译到网站中,只有在使用userDAL的时候才会生成。最重要的是,工厂模式有效地切断了BLL和DAL层的强关联。那么实例化userDAL的时候,我们需要传入userDAL所在的DLL的路径、userDAL的全路径,这样看似有点“剪不断,理还乱”的关系,其实不然。我们传入的参数是字符串(string)类型的。也就是说,传入Elands.JinCard.DAL是正确的,传入“阿猫阿狗”也未尝不可。重点是“字符串”,作为字符串,传入的参数就可以写在配置文件中,当需要新增或修改的时候,直接修改配置文件就可以了,而不用重新编译项目。
例如,配置文件是这样的:
<appSettings> <add key="userDalRef" value="Elands.JinCard.DAL,Elands.JinCard.DAL.userDal"/> <add key="customerDalRef" value="Elands.JinCard.DAL,Elands.JinCard.DAL. customerDal"/> <add key="orderDalRef" value="Elands.JinCard.DAL,Elands.JinCard.DAL. orderDal"/> </appSettings>
这样,就把每一个DAL的配置放在了web.config中。每一个add节点都是一个DAL配置,其中,key值表示DAL配置名称,value表示配置值。key和value中描述的配置名称一定要一目了然,比如上面的配置中,key="userDalRef"表示userDal这个DAL的引用(Refrence);value中是一个以逗号分隔的字符串,其中,逗号前面的部分是DAL所在的DLL的路径,逗号后面的部分是DAL类的全路径。例如,要实例化一个userDAL,那么实例化的代码就是:
public void GetuserDal() { string[] dalCfgArr = System.Configuration.ConfigurationManager.AppSettings ["userDalRef "].Split(','); IuserDAL userDal=GetInstences(dalCfgArr[0],dalCfgArr[1]) as IuserDAL; //调用IuserDAL的方法 userDal.Select(); }
这样就完美解决了鹏辉所提到的硬编码的问题。
刘朋:这样做确实是达到了“解耦”的目的,但调用工厂去创建一个实体,也就是说,怎么老感觉有点没有“解”干净的意思呢?BLL虽然不依赖于DAL了,但却依赖了工厂。
MOL:对,BLL从依赖具体的DAL,变成了依赖抽象的工厂,这是工厂模式给我们带来的最大好处。但是DIP(Dependence Inversion Principle,依赖倒置原则)告诉我们,高层模块不应该依赖低层模块,两者都应该依赖于抽象。显然,BLL依赖了工厂,而工厂又依赖了DAL,这样互相依赖,造成了解耦不彻底。那么如何彻底地解耦呢?
PS:顺便提一下,new一个class的时候,.NET会先在内存堆中开辟一块用来存放实例的空间,然后再对实例进行初始化。而开辟内在堆空间的时候,这些空间并不是连续的,所以当一个项目中大量地使用了new的时候,就会造成内存堆中存在大量的碎片。这种现象会对真实的系统造成故障,而且这种故障会给排查过程带来很大的困难。
例如,一个服务器的内存是1GB,我们把一个网站发布到这台服务器上,这个网站在运行一段时间后,服务器的空间内存可能只剩余200MB,这200MB由一些小于2KB的内存碎片组成。如果这个时候再去new一个大于2KB的对象,那么程序就会报错。