一. 引子(Foreword)
随着国际之间的交流日益频繁,现在的学校都开始招收国外的学生,来促进中国与其他国家之间教育事业沟通。学校对每一个不同国家的学生,在注册学籍、教学安排乃至住宿班级安排都会有所不同。现在我们通过程序的方式模拟一下不同国家学生入学的时候,进行注册的处理。
二. 抽象为代码(Abstract Coding)
我们将上面的引子抽象为代码。并且假定它存在于学校某个管理软件的某一个对象的方法中。并且会将一个学生对象当做参数传入。如下代码:
(1)学生信息类
public class StudentInfo { //学生姓名 public string Name{get;set;} //学生国家 public string Country{get;set;} }
(2)注册方法
//… public void StudentRegister (StudentInfo studentInfo) { if(studentInfo.Country == "中国人") { ChineseStudentRegister(studentInfo); } else if(studentInfo.Country == "英国人") { BritishStudentRegister(studentInfo); } else if(studentInfo.Country == "美国人") { AmericanStudentRegister(studentInfo); } else if(studentInfo.Country == "日本人") { JapaneseStudentRegister(studentInfo); } else if(studentInfo.Country == "德国人") { GermanStudentRegister(studentInfo); } else if(studentInfo.Country == "韩国人") { KoreanStudentRegister(studentInfo); } } //…
三. 对上面代码的点评(Comment)
通过判断上面studentInfo.Country中的国家,我们使用多个if-else条件可以让学生的实体正确的进去对应的注册条目。也就是说基本的对学生的分类入学注册的功能已经基本的实现。
但是,如果要再加入一个学生,那么我们要再加入一个if-else。换句话来说,我们每次都要添加新的代码,并且要对原来的工程进行重新的编译。而且每次添加或者删除都需要重复这个过程。这无疑是一件很麻烦的事,而且总是修改原有工程对用户来说是一种很差的体验。
那么我们怎么样才能做到,对上面的if-else增加,又不用修改原有代码呢?这样的问题能否解决呢?答案是肯定的,下面我们就来介绍如何解决这个问题。
四. 代码抽象(Code Abstraction)
每次增加if-else的时候,都是要增加一个if的条件和一个if的执行方法体。我们对if的条件称为业务条件,把if的方法体称为业务逻辑。
由于要对if体进行统一的管理,所以我们采用面向对象的思维。把if的条件即业务条件抽象为一个业务条件判断方法,把if的执行体即业务逻辑抽象为一个业务逻辑过程处理的方法。然后把他们整体放入一个类当中即放入一个业务体中。
我们按这种思路,对上面的代码进行抽象,先抽象一个业务体的接口。代码如下:
(1)if-else抽象为统一的接口
/// <summary> /// 学生业务体 if-else的抽象 /// </summary> public interface IBusinessOperation { //用来判断这个学生是不是要做业务逻辑 bool isDoBusiness(StudentInfo studentInfo); //用来对学生进行对应的注册 void RegisterOperation(StudentInfo studentInfo); }
说明:上面的代码中isDoBusiness方法相当于原来的if的条件体,用来判断这个学生是不是要进行处理;而RegisterOperation方法就是用来在要处理的情况执行的if执行体。
(2)对每一个学生的业务处理体继承接口实现
public class ChineseStudentBusiness : IBusinessOperation //实现if的抽象接口 { /// <summary> /// 对if条件的抽象 /// </summary> public bool isDoBusiness(StudentInfo studentInfo) { if (studentInfo.Country == "中国人") { return true; } else { return false; } } /// <summary> /// 对if内容体的抽象 /// </summary> public void RegisterOperation(StudentInfo studentInfo) { Console.WriteLine("对中国学生:"+ studentInfo.Name+" 进行注册!"); } }
(3)改写入学注册方法
//… public void StudentRegister(StudentInfo studentInfo) { if (ChineseStudentBusiness.isDoBusiness(studentInfo)) { ChineseStudentBusiness.RegisterOperation(studentInfo); } else if (BritishStudentBusiness.isDoBusiness(studentInfo)) { BritishStudentBusiness.RegisterOperation(studentInfo); } else if (AmericanStudentBusiness.isDoBusiness(studentInfo)) { AmericanStudentBusiness.RegisterOperation(studentInfo); } else if (JapaneseStudentBusiness.isDoBusiness(studentInfo)) { JapaneseStudentBusiness.RegisterOperation(studentInfo); } else if (GermanStudentBusiness.isDoBusiness(studentInfo)) { GermanStudentBusiness.RegisterOperation(studentInfo); } else if (KoreanStudentBusiness.isDoBusiness(studentInfo)) { KoreanStudentBusiness.RegisterOperation(studentInfo); } } //…
相信很多人看到这里,相信大部分的读者一定会说:“窝草!这和原来的有什么却别,感觉写的更加复杂了,而且更加难以被理解。
确实,在某种意义上抽象意味着统一,但与此同时会增加原有代码的复杂度。有的时候想要优化代码,必然会造成某些方面的损耗。
但是,让我们看看上面代码的优点:
(1)每一个if-else中无论是条件体还是执行体都有一个统一的格式,而且每一个学生对应的类都有一个统一的基类,即继承自IbusinessRegister
(2)只要实现一个继承自IbusinessRegister的类,我们就可以用这个类用上面的写法进行插入到代码中;
五. 再次修改代码(Modify The Code)
由于我们观察到上面的if-else每一个格式基本相同,于是考虑是不是可以把他们放在一个foreach中,然后遍历一个存放着这些学生业务体类的列表中。于是我们将代码修改为如下形式:
//保存着学生业务对象的列表容器 private static List<IBusinessOperation> countainer = null; //将对象放入容器中 public static void Register() { countainer = new List<IBusinessOperation>(); //创建学生业务处理实体 ChineseStudentBusiness chineseStudentBusiness = new ChineseStudentBusiness(); //...省略了其他的创建 //学生业务处理实体放入容器中 注册 countainer.add(chineseStudentBusiness); //...省略了其他的注册 } //从容器中获得第一个能让if中条件通过的对象 private static IBusinessOperation GetObjectInstance(StudentInfo studentInfo) { foreach (var item in countainer) { if(item.isDoBusiness(studentInfo)) //执行if中的条件判断 { return item; } } return null; } //如果有选出的对象,那么执行业务逻辑 public static void DoBussiness(StudentInfo studentInfo) { try { IBusinessOperation obj = GetObjectInstance(studentInfo); if (obj != null) { obj.RegisterOperation(studentInfo); } } catch { } }
稍微解释一下代码,我们先将上面的学生业务处理体放入容器中,然后通过执行每一个业务体的判断方法,直到有一个返回为true时,获取这个对象。然后再执行这个对象的业务逻辑处理方法。
我们可以观察上面的代码,很明显的可以看到,这个代码要比之前的代码更加的复杂,但是执行的效果却和原来的一样。那么他的作用是什么呢?那么下面将进入我这次文章要讲述的正题,先引入两个概念。
六. 控制反转(IOC)
我们先来看看百度上对控制反转、依赖注入和依赖查找的解释:
(1)控制反转:控制反转(Inversion of Control,英文缩写为IoC)把创建对象的权利交给框架,是框架的重要特征,并非面向对象编程的专用术语。它包括依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup)。
(2)依赖注入:将一个有框架选择对象注入一个需要的参数中,这个参数可能是成员字段、属性、构造函数参数和方法函数的参数。
(3)依赖查找:框架选择根据某种条件选择一个已有的参数或者等等匹配的对象的过程。
在我上面修改的程序中,GetObjectInstance方法就是依赖查找的实现,而DoBussiness中将找到的对象进行了注入并且调用。上面的代码的过程即为一个控制反转的过程。在用户代码调用DoBussine的时候,它根本不知道程序中对学生的业务体对象进行选择,而这个选择完全是由我写的框架实现。
用户代码根本不知道学生的业务体对象的存在,也就是说我们对用户代码和学生的业务体对象进行了解耦。学生的业务体对象可以独立于客户端代码存在。这便是我们解决如何才能不用修改原有代码(客户代码)实现if-esle的添加的基础。
(“博主等一下,你还没有讲什么是谓词呢!”
“哼哼...”
由于谓词不是这一篇内容的重点,这个我简单的介绍一下,具体的详细描述我会在以后的文章中细讲。这个东西和SOA比较有关。
(4)谓词:我们把if-else和do-while的抽象统一的称为谓词。注意是抽象不是原来的代码。)
七. 进一步解释(More Example)
可能有的人看的云里雾里的,这里我给一个找保姆的例子来解释一下,如果读者看懂了上面的解释,那么可以跳过着一段。
有的人比较有钱,会找保姆来清洁房子,每次找的保姆都会一次换一个。如下图一:
图一
每次找保姆,客户都会和保姆形成直接的雇佣关系。如果把客户看做客户代码,把保姆看做一个实体类,那么客户会和保姆形成直接的依赖关系,客户通过调用保姆的清洁的方法来打扫房间。每次换一个保姆,客户都要重新和新的保姆形成依赖。即要修改原来的客户端代码。
现在我们引入一个家政公司,由家政公司来处理清洁。如下图二:
图二
家政公司中有一个保姆的列表,如果客户有需要,家政公司会寻找一个保姆,为客户进行清洁工作。对于客户来说,他的直接的依赖是家政公司,如果把家政公司看做一个类。那么客户调用的家政公司的代码,客户根本不会去关心,哪一个保姆会去打扫。选择保姆的事情,完全是由家政公司进行处理,如果把这个看做代码,即由框架实现了保姆的选择。这个就是控制反转。由原来客户直接选保姆,变成了家政(框架)来选。
当然要增加保姆,只要让家政公司再去聘请一个就行了,完全和客户无关。
通过这个例子,想必大家已经很清楚了上面(1)-(3)这三个概念了。如果还有不懂的,也可以自己去百度上看看别人的解释。这个对理解我上面的代码很重要。
八. 修改注册(Change Register Way)
可能有人会问,上面的代码还不是要自己声明学生业务体的对象来把其添加到容器中吗。确实,如果单纯的创建实体来添加,那么代码还是回合学生业务体的对象耦合。这里我们通过反射的方式,从一个程序集中动态的查找继承自IbusinessRegister的类,并反射创建出来,将其放入容器中。而这个实现仅仅只需要告知程序,需要载入哪一个程序集即可。
由于比较流行程序载入对象和配置载入对象(好像好多网上的库都是这样写的,这里我们也模仿一下),我们保留原来的程序载入,给程序添加成为可以。
我们以优先从程序载入,后从配置载的方式,将继承自IbusinessRegister的类的对象,加入容器中。修改代码如图所示:
//不带有程序中注册,仅仅从配置中获取的方法 public static void Register() { //从配置中读取 这里使用了反射 try { //从配置中读取程序集的位置 string dllPathString = ConfigurationManager.AppSettings["DllPath"]; //判断是不是读取到了 if (!string.IsNullOrEmpty(dllPathString)) { //获取dll的程序集 Assembly assembly = Assembly.LoadFrom(dllPathString); //找到里面定义的所有的类 foreach (Type type in assembly.GetTypes()) { try { //找到实现了IBusinessOperation并且不是抽象接口或者抽象类的类 if (typeof(IBusinessOperation).IsAssignableFrom(type) && type.IsAbstract == false) { //创建实例 IBusinessOperation iBusinessOperation = Activator.CreateInstance(type) as IBusinessOperation; //把对象放入容器中 countainer.Add(iBusinessOperation); } } catch { } } } } catch { } } //带有程序中注册的方法 public static void Register(IEnumerable<IBusinessOperation> array) { //先从程序中读入 if (array != null) { countainer.AddRange(array); } Register(); }
说明:上面的代码要注意的是,在反射程序集之后进行帅选的时候,一定要把抽象类进行排除。如果你把这些学生业务体类和接口写在一起。在反射的时候,接口也会被当做一个IbusinessRegister来被反射出来,在创建的时候会报出不能创建抽象接口的错误。
九. 结果显示(Show Result)
进过上面的多次修改,我们已经可以从一个外部的dll中动态的载入一个学生的业务实体。即可以外部添加对学生的新的处理而不修改代码,仅仅需要修改一下配置。
这里我就不把所有的代码抄下来了,工程源码会在下面给出下载地址。然后,就仅仅截了显示结果的一些图,图下面配一些文字介绍步骤。
这个是输出的结果一个是英国的学生的处理,一个是中国学生的处理。英国的学生是通过配置加入容器中的,而中国的学生是通过程序直接创建对象来注册的。
并没有将BusinessLib进行引用,而是直接通过复制dll加进去的,然后在配置里写配置一下文件路径。
十. 写在最后(Last)
(1)终于把蓄谋已久的这篇文章写完了,前后准备整理用了一周的时间。不想写的太简单,希望大家都可以看明白,就写成现在这样了。写完之后看了一下,似乎有那么一点点啰嗦,希望大家有那个耐心看得完。
(2)公司的总监叫我写一个可以把一篇图文书分成左右两页的算法,要求可以对多种类型的图文进行处理。还可以对处理的方法进行扩展。我当时就用了这个方法来写的,感觉还行。
(3)不说了写的有点累了,我把我的工程文件给一下就睡觉啦,晚安。对了如果觉得好的请点个赞,关注一下是最好的了。然后要是转载请注明一下出处哦。
博主镇楼