2006年8月15日
#
0.引言
在ChinaITLAB导师制辅导中,笔者发现问得最多的问题莫过于"如何学习编程?JAVA该如何学习?"。类似的问题回答多了,难免会感觉厌烦,就萌生了写下本文的想法。到时候再有人问起类似的问题,我可以告诉他(她),请你去看看《JAVA学习之路》。拜读过台湾蔡学镛先生的《JAVA夜未眠》,有些文章如《JAVA学习之道》等让我们确实有共鸣,本文题目也由此而来。
软件开发之路是充满荆棘与挑战之路,也是充满希望之路。JAVA学习也是如此,没有捷径可走。梦想像《天龙八部》中虚竹一样被无崖子醍醐灌顶而轻松获得一甲子功力,是很不现实的。每天仰天大叫"天神啊,请赐给我一本葵花宝典吧",殊不知即使你获得了葵花宝典,除了受自宫其身之苦外,你也不一定成得了"东方不败",倒是成"西方失败"的几率高一点。
"不走弯路,就是捷径",佛经说的不无道理。
1.如何学习程序设计?
JAVA是一种平台,也是一种程序设计语言,如何学好程序设计不仅仅适用于JAVA,对C++等其他程序设计语言也一样管用。有编程高手认为,JAVA也好C也好没什么分别,拿来就用。为什么他们能达到如此境界?我想是因为编程语言之间有共通之处,领会了编程的精髓,自然能够做到一通百通。如何学习程序设计理所当然也有许多共通的地方。
1.1 培养兴趣
兴趣是能够让你坚持下去的动力。如果只是把写程序作为谋生的手段的话,你会活的很累,也太对不起自己了。多关心一些行业趣事,多想想盖茨。不是提倡天天做白日梦,但人要是没有了梦想,你觉得有味道吗?可能像许多深圳本地农民一样,打打麻将,喝喝功夫茶,拜拜财神爷;每个月就有几万十几万甚至更多的进帐,凭空多出个"食利阶层"。你认为,这样有味道吗?有空多到一些程序员论坛转转,你会发现,他们其实很乐观幽默,时不时会冒出智慧的火花。
1.2 慎选程序设计语言
男怕入错行,女怕嫁错郎。初学者选择程序设计语言需要谨慎对待。软件开发不仅仅是掌握一门编程语言了事,它还需要其他很多方面的背景知识。软件开发也不仅仅局限于某几个领域,而是已经渗透到了各行各业几乎每一个角落。
如果你对硬件比较感兴趣,你可以学习C语言/汇编语言,进入硬件开发领域。如果你对电信的行业知识及网络比较熟悉,你可以在C/C++等之上多花时间,以期进入电信软件开发领域。如果你对操作系统比较熟悉,你可以学习C/Linux等等,为Linux内核开发/驱动程序开发/嵌入式开发打基础。如果你想介入到应用范围最广泛的应用软件开发(包括电子商务电子政务系统)的话,你可以选择J2EE或.NET,甚至LAMP组合。每个领域要求的背景知识不一样。做应用软件需要对数据库等很熟悉。总之,你需要根据自己的特点来选择合适你的编程语言。
1.3 要脚踏实地,快餐式的学习不可取
先分享一个故事。
有一个小朋友,他很喜欢研究生物学,很想知道那些蝴蝶如何从蛹壳里出来,变成蝴蝶便会飞。 有一次,他走到草原上面看见一个蛹,便取了回家,然后看着,过了几天以后,这个蛹出了一条裂痕,看见里面的蝴蝶开始挣扎,想抓破蛹壳飞出来。 这个过程达数小时之久,蝴蝶在蛹里面很辛苦地拼命挣扎,怎么也没法子走出来。这个小孩看着看着不忍心,就想不如让我帮帮它吧,便随手拿起剪刀在蛹上剪开,使蝴蝶破蛹而出。 但蝴蝶出来以后,因为翅膀不够力,变得很臃肿,飞不起来。
这个故事给我们的启示是:欲速则不达。
浮躁是现代人最普遍的心态,能怪谁?也许是贫穷落后了这么多年的缘故,就像当年的大跃进一样,都想大步跨入共产主义社会。现在的软件公司、客户、政府、学校、培训机构等等到处弥漫着浮躁之气。就拿笔者比较熟悉的深圳IT培训行业来说吧,居然有的打广告宣称"参加培训,100%就业",居然报名的学生不少,简直是藐视天下程序员。社会环境如是,我们不能改变,只能改变自己,闹市中的安宁,弥足珍贵。许多初学者C++/JAVA没开始学,立马使用VC/JBuilder,会使用VC/JBuilder开发一个Hello World程序,就忙不迭的向世界宣告,"我会软件开发了",简历上也大言不惭地写上"精通VC/JAVA"。结果到软件公司面试时要么被三两下打发走了,要么被驳的体无完肤,无地自容。到处碰壁之后才知道捧起《C++编程思想》《JAVA编程思想》仔细钻研,早知如此何必当初呀。
"你现在讲究简单方便,你以后的路就长了",好象也是佛经中的劝戒。
1.4 多实践,快实践
彭端淑的《为学一首示子侄》中有穷和尚与富和尚的故事。
从前,四川边境有两个和尚,一个贫穷,一个有钱。一天,穷和尚对富和尚说:"我打算去南海朝圣,你看怎么样?"富和尚说:"这里离南海有几千里远,你靠什么去呢?"穷和尚说:"我只要一个水钵,一个饭碗就够了。"富和尚为难地说:"几年前我就打算买条船去南海,可至今没去成,你还是别去吧!" 一年以后,富和尚还在为租赁船只筹钱,穷和尚却已经从南海朝圣回来了。
这个故事可解读为:任何事情,一旦考虑好了,就要马上上路,不要等到准备周全之后,再去干事情。假如事情准备考虑周全了再上路的话,别人恐怕捷足先登了。软件开发是一门工程学科,注重的就是实践,"君子动口不动手"对软件开发人员来讲根本就是错误的,他们提倡"动手至上",但别害怕,他们大多温文尔雅,没有暴力倾向,虽然有时候蓬头垢面的一副"比尔盖茨"样。有前辈高人认为,学习编程的秘诀是:编程、编程、再编程,笔者深表赞同。不仅要多实践,而且要快实践。我们在看书的时候,不要等到你完全理解了才动手敲代码,而是应该在看书的同时敲代码,程序运行的各种情况可以让你更快更牢固的掌握知识点。
1.5 多参考程序代码
程序代码是软件开发最重要的成果之一,其中渗透了程序员的思想与灵魂。许多人被《仙剑奇侠传》中凄美的爱情故事感动,悲剧的结局更有一种缺憾美。为什么要以悲剧结尾?据说是因为写《仙剑奇侠传》的程序员失恋而安排了这样的结局,他把自己的感觉融入到游戏中,却让众多的仙剑迷扼腕叹息。
多多参考代码例子,对JAVA而言有参考文献[4.3],有API类的源代码(JDK安装目录下的src.zip文件),也可以研究一些开源的软件或框架。
1.6 加强英文阅读能力
对学习编程来说,不要求英语, 但不能一点不会,。最起码像JAVA API文档(参考文献[4.4])这些东西还是要能看懂的,连猜带懵都可以;旁边再开启一个"金山词霸"。看多了就会越来越熟练。在学JAVA的同时学习英文,一箭双雕多好。另外好多软件需要到英文网站下载,你要能够找到它们,这些是最基本的要求。英语好对你学习有很大的帮助。口语好的话更有机会进入管理层,进而可以成为剥削程序员的"周扒皮"。
1.7 万不得已才请教别人
笔者在ChinaITLab网校的在线辅导系统中解决学生问题时发现,大部分的问题学生稍做思考就可以解决。请教别人之前,你应该先回答如下几个问题。
你是否在google中搜索了问题的解决办法?
你是否查看了JAVA API文档?
你是否查找过相关书籍?
你是否写代码测试过?
如果回答都是"是"的话,而且还没有找到解决办法,再问别人不迟。要知道独立思考的能力对你很重要。要知道程序员的时间是很宝贵的。
1.8 多读好书
书中自有颜如玉。比尔?盖茨是一个饱读群书的人。虽然没有读完大学,但九岁的时候比尔?盖茨就已经读完了所有的百科全书,所以他精通天文、历史、地理等等各类学科,可以说比尔?盖茨不仅是当今世界上金钱的首富,而且也可以称得上是知识的巨富。
笔者在给学生上课的时候经常会给他们推荐书籍,到后来学生实在忍无可忍开始抱怨,"天呐,这么多书到什么时候才能看完了","学软件开发,感觉上了贼船"。这时候,我的回答一般是,"别着急,什么时候带你们去看看我的书房,到现在每月花在技术书籍上的钱400元,这在软件开发人员之中还只能够算是中等的",学生当场晕倒。(注:这一部分学生是刚学软件开发的)
对于在JAVA开发领域的好书在笔者另外一篇文章中会专门点评。该文章可作为本文的姊妹篇。
1.9 使用合适的工具
工欲善其事必先利其器。软件开发包含各种各样的活动,需求收集分析、建立用例模型、建立分析设计模型、编程实现、调试程序、自动化测试、持续集成等等,没有工具帮忙可以说是寸步难行。工具可以提高开发效率,使软件的质量更高BUG更少。组合称手的武器。到飞花摘叶皆可伤人的境界就很高了,无招胜有招,手中无剑心中有剑这样的境界几乎不可企及。在笔者另外一篇文章中会专门阐述如何选择合适的工具(该文章也可作为本文的姊妹篇)。
2.软件开发学习路线
两千多年的儒家思想孔孟之道,中庸的思想透入骨髓,既不冒进也不保守并非中庸之道,而是找寻学习软件开发的正确路线与规律。
从软件开发人员的生涯规划来讲,我们可以大致分为三个阶段,软件工程师→软件设计师→架构设计师或项目管理师。不想当元帅的士兵不是好士兵,不想当架构设计师或项目管理师的程序员也不是好的程序员。我们应该努力往上走。让我们先整理一下开发应用软件需要学习的主要技术。
A.基础理论知识,如操作系统、编译原理、数据结构与算法、计算机原理等,它们并非不重要。如不想成为计算机科学家的话,可以采取"用到的时候再来学"的原则。
B.一门编程语言,现在基本上都是面向对象的语言,JAVA/C++/C#等等。如果做WEB开发的话还要学习HTML/JavaScript等等。
C.一种方法学或者说思想,现在基本都是面向对象思想(OOA/OOD/设计模式)。由此而衍生的基于组件开发CBD/面向方面编程AOP等等。
D.一种关系型数据库,ORACLE/SqlServer/DB2/MySQL等等
E.一种提高生产率的IDE集成开发环境JBuilder/Eclipse/VS.NET等。
F.一种UML建模工具,用ROSE/VISIO/钢笔进行建模。
G.一种软件过程,RUP/XP/CMM等等,通过软件过程来组织软件开发的众多活动,使开发流程专业化规范化。当然还有其他的一些软件工程知识。
H.项目管理、体系结构、框架知识。
正确的路线应该是:B→C→E→F→G→H。
还需要补充几点:
1).对于A与C要补充的是,我们应该在实践中逐步领悟编程理论与编程思想。新技术虽然不断涌现,更新速度令人眼花燎乱雾里看花;但万变不离其宗,编程理论与编程思想的变化却很慢。掌握了编程理论与编程思想你就会有拨云见日之感。面向对象的思想在目前来讲是相当关键的,是强势技术之一,在上面需要多投入时间,给你的回报也会让你惊喜。
2).对于数据库来说是独立学习的,这个时机就由你来决定吧。
3).编程语言作为学习软件开发的主线,而其余的作为辅线。
4).软件工程师着重于B、C、E、 D;软件设计师着重于B、C、E、 D、F;架构设计师着重于C、F、H。
3.如何学习JAVA?
3.1 JAVA学习路线
3.1.1 基础语法及JAVA原理
基础语法和JAVA原理是地基,地基不牢靠,犹如沙地上建摩天大厦,是相当危险的。学习JAVA也是如此,必须要有扎实的基础,你才能在J2EE、J2ME领域游刃有余。参加SCJP(SUN公司认证的JAVA程序员)考试不失为一个好方法,原因之一是为了对得起你交的1200大洋考试费,你会更努力学习,原因之二是SCJP考试能够让你把基础打得很牢靠,它要求你跟JDK一样熟悉JAVA基础知识;但是你千万不要认为考过了SCJP就有多了不起,就能够获得软件公司的青睐,就能够获取高薪,这样的想法也是很危险的。获得"真正"的SCJP只能证明你的基础还过得去,但离实际开发还有很长的一段路要走。
3.1.2 OO思想的领悟
掌握了基础语法和JAVA程序运行原理后,我们就可以用JAVA语言实现面向对象的思想了。面向对象,是一种方法学;是独立于语言之外的编程思想;是CBD基于组件开发的基础;属于强势技术之一。当以后因工作需要转到别的面向对象语言的时候,你会感到特别的熟悉亲切,学起来像喝凉水这么简单。
使用面向对象的思想进行开发的基本过程是:
●调查收集需求。
●建立用例模型。
●从用例模型中识别分析类及类与类之间的静态动态关系,从而建立分析模型。
●细化分析模型到设计模型。
●用具体的技术去实现。
●测试、部署、总结。
3.1.3 基本API的学习
进行软件开发的时候,并不是什么功能都需要我们去实现,也就是经典名言所说的"不需要重新发明轮子"。我们可以利用现成的类、组件、框架来搭建我们的应用,如SUN公司编写好了众多类实现一些底层功能,以及我们下载过来的JAR文件中包含的类,我们可以调用类中的方法来完成某些功能或继承它。那么这些类中究竟提供了哪些方法给我们使用?方法的参数个数及类型是?类的构造器需不需要参数?总不可能SUN公司的工程师打国际长途甚至飘洋过海来告诉你他编写的类该如何使用吧。他们只能提供文档给我们查看,JAVA DOC文档(参考文献4.4)就是这样的文档,它可以说是程序员与程序员交流的文档。
基本API指的是实现了一些底层功能的类,通用性较强的API,如字符串处理/输入输出等等。我们又把它成为类库。熟悉API的方法一是多查JAVA DOC文档(参考文献4.4),二是使用JBuilder/Eclipse等IDE的代码提示功能。
3.1.4 特定API的学习
JAVA介入的领域很广泛,不同的领域有不同的API,没有人熟悉所有的API,对一般人而言只是熟悉工作中要用到的API。如果你做界面开发,那么你需要学习Swing/AWT/SWT等API;如果你进行网络游戏开发,你需要深入了解网络API/多媒体API/2D3D等;如果你做WEB开发,就需要熟悉Servlet等API啦。总之,需要根据工作的需要或你的兴趣发展方向去选择学习特定的API。
3.1.5 开发工具的用法
在学习基础语法与基本的面向对象概念时,从锻炼语言熟练程度的角度考虑,我们推荐使用的工具是Editplus/JCreator+JDK,这时候不要急于上手JBuilder/Eclipse等集成开发环境,以免过于关注IDE的强大功能而分散对JAVA技术本身的注意力。过了这一阶段你就可以开始熟悉IDE了。
程序员日常工作包括很多活动,编辑、编译及构建、调试、单元测试、版本控制、维持模型与代码同步、文档的更新等等,几乎每一项活动都有专门的工具,如果独立使用这些工具的话,你将会很痛苦,你需要在堆满工具的任务栏上不断的切换,效率很低下,也很容易出错。在JBuilder、Eclipse等IDE中已经自动集成编辑器、编译器、调试器、单元测试工具JUnit、自动构建工具ANT、版本控制工具CVS、DOC文档生成与更新等等,甚至可以把UML建模工具也集成进去,又提供了丰富的向导帮助生成框架代码,让我们的开发变得更轻松。应该说IDE发展的趋势就是集成软件开发中要用到的几乎所有工具。
从开发效率的角度考虑,使用IDE是必经之路,也是从一个学生到一个职业程序员转变的里程碑。
JAVA开发使用的IDE主要有Eclipse、JBuilder、JDeveloper、NetBeans等几种;而Eclipse、JBuilder占有的市场份额是最大的。JBuilder在近几年来一直是JAVA集成开发环境中的霸主,它是由备受程序员尊敬的Borland公司开发,在硝烟弥漫的JAVA IDE大战中,以其快速的版本更新击败IBM的Visual Age for JAVA等而成就一番伟业。IBM在Visual Age for JAVA上已经无利可图之下,干脆将之贡献给开源社区,成为Eclipse的前身,真所谓"柳暗花明又一村"。浴火重生的Eclipse以其开放式的插件扩展机制、免费开源获得广大程序员(包括几乎所有的骨灰级程序员)的青睐,极具发展潜力。
3.1.6 学习软件工程
对小型项目而言,你可能认为软件工程没太大的必要。随着项目的复杂性越来越高,软件工程的必要性才会体现出来。参见"软件开发学习路线"小节。
3.2学习要点
确立的学习路线之后,我们还需要总结一下JAVA的学习要点,这些要点在前文多多少少提到过,只是笔者觉得这些地方特别要注意才对它们进行汇总,不要嫌我婆婆妈妈啊。
3.2.1勤查API文档
当程序员编写好某些类,觉得很有成就感,想把它贡献给各位苦难的同行。这时候你要使用"javadoc"工具(包含在JDK中)生成标准的JAVA DOC文档,供同行使用。J2SE/J2EE/J2ME的DOC文档是程序员与程序员交流的工具,几乎人手一份,除了菜鸟之外。J2SE DOC文档官方下载地址:
http://java.sun.com/j2se/1.5.0/download.jsp,你可以到google搜索CHM版本下载。也可以在线查看:
http://java.sun.com/j2se/1.5.0/docs/api/index.html。
对待DOC文档要像毛主席语录,早上起床念一遍,吃饭睡觉前念一遍。
当需要某项功能的时候,你应该先查相应的DOC文档看看有没有现成的实现,有的话就不必劳神费心了直接用就可以了,找不到的时候才考虑自己实现。使用步骤一般如下:
●找特定的包,包一般根据功能组织。
●找需要使用类,类命名规范的话我们由类的名字可猜出一二。
●选择构造器,大多数使用类的方式是创建对象。
●选择你需要的方法。
3.2.2 查书/google->写代码测试->查看源代码->请教别人
当我们遇到问题的时候该如何解决?
这时候不要急着问别人,太简单的问题,没经过思考的问题,别人会因此而瞧不起你。可以先找找书,到google中搜一下看看,绝大部分问题基本就解决了。而像"某些类/方法如何使用的问题",DOC文档就是答案。对某些知识点有疑惑是,写代码测试一下,会给你留下深刻的印象。而有的问题,你可能需要直接看API的源代码验证你的想法。万不得已才去请教别人。
3.2.3学习开源软件的设计思想
JAVA领域有许多源代码开放的工具、组件、框架,JUnit、ANT、Tomcat、Struts、Spring、Jive论坛、PetStore宠物店等等多如牛毛。这些可是前辈给我们留下的瑰宝呀。入宝山而空手归,你心甘吗?对这些工具、框架进行分析,领会其中的设计思想,有朝一日说不定你也能写一个XXX框架什么的,风光一把。分析开源软件其实是你提高技术、提高实战能力的便捷方法。
3.2.4 规范的重要性
没有规矩,不成方圆。这里的规范有两层含义。第一层含义是技术规范,多到
http://www.jcp.org下载JSRXXX规范,多读规范,这是最权威准确最新的教材。第二层含义是编程规范,如果你使用了大量的独特算法,富有个性的变量及方法的命名方式;同时,没给程序作注释,以显示你的编程功底是多么的深厚。这样的代码别人看起来像天书,要理解谈何容易,更不用说维护了,必然会被无情地扫入垃圾堆。JAVA编码规范到此查看或下载
http://java.sun.com/docs/codeconv/,中文的也有,啊,还要问我在哪,请参考3.2.2节。
3.2.5 不局限于JAVA
很不幸,很幸运,要学习的东西还有很多。不幸的是因为要学的东西太多且多变,没时间陪老婆家人或女朋友,导致身心疲惫,严重者甚至导致抑郁症。幸运的是别人要抢你饭碗绝非易事,他们或她们需要付出很多才能达成心愿。
JAVA不要孤立地去学习,需要综合学习数据结构、OOP、软件工程、UML、网络编程、数据库技术等知识,用横向纵向的比较联想的方式去学习会更有效。如学习JAVA集合的时候找数据结构的书看看;学JDBC的时候复习数据库技术;采取的依然是"需要的时候再学"的原则。
4.结束语
需要强调的是,学习软件开发确实有一定的难度,也很辛苦,需要付出很多努力,但千万不要半途而废。本文如果能对一直徘徊在JAVA神殿之外的朋友有所帮助的话,笔者也欣慰了。哈哈,怎么听起来老气横秋呀?没办法,在电脑的长期辐射之下,都快变成小老头了。最后奉劝各位程序员尤其是MM程序员,完成工作后赶快远离电脑,据《胡播乱报》报道,电脑辐射会在白皙的皮肤上面点缀一些小黑点,看起来鲜艳无比……
5.参考文献
5.1《JAVA夜未眠》
5.2
http://www.chinaitlab.com/www/news/article_show.asp?id=33934 5.3
http://javaalmanac.com/egs/ 5.4
http://java.sun.com/j2se/1.5.0/docs/api/index.html
摘要: 继续检查
rental
类中函数
getCharge()
的语句
switch (getMovie().getPriceCode())
,它提示我们应该将计算
charge
的职责交给
movie
类来完成。这是租借天数作为参数传给
movie
类的相关函数进行计算。我们在
...
阅读全文
编写的4个测试用例全部通过了,但是如果想对迭代器中每一个元素的计算结果进行验证现有的函数就无法完成要求了。
下面运用重构中的抽取函数分解Customer类中超长的方法statement(),这个函数完成的功能有些无所不包了。既计算各个租借的费用和常客积点,又要计算客户需要交纳的费用总和,最后还要输出报表的表头和表尾。另外,现有的快速设计对变化的应对不足,客户的电影分类可能会出现变化,如果去掉儿童片加上科幻片、故事片怎么办,客户的收费规则发生变化了怎么办。根据前面所说,他的味道不是一般的臭。
第一步:抽取函数
我们先把statement计算各个租借的费用的职责分离到一个单独的函数中。
private double amountFor(Rental rental){
double thisAmount=0.0;
switch (rental.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount+=2;
if(rental.getDayRented()>2)
thisAmount+=(rental.getDayRented()-2)*1.5;
break;
case Movie.CHILDRENS:
thisAmount+=1.5;
if(rental.getDayRented()>3)
thisAmount+=(rental.getDayRented()-3)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount+=(rental.getDayRented())*3;
break;
}
return thisAmount;
}
把原来的计算各个租借费用的部分注释起来,把private double thisAmount=0.0; 一句改为double thisAmount=amountFor(each);再次运行测试用例,和第一次的结果对比
我们通过人工比对输出结果,输出结果相同。我们稍后会通过测试用例由套件自动比对。
如果这一步我们的测试fail掉了,由于我们只走了很小的一步,因此我们可以轻易的退回去。记住,我们始终保持测试-〉编码(重构)-〉测试的步骤,小步快走的稳定节奏。
测试全部通过后,我们可以果断的删除刚才留在statement函数中我们注释过的临时代码。
分析我们刚刚抽取出来的amountFor函数,它只使用了rental类中的成员变量,放在Customer
类中并不合适,因此我们继续重构,把amountfor函数移动到Rental类中。
这里我们使用了集成开发环境中的功能:
这里我们看到eclipse自动侦测出目标的类为rental,我们把新的函数改名为getcharge,并勾选在原类型中创建委托,可以点击预览按钮察看重构结果,最后的结果如下:
Rental 类中:
double getCharge(){
double thisAmount=0.0;
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount+=2;
if(getDayRented()>2)
thisAmount+=(getDayRented()-2)*1.5;
break;
case Movie.CHILDRENS:
thisAmount+=1.5;
if(getDayRented()>3)
thisAmount+=(getDayRented()-3)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount+=(getDayRented())*3;
break;
}
return thisAmount;
}
可以看到比amountfor函数更加简化。
Customer类中:
private double amountFor(Rental rental){
return rental.getCharge();
}
保留了一个rental示例的委托调用。
再次运行测试用例,通过。
我们继续前进!
我们回到Customer类中,察看Statement函数中的thisAmount变量,我们发现他的结果并没有发生变化,我们可以通过重构把这个无用的临时变量除去。直接把thisamount=amountFor(each);的语句右侧考到totalAmount+=thisAmount;语句右侧,编译器会提示thisamount变量并未使用,直接双击quick fix 除去这个变量。再次运行测试用例,通过。
下一步我们把计算常客积点的职责也从statement函数中分离出来:
我们添加函数
private int getFrequentCount(Rental rental){
if (rental.getMovie().getPriceCode()==Movie.NEW_RELEASE&&rental.getDayRented()>1)
return 2;
else return 1;
}
再把计算常客积点的部分改写如下:
frequentCount+=getFrequentCount(each);
测试后我们发现常客积点的计算和类Rental的关系更密切,我们再次移动getFrequentCount到rental类中。测试通过后,计算常客积点的部分改写如下:
frequentCount+= each.getFrequentCount();
接下来,我们把statement最后的两个临时变量frequentCount、totalAmount也通过替换为函数查询消除掉。
添加函数
private double getTotalCharge(){
double result=0.0;
Enumeration rental=rentals.elements();
while (rental.hasMoreElements()) {
Rental rent = (Rental) rental.nextElement();
result+=rent.getCharge();
}
return result;
}
把临时变量totalAmount替换为getTotalCharge()
添加函数
private int getTotalFrequentCount(){
int result=0;
Enumeration rental=rentals.elements();
while (rental.hasMoreElements()) {
Rental each = (Rental) rental.nextElement();
result+=each.getFrequentCount();
}
return result;
}
把临时变量frequentCount替换为getTotalFrequentCount
相应的,我们在测试中添加以下四条测试用例:
public void testGetCharge(){
Enumeration rentals=tom.getRentals().elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
switch(each.getMovie().getPriceCode()){
case Movie.CHILDRENS:
assertEquals(1.5,each.getCharge(),.1);
break;
case Movie.NEW_RELEASE:
assertEquals(15,each.getCharge(),.1);
break;
case Movie.REGULAR:
assertEquals(6.5,each.getCharge(),.1);
break;
}
}
}
public void testgetFrequentCount(){
Enumeration rentals=tom.getRentals().elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
switch(each.getMovie().getPriceCode()){
case Movie.CHILDRENS:
assertEquals(1,each.getFrequentCount(),.1);
break;
case Movie.NEW_RELEASE:
assertEquals(2,each.getFrequentCount(),.1);
break;
case Movie.REGULAR:
assertEquals(1,each.getFrequentCount(),.1);
break;
}
}
}
public void testGetTotalCharge(){
assertEquals(23,tom.getTotalCharge(),.1);
}
public void testGetTotalFrequentCount(){
assertEquals(4,tom.getTotalFrequentCount(),.1);
}
现在我们就可以对迭代器中的每个元素的结果进行验证了。
测试驱动示例,应用重构优化设计
下面通过一个简单的测试驱动示例,并经过重构完成设计的更改。详细的例子见重构。
影片出租店的程序,计算每一位顾客的消费金额并打印报表。操作者告诉程序:顾客租用了哪些影片,租期多长,程序根据租赁时间和影片类型(普通片,儿童片和新片)。除了计算费用,还要为常客计算点数,点数的计算会由于租片种类是否为新片而有所不同。
根据上述描述,我们画出简单的类图
其中影片和租借都是简单的纯数据类。
package chapter01;
public class Movie {
public static final int CHILDRENS=2;//
影片类型
public static final int REGULAR=1;
public static final int NEW_RELEASE=0;
private String title;//
名称
private int priceCode;//
价格代码
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
}
public class Rental {
private Movie movie;
private int dayRented;
public Rental(Movie movie, int dayRented) {
this.movie = movie;
this.dayRented = dayRented;
}
public int getDayRented() {
return dayRented;
}
public Movie getMovie() {
return movie;
}
}
public class Customer {
private String name;
private Vector rentals=new Vector();
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Vector getRentals() {
return rentals;
}
@SuppressWarnings("unchecked")
public void addRental(Rental rental){
rentals.add(rental);
}
public String statement(){//
计算费用
double totalAmount=0;//
总和
int frequentCount=0;//
常客积点
Enumeration rental=rentals.elements();
String result="rental record for "+getName()+"\n";
while (rental.hasMoreElements()) {
Rental each = (Rental) rental.nextElement();//
取得一批租借记录
double thisAmount=0;
//
根据类型,计算价格
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount+=2;
if(each.getDayRented()>2)
thisAmount+=(each.getDayRented()-2)*1.5;
break;
case Movie.CHILDRENS:
thisAmount+=1.5;
if(each.getDayRented()>3)
thisAmount+=(each.getDayRented()-3)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount+=(each.getDayRented())*3;
break;
}
//
添加常客积点
frequentCount++;
if (each.getMovie().getPriceCode()==Movie.NEW_RELEASE&&each.getDayRented()>1)
frequentCount++;
//
显示此笔数据
result+=
"\t"+each.getMovie().getTitle()+
"\t"+String.valueOf(frequentCount)+"\n";
totalAmount+=thisAmount;
}
//
打印结尾
result+="amount owned "+String.valueOf(totalAmount)+"\n";
result+="you earned "+String.valueOf(frequentCount)+"frequentCount\n";
return result;
}
}
下面是对应的测试用例
public class testCustomerStatement extends TestCase {
Movie childrenMovie=new Movie("HARRY POTTY",Movie.CHILDRENS);
Movie regularMovie=new Movie("Titanic",Movie.REGULAR);
Movie newMovie=new Movie("Ice Age 2",Movie.NEW_RELEASE);
Rental childRental=new Rental(childrenMovie,3);
Rental newRental=new Rental(newMovie,5);
Rental regRental=new Rental(regularMovie,5);
Customer tom=new Customer("Tom");
protected void setUp() throws Exception {
super.setUp();
tom.addRental(childRental);
tom.addRental(newRental);
tom.addRental(regRental);
}
public void testCaustomerName(){
assertEquals("Tom",tom.getName());
}
public void testIteratorSize(){
assertEquals(3,tom.getRentals().size());
}
public void testIteratorName(){
Enumeration rentals=tom.getRentals().elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
switch(each.getMovie().getPriceCode()){
case Movie.CHILDRENS:
System.out.println("childrens");
assertEquals("HARRY POTTY",each.getMovie().getTitle());
assertEquals(3,each.getDayRented());
break;
case Movie.NEW_RELEASE:
System.out.println("new");
assertEquals("Ice Age 2",each.getMovie().getTitle());
assertEquals(5,each.getDayRented());
break;
case Movie.REGULAR:
System.out.println("regular");
assertEquals("Titanic",each.getMovie().getTitle());
assertEquals(5,each.getDayRented());
break;
}
}
}
public void testOutput(){
System.out.println(tom.statement());
}
}
面向对象软件开发的敏捷过程
软件开发的复杂性
:
计算机硬件界的摩尔定律(每隔
18
个月计算机硬件的运算速度提高一倍,价格下降一半)
适用于硬件的发展规律已经超过三十年了。人们想当然的认为计算机软件的发展速度和硬件的发展速度相当,但是不幸的是:每次重大的硬件升级之后,随着更大功能更丰富的软件的出现,硬件的潜能再一次被无情的榨取殆尽。许多开发的软件系统不断的遭受进度延期,人员资金和时间等预算无休止的增加,软件质量的不断反复,开发出来的系统对客户的新需求响应缓慢,更改困难的噩梦。
这样的现实是由软件的固有复杂性造成的,软件不同于硬件的生产过程,是由人的智力劳动完成人的需求到机器程序的翻译转换过程。需求可能不清晰,对需求可能出现个人理解上的差异,选择实现方法的差异,需求的不断变化,具体实现语言平台的差异,软件生产中采用的过程,具体实现人员的变动等等都会对最终的产品产生影响。想象一下,如果一种变化的因素只有两种可能,那么可以使用简单的
0
,
1
表示,只有
10
个变化因素的组合就已经达到了
210=1024
种可能性,而实际开发中变化的因素轻易就超过
10
个以上,每个变化的可能是还不止两个,因此软件的复杂性很快就会超出人的理解程度。有一句经典的软件开发名言:世界上唯一不变的是变化本身。不断出现的变化,会使初始的设计和最终的需求之间的距离越来越远。
软件的臭味:
软件开发,使用,维护中出现了以下的“臭味”:
僵化性:
rigidity
很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其他改动。即使是简单的改动,也会迫使导致右依赖关系的模块的连锁改动。
脆弱性:
fragility
对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。出现新问题的地方和改动的地方没有概念上的关联,难以排错,排错的过程中又会引入更多的“臭虫”。
牢固性
immobility
很难解开系统的纠结,使它成为其他系统中重用的组件。系统中包含了对其他系统中有用的功能,当其他人想复用这个功能到新的系统时,剥离出独立的组件的难度远远大于重新实现的难度,在时间和进度的压力下,大多数人只有选择拷贝涂鸦的方式来实现新系统的功能。
粘滞性:
viscosity
做正确的事情比错误的事情要困难。程序完成正常的功能总是倾向于得到不正确的结果。
不必要的复杂性:
needless complexity
设计中包含有不具有任何直接好处的基础结构。为了预防后期维护更改需求的对源码的修改,在设计之初放置了那些处理潜在变化的代码来保持软件的灵活性,这样的结果是软件中包含了很多复杂的结构,理解起来更加困难。
不必要的重复:
needless repetition
设计中包含有重复的结构,而该重复的结构可以使用单一的抽象进行统一。对鼠标右键(剪切,复制,粘贴)的滥用,使得完成同一或类似的代码片断出现在系统各处。如果原始的代码段完成的功能需要变化,或者存在错误,排错和增加新的功能变得非常困难。
晦涩性:
opacity
很难阅读,理解。没有很好的表现出意图。
以上讨论了系统构架的臭味,下面讨论微观层次上代码的臭味:
代码的臭味
重复代码:重复的代码使得更改功能和排错更加困难。同样的模块错误会在拷贝粘贴的程序各处多次出现。
过长的函数:程序越长越难于理解,这已经是软件业开发的常识。越难理解的程序,使用维护的成本就越大。如果一个函数的行数超过一页,很少有人能够在看到下一页的时候还清楚的记得函数开头的变量定义,理解和查错更加困难。
过大类:在一个类中完成几乎所有需要的功能。十项全能的人是不存在的,软件也一样。
过长的参数列:如果一个函数(方法)的调用参数过长,使用这个函数的调用过程也一定是困难的。想象一下,调用一个十个以上参数存储过程会有多么痛苦。这还只是开始,如果任一个参数的定义(名称,类型)发生轻微的变化,函数的调用客户端会有多么大的改动。
其他的臭味还有发散式变化,散弹枪修改,依恋情结,数据泥团,基本型别偏执,复杂的
switch
分支语句,平行的继承体系,冗赘类,夸夸其谈的未来性,令人迷惑的暂时值域,过度耦合的消息链,中间转手人,狎昵关系,异曲同工的类,不完美的程序库类,纯数据类(数据哑元),子类不需要父类的某些特性,过多注释。详细的讨论可以参见《重构》的介绍。
面向对象软件设计的原则
:
总体原则
:
1.
针对于接口(抽象)编程,而不要针对于实现(具体)编程。
举例来说:操作系统是对逻辑计算机的抽象,通过操作系统的抽象我们不需要考虑具体使用的硬件配置,可以在较高的层次上进行更高生产力的应用。再如:汇编语言对机器的
0
,
1
代码进行了抽象,大大加快了开发效率,后来使用的高级语言和第四代语言模型驱动抽象的级别更高,生产力也更高。再如:
java
和
.net
实现于一个抽象的软件虚拟机,进一步使开发出来的组件可以跨平台和操作系统。通过抽象出数据访问层(持久化层),可以使业务逻辑和具体的数据库访问代码分离,更换数据库提供商对已有的组件没有影响。具体实现可以参照
hibernate
实现和
dao
(数据访问对象)模式。
优势:
1
)降低程序各个部分之间的耦合性,使程序模块互换成为可能。调用的客户端无需知道具体使用的对象类型,只要对象有客户希望的接口就可以使用,对象具体是如何实现这些接口的,客户并不需要考虑。
2
)简化了程序各个部分的单元测试,将需要测试的程序模块中通过重构提炼出抽象的接口,然后编制和接口一致的
Mock
类,测试就会变得很容易。如果应用了测试优先的方法,从简化客户端调用的角度,还可以经过抽象改善软件模块的设计。
3
)模块的部署升级由于模块之间的耦合度降低变得更加容易。
相关的设计模式有创建型模式中的工厂模式,结构型模式中的代理模式和组合模式等。
2.
对象组合优于类继承。
面向对象为软件开发引入了三大工具:继承,多态和重载。继承使得程序员可以快速的通过扩展子类来增加功能,但是由于继承是在编译时确定的,因此增加的功能较多时,继承不够灵活,还有可能出现“子类爆炸”的局面(为了完成新添功能,不得不在继承体系中添入大量的之间只有细微差别的子类,掌握使用扩展都会变得非常困难)。而通过对象的组合,可以动态透明的添加功能。相关设计模式有装饰模式和代理模式等。
3.
分离变化。前面说过需求是在不断变化的,不同的变化可能是仓库安全库存的计算方法,可能是报表和数据的展现形式,通过把这些不同的变化识别并分离出来不同的对象委托,简化了客户端的调用和升级。相关的设计模式有命令模式,观察者模式,策略模式,状态模式,模版方法模式等。
具体原则:
1.
单一职责原则
srp
(
single responsibility principle
):一个模块的功能应该尽可能的内聚。如果一个类发生了变化,引起变化的原因应该有且只有一个。每一个类承担的职责都是一个变化的轴线,需求变化时,会体现为类的职责的变化。如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化会影响这个类完成其他职责的能力,会出现前面所说的软件的臭味之一脆弱性。相关的设计模式有
2.
开放封闭原则
ocp
(
open closed principle
):一个模块应该对功能的扩展开放,支持新的行为,对自身的更改封闭。每次对模块的修改都可能会引入新的错误和新的依赖。因此扩展新功能时,已经编好的模块源码和二进制代码都是不应该修改的。相关的设计模式有适配器模式,桥接模式,访问者模式等。
3.
Liskov
替换原则
lsp
(
liskov subtitle principle
)子类型必须可以替换掉他的基类型。一个基类的多个子类型之间要完成动态的替换,各个子类型必须都可以被他们的基类型替换,这样他们之间动态替换后,客户端调用的代码就不需要冗赘的
switch
类型判断代码。如果
子类型无法替换基类型,将会导致在派生类对象作为基类对象进行传值时的错误。这样多态机制处于瘫痪状态了。相关设计模式为组合模式。
4.
依赖倒置原则
dip
(
dependent inverse principle
)高层模块不应该依赖于底层模块,抽象不应该依赖于细节,细节应该依赖于抽象。假定所有的具体类都是回变化的,因此如果一个客户端依赖于(调用或声明)具体的类型,那么当这个具体的类型变化时,依赖的客户端业必须同时进行修改。这些具体的更改可能出现在使用了某个特定的网络协议,特殊的系统
api
调用,特定的数据库储存过程等,这些用法或多或少都会使客户端调用和具体类型成为铁板一块,比较难于重用。
Java
社区中比较热门的
j2ee
轻量级容器框架
spring
就很好的实现了本原则。
5
.接口隔离原则
isp
(
interface segregation principle
)不应该强迫客户依赖于它们不使用的方法。因为每一个实现接口的对象必须实现所有接口中定义的方法。如果接口的粒度比较小,实现接口的对象可以使用一种即用即付的方式动态实现接口。每个接口的粒度很小,复用起来也非常容易。
这体现了一个趋势:为了更好的实现重用,接口,函数,模块和类等倾向于更容易使用的“小”体积。
敏捷软件开发的宣言和实践:
软件开发项目的失败使得人们开始思考软件开发的过程,人们希望通过引入严格的过程控制产生软件生命周期中各个阶段的文档和制品来保证软件的质量。比较出名的业界实施方法论有
cmmi
(能力成熟度模型)和
rup
(瑞理统一过程),这些方法论都是重型的。举例来说,没有经过剪裁的
Rup
实现起来,至少需要在全周期完成四十个以上的制品文档,文档的编写维护和源代码的同步等需要非常多的资源,十人以下的开发团队一般没有精力、时间、人员配置完成这些制品。失控的过程的膨胀迫使人们寻找一种快速工作,相应变化的“敏捷的”方法。敏捷团队提倡通过团队成员之间充分有效的沟通统一大家的目标,结伴的方式完成开发技术的团队内传承,使用“够用就好”的轻量甚至免费的工具管理过程。可以正常工作的代码摆在首要地位,只有必要的时候才生产必要的文档。强调和客户面对面地交流合作,积极地响应客户需求的变化而不是遵循机械的计划。使用较短的迭代周期,近早和持续提交有价值的软件给客户来验证并修正和用户需求的吻合程度。提倡可以持续的稳定的开发节奏,长期“小步快走”的方式代替突然的“百米冲刺”。保持设计最优,最简单的设计并且持续改进,不断调整。