书中人物两位:小菜和大鸟
故事从一份题目开始:请用C++、Java、C#任意一种面向对象语言实现一个计算器控制台程序,要求输入两个数和运算符号,得到结果。
小菜第一次答题,代码如下:
import java.util.Scanner;
public class SimpleFactoryModel {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("请输入数字A: ");
double A = in.nextDouble();
System.out.println("请选择运算符号(+ - * /): ");
String c = in.next();
System.out.println("请输入数字B: ");
double B = in.nextDouble();
double res = 0;
if(c.equals("+")){
res = A + B;
}
if(c.equals("-")){
res = A - B;
}
if(c.equals("*")){
res = A * B;
}
if(c.equals("/")){
res = A / B;
}
System.out.println("结果是: " + res);
}
}
老鸟一眼就看出了三处问题,在不考虑出题人本意的情况下,哈哈哈
第一处:命名不规范问题:A、B、c 命名最好不要在代码中出现
第二处:判断分支问题:四个if意味着每个条件都要做判断,计算机相当于做了三次无用功
第三处:未考虑除数为0的情况
小菜去修改代码了…
import java.util.Scanner;
public class SimpleFactoryModel {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
try {
System.out.println("请输入数字A: ");
double numberA = in.nextDouble();
System.out.println("请选择运算符号(+ - * /): ");
String strOperate = in.next();
System.out.println("请输入数字B: ");
double numberB = in.nextDouble();
double result = 0;
switch (strOperate){
case "+":result = numberA + numberB;break;
case "-":result = numberA - numberB;break;
case "*":result = numberA * numberB;break;
case "/":
if(numberB!=0){
result = numberA / numberB;
}else {
System.out.println("除数不能为0!");
}break;
}
if(numberB!=0){
System.out.println("结果是: " + result);
}
}catch (Exception e){
System.out.println("您的输入有错:"+e.getMessage());
}
}
}
大鸟:不错不错,代码改的很快嘛,但是这样写出的代码是否符合出题人的意思呢?
小菜:你的意思是面向对象?
大鸟:哈哈,小菜非小菜也!
不要走开,下面的故事非常精彩!
片段一:话说三国时期,曹操带领百万大军攻打东吴,大军在长江赤壁驻扎,军船连成一片,眼看就要灭掉东吴,一统天下,曹操大悦,于是大宴众文武,在酒席间,曹操诗兴大发,不觉吟道:喝酒唱歌,人生真爽 !。。。众文武齐呼:丞相好诗!,于是一臣子速命印刷工匠刻板印刷,以便流传天下。
片段二:样张出来给曹操一看,曹操感觉不妥,说道:喝与唱,此话过俗,应该为 对酒当歌 较好,于是此臣就命令工匠重新来过。工匠眼看连夜刻板之工,彻底白费,心中叫苦不迭,只得照办。
片段三:样张再次出来请曹操过目,曹操细细一品,觉得还是不好,说人生太爽太过直接,应改成问语才够有意境,因此应改为:对酒当歌,人生几何? 当臣子转告工匠之时,工匠晕倒。。。
三国时,还未有活字印刷,所以要改字的时候,就必须将整个刻板全部重新刻,但有了活字印刷,则只需要改四个字即可,其余工作也未白做,岂不妙哉!
可维护:要改,只需要改要改之字;
可复用:这些字,并非用完之后就无用,完全可以在后来的印刷术中重复使用;
可扩展:此诗如果要加字,只需另刻字加入即可;
灵活性好:字的排列可以是横排,也可以是竖排,只需要将活字移动即可;
在活字印刷出现之前,上面的四种特性都无法满足,要修改,必须重刻;要加字,必须重刻;要重新排列,必须重刻;印完这本书后,此版已无任何再利用的价值。
中国古代四大发明,火药、指南针、造纸术都是技术上的进步,而活字印刷则是思想上的成功,是面向对象的胜利。那么如何写出容易维护、容易扩展、又容易复用的计算器程序呢?
复用并不是复制,编程有一个原则就是尽可能的去避免重复。
计算器可以分为计算和显示,即业务逻辑和界面逻辑,将计算业务封装成一个类,与显示界面分开,业务端代码如下:
public class Operation{
public static double GetResult(double numberA,double numberB,String strOperate){
double result = 0;
switch (strOperate){
case "+":result = numberA + numberB;break;
case "-":result = numberA - numberB;break;
case "*":result = numberA * numberB;break;
case "/":result = numberA / numberB;break;
}
return result;
}
}
客户端代码如下:
import java.util.Scanner;
public class SimpleFactoryModel {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
try {
System.out.println("请输入数字A: ");
double numberA = in.nextDouble();
System.out.println("请选择运算符号(+ - * /): ");
String strOperate = in.next();
System.out.println("请输入数字B: ");
double numberB = in.nextDouble();
if(strOperate.equals("/") && numberB==0){
System.out.println("请重新输入数字B(除数不能为0): ");
numberB = in.nextDouble();
}
double result = Operation.GetResult(numberA,numberB,strOperate);
System.out.println("结果是:"+result);
}catch (Exception e){
System.out.println("您的输入有错:"+e.getMessage());
}
}
}
上述的代码确实考虑了面向对象,但是只用到了面向对象三大特性中的一个:封装,另外两个特性继承和多态是不是也可以被应用呢?
小菜:你说计算器这样的小程序还可以用到面向对象的三大特性,继承和多态怎么可能用得上,我实在是不能理解
大鸟:很有钻研精神嘛,看我让你功力加深一级,你先考虑一下,现在的代码能否做到灵活的可修改和扩展呢
小菜:业务和界面已经分离了呀,不是很灵活了吗
大鸟:那我问你,现在要加一个开根运算(sqrt),你要如何改
小菜:只需要修改Operation类,在switch中加一个分支就可以了
大鸟:那么问题来了,你只是加一个开跟运算,却要让加减乘除的运算都参与编译,如果你一不小心把加法改成了减法,这不是太糟糕了!打个比方,公司现在要求你为公司的薪资管理系统维护,原来只有技术人员(月薪)、市场销售人员(底薪+提成)、经理(年薪+股份)三种运算算法,现在要增加兼职工作人员(时薪)的算法,按照你之前的写法,公司就要把包含原三种算法的运算类给你,让你修改,如果你心中小算盘一打,TMD,公司给我的工资这么低,我真是郁闷,这下可让我逮着机会了,于是你在增加了兼职算法以外,在技术人员(月薪)算法中加了一句
if(员工是小菜)
salary = salary * 1.1;
那就意味着,你的月薪每个月都会增加10%,本来是增加一个功能,却让原有的运行良好的代码产生了变化,这个风险太大了,懂了吗
小菜:哦,你的意思是,我应该把加减乘除等运算分离,修改其中一个不影响另外的几个,增加新的运算算法也不影响其他代码,是这样吗
大鸟:自己想去吧,如何用继承和多态
。。。
思路如下:
1. 运算类:
/**
*角色:抽象产品 Product
* 职责:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
*/
public abstract class Operation{
private double numberA = 0;
private double numberB = 0;
public double getNumberA() {
return numberA;
}
public void setNumberA(double numberA) {
this.numberA = numberA;
}
public double getNumberB() {
return numberB;
}
public void setNumberB(double numberB) {
this.numberB = numberB;
}
public abstract double GetResult();
}
2. 加法类继承运算类
/**
* 角色:具体产品 Concrete Product
* 职责:简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例
*/
public class OperationAdd extends Operation {
@Override
public double GetResult(){
return getNumberA() + getNumberB();
}
}
类似的还有减法类、乘法类和除法类如下:
public class OperationSub extends Operation{
@Override
public double GetResult(){
return getNumberA() - getNumberB();
}
}
public class OperationMul extends Operation {
@Override
public double GetResult() {
return getNumberA() * getNumberB();
}
}
public class OperationDiv extends Operation {
@Override
public double GetResult() {
return getNumberA()/getNumberB();
}
}
3. 工厂类来负责具体实例化哪一个对象
/**
* 角色:工厂 Creator
* 职责:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。
* 工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
*/
public class OperationFactory {
public static Operation createOperate(String operate){ //operate是运算符
Operation operation = null; //operation是实例化对象
//只需要输入运算符号,运算类工厂就可以实例化出合适的对象,通过多态,返回父类的方式实现计算结果
switch (operate){
case "+":
operation = new OperationAdd();
break;
case "-":
operation = new OperationSub();
break;
case "*":
operation = new OperationMul();
break;
case "/":
operation = new OperationDiv();
break;
case "@"://以@符号表示两数之和的开跟运算,在添加新的运算方式时,在case分支中要记得添加具体的实例化对象
operation = new OperationSqrt();
break;
}
return operation;
}
}
4. 客户端代码:
/**
* 什么是工厂:用一个单独的类来做这个类创造实例的过程,这就是工厂
* 简单工厂模式解决了对象的创建问题
* 在已知某些条件后,对于类的选择(这些待选择的类都是同一父类的子类)时使用简单工厂模式
*/
import java.util.Scanner;
public class SimpleFactoryModel {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
try {
System.out.println("请输入数字A: ");
double numberA = in.nextDouble();
System.out.println("请选择运算符号(+ - * /): ");
String strOperate = in.next();
System.out.println("请输入数字B: ");
double numberB = in.nextDouble();
if(strOperate.equals("/") && numberB==0){
System.out.println("请重新输入数字B(除数不能为0): ");
numberB = in.nextDouble();
}
Operation operation;
operation = OperationFactory.createOperate(strOperate); //实例化运算类
operation.setNumberA(numberA); //确定运算数字
operation.setNumberB(numberB);
double result = operation.GetResult(); //调用方法得到运算结果
System.out.println("结果是:"+result);
}catch (Exception e){
System.out.println("您的输入有错:"+e.getMessage());
}
}
}
这样如果我们要修改加法运算,只需要修改OperationAdd就可以了,如果要增加平方根运算等其他运算,只要增加相应的运算子类,同时修改运算类工厂,在switch中增加分支。
比如增加两数之和的开跟运算,增加如下子类,分支已在之前的代码中增加
public class OperationSqrt extends Operation {
@Override
public double GetResult() {
double mul = getNumberA()+getNumberB();
return Math.sqrt(mul);
}
}
总结:
简单工厂类通过createOperate方法来实例化具体的对象(运算类的子类),与运算类之间是依赖关系,运算类与加法类、减法类等是继承关系。
一个小小的计算器也可以写出精彩的代码,编程是一门技术,更是一门艺术,不能只满足于写完代码运行结果正确就完事,要时常考虑如何让代码更简练,更容易维护,容易扩展和复用,只有这样才能真正得到提高。
这只是理解面向对象的开始,一起加油吧!