大话设计模式,小菜去公司面试,前台小姐给出一份面试题:使用任意一种面向对象语言实现一个计算机控制台程序,要求输入两个数和运算符,得到结果。
小菜随手写下下面的代码:之后面试公司就没有音讯了。
这段代码是从控制台获取数据,然后在控制台显示数据。
int operandOne; int operandTwo; char op; int result = null; //获取两个操作数 //从标准流中读取下一个字符,返回其ASCII码 op = (char)Console.Read(); switch (op) { case '+': result = operandOne + operandTwo; break; case '-': result = operandOne - operandTwo; break; case '*': result = operandOne * operandTwo; break; case '/': if (operandTwo != "0") { result = operandOne / operandTwo; } else { result = "0 is invalid dividend"; } break; default: result = "wrong opertor"; break; } //显示计算结果
四大发明之活字印刷——面向对象思想的胜利
话说三国时期,曹操带领百万大军攻打东吴,大军在长江赤壁驻扎,军船连成一片,眼看就要灭掉东吴,统一天下,曹操大悦,于是大宴众文武,在酒席间,曹操诗性大发,不觉吟道:“喝酒唱歌,人生真爽。…………”。众文武齐呼:“丞相好诗!”于是一臣子速命印刷工匠刻版印刷,以便流传天下。
样张出来给曹操一看,曹操感觉不妥,说道:“喝与唱,此话过俗,应改为‘对酒当歌’较好!”,于是此臣就命工匠重新来过。工匠眼看连夜刻版之工,彻底白费,心中叫苦不喋。只得照办。
样张再次出来请曹操过目,曹操细细一品,觉得还是不好,说:“人生真爽太过直接,应改问语才够意境,因此应改为‘对酒当歌,人生几何?…………’!”当臣转告工匠之时,工匠晕倒…………!
可惜三国时期活字印刷还未发明,所以类似事情应该时有发生,如果是有了活字印刷。则只需更改四个字就可,其余工作都未白做。实在妙哉。
第一,要改,只需更改要改之字,此为可维护;第二,这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用;第三,此诗若要加字,只需另刻字加入即可,这是可扩展;第四,字的排列其实有可能是竖有可能是横排,此时只需将活字移动就可做到满足排列需求,此是灵活性好。
而在活字印刷术之前,上面的四种特性都无法满足,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完这本书后,此版已无任何可再利用价值。
小时候,我一直奇怪,为何火药、指南针、造纸术都是从无到有,从未知到发现的伟大发明,而活字印刷仅仅是从刻版印刷到活字印刷的一次技术上的进步,为何不是评印刷术为四大发明之一呢?
做了软件开发几年后,经历了太多的客户(曹操)改变需求,更改最初想法的事件,才逐渐明白当中的道理。其实客观的说,客户的要求也并不过份(改几个字而已),但面对已完成的程序代码,却是需要几乎重头来过的尴尬,这实在是痛苦不堪。说白了,原因就是因为我们原先所写的程序,不容易维护,灵活性差,不容易扩展,更谈不上复用,因此面对需求变化,加班加点,对程序动大手术的那种无耐也就非常正常的事了。
之后当我学习了面向对象分析设计编程思想,开始考虑通过封装、继承、多态把程序的耦合度降低(传统印刷术的问题就在于所有的字都刻在同一版面上造成耦合度太高所制),开始用设计模式使得程序更加的灵活,容易修改,并且易于复用。体会到面向对象带来的好处,那种感觉应该就如同是一中国酒鬼第一次喝到了茅台,西洋酒鬼第一次喝到了XO一样,怎个爽字可形容呀。
再次回顾中国古代的四大发明,另三种应该都是科技的进步,伟大的创造或发现。而唯有活字印刷,实在是思想的成功,面向对象的胜利。不知您是否也有所感呢?
面向对象的特性一:封装
业务的封装,利于代码的复用。现在要求把控制台下的计算机移植到Window窗体中去,前面的代码就是曹操工匠的一块印刷版,整个东西都必须重新来过。解决的办法就是让业务逻辑和界面逻辑分开,降低他们之间的耦合度。
Operation运算类:
public class Operation { public static double GetResult(double operandOne, double operandTwo, char op) { double result = 0.0; switch (op) { case '+': result = operandOne + operandTwo; break; case '-': result = operandOne - operandTwo; break; case '*': result = operandOne * operandTwo; break; case '/': if (operandTwo != 0) { result = operandOne / operandTwo; } else { throw new Exception("invalid divisor 0"); } break; default: break; }//switch return result; }//Getresult }//class Operation
这个类中只涉及到业务逻辑,也就是运算,没有任何的界面相关的东西,不会接收控制台的数据,也不会向控制台传送数据,只是计算。
客户端代码的实现方式:
不管是两个操作数是从控制台还是从控件中获取的,客户端负责将他们转换成double类型,同时调用这个类的计算函数就能得到结果。这样业务逻辑和界面逻辑就分开了,有利于代码的复用和平台的移植。
提示:如果除数是0,就能接收到OPeration类抛出的exception。
面向对象的另外两个特性:继承和多态
现在曹操又提出了新的要求,要增加一个运算,增加开根号的功能,看似很简单,要做的就是修改Operation类,增加一个switch分支就能搞定了。
但是问题来了:要加一个平方根运算,却需要把加减乘除的运算都得来参与编译,如果你一不小心,把加法运算改成了减法,这不是大大的糟糕。
所以这里要做的是,增加新的功能,对原来已经实现的功能尽最大努力的不要做任何的改动。
打个比方,如果现在公司要求你为公司的薪资管理系统做维护,原来只有技术人员(月薪),市场销售人员(底薪+提成),经理(年薪+股份)三种运算算法,现在要增加兼职工作人员的(时薪)算法,但按照你昨天的程序写法,公司就必须要把包含有的原三种算法的运算类给你,让你修改,你如果心中小算盘一打,‘TMD,公司给我的工资这么低,我真是郁闷,这会有机会了’,于是你除了增加了兼职算法以外,在技术人员(月薪)算法中写了一句
if (员工是小菜) { salary = salary * 1.1; }
那就意味着,你的月薪每月都会增加10%(小心被抓去坐牢),本来是让你加一个功能,却使得原有的运行良好的功能代码产生了变化,这个风险太大了。
所以应该把加减乘除的运算分开写,修改其中一个不会影响另外几个,增加运算也不影响其他代码。
Operation运算类:
public class Operation { private double _operandOne; private double _operandTwo; //effective C++中建议所有的成员变量都要声明为private类型的 //但是在子类继承中对这些私有成员变量是看不见的,这里先设置为protected public virtual double GetResult() { double result = 0.0; return result; }//GetResult }//class Operation
加减乘除类:
public class OperationAdd : Operation { public override double GetResult() { double result = 0; result = operandOne + operandTwo; return result; }//GetResult }//class OperationAdd public class OperationSub : Operation { public override double GetResult() { double result = 0; result = operandOne - operandTwo; return result; }//GetResult }//class OperationSub public class OperationDiv : Operation { public override double GetResult() { double result = 0; if (operandTwo != 0) { result = operandOne / operandTwo; } else { throw new Exception("invalid divisor 0"); } return result; }//GetResult }//class OperationDiv public class OperationMul : Operation { public override double GetResult() { double result = 0; result = operandOne * operandTwo; return result; }//GetResult }//class OperationMul
其实分开实现各个业务模块,根本的原因就是当我的某一个小的业务模块需要修改或者要新增加一个新的业务模块。不要对其他正常运行的模块做任何的改动。唯一的原因就是他们现在能正常的运行。
客户端类:
public class Program { static void Main(string[] args) { Operation operation = null; string operandOne; string operandTwo; char op; string result = ""; Console.WriteLine("A Calculate implementations:"); Console.WriteLine("Input the first operand:"); operandOne = Console.ReadLine(); Console.WriteLine("Input the second operand"); operandTwo = Console.ReadLine(); Console.WriteLine("Input the op:(+-*/)"); //从标准流中读取下一个字符,返回其ASCII码 op = (char)Console.Read(); //数据初始化 switch (op) { case '+': operation = new OperationAdd(); break; case '-': operation = new OperationSub(); break; case '*': operation = new OperationMul(); break; case '/': operation = new OperationDiv(); break; default: break; }//switch operation.operandOne = Convert.ToDouble(operandOne); operation.operandTwo = Convert.ToDouble(operandTwo); //计算结果 result = Convert.ToString(operation.GetResult()); //如果上面的语句发生异常,那么就直接跳到catch语句中执行,不会执行下面的语句 Console.WriteLine("result is {0}", result); } }
上面的代码时候业务逻辑和界面显示逻辑又混在一起了,那么如何在用继承多态的同时,实现业务逻辑的封装呢?那就是简单工厂模式。
简单工厂类:
public class OperationFactory { public static Operation CreateOperation(char op) { //对operation的定义也可以为成员变量,如果是成员变量这个函数就不能是static的。 //并且在使用的时候,必须new出实例对象。 Operation operation = null; switch (op) { case '+': operation = new OperationAdd(); break; case '-': operation = new OperationSub(); break; case '*': operation = new OperationMul(); break; case '/': operation = new OperationDiv(); break; default://这时的operation是null,所以不用throw new Exception("操作符错误"); //break和return或throw不能同时用,会产生“检测到无法访问的代码”警告 break; }//switch return operation; }//CreateOpertion }//class OperationFactory
新的客户端类:
public class Program { static void Main(string[] args) { Operation operation = null; string operandOne; string operandTwo; char op; string result = ""; //读入两个操作数 operandOne = Console.ReadLine(); operandTwo = Console.ReadLine(); Console.WriteLine("Input the op:(+-*/)"); //从标准流中读取下一个字符,返回其ASCII码 op = (char)Console.Read(); //数据初始化 operation = OperationFactory.CreateOperation(op); //如果op传的不是+-*/,这里就返回一个null if (operation == null) { Console.WriteLine("操作符错误"); return; } //如果操作数有错误会在这里抛出异常 operation.operandOne = Convert.ToDouble(operandOne); operation.operandTwo = Convert.ToDouble(operandTwo); //计算结果 //如果计算除法是0作为除数,会在这里抛出异常 result = Convert.ToString(operation.GetResult()); //如果上面的语句发生异常,那么就直接跳到catch语句中执行,不会执行下面的语句 Console.WriteLine("result is {0}", result); } }
看到了,对类的成员变量的初始化要交给客户端类,因为这些数据可能来自控制台可能来自窗体标签等。客户端是看不到具体的实体类的,只能看到一个父类Operation,并拿到一个制造Operation的工厂。
如果想修改OperationAdd那么就直接修改那一个类就好了;如果想增加新的计算功能,比如说开根号,只需要增加一个继承自Operation的子类完成计算,修改一下工厂的switch分支。
关于界面的任何修改都跟计算没有任何的关系。
下一篇文章我们来看一下UML类图:
静态工厂模式其实是这个样子一步一步产生的:
1.界面逻辑和业务逻辑杂糅在一起,不利于代码的服用和移植。 2.分离界面逻辑和业务逻辑。 3.为了实现新增业务逻辑时,对原来的已实现的业务逻辑不做改动,所以要分割业务逻辑。 4.为了再次分离界面逻辑和业务逻辑,增加静态工厂。在工厂里选择创建什么样的实例,使用哪一块业务逻辑。
很好的学习工厂模式的案例
http://wiki.jikexueyuan.com/project/design-pattern-creation/simple-two.html