1. 代码无错就是优? ---简单工厂模式(Simple Factory Pattern)

1.1 面试受挫

     小菜今年计算机专业大四了, 学了不少软件开发方面的东西, 也学这边了些小程序, 踌躇满志, 一心要找一个好单位. 当投了无数份简历后, 终于收到了一个面试通知, 小菜欣喜若狂.
     到了人家单位, 前台小额给他一份题目, 上面写着: “请用C++, Java , C#, VN.NET 任意一种面向对象语言实现一个计算器控制台程序, 要求输入两个数和运算符号, 得到结果.”
    小菜一看, 这个还不简单, 三下五除二, 10分钟不到, 小菜写完了, 感觉也没错误. 交卷后, 单位说一周内等通知吧, 遇事小菜值得耐心等待. 可是半个月过去了, 什么消息也没有, 小菜很是纳闷, 我的代码实现了呀, 为什么不给我机会呢.

  • 时间: 2月26日20点 地点: 大鸟房间 人物: 小菜, 大鸟

    小菜找到从事软件开发工作七年的表哥大鸟, 请教原因, 大鸟问了题目和了解了小菜代码的细节以后, 哈哈大笑, 说道: “小菜呀小菜, 你上当了, 人家单位出题的意思, 你完全都没明白, 当然不会再联系你了.”
    小菜: “我的代码有错吗? 单位题目不就是要我实现一个计算器的代码吗, 我这样写有什么问题.”

public class Program {
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        System.out.println("请输入数字A:");
        String A = s.nextLine();
        System.err.println("请输输入运算符号(+,-,*,/):");
        String B = s.nextLine();
        System.out.println("请输入数字B:");
        String C = s.nextLine();
        String D = "";

        if (B == "+")
            D = Double.toString(Double.valueOf(A) + Double.valueOf(B));
        if (B == "-")
            D = Double.toString(Double.valueOf(A) - Double.valueOf(B));
        if (B == "*")
            D = Double.toString(Double.valueOf(A) * Double.valueOf(B));
        if (B == "/")
            D = Double.toString(Double.valueOf(A) / Double.valueOf(B));

        System.out.println("结果是: " + D);
    }
}


1.2 初学者代码的毛病

     大鸟说: “且先不说出题人的意思, 单就你现在的代码, 就有很多不足的地方需要改进.”

public class Program {
    public static void main(String[] args) {

        // 像s,A,B,C,D 这个样子命名是非常不规范的
        Scanner s = new Scanner(System.in);
        System.out.println("请输入数字A:"); 
        String A = s.nextLine();
        System.err.println("请输输入运算符号(+,-,*,/):");
        String B = s.nextLine();
        System.out.println("请输入数字B:");
        String C = s.nextLine();
        String D = "";

        // 判断分支, 你这样的写法, 意味着每条分支都要做判断
        // 等于计算机做了三次无用功
        if (B == "+")
            D = Double.toString(Double.valueOf(A) + Double.valueOf(C));
        if (B == "-")
            D = Double.toString(Double.valueOf(A) - Double.valueOf(C));
        if (B == "*")
            D = Double.toString(Double.valueOf(A) * Double.valueOf(C));
        if (B == "/")
            // 如果除数, 客户输入了0怎么办
            // 如果用户输入的是字符符号, 而不是数字怎么办
            D = Double.toString(Double.valueOf(A) / Double.valueOf(B));

        System.out.println("结果是: " + D);
    }
}


1.3 代码规范

     “哦, 说的没错, 这个我一以前听老师说过, 可是从来没有在意过, 我马上改, 改完再给你看看.”

public class Program {
    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入数字A:");
            String strNumberA = scanner.nextLine();
            System.err.println("请输输入运算符号(+,-,*,/):");
            String strOperate = scanner.nextLine();
            System.out.println("请输入数字B:");
            String strNumberB = scanner.nextLine();
            String strResult = "";

            switch (strOperate) {
            case "+":
                strResult = Double.toString(Double.valueOf(strNumberA) + Double.valueOf(strNumberB));
                break;
            case "-":
                strResult = Double.toString(Double.valueOf(strNumberA) - Double.valueOf(strNumberB));
                break;
            case "*":
                strResult = Double.toString(Double.valueOf(strNumberA) * Double.valueOf(strNumberB));
                break;
            case "/":
                if (!strNumberB.equals("0")) {
                    strResult = Double.toString(Double.valueOf(strNumberA) / Double.valueOf(strNumberB));
                } else {
                    strResult = "除数不能为0";
                }
                break;
            default:
                break;
            }
            System.out.println("结果是: " + strResult);
            scanner.close();

        } catch (Exception e) {
            System.out.println("您的输入有错: " + e);
        }
    }
}

      大鸟: “吼吼, 不错不错, 改的很快嘛? 至少就目前的代码来说, 实现计算其实没有问题了, 但这样写出的代码是否符合出题人的意思呢?”

      小菜: “你的意思是面小对象?”
      大鸟: “哈, 小菜非小菜也!”

1.4 面向对象的编程

      小菜: ” 我明白了, 他说用任意一种面向对象的语言实现, 那意思就是要用面向对象的变成方法去实现, 对吗? OK, 这个我学过, 只不过当时我没想到而已.”
      大鸟: “所有b初编程学者都会有这样的问题, 就是碰到问题就只觉得用计算机能够理解的逻辑来描述和表达待解决的问题. 这其实使用计算机的方式去思考, 比如计算器这个程序, 现要求输入两个数和运算符号, 然后根据运算符号判断选择如何运算, 得到结果, 这本身没有错, 打这样的思维缺失的我们的程序只为满足实现当前的需求, 程序不容易维护, 不容易扩展, 更不容易复用. 从而达不到高质量代码的要求.”
     小菜: “鸟哥呀, 我有点糊涂了, 如何才能容易为辅, 容易扩展, 又容易复用呢, 能不能具体点.”

1.5 活字印刷, 面向对象

     大鸟: “这样吧, 我给你讲个故事. 你就明白了.”
    “话说三国时期, 曹操带领百万大军攻打东吴, 大军在长江赤壁驻扎, 军船连成一片, 眼看就要灭掉东吴, 统一天下, 曹操大悦, 于是大宴众文武, 在酒席间, 曹操诗兴大发, 不觉吟道, ‘喝酒唱歌, 人生真爽….’ .众文物其呼: ‘城乡好诗!’ 于是一臣子宿命印刷工匠刻板一刷, 以便流传天下.”
    “样张给出来给曹操一看, 曹操感觉不妥, 说道: ‘喝与唱, 此话过俗, 应该为 ‘对酒当歌’ 较好!’ ,于是此臣就命工匠重新来过. 工匠眼看连夜刻板之功, 彻底白费, 心中叫苦不迭. 只得照办.”
    “样张再次出来请曹操过目, 曹操细细一品, 觉得还是不好, 说: ‘人生真爽太过直接, 应该为问语才够意境, 因此应该为 ‘对酒当歌, 人生几何?…..’ 当臣装个工匠之时, 工匠晕倒….!”
    “小菜你说, 这里面的问题出在哪里?” 大鸟问道.
      小蔡说: “是不是因为三国时期呢活字印刷术还未发明, 所以要改字的时候, 就必须要整个刻板全部重新刻.”
      大鸟: “说得好! 如果是有了伙子印刷, 则只需要更改四个字就可, 其余工作都为白做. 岂不妙哉.”
    “第一, 要改, 只需要该要改的字, 此为可维护; 第二, 这些字并非用完这次就无用, 完全可以在后来的印刷中重复使用, 此乃可复用; 第三, 此诗若要加字, 只需另刻字加入即可, 这是可扩展; 第四, 字的排列其实可能是竖排可能是横排, 此诗只需要移动就将活字可做到满足排列需求, 此是灵活性好.”
    “而在活字印刷术出现之前, 上面的四种特性都无妨满足, 要修改, 必须重刻, 要加字, 必须冲克, 要重新排列, 必须重刻, 印完这本书后, 此版已无任何可再利用价值.”
    小菜: “是的, 小时候, 我一直奇怪, 为何火药, 指南针, 造纸术都是从无到有, 从未知到发现的伟大发明, 而活字印刷术仅仅是从刻版印刷到活字印刷的一次技术上的进步, 为何不是评印刷术为四大发明之一呢? 原来活字印刷的成功是这个原因.”

1.6 面向对象的好处

    大鸟: “哈, 这下你明白了! 我以前也不懂 不过做了软件开发几年后, 经历了太多的类似曹操这样的客户要改变需求, 更改最初想法的时间, 才逐渐明白当中的道理. 其实客观的说, 客户的要求也并不过分, 不就是改几个字吗, 但面对应经完成的代码, 却是需要几乎重头来过的尴尬, 这实在是痛苦不堪. 说白了, 原因就是因为我们原先所写的程序, 不容易维护, 灵活性差, 不容易扩展, 更谈不上复用, 因此面对需求变化, 加班加点, 对程序东大手术的那种无奈也就成了非常正常的事了. 之后当我学习了面向对象的分析设计编程思想, 开始考虑通过封装, 继承, 多台把程序的耦合度降低, 传统的尹叔叔的问题就在于所有的字都可在统一版面上造成耦合度太高所致, 开始使用设计模式是的程序更加的灵活, 容易修改, 并且易于复用.
    “是呀, 你说的没错, 中国的四大发明, 另三种应该都是科技的进步, 伟大的创造或发现. 唯有活字印刷, 实在是思想的成功, 面向对象的胜利.” 小菜兴奋起来: “你的意思是, 面试公司出的目的是要我写出容易维护, 容易扩展, 又容易服用的计算器程序? 那该如何做啊?”

1.7 复制 vs. 复用

    大鸟: “比如说, 我现在要求你在写一个 Windows 的计算器, 你现在的代码能不能复用呢?”
    小菜: “那还不简单, 把代码复制过去不就行了吗? 改动又不大, 不算麻烦.”
    大鸟: ” 小菜看来还是小菜啊, 初级程序员的工作就是 ctrl + c 和 ctrl + v, 这其实是非常不好的编码习惯, 因为当你的代码中重复的代码多到一定程度, 维护的时候, 可能就是一场灾难. 越大的系统, 何种方式带来的问题越严重, 编程有一原则, 就是用尽可能的办法去避免重复. 想想看, 你写的这段代码, 有哪些是和控制台无关的, 而只是和计算器有关的?”
    小菜: “你的意思是分一个类出来? 哦, 对的, 让计算和显示分开.”

1.8 业务的封装

    大鸟: “准确的说, 就是让业务逻辑与界面逻辑分开, 让他们之间的耦合度下降. 只有分离开, 才可以达到容易维护或扩展.”
    小菜: “Let me try.”

  • Operation 运算类
public class Operation {
    public static double GetResult(double numberA, double numberB, String operate){
        double result = 0d;
        switch (operate) {
        case "+":
            result = numberA + numberB;
            break;
        case "-":
            result = numberA - numberB;
            break;
        case "*":
            result = numberA * numberB;
            break;
        case "/":
            result = numberA / numberB;
            break;
        }
        return result;
    }
}
  • 客户端代码
public class Client {
    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入数字A:");
            String strNumberA = scanner.nextLine();
            System.err.println("请输输入运算符号(+,-,*,/):");
            String strOperate = scanner.nextLine();
            System.out.println("请输入数字B:");
            String strNumberB = scanner.nextLine();

            double strResult = Operation.GetResult(Double.valueOf(strNumberA), 
                    Double.valueOf(strNumberB), strOperate);
            System.out.println("结果是: " + strResult);
            scanner.close();

        } catch (Exception e) {
            System.out.println("您的输入有错: " + e);
        }
    }
}

    小菜: “鸟哥, 我写好了, 你看看!”
    大鸟: “如鸟可教也, 写的不错, 这样就完全吧业务和界面分离了.”
    小菜: ” 如果现在要我写一个 windows 应用程序的计算器, 我就可以复用这个运算类 (Operation) 了.”
    大鸟: “不但是 windows 程序 , Web 版的程序需要运算可以用它, PDA, 手机等需要移动系统的软件需要运算也可以用它呀.”
    小菜: “哈, 面向对象不过如此. 下诙谐类似的代码不怕了.”
    大鸟: “别介, 仅此而已, 是在谈不上完全面向对象, 你只用了面向对象三大特性中的一个, 还有两个没用呢?”
    小菜: “面向对象的三大特想不就是封装, 继承和堕胎吗, 这里我用到时应该是封装. 这还不够吗?我实在看不出, 这么小的程序如何用到继承. 至于多态, 其实我一直被不太了解他到底有什么好处, 如何使用它.”
    大鸟 :”慢慢来, 要学的东西多着呢, 你好好想想该如何应用面向对象的继承和多态.”

1.9 紧耦合 vs. 松耦合

    第二天.
    小菜问道: “你说计算器这样的小程序还可以用到面向对象的三大特性? 继承和多态怎么可能用得上, 我实在是不理解.”
    大鸟: “小菜很有钻研精神嘛, 号, 今天我让你功力加深一级. 你先考虑一下, 你昨天写的这个代码, 能否做到很灵活的修改和扩展呢?”
    小菜: “我已经把业务和界面分离了啊, 很灵活了吧.”
    大鸟: 那我问你, 现在如果我希望增加一个开根(sqrt) 运算, 你如何改?”
    小菜: “那只需要该Operation 类就行了, 在 switch 中加一个分支就行了.”
    大鸟: “问题是你要加一个平方根运算, 却需要让加减乘除运算都得来参与编译, 如果你不小心, 把加法运算改成了减法, 这岂不是很糟糕. 打个比方, 如果公司要求你为公司的薪资管理系统做维护, 原来只有技术人员(月薪), 事成销售人员(底薪+提成), 经理(年薪+股份)三种运算算法, 现在要增加兼职工作人员(时薪)算法, 但按照你昨天程序的写法, 公司就必须要把包含元三种算法的运算类给你, 让你改, 如果心中小算盘一打, ‘TMD, 公司给我的工资这么低, 我真的是郁闷, 这下有机会了’ ,于是你除了增加了价值算法意外, 自技术人员(月薪)算法中写了一句

if(员工是小菜){
    salary = salary * 1.1;
}

    那就意味着, 你的月薪每月都会增加 10$(小心被抓去坐牢), 本来是让你加一个功能, 却使得原来运行良好的功能代码产生了变化, 这个风险太大, 你明白了吗?”
    小菜: “哦, 你的意思是, 我应该吧加减乘除等运算分离, 修改其中一个不影响另外几个, 增加运算算法与不影响其他代码, 是这样吗?”
    大鸟: “自己想去吧, 如何用继承和多态, 你应该有感觉了.”
    小菜: “OK”

  • Operation 运算类
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();
}
  • 加减乘除类
public class OperationAdd extends Operation {
    @Override
    public double getResult() {
        double result = 0;
        result = getNumberA() + getNumberB();
        return result;
    }
}

public class OperationSub extends Operation {
    @Override
    public double getResult() {
        double result = 0;
        result = getNumberA() - getNumberB();
        return result;
    }
}

public class OperationMulti extends Operation {
    @Override
    public double getResult() {
        double result = 0;
        result = getNumberA() * getNumberB();
        return result;
    }
}

public class OperationDiv {
    @Override
    public double getResult() throws Exception {
        double result = 0;
        if (0 == getNumberB()) {
            System.out.println("出书不能为0.");
            return result;
        }
        result = getNumberA() / getNumberB();
        return result;
    }
}

    小菜: “鸟哥, 我按照你说的方法写出来了一部分, 首先是一个运算类, 他有两个 Number 属性, 主要用于计算器的前后数, 然后又一个虚方法 getResult(), 用于得到结果, 然后我把佳节惩处都写成了运算类的子类, 继承他后, 重写getResult() 方法, 这样如果妖修噶任何一个算法, 就不需要提供其他算法的代码了. 但是问题来了, 我如何让计算器知道我是希望用哪一个算法呢?”

1.10 简单工厂模式(Simple Factory Pattern)

    大鸟: “写得很不错嘛, 大大超出我的想象了, 你现在的问题其实就是如何去实例化对象的问题, 哈, 今天心情不错, 再教你一招 ‘简单工厂模式’ 也就是说, 到底要实力划水, 将来会不会增加实例化的对象, 比如增加开更运算, 这是很容易变化的地方, 应该考虑用一个单独的类来做这个创造实例的过程, 这就是工厂.”

  • 简单运算工厂类
public class OperationFactory {
    public static Operation createOperation(String operate){
        Operation oper = null;
        switch (operate) {
        case "+":
            oper = new OperationAdd();
            break;
        case "-":
            oper = new OperationSub();
            break;
        case "*":
            oper = new OperationMulti();
            break;
        case "/":
            oper = new OperationDiv();
            break;
        }
        return oper;
    }
}

    大鸟: “哈, 看到了吧, 这样子, 你只需要输入运算符号, 工厂就实例化出合适的对象, 通过多态, 返回父类的方式实现了计算器的结果.”

  • 客户端代码
public class Test {
    public static void main(String[] args) {
        Operation oper;
        oper = OperationFactory.createOperation("+");
        oper.setNumberA(1);
        oper.setNumberB(2);
        double result = oper.getResult();
        System.out.println(result);
    }
}

    大鸟: “哈, 界面的实现就是这样的代码, 不挂你是控制台程序, windows 程序, web 程序, pda 程序或手机程序, 都可以用这段代码来实现计算器的功能, 如果有一天我们需要更改加法运算, 我们只需要改OperationAdd 就可以了. 如果我们需要增加各种复杂运算, 比如平方根, 立方根, 自然对数, 正余弦等, 如何做?”
    小菜: “只要增加哦相应的运算子类就可以了.”
    大鸟: “嗯? 够了吗?”
    小菜: “对了, 还需要去修改运算类工厂, 在 switch 中增加分支. ”
    大鸟: “哈, 那才对, 那如果要修改界面呢?”
    小菜: “那就去该界面, 不管运算的事.”
    大鸟: “最后, 我们来看看这几个类的结构图.”

运算类的工厂方法

1.11 UML 类图

   0.0 番外篇 UML 类图


     

所谓的工厂就是指: 用一个单独的类创造实例的过程.
    准确的来说, 简单工厂方法并不是一种设计模式, 它是用一个方法来生成一个类的对象. 当实例化的的类容易发生变化时, 可以考虑用一个单独的类来执行来执行这个实例化的过程.
    比如说定义了一个运算类, 包含加减乘除运算, 将来还可能增加平方, 根号运算等, 这个运算类就可以考虑用简单工厂方法来实现.

  • 相关的类图

    一个运算类, Operation, 有四个子类分别是, Add, Subtract, Multiply, Divide. 一个简单工厂类, SimpleFactory 依赖于一个Operation.

    当需要一个运算对象时, 给SimpleFactory.createOperation() 传入相应的参数, 得到对应的Operation 对象, 执行相应的操作.

    当需要扩展时, 比如说增减一个开方运算. 只需要增加一个 Sqrt 类, 然后修改 SimpleFactory 中的createOperation 方法, 客户端并不需要做修改. 也是在一定的程度上体现了 开放封闭原则 .

只不过并不完美.



工厂方法模式(Factory Method Pattern)

    与简单工厂方法相比较, 工厂方法模式把Factory 的每个方法抽取了出来, (Factory Method) 定义了一个用于创建对象的接口, 让子类决定实例化那一个类. 工厂方法使一个类的实例化延迟到其子类进行.

    简单工厂的最大的优点就是工厂类包含了必要的逻辑判断, 根据客户端的选择来实例化具体的类, 减少了主应用程序对产品的依赖.

     但是, 对于简单工厂方法来说, 如果需要添加新的生成类, 就需要修改工厂类, 也是在一定程度上违反了开放-封闭原则 所以就引入了工厂方法模式. 可以先看一下, 使用工厂方法模式实现的运算类的相关类图(uml):

工厂方法模式

    可以看到, 每一个工厂类的子类依赖(dependency) 于一个产品对象. 而抽象的工厂与抽象的产品之间没有依赖, 做到了二者之间的解耦(decoupling).

    当我们需要一个新的产品时, 只需要增加一个实现的工厂, 而不需要去修改之前的代码, 做到了对扩展开放, 对修改关闭 这一设计原则.

  • 工厂方法模式结构图

这里写图片描述

    简单工厂模式的最大优点在于工厂类中间包含了必要的逻辑判断, 根据客户端的选择条件动态的实例化相关的类,对于客户端来说, 出去了与具体产品的依赖.
    但是工厂方法实现时, 客户端需要决定实例化哪一个工厂来实现运算类, 选择判断的问题还是存在的, 也就是说, 工厂方法把简单工厂的内部逻辑判断移到了客户端代码来进行. 你想要加功能, 本来是修改工厂类的, 而现在是修改客户端.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值