今天开始看设计模式,后面基本每天都会按照“大话设计模式”这本书更新一章关于设计模式的内容。有兴趣的话可以每天一起学习呀!!!话不多说,我们先看看第一章简单工厂模式
这本书是以小菜面试失败,和大鸟讨论为什么失败而展开的。首先小鸟面试的时候面试官出了一道题,用任意一门面向对象的语言来实现一个计算器控制台程序。
我觉得这道题大多数人都一样觉得很简单,马上就写出了如下的代码(书上是C++实现,这里用Java)
/**
* 功能: 实现一个控制台的计算器
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class Calc {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("请输入操作数1:");
String option1 = in.next();
System.out.println("请输入运算符:");
String symbol = in.next();
System.out.println("请输入操作数2:");
String option2 = in.next();
// 结果
String result = null;
// 先将String转换为Double运算后再转换为String,这么做的目的是防止输入别的类型报错
if(symbol.equals("+")) {
result = String.valueOf(Double.valueOf(option1) + Double.valueOf(option2));
}
if(symbol.equals("-")){
result = String.valueOf(Double.valueOf(option1) - Double.valueOf(option2));
}
if(symbol.equals("*")){
result = String.valueOf(Double.valueOf(option1) * Double.valueOf(option2));
}
if(symbol.equals("/")){
result = String.valueOf(Double.valueOf(option1) / Double.valueOf(option2));
}
System.out.println("结果为:" + result);
in.close();
}
}
这样写的话乍一看好像是对的,其实是有很多问题的,看上图。
1、首先四个if,意味着每个条件都要做判断,多做了三次无用功。
2、如果除数为0的时候,如果用户输入的时候字符符号怎么办,都会抛异常
针对这些问题将代码改为下面这个样子
/**
* 功能: 实现一个控制台的计算器
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class Calc {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
try {
System.out.println("请输入操作数1:");
String option1 = in.next();
System.out.println("请输入运算符:");
String symbol = in.next();
System.out.println("请输入操作数2:");
String option2 = in.next();
// 结果
String result = null;
// 注意要在jdk1.7(包含1.7)之后才可以在switch里写String
switch(symbol) {
case "+":
result = String.valueOf(Double.valueOf(option1) + Double.valueOf(option2));
break;
case "-":
result = String.valueOf(Double.valueOf(option1) - Double.valueOf(option2));
break;
case "*":
result = String.valueOf(Double.valueOf(option1) * Double.valueOf(option2));
break;
case "/":
if(option2 != "0")
result = String.valueOf(Double.valueOf(option1) / Double.valueOf(option2));
else
result = "除数不能为0";
break;
default:
System.out.println("无此运算符");
}
} catch (Exception e) {
System.out.println("您的输入有误" + e.getMessage());
}
in.close();
}
}
这样写似乎没有什么问题了,不论怎么输入都不会报错,但是并没有采用面向对象的思路来写,违背了题意(补充:try-catch是十分浪费资源的,这种输入的错误应该用正则表达式来判断)
这里先简单的说一下面向对象和面向对象的好处,大家写代码的时候都直觉地用计算机能够理解的逻辑来描述和表达待解决的问题及具体的求解过程(说人话就是在一个方法里面一套打完)。这其实是用计算机的方式去思考,比如计算机这个程序,先要求输入两个数和符号,然后根据符号去计算,得到结果,这本身没有错,但这样的思维缺使得我们的程序只为满足实现当前的需求,程序不容易维护,不容易扩展,更不容易复用。从而达不到高质量代码的要求。这样说可能会很抽象,下面举个例子。
(书上的故事前言就不说了)古代的时候一开始使用刻板印刷,把字都刻在石板上,假设这个时候我们花了一个月刻好了几百个字,这个时候发现中间有一个错字把“大”刻成了“人”,这个时候怎么办,这个板子只有作废,我们需要重头开始再刻一个月。。。这谁顶得住,如果这个时候我们有了活体印刷呢,只需要修改一个字就可以了,几分钟就搞定了。
通过这个故事我们可以知道“第一,要改,只需更改要改之字,此为可维护;第二,这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用;第三,加字的话,只需要另刻字加入即可,这是可扩展;第四,字的排列其实可能是竖排可能是横排,此时只需将活字移动就可以做到满足排列的要求,此是灵活性好”
在真实开发中,用户一天改一个需求,要是每次都重头写怕是IT就没人干了。之前的计算机程序就是不容易维护,灵活性差,不容易扩展,更谈不上复用。那我们如何通过封装、继承、多态去让我们的计算器程序容易维护,容易扩展,容易复用呢?
比如这个时候我们要写一个图形界面的计算机,之前的代码显然是不容复用的。可能有人说我直接CV,修改几行代码不就OK了嘛,改动又不大。这是初级程序员的思维,当代码多到了一定程度的时候,比如CSDN现在要换一套全新的界面,难道我们从前到后重新写代码吗?显然不可能,这个时候我们只需要修改页面代码即可。这个时候就要将我们的代码给拆开,可以想想看,我们的代码有哪些是和控制台有关,有哪些只是和计算器有关?
这个时候我们需要将业务逻辑和界面逻辑分开,让它们的耦合度下降。只有分离开,才可以达到容易维护或扩展。看代码
/**
* 功能: 实现一个控制台的计算器
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class Calc {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 结果
String result = null;
try {
// 这里就可以复用了,如果想改成界面的,直接把这里改成界面就好了,核心的计算根本不用动
System.out.println("请输入操作数1:");
String option1 = in.next();
System.out.println("请输入运算符:");
String symbol = in.next();
System.out.println("请输入操作数2:");
String option2 = in.next();
result = String.valueOf(Operation.getResult(
Double.valueOf(option1),
Double.valueOf(option2),
symbol));
} catch (NumberFormatException e) {
System.out.println("您的输入有误" + e.getMessage());
}
System.out.println("结果为:" + result);
in.close();
}
}
/**
* 运算类
* 功能:
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class Operation {
public static double getResult(double option1, double option2, String symbol) {
double result = 0;
switch(symbol) {
case "+":
result = option1 + option2;
break;
case "-":
result = option1 - option2;
break;
case "*":
result = option1 * option2;
break;
case "/":
// 这个判断书上没有,觉得不判断难受
if(option2 == 0) throw new RuntimeException("除数不能为0");
result = option1 / option2;
break;
default:
System.out.println("无此运算符");
}
return result;
}
}
上面的代码就完全把业务和界面分离了,不管要做成什么界面的,都可以复用运算类(Operation),但是这里并没有用到继承和多态。
如果我们的要增加可以取模运算,怎么办呢。可能大家想,这还不容易直接switch加一个分支不就ok了。但是这个时候又有一个问题了,我们仅仅是增加一个功能,但是加减乘除都需要重新来参与编译。举个栗子,如果公司要你来为薪资管理系统做维护,原来只有技术人员(时薪)、市场销售人员(时薪)等,现在要求加一个兼职人员的(时薪)的算法,但是按照之前的想法,要把包含原来算法的运算类给你,让你修改,如果你心中小算盘一打,‘TMD,公司给我的工资这么低,我真是郁闷,现在有机会了’,于是你除了增加兼职人员(时薪)之外,给技术人员算法里面加一句
if(员工使是我) {
salary = salary * 1.1;
}
每个月月薪都增加了10%(面向监狱编程警告),本来只是增加一个功能,却使得原有的运行良好的功能代码发生了变化,分析太大,所以将代码修改为
/**
* 运算的通用操作
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public abstract class Options {
/**
* 操作数1
*/
private double option1;
/**
* 操作数2
*/
private double option2;
public Options(double option1, double option2) {
this.option1 = option1;
this.option2 = option2;
}
/**
* 得到结果
* @return
*/
public abstract double getResult();
public double getOption1() {
return option1;
}
public void setOption1(double option1) {
this.option1 = option1;
}
public double getOption2() {
return option2;
}
public void setOption2(double option2) {
this.option2 = option2;
}
}
/**
* 功能:加法运算
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class AddOption extends Options{
public AddOption(double option1, double option2) {
super(option1, option2);
}
@Override
public double getResult() {
return getOption1() + getOption2();
}
}
/**
* 功能:加法运算
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class AddOption extends Options{
public AddOption(double option1, double option2) {
super(option1, option2);
}
@Override
public double getResult() {
return getOption1() + getOption2();
}
}
/**
* 减法
* 功能:
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class SubOption extends Options{
public SubOption(double option1, double option2) {
super(option1, option2);
}
@Override
public double getResult() {
return getOption1() - getOption2();
}
}
这里写出来了一部分代码,首先定义了一个抽象类,里面定义了两个进行运算的参数,还有一个抽象方法getResult(),用于得到结果,然后将加减乘除(这里只写了加减)都写成子类,继承重写getResult方法即可。如果添加新功能直接继承重写方法即可。(如果对上面代码不清楚,建议先把Java的基础语法弄懂)
现在还剩下一个问题如何去实例化对象呢,这个时候要用到“简单的工厂模式”
/**
* 功能:用于计算的工厂模式
*
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class OperationFactory {
public static Options createOperate(double option1, double option2, String symbol) {
Options options = null;
switch (symbol) {
case "+":
options = new AddOption(option1, option2);
break;
case "-":
options = new SubOption(option1, option2);
break;
// 后面就不写了,可以自行扩展
case "*":
break;
case "/":
break;
default:
break;
}
return options;
}
}
/**
* 用于计算的测试类
* 功能:
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class Calculator {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
Options options = null;
try {
System.out.println("请输入操作数1:");
double option1 = in.nextInt();
System.out.println("请输入操作数2:");
double option2 = in.nextInt();
System.out.println("请输入运算符:");
String symbol = in.next();
options = OperationFactory.createOperate(option1, option2, symbol);
System.out.println(options.getResult());
} catch (Exception e) {
System.out.println("输入有误" + e.getMessage());
}
in.close();
}
}
这样就十分的方便了,如果有一天我们希望去改加法运算只需要去改AddOption 方法即可。如果再加个运算方式呢,我们只需要继承Options实现计算方法,再去switch新增一个分支即可。
书上第一章计算器就写到这里,但是我想再优化一下switch,如果运算方法有100个,switch太长了,再者说一个方法理论了不可以超过70行。这里就用到了反射,仅仅修改工厂类就可以了,代码如下
/**
* 功能:用于计算的工厂模式
*
* @remarks TODO
* @author 小文
* @version 1.0
* @data 2020年4月21日
*/
public class OperationFactory {
/**
* 运算符和执行的类对应的集合
* 第一个参数是符号,第二个参数是对应方法的全路径类名,我这里包名用了中文,规范来说是不可以的
*/
private static Map<String, String> mappings = new HashMap<>();
static {
mappings.put("+", "工厂模式.calc.AddOption");
mappings.put("-", "工厂模式.calc.SubOption");
}
/**
* 根据不同的运算符返回不同的计算方法
*
* @param symbol
* @return
*/
public static Options createOperate(double option1, double option2, String symbol) {
try {
Class<?> clazz = Class.forName(mappings.get(symbol));
Constructor<?> cs = clazz.getConstructor(double.class, double.class);
return (Options) cs.newInstance(option1, option2);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
写的有点乱,但是如果看完相信对面向对象和工厂模式都会有一定的理解。
最后用书中的一句话结束第一章:编程是一门技术,更加是一门艺术。