该篇文章通过对《大话设计模式》之简单工厂模式的阅读,使用Java编写文中对应的案例,逐步理解文中所讲解的简单工厂模式。
简单工厂模式 该文档基于《大话设计模式》中的内容,因为书中使用的是C#编写的,所以我根据书中的案例自己去写的Java版的,一步一步的去理解实践设计模式。
如果您想阅读此书,请下载《大话设计模式》,有条件请购买正版图书进行学习。
资源来源于网络,如有侵权,立刻删除。
阿里云盘链接地址:https://www.aliyundrive.com/s/FxoBjRd3rd3
一、面试落榜 1.1 计算器程序面试题 小菜 面试的时候碰到这样的题目:”请用C++、Java、C#或VB.NET”任意一种面向对象语言实现一个计算器控制台程序,要求输入两个数和运算符号,得到结果。”
小菜 用C#写了一段程序,我在学习过程中按照他的代码使用Java写了一段类似的程序,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import cn.hutool.core.convert.Convert;import java.util.Scanner;public class CalculatorProgram { public static void main (String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入数字A:" ); String A = scanner.nextLine(); System.out.println("请输入运算符号B(+、-、*、/):" ); String B = scanner.nextLine(); System.out.println("请输入数字C:" ); String C = scanner.nextLine(); String D = "" ; if (B.equals("+" )){ D = Convert.toStr((Convert.toDouble(A) + Convert.toDouble(C))); } if (B.equals("-" )){ D = Convert.toStr((Convert.toDouble(A) - Convert.toDouble(C))); } if (B.equals("*" )){ D = Convert.toStr((Convert.toDouble(A) * Convert.toDouble(C))); } if (B.equals("/" )){ D = Convert.toStr((Convert.toDouble(A) / Convert.toDouble(C))); } System.out.println("计算结果为:" + D); } }
但是小菜 ,却因为这段代码而没有应聘上,内心郁闷的小菜 ,去询问大鸟 。大鸟 指出了它存在的问题,并详细引导他了解简单工厂模式,请细看下述内容。
补充说明:上述程序中使用到了HuTool工具类,需在pom.xml
中导入HuTool的依赖。
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 4.6.3</version > </dependency > </dependencies >
1.2 程序中出现的问题
1.2.1 命名规范问题 查阅《阿里Java开发手册-泰山版》,对于命名做了如下规范:
点击查看阿里命名规范
如果您想查看更为完整的内容,请下载《阿里Java开发手册-泰山版》,有条件请购买正版图书进行学习。
资源来源于网络,如有侵权,立刻删除。
阿里云盘链接地址:https://www.aliyundrive.com/s/PcdEnB8JJpT
1.2.2 判断条件问题 对于上述这种多次无用功的判断,可以考虑通过 switch case 来处理。
1.2.3 做除法问题 一旦要做除法运算,应该考虑以下几个问题:
除数不能为0,或者其他非数字的字符;
结果有小数,如何做处理;
1.3 计算器程序代码优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import cn.hutool.core.convert.Convert;import java.util.InputMismatchException;import java.util.Scanner;public class CalculatorProgram { public static void main (String[] args) { String result = "" ; Scanner scanner = new Scanner(System.in); System.out.println("请输入数字A:" ); int numberA = scanner.nextInt(); System.out.println("请输入运算符号B(+、-、*、/):" ); char strOperate = scanner.next().charAt(0 ); System.out.println("请输入数字C:" ); int numberC = 0 ; try { numberC = scanner.nextInt(); } catch (InputMismatchException e) { System.out.println("请输入正确的非零数字!" ); } switch (strOperate) { case '+' : result = Convert.toStr((Convert.toDouble(numberA) + Convert.toDouble(numberC))); break ; case '-' : result = Convert.toStr((Convert.toDouble(numberA) - Convert.toDouble(numberC))); break ; case '*' : result = Convert.toStr((Convert.toDouble(numberA) * Convert.toDouble(numberC))); break ; case '/' : if (0 != numberC) { result = Convert.toStr((Convert.toDouble(numberA) / Convert.toDouble(numberC))); } else { System.out.println("除数不能为0" ); } break ; default : System.out.println("输入的运算符号有误!" ); } System.out.println("计算结果为:" + result); } }
switch case 补充说明:
点击查看switch case用法详解
小菜 完成这个程序之后,大鸟 评价道:”不错不错,改的挺快的,但从计算器程序上来说,已经够用了,但是这样的代码是否符合出题人的意思呢?”
小菜 顿悟:”你的意思是面向对象?“
二、面向对象编程 小菜 :”我明白了,出题人说要用任意一种面向对象语言实现,意思就是要使用面向对象的编程方法去实现。这个我学过,只不过当时没有想到而已。”
大鸟 :”所有编程初学者都会有这样的问题,就是碰到问题就直觉地用计算机能够理解地逻辑来描述和表达待解决的问题及具体的求解过程。这其实是用计算机的方式去思考,比如这个计算器这个程序,先要求输入两个数和运算符号,然后根据运算符号判断如何选择运算,得到结果,这本身没有错,但这样的思维却使得我们的程序只为满足实现当前的需求,程序不容易维护,不容易扩展,更不容易复用。从而达不到高质量代码的要求。”
小菜:”鸟哥,我有点懵逼了,如何才能容易维护,容易扩展,又容易复用呢,能不能具体说说?”
不光是小菜懵逼,其实我也懵逼,我也是菜鸟,哈哈。
2.1 面向对象的故事 “话说三国时期,曹操带领百万大军攻打东吴,大军在常见赤壁驻扎,军船连成一片,眼看就要灭掉东吴,统一天下,曹操大悦,于是大宴众文武,在酒席间,曹操诗兴大发,不觉吟道:’喝酒唱歌,人生真爽。’众文武齐呼:’丞相好诗’,于是一臣子宿命印刷工匠刻版印刷,以便流传天下。”
样张出来给曹操一看,曹操感觉不妥,说到:”喝与唱,此话过俗,应改为’对酒当歌’较好。”,于是此臣酒命工匠重新来过。工匠眼看连夜刻板之功,彻底白费,心中叫苦不迭,只得照办。
样章再次出来请曹操过目,曹操细细一品,觉得还是不好,说:”人生真爽太过直接,应该为问语才够意境,因此应改为’对酒当歌,人生几何?’当臣转告工匠之时,工匠晕倒……”
上述故事的问题在于,由于需求的改变,之前所有的工作都白费,需要重新来过。
这里面就涉及到了,可维护、可扩展、可复用。
大鸟 :”第一,要改只需更改要改之字,此为可维护 ;第二,这些字并非这次用完就无用,完全可以在后来的印刷中重复使用,此乃可复用 ;第三,此诗要加字,只需另外刻字加入即可,这是可扩展 ;第四,字的排列其实可能是竖向排列可能是横向排列,此时只需将活字移动就可做到满足排列需求,此是灵活性好 。”
“而在活字印刷术出现之前,上面的四种特性都无法满足,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完这本书之后,此版已无任何可再利用价值。”
对应到上面的计算器程序当中,我们是不是也只顾当前的计算需求所进行的编程,而并未考虑到扩展的情况,比如要进行开根运算,上面的程序就没法用了。
2.2 面向对象的好处 大鸟 :”当我学习了面向对象的分析设计编程思想,开始考虑通过封装、继承、多态把程序的耦合度降低 ,传统印刷术的问题就在于所有的字斗刻在同一版面上造成耦合度太高所致,开始用设计模式使得程序更加的灵活,容易修改,并且易于复用 。”
小菜 :”那按照题目的意思,要我做出一个可维护,可扩展,可复用的计算器程序,我该怎么做呢?”
大鸟 :”编程有一原则,尽量避免重复。想想看,你的代码,有哪些是何控制台有关的,哪些适合计算器有关的?”
小菜 :”你的意思是让计算和显示分开?”
2.3 业务的封装 大鸟 :”准确的说,就是让业务逻辑和界面逻辑分开,让它们之间的耦合度下降。只有分离开,才可以达到容易维护或扩展。”
小菜 :”哦哦哦,让我来试试!”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.InputMismatchException;import java.util.Scanner;public class Calculator { public static void main (String[] args) { try { Scanner scanner = new Scanner(System.in); System.out.println("请输入数字A:" ); int numberA = scanner.nextInt(); System.out.println("请输入运算符号B(+、-、*、/):" ); char strOperate = scanner.next().charAt(0 ); System.out.println("请输入数字C:" ); int numberC = scanner.nextInt(); String result = Operator.operate(numberA, strOperate, numberC); System.out.println("计算结果为:" + result); } catch (InputMismatchException e) { System.out.println("您的输入有误!" ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import cn.hutool.core.convert.Convert;public class Operator { public static String operate (double numberA, char operator, double numberC) { String result = "" ; switch (operator) { case '+' : result = Convert.toStr((Convert.toDouble(numberA) + Convert.toDouble(numberC))); break ; case '-' : result = Convert.toStr((Convert.toDouble(numberA) - Convert.toDouble(numberC))); break ; case '*' : result = Convert.toStr((Convert.toDouble(numberA) * Convert.toDouble(numberC))); break ; case '/' : if (0 != numberC) { result = Convert.toStr((Convert.toDouble(numberA) / Convert.toDouble(numberC))); } else { System.out.println("除数不能为0" ); } break ; default : System.out.println("输入的运算符号有误!" ); } return result; } }
小菜 :”鸟哥,你看,我把计算和界面完全分离了,面向对象也不过如此,下回写类似的代码不怕了。”
大鸟 :”别急,仅此而已,实在谈不上完全面向对象,你只用了面向对象三大特征中的一个,还有两个没用呢?”
小菜 :”面向对象三大特征不就是封装、继承和多态 吗,这里我用到的应该是封装。这还不够吗?我实在看不出,这么小的程序如何用到继承。至于多态,其实我一直也不太了解它到底有什么好处,如何使用它。”
大鸟 :”你好好想想该如何应用面向对象的继承和多态。慢慢来。”
2.4 紧耦合和松耦合 大鸟 :”现在如果我希望增加一个开根(sqrt)运算,你如何改?”
小菜 :”那只需要改Operator类就行了,在switch中加一个分支就行了。”
大鸟 :”问题是你要加一个平方根运算,却需要让加减乘除的运算都得来参与编译,如果你一不小心,把加法运算改成了减法,这岂不是很糟糕。”
小菜 :”那我再改改。”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public abstract class Operator { private double numberA; private double numberC; public double getNumberA () { return numberA; } public void setNumberA (double numberA) { this .numberA = numberA; } public double getNumberC () { return numberC; } public void setNumberC (double numberC) { this .numberC = numberC; } public abstract double getResult () ; }
1 2 3 4 5 6 7 8 9 10 public class OperatorAdd extends Operator { @Override public double getResult () { double result = 0 ; result = super .getNumberA() + super .getNumberC(); return result; } }
1 2 3 4 5 6 7 8 9 10 public class OperatorSub extends Operator { @Override public double getResult () { double result = 0 ; result = super .getNumberA() - super .getNumberC(); return result; } }
1 2 3 4 5 6 7 8 9 10 public class OperatorMul extends Operator { @Override public double getResult () { double result = 0 ; result = super .getNumberA() * super .getNumberC(); return result; } }
1 2 3 4 5 6 7 8 9 10 11 public class OperatorDiv extends Operator { @Override public double getResult () { double result = 0 ; result = super .getNumberA() / super .getNumberC(); return result; } }
小菜 :”大鸟哥,我按照你说的方法写出来了一部分,首先是一个运算类(父类),它有两个number字段,主要用于计算器的前后数,然后有一个抽象方法getResult(),用于得到结果,然后我把加减乘除都写成运算类的子类,继承它后,重写了getResult()方法,这样如果要修改任何一个算法,就不需要提供其他算法的代码了。但问题来了,我如何让计算器知道我是希望用哪一个算法呢?”
三、简单工厂模式 大鸟 :”写的不错嘛。你现在的问题其实就是如何去实例化对象的问题,也就是说,到底要实例化谁,将来会不会增加实例化的对象,比如增加开根运算,这是很容易变化的地方,应该考虑用一个单独的类来做这个创造实例的过程,这就是工厂,来,我们看看这个类如何写。”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class OperatorFactory { public static Operator getOperator (char operator) { Operator opera = null ; switch (operator) { case '+' : opera = new OperatorAdd(); break ; case '-' : opera = new OperatorSub(); break ; case '*' : opera = new OperatorMul(); break ; case '/' : opera = new OperatorDiv(); break ; default : System.out.println("输入的运算符号有误!" ); } return opera; } }
大鸟 :”哈,看到了吧,这样子,你只需要输入运算符号,工厂就实例化出合适的对象,通过多态,返回父类的方式实现了计算器的结果。”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import cn.hutool.core.convert.Convert;import java.util.InputMismatchException;import java.util.Scanner;public class Calculator { public static void main (String[] args) { double result = 0.0 ; try { Scanner scanner = new Scanner(System.in); System.out.println("请输入数字A:" ); int numberA = scanner.nextInt(); System.out.println("请输入运算符号B(+、-、*、/):" ); char strOperate = scanner.next().charAt(0 ); System.out.println("请输入数字C:" ); int numberC = scanner.nextInt(); if (numberC != 0 ) { Operator operator = OperatorFactory.getOperator(strOperate); operator.setNumberA(Convert.toDouble(numberA)); operator.setNumberC(Convert.toDouble(numberC)); result = operator.getResult(); System.out.println("计算结果为:" + result); } else { System.out.println("除数不能为0" ); } } catch (InputMismatchException e) { System.out.println("您的输入有误!" ); } } }
四、总结 对于上述的案例来讲,整个的逻辑顺序是:
先写代码满足于目前的需求;
然后思考如何将整个业务逻辑进行分离和封装;
然后思考如何应用继承和多态进行解耦合;
最后对简单工厂模式进行应用实例化对象。
对于简单工厂的设计模式的学习,不能止步于此,因为要养成简单工厂设计模式的思维习惯绝不是一蹴而就的,而是不断实践修改的过程中,逐渐融入自己的思维体系当中。
2022年1月6日
-------- The End
Thanks For Reading --------