还记得我的软件工程老师是这么说的:软件应该往高内聚,低耦合的方向进行设计。
当时,还身为一个初学者的我,不太明白老师的这句话——既然面向对象提供给了我们”继承“这种高耦合的概念,那为何我们还要低耦合高内聚呢?难道放着继承的概念不用,而改为面向过程吗?
带着这一疑问,我请教了我的老师,他给我的回答是:通过接口来分隔分离逻辑,就可以达到低耦合的效果。
我们来回顾一下前一篇所学习的"控制反转"设计思想,其实质就是上面所说的"通过接口来分离逻辑"。
但以上一篇的示例来说,达到这个目标,还有一些欠缺,首先让我们来回顾一下上一篇的代码内容
/// <summary> /// 这是一个负责登录的接口类型 /// </summary> public interface ILoginChecker { /// <summary> /// 登录,需要的参数是用户名和密码,需要返回一个bool类型表示登录是否成功 /// </summary> /// <param name="loginName"></param> /// <param name="password"></param> /// <returns></returns> bool Login(string loginName, string password); } public class LoginWindow : Form { private ILoginChecker loginChecker; /// <summary> /// 通过构造函数决定使用哪个ILoginChecker来负责登录流程 /// </summary> /// <param name="chcker"></param> public LoginWindow(ILoginChecker chcker) { this.loginChecker = chcker; } void LoginButton_Click(object sender, EventArgs e) { string loginName = null, password = null; //从控件上取值 //判断空值 //等一切OK时 if (this.loginChecker.Login(loginName, password)) { //登录成功 } else { //登录失败 } } }
肯定会有一些人带着这样的疑问:在代码的某个地方一定写着类型这样的内容:
///实例化某个实现了ILoginChecker接口的类型 ILoginChecker checker = new XXXLoginChecker(); LoginWindow window = new LoginWindow(checker); window.Show();
那么随着以后功能的更新,在不断地修改这里的ILoginChecker checker = new XXXLoginChecker();
时间久了,记不得这段代码写在哪儿,也是很头疼的一件事,最重要的是,依然没达到”易维护“的效果,和之前相同,每次的须求改更,或是环境变化,都需要重写这句话。
如果能有一个容器,能够在运行时(注1)判断使用哪个ILoginChecker类型,那就方便多了。
运行时:与"编译时"相对应,比如我说定义一个变量Int32 i,这个i的类型就是Int32,这是在编译时决定的,因为程序在由代码编译为程序时,已经可以确定i就是一个Int32类型了。
再比如说我定义一个变量Object obj,这个obj的类型虽然是Object,但是会根据实际的赋值,发生类型的变换,而赋的什么值给它,程序在编译时是无法得知的,只有在运行到这里的时候才能知道,这就叫运行时。
我们来假设一个现实场景:
场景中,我们把ILoginChecker这个接口
1、门卫是由保安公司派出的
2、保安公司根据每个需求方的要求不同,配备不同的保安
3、保安公司拥有满足各种需求的保安
这样一来,我们就要再补充一个相当于是保安公司的类型了,用来创建ILoginChecker实例
所以我们起个名字叫LoginCheckerFactory
里面只有一个主要方法:CreateLoginChecker,根据我们指定的名称,返回一个具体的IloginChecker实例,我们来看一下示例代码:
class LoginCheckerFactory { public ILoginChecker CreateLoginChecker(string checkerName) { switch (checkerName) { case "Database": return new DatabaseLoginChecker(); //由数据库完成的登录验证 case "WebService": return new WebServiceLoginChecker(); //由WebService完成的登录验证 case " TCP": return new TCPLoginChecker(); //由TCP通讯完成的登录验证 default: throw new ApplicationException("不存在这个名称的Checker实例"); } } }
此时,你会发现,登录的判定流程是依赖于一个字符串,Database或WebService或TCP。到了这一步,我想大部分人都明白,这个字符串只要写在配置文件里,就大功告成了。
然后项目发布,在不同的运行环境中间,我只需要改一下配置文件,就能实现各种方式的登录了。
对于更新与维护是同样的便捷,你可以把不同版本的ILoginChecker都放在这个工厂里,然后根据外部的一些版本号来更改实例。
在BUG的修正中,你也不必直接修改类型本身,可以拷备一个出来,比如叫WebServiceLoginChecker2,这样即保证了程序原有的可运行性,又可以进行BUG的修正,一旦出现了”动一发触全身“的情况,也非常容易全身而退。
这样的设计思想、模式,我们将其称之为工厂模式。工厂模式还分为简单工厂模式和抽象工厂模式,但是其最核心的思想,就是创建一个工厂,由工厂在运行时,进行动态的实例创建、返回。大大降底的类与类、模块与模块之间的耦合度,为更新与维护提供了非常好的隔离环境。
小结
通过第一、第二篇文章的学习,一种初步的构架思路已经产生。
1、分析当前方法要的主要事情
2、将可能存在变更的逻辑,建立接口,待以后实现
3、考虑到上述接口的实现多样性,建立工厂类型,由工厂类型负责创建接口实例
利与弊的权衡
从做产品的角度考虑,一个好的基本构架是产品最核心的保障,有了这样的保障,可以说,除非到你换语种的那一天,否则永远不会存在(推倒重来)的那一天,因为你的每一个环节都被低耦合了,它们全部都可以被单独替换。
从做项目的角度考虑,一个好的基本构架是项目中最耗时间、最耗成本的阶段。在面向结果的项目负责人眼中,相对于可维护性进度才是最重要的,所以在做项目这种情况下,对使用架构应该进行一个速与质的权衡。
文章为作者原创,转载请标明出处,谢谢 http://www.cnblogs.com/ShimizuShiori/p/4929300.html