大话设计模式读书笔记之简单工厂模式(Java版)

书中人物两位:小菜和大鸟
故事从一份题目开始:请用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方法来实例化具体的对象(运算类的子类),与运算类之间是依赖关系,运算类与加法类、减法类等是继承关系。

一个小小的计算器也可以写出精彩的代码,编程是一门技术,更是一门艺术,不能只满足于写完代码运行结果正确就完事,要时常考虑如何让代码更简练,更容易维护,容易扩展和复用,只有这样才能真正得到提高。

这只是理解面向对象的开始,一起加油吧!

发布了22 篇原创文章 · 获赞 21 · 访问量 6973

猜你喜欢

转载自blog.csdn.net/weixin_43108122/article/details/104551007