1.1 面试受挫
小菜今年计算机专业大四了, 学了不少软件开发方面的东西, 也学这边了些小程序, 踌躇满志, 一心要找一个好单位. 当投了无数份简历后, 终于收到了一个面试通知, 小菜欣喜若狂.
到了人家单位, 前台小额给他一份题目, 上面写着: “请用C++, Java , C#, VN.NET 任意一种面向对象语言实现一个计算器控制台程序, 要求输入两个数和运算符号, 得到结果.”
小菜一看, 这个还不简单, 三下五除二, 10分钟不到, 小菜写完了, 感觉也没错误. 交卷后, 单位说一周内等通知吧, 于是小菜只能耐心等待. 可是半个月过去了, 什么消息也没有, 小菜很是纳闷, 我的代码实现了呀, 为什么不给我机会呢.
- 时间: 2月26日20点 地点: 大鸟房间 人物: 小菜, 大鸟
小菜找到从事软件开发工作七年的表哥大鸟, 请教原因, 大鸟问了题目和了解了小菜代码的细节以后, 哈哈大笑, 说道: “小菜呀小菜, 你上当了, 人家单位出题的意思, 你完全都没明白, 当然不会再联系你了.”
小菜: “我的代码有错吗? 单位题目不就是要我实现一个计算器的代码吗, 我这样写有什么问题.”
|
|
1.2 初学者代码的毛病
大鸟说: “且先不说出题人的意思, 单就你现在的代码, 就有很多不足的地方需要改进.”
|
|
1.3 代码规范
“哦, 说的没错, 这个我一以前听老师说过, 可是从来没有在意过, 我马上改, 改完再给你看看.”
|
|
大鸟: “吼吼, 不错不错, 改的很快嘛? 至少就目前的代码来说, 实现计算其实没有问题了, 但这样写出的代码是否符合出题人的意思呢?”
小菜: “你的意思是面小对象?”
大鸟: “哈, 小菜非小菜也!”
1.4 面向对象的编程
小菜: “ 我明白了, 他说用任意一种面向对象的语言实现, 那意思就是要用面向对象的变成方法去实现, 对吗? OK, 这个我学过, 只不过当时我没想到而已.”
大鸟: “所有编程初学者都会有这样的问题, 就是碰到问题就只觉得用计算机能够理解的逻辑来描述和表达待解决的问题. 这其实使用计算机的方式去思考, 比如计算器这个程序, 现要求输入两个数和运算符号, 然后根据运算符号判断选择如何运算, 得到结果, 这本身没有错, 但这样的思维却使的我们的程序只为满足实现当前的需求, 程序不容易维护, 不容易扩展, 更不容易复用. 从而达不到高质量代码的要求.”
小菜: “鸟哥呀, 我有点糊涂了, 如何才能容易维护, 容易扩展, 又容易复用呢, 能不能具体点.”
1.5 活字印刷, 面向对象
大鸟: “这样吧, 我给你讲个故事. 你就明白了.”
“话说三国时期, 曹操带领百万大军攻打东吴, 大军在长江赤壁驻扎, 军船连成一片, 眼看就要灭掉东吴, 统一天下, 曹操大悦, 于是大宴众文武, 在酒席间, 曹操诗兴大发, 不觉吟道, ‘喝酒唱歌, 人生真爽….’ .众文物其呼: ‘城乡好诗!’ 于是一臣子宿命印刷工匠刻板一刷, 以便流传天下.”
“样张给出来给曹操一看, 曹操感觉不妥, 说道: ‘喝与唱, 此话过俗, 应该为 ‘对酒当歌’ 较好!’ ,于是此臣就命工匠重新来过. 工匠眼看连夜刻板之功, 彻底白费, 心中叫苦不迭. 只得照办.”
“样张再次出来请曹操过目, 曹操细细一品, 觉得还是不好, 说: ‘人生真爽太过直接, 应该为问语才够意境, 因此应该为 ‘对酒当歌, 人生几何?…..’ 当臣转告工匠之时, 工匠晕倒….!”
“小菜你说, 这里面的问题出在哪里?” 大鸟问道.
小蔡说: “是不是因为三国时期呢活字印刷术还未发明, 所以要改字的时候, 就必须要整个刻板全部重新刻.”
大鸟: “说得好! 如果是有了活字印刷, 则只需要更改四个字就可, 其余工作都没白做. 岂不妙哉.”
“第一, 要改, 只需要该要改的字, 此为可维护; 第二, 这些字并非用完这次就无用, 完全可以在后来的印刷中重复使用, 此乃可复用; 第三, 此诗若要加字, 只需另刻字加入即可, 这是可扩展; 第四, 字的排列其实可能是竖排可能是横排, 此诗只需要移动就将活字可做到满足排列需求, 此是灵活性好.”
“而在活字印刷术出现之前, 上面的四种特性都无妨满足, 要修改, 必须重刻, 要加字, 必须重刻, 要重新排列, 必须重刻, 印完这本书后, 此版已无任何可再利用价值.”
小菜: “是的, 小时候, 我一直奇怪, 为何火药, 指南针, 造纸术都是从无到有, 从未知到发现的伟大发明, 而活字印刷术仅仅是从刻版印刷到活字印刷的一次技术上的进步, 为何不是评印刷术为四大发明之一呢? 原来活字印刷的成功是这个原因.”
1.6 面向对象的好处
大鸟: “哈, 这下你明白了! 我以前也不懂 不过做了软件开发几年后, 经历了太多的类似曹操这样的客户要改变需求, 更改最初想法的时间, 才逐渐明白当中的道理. 其实客观的说, 客户的要求也并不过分, 不就是改几个字吗, 但面对应经完成的代码, 却是需要几乎重头来过的尴尬, 这实在是痛苦不堪. 说白了, 原因就是因为我们原先所写的程序, 不容易维护, 灵活性差, 不容易扩展, 更谈不上复用, 因此面对需求变化, 加班加点, 对程序动大手术的那种无奈也就成了非常正常的事了. 之后当我学习了面向对象的分析设计编程思想, 开始考虑通过封装, 继承, 多台把程序的耦合度降低, 传统的印刷术的问题就在于所有的字都可在统一版面上造成耦合度太高所致, 开始使用设计模式是的程序更加的灵活, 容易修改, 并且易于复用.
“是呀, 你说的没错, 中国的四大发明, 另三种应该都是科技的进步, 伟大的创造或发现. 唯有活字印刷, 实在是思想的成功, 面向对象的胜利.” 小菜兴奋起来: “你的意思是, 面试公司出的目的是要我写出容易维护, 容易扩展, 又容易复用的计算器程序? 那该如何做啊?”
1.7 复制 vs. 复用
大鸟: “比如说, 我现在要求你在写一个 Windows 的计算器, 你现在的代码能不能复用呢?”
小菜: “那还不简单, 把代码复制过去不就行了吗? 改动又不大, 不算麻烦.”
大鸟: “ 小菜看来还是小菜啊, 初级程序员的工作就是 ctrl + c 和 ctrl + v, 这其实是非常不好的编码习惯, 因为当你的代码中重复的代码多到一定程度, 维护的时候, 可能就是一场灾难. 越大的系统, 这种方式带来的问题越严重, 编程有一原则, 就是用尽可能的办法去避免重复. 想想看, 你写的这段代码, 有哪些是和控制台无关的, 而只是和计算器有关的?”
小菜: “你的意思是分一个类出来? 哦, 对的, 让计算和显示分开.”
1.8 业务的封装
大鸟: “准确的说, 就是让业务逻辑与界面逻辑分开, 让他们之间的耦合度下降. 只有分离开, 才可以达到容易维护或扩展.”
小菜: “Let me try.”
- Operation 运算类
|
|
- 客户端代码
|
|
小菜: “鸟哥, 我写好了, 你看看!”
大鸟: “如鸟可教也, 写的不错, 这样就完全吧业务和界面分离了.”
小菜: “ 如果现在要我写一个 windows 应用程序的计算器, 我就可以复用这个运算类 (Operation) 了.”
大鸟: “不但是 windows 程序 , Web 版的程序需要运算可以用它, PDA, 手机等需要移动系统的软件需要运算也可以用它呀.”
小菜: “哈, 面向对象不过如此. 下诙谐类似的代码不怕了.”
大鸟: “别介, 仅此而已, 是在谈不上完全面向对象, 你只用了面向对象三大特性中的一个, 还有两个没用呢?”
小菜: “面向对象的三大特性不就是封装, 继承和多态吗, 这里我用到时应该是封装. 这还不够吗?我实在看不出, 这么小的程序如何用到继承. 至于多态, 其实我一直被不太了解他到底有什么好处, 如何使用它.”
大鸟 :”慢慢来, 要学的东西多着呢, 你好好想想该如何应用面向对象的继承和多态.”
1.9 紧耦合 vs. 松耦合
第二天.
小菜问道: “你说计算器这样的小程序还可以用到面向对象的三大特性? 继承和多态怎么可能用得上, 我实在是不理解.”
大鸟: “小菜很有钻研精神嘛, 好, 今天我让你功力加深一级. 你先考虑一下, 你昨天写的这个代码, 能否做到很灵活的修改和扩展呢?”
小菜: “我已经把业务和界面分离了啊, 很灵活了吧.”
大鸟: 那我问你, 现在如果我希望增加一个开根(sqrt) 运算, 你如何改?”
小菜: “那只需要该Operation 类就行了, 在 switch 中加一个分支就行了.”
大鸟: “问题是你要加一个平方根运算, 却需要让加减乘除运算都得来参与编译, 如果你不小心, 把加法运算改成了减法, 这岂不是很糟糕. 打个比方, 如果公司要求你为公司的薪资管理系统做维护, 原来只有技术人员(月薪), 事成销售人员(底薪+提成), 经理(年薪+股份)三种运算算法, 现在要增加兼职工作人员(时薪)算法, 但按照你昨天程序的写法, 公司就必须要把包含元三种算法的运算类给你, 让你改, 如果心中小算盘一打, ‘TMD, 公司给我的工资这么低, 我真的是郁闷, 这下有机会了’ ,于是你除了增加了价值算法意外, 自技术人员(月薪)算法中写了一句
|
|
那就意味着, 你的月薪每月都会增加 10%(小心被抓去坐牢), 本来是让你加一个功能, 却使得原来运行良好的功能代码产生了变化, 这个风险太大, 你明白了吗?”
小菜: “哦, 你的意思是, 我应该吧加减乘除等运算分离, 修改其中一个不影响另外几个, 增加运算算法与不影响其他代码, 是这样吗?”
大鸟: “自己想去吧, 如何用继承和多态, 你应该有感觉了.”
小菜: “OK”
- Operation 运算类
|
|
- 加减乘除类
|
|
小菜: “鸟哥, 我按照你说的方法写出来了一部分, 首先是一个运算类, 他有两个 Number 属性, 主要用于计算器的前后数, 然后又一个抽象方法 getResult()
, 用于得到结果, 然后我把加减乘除都写成了运算类的子类, 继承他后, 重写getResult()
方法, 这样如果妖修噶任何一个算法, 就不需要提供其他算法的代码了. 但是问题来了, 我如何让计算器知道我是希望用哪一个算法呢?”
1.10 简单工厂模式(Simple Factory Pattern)
大鸟: “写得很不错嘛, 大大超出我的想象了, 你现在的问题其实就是如何去实例化对象的问题, 哈, 今天心情不错, 再教你一招 ‘简单工厂模式’ 也就是说, 到底要实力化谁, 将来会不会增加实例化的对象, 比如增加开更运算, 这是很容易变化的地方, 应该考虑用一个单独的类来做这个创造实例的过程, 这就是工厂.”
- 简单运算工厂类
|
|
大鸟: “哈, 看到了吧, 这样子, 你只需要输入运算符号, 工厂就实例化出合适的对象, 通过多态, 返回父类的方式实现了计算器的结果.”
- 客户端代码
|
|
大鸟: “哈, 界面的实现就是这样的代码, 不挂你是控制台程序, windows 程序, web 程序, pda 程序或手机程序, 都可以用这段代码来实现计算器的功能, 如果有一天我们需要更改加法运算, 我们只需要改OperationAdd
就可以了. 如果我们需要增加各种复杂运算, 比如平方根, 立方根, 自然对数, 正余弦等, 如何做?”
小菜: “只要增加相应的运算子类就可以了.”
大鸟: “嗯? 够了吗?”
小菜: “对了, 还需要去修改运算类工厂, 在 switch 中增加分支. “
大鸟: “哈, 那才对, 那如果要修改界面呢?”
小菜: “那就去该界面, 不管运算的事.”
大鸟: “最后, 我们来看看这几个类的结构图.”