1、移除重复代码
其根本方法是把大于1次使用的代码分离成共有方法。
重构的目标应该是在不降低代码效率的基础上提高代码的复用性和质量。重构应该考虑效率、结构、代价之间的平衡。
2、把注释化为代码
看到代码就能见文知义,消除无谓注释。一般注释用于说明该处的想法、算法、优劣、记录等,能用代码说明的尽量不要用注释。
当我们要加注释的时候,我们应该再三的想想:
注释能不能转化在代码里面,让代码跟注释一样的清晰?
大多数情况下是能!
每一个注释都是一个改进代码的好机会!
包含太多注释的代码,绝对不是高质量的代码!
3、除去代码异味
理想情况下,我们希望一个类、方法或其他代码设计完成以后,就不用再做修改了。它们应该稳定到不用修改就能重用。
判断代码的稳定性,我们可能会这样:先假设一些具体的情况或者需求改变了,然后来看一看,要满足这些新的需求,代码是否需要被修改?
有个简单的方法,如果我们发现已经第三次修改这些代码了,那我们就认定这些代码是不稳定的。这方法比较“懒惰”,而且“被动”,虽然这种方法还算有效。
还有一个简单的方法,而且“主动”。如果这段代码是不稳定的或者有一些潜在问题,那么代码往往会包含一些明显的痕迹。这些痕迹就是“代码异味”。
普遍的代码异味:
类别代码和switch表达式是比较普遍的代码异味。此外,还有其他的代码异味也很普遍。
下面是大概的异味列表:
代码重复
太多的注释
类别代码(type code)
switch或者一大串if-then-else-if
想给一个变量,方法或者类名取个好名字时,也怎么也取不好
用类似XXXUtil, XXXManager, XXXController 和其他的一些命名
在变量,方法或类名中使用这些单词“And”,“Or”等等
一些实例中的变量有时有用,有时没用
一个方法的代码太多,或者说方法太长
一个类的代码太多,或者说类太长
一个方法有太多参数
两个类都引用了彼此(依赖于彼此)
要移动一些类别代码和switch表达式,有两种方法:
1.用基于同一父类的不同子类来代替不同的类别。
2.用一个类的不同对象来代替不同的类别。
当不同的类别具有比较多不同的行为时,用第一种方法。当这些类别的行为非常相似,或者只是差别在一些值上面的时候,用第二个方法。
4、保持代码简洁
判断一个类是否需要修整,一个比较主观的方法是:看看我们会不会觉得这个类“太长了”,“太复杂了”,或者包含的概念“太多了”?如果会这样觉得的话,我们就认定这个类需要修整。
另外一个比较简单(虽然懒惰、被动)而且客观的方法是:当发现我们已经在第二次或者第三次扩充这个类的时候,我们认定这个类药修整了。
5、慎用继承
Java中传递调用的中间介叫“代理(delegation)”。其实就是和继承并列的组合。
如果一个父类描述的东西不是所有的子类共有的,那这个父类的设计肯定不是一个好的设计。
当我们想要让一个类继承自另一个类时,我们一定要再三的检查:子类会不会继承了一些它不需要的功能(属性或者方法)?如果是的话,我们就得认真再想想:它们之间有没有真正的继承关系?如果没有的话,就用代理。如果有的话,将这些不用的功能从基类转移到另外一个合适的地方去。
6、处理不合适的引用(依赖)
互相依赖也是一种代码异味,我们就认定这样的代码,是“不合适的依赖”。
要判断一个代码是不是包含了“不合适的依赖”,共有四个方法:
1.看代码:有没有互相依赖?
2.认真想想:它真正需要的是什么?
3.推测一下:它在以后的系统中可以重用吗?
4.到要重用的时候就知道了:现在我要重用这个类,能不能重用?
方法1跟4是最简单的方法,推荐初学者可以这样来判断。有更多的设计经验了,再用方法2跟3会好一些。
7、将数据库访问、UI和域逻辑分离
域逻辑,原文是叫Domain logic,“Domain logic is also called "domain model" or
"business logic".”,即“域逻辑又称为域模型或者业务逻辑”。
这三个不同类别的代码混在一起,会造成下面的问题:
1.代码很复杂。
2.代码很难重用。如果是在一个web系统中,就更难重用了。
3.代码很难测试。每次要测这样的一段代码,我们都要建一个数据库,还要通过一个用户操作界面来测试。
4.如果数据库表结构更改了,很多地方都要跟着更改。
5.它导致我们一直在考虑一些低层的太细节的概念,比如数据库字段,表的记录之类的,而不是类,对象,方法和属性这一类的概念。或者说白了一点,一直在考虑怎么往数据库里面装数据,而没有了面向对象的概念,没有了建立业务模型的思维。
因此,我们应该将这三种类别的代码分离开(UI,数据库访问,域逻辑)。
一般而言,比较完美的方案是UI层和数据库访问层依赖于域逻辑层,那么域逻辑就能得到最大程度的复用,而这就是我们设计结构的目标(特别是大项目)。
很多东西都属于UI层,不仅仅窗口、按钮这些,报表,Servlet,Jsp,文本控制台等等也算。搞J2EE开发的要注意,请不要在Servlet,Jsp里面放置任何的域逻辑或者数据库访问的代码;也不要在域逻辑中调用UI的代码,如System.out.print.
8、以用户例事管理项目
用户例事 User Case or User Story
在一个系统面前,每个用户要完成同样的目标,都要做这个系统设定的例行的事,这件事情不是一个例子,所以不叫事例,这也不是故事,也不能算一段历程,而是一个例行的事。
一个用户例事只是以客户能明白的方式,描述了一个系统的外在行为,它完全忽略了系统的内部动作。千万不要提及任何有关数据库、记录、字段之类的对客户一点意义都没有的东西。
一开始我们可以用例事点来评估发布时间。
为了做到更准的估计,我们希望客户给我们一段时间做些实际的开发,来测量一下我们在这段时间里面可以做多少用户例事,这段时间就叫“迭代周期”。通过迭代周期,我们可以进一步评估发布时间。
如果不能如期完成,一种方法是我们和客户达成协议推迟发布周期,我们先选择客户认为比较重要的例事点来实现,而把相对次要的放在下一个发布周期去;另一种方法,可以增加开发人员来满足发布期限,但是这里要注意,团队人数加倍不等于开发周期的减半,如果团队超过10个人,增加更多的人可能反而会延缓项目的进度,项目开发周期越长,团队内的成员对整个项目的代码熟悉度越少,加上不确定的人员流动,这个项目会越来越复杂。
9、用CRC卡协助设计
CRC(Class,Responsibility,Collaboration),在卡里写上类名,它的职责,以及它的协作关系。
之所以用CRC卡,因为
1)卡片上的空间小,可以防止我们给这个类太多的职责。如果职责过多的话(比如大于4个),尝试以更抽象的方式去考虑一下,将职责划分。
2)CRC卡主要是用在探索或者讨论类的设计阶段。如果我们觉得这个设计不好,既不用修改文档,也不用修改类图,只要把卡片丢了就行。此外,一旦设计完成,我们就可以把所有的卡丢了,它们不是用来做文档的。
3)如果我们觉得现在的卡片不合适,之前设计的比较好,我们只要简单的把之前的卡片拿出来组合就行了。
CRC卡主要是用来快速的组织设计。不应该花很长时间做CRC卡,也不要指望按照CRC卡的设计就一切ok。在编码的时候,肯定还会不断地调整设计。将设计和编码结合起来,我们才能做出好而有效的设计。
一句话,CRC卡是用来帮忙清理设计的思路,它不是UML图,也不是精确的类结构图。只要我们在处理这些卡的时候不断讨论,我们设计的思路将会变得非常清楚。
10、验收测试
验收测试,也叫功能测试,只是测试系统的外部行为,忽略系统里面有哪些类,哪些模块。
由于手动测试用例要花很多的时间和精力,我们希望这些测试用例可以在不需要人为干预的情况下自动运行。这样的测试,叫“自动验收测试”。
测试代码是需要调用“正式”代码的,而“正式”代码绝对不能调用测试代码。
测试代码应该在实现用户例事之前写。测试用力使我们软件需求的一个重要组成部分(现场客户是另一个重要部分)。作为需求的一部分,测试用例可以知道我们实现用户例事。而现场客户的优点是,最新的信息,更有效的沟通;而测试用例的优点则是更准确,自动化的测试则可以很频繁地运行。
测试文件不一定是个文本文件,可以是excel,也可以是html或者其他各式的文件,只要我们可以解析出来就可以了。最关键的一点是客户能够明白和操作这些测试文件。
用测试用例防止系统走下坡路。每次我们跟客户一起建立了一个测试文件,我们都把它放在一个名为“测试队列”的文件夹里。开始的时候所有的测试文件都放在这边。每运行通过一个测试文件,我们就将这个测试放到另外一个叫“通过”的文件夹里。之后不管是重构,还是增加新的功能,或者修改代码以后,都要将“通过”这个文件夹里的所有测试文件跑一边。如果有测试文件通不过,证明代码有错,我们马上修改代码,让它通过。而不是将通不过的测试文件放回“测试队列”的文件夹里。也就是说,除非是测试文件本身出了问题,否则,绝对不能把测试文件从“通过”放回到“测试队列”中去。当最终“测试队列”里面的文件都移到“通过”队列后,系统完成了。
11、对UI进行验收测试
UI测试的原则是:分开测试每个UI组件。
12、单元测试
验收测试测试的是系统的外部行为,单元测试是测试系统的内部结构,它只测一个单元(类、甚至一个方法)。验收测试属于客户的,我们没有权利决定验收测试的内容,我们顶多只是帮忙客户根据用户例事写出验收测试。单元测试属于我们,我们只是根据我们对这个单元的期望写出单元测试。
13、测试驱动编程
代码写得越复杂,我们就越担心;写得越多,我们也越担心。这时候,测试先行的方法就值得考虑了。
测一点,写一点的好处:如果我们弄出一个bug,它会马上被发现,然后很容易定位到出错源。
我们要在实现代码之前,保证我们的测试通不过。如果测试通过了,没有像预期中的失败了,说明我们的测试有问题。
我们要测试代码是不是如我们预期的调用了某一个方法,我们要判断2点:1。它确实调用了我们希望它调用的方法。2。它调用时传递过去当参数的对象,应该是我们预期的对象。
TDD(Test Driven Development)的优点:
1,为了更容易写单元测试,我们会广泛地使用接口。这个会让单元测试代码很容易读写,因为测试代码里面没有多余的数据。
2,因为广泛的使用接口,我们的类之间就不会耦合,因此重用性更好。
3,写单元测试的时候,很容易就可以为一个行为写一个测试用例,让它通过,然后为另一种行为写另一个测试用例。也就是说,整个任务会被划分成很多小的任务,独立完成。
要做什么,不要做什么:
1,不要再测试里包含多余的数据,为此,你会用更多地使用接口和匿名类。
2,如果你需要写一堆代码来建立测试上下文(比如数据库连接),那你绝对不要忍受这种痛苦。如果它确实发生了,那就用接口吧。
3, 不要在两个测试里面测试了同样的东西。
4,在写出通不过的测试之前,绝对不要写代码,除非代码非常简单。
5,不要(至少尽量避免)一次性将所有的测试弄坏。
6,不要一次性写太多东西,或者做一些要花费几个小时的事情。尽量将这些东西划分为更小的行为,使用TODD列表。
7,每写完一个测试用例,在几分钟内通过它,并不是放在一边。
6,每做一个或者一些小改动后,就要运行所有测试用例。
9,先挑出你比较有兴趣的,或者可以从中尝到东西的任务,而不是先挑那些烦人的任务。
10,测试调用顺序的时候,一定要使用调用日记。
11,任何时候,脑中都要有个完整的概念。
14、结对编程
如果我们不懂对方在说什么,最好的办法就是让他举个例子。这是沟通(也是结对编程中)最重要的方法。
有经验的开发人员,有时候还是可以从年青的开发人员上学到一些东西。
交流设计的最好方法是边看代码边解释。如果还没有代码,就用图表。
在结对编程里,一起设计是主要活动之一。除此,一起测试、一起查错也是另两个主要活动。
在结对编程里,有着不同的知识/技能的人可以把他们的技能共享来解决一个困难的问题。经常可以交流知识和好的实践经验。开发人员会比较开心,做事也比较有信心。
如果我们不能明白另一个人的设计意图,那最好的方法就是,写代码。让比较不明白的那个人来写。
即使一个人在某一方面很擅长,另一个人还是有空间做一些有意义的贡献的。有经验不代表每次都能更快找到错误。每个人都可能擅长找某一方面的问题,而不擅长于其他方面的。幸运的是,两个人的“盲区”通常不会重叠。
一起写代码也是结对编程的主要活动。
因为结对编程两人的精力都会很集中,精神容易紧张,所以经常的休息开发效率才会更高。
在这次结对编程以后,两个人对代码和业务都更熟悉了。如果他们中有人要去休假或者离开公司的话,另一个也可以维护。也就是说,结对编程可以减少职工离职对公司的损失。
经常地换搭档是好事(特别是在一个新任务的开始阶段)。现在其中一个可以继续把他懂的东西教给第三个人,同时也能从第三个人身上学到另外一些。
结对编程的好处:
1,联合两人的知识去对付一个难题。
2,知识互相传递。
3,更有效的差错跟纠错。
4,程序员都很开心。
5,减少员工离职的损失。
结对编程需要的一些技能:
1,用代码解释已有的设计结构。
2,用例子来解释。
3,用图表来解释设计思路。
4,如果你无法把你的设计思路表达清楚,把代码写出来。
5,让比较迷惑的搭档来写代码,这样他就可以较好地融入你的概念。
6,经常地休息。
7,经常地更换搭档。
研究表明,在一个大学的环境里,让两个人做一件事情,花费的时间比两个人分工所需的时间多15%。也就是说,我们没必要加倍。所以一方面来讲,结对编程有很大的好处,但同时也要多花费15%的开发时间。但研究还表明,结对编程开发出来的软件,bug的数量比分工开发出来的少不止15%。单人修复bug所花的时间,是结对的人所花时间的15-60倍。很明显,结对编程远远抵消了那多费的15%的开发时间。
结对编程不是万灵的。因为它需要两个人不断地沟通,一起做决定,如果不能沟通或者做不了决定的话,结对编程就行不通了。下面是常见的问题,会造成结对编程无法正常工作:
1,不情愿的配合。
2,拒绝别人的意见,甚至攻击对方。
3,小心翼翼有意见不敢提。
4,怕别人觉得自己笨不敢问问题。
注:
开闭原则(Open Closed Principle):如果我们需要增加新的功能,我们只需要增加新的代码,而不是改变原有的。移除switch和类别代码是达到开闭原则的普遍方法.
单一职责原则(The Single Responsibility Principle):每个类都应该只为一个理由而修改。当一个类包含许多其他功能时,很明显违反了单一职责原则。
里斯科夫替换原则(LSP)(Subtype must be substitutable for their base types): 子类应该能够代替父类的功能。或者直接点说,我们应该做到,将所有使用父类的地方改成使用子类后,对结果一点影响都没有。或者更直白一点吧,请尽量不要用重载,重载是个很坏很坏的主意!
依赖反转原则(Dependency Inversion Principle ):抽象不应该依赖于具体,高层的比较抽象的类不应该依赖于低层的比较具体的类。当这种问题出现的时候,我们应该抽取出更抽象的一个概念,然后让这两个类依赖于这个抽取出来的概念。
《重构——改善既有代码的设计》
下面粗略地概括一下对重构的理解,也整理一下之前不是很清楚的概念。
1、《重构》有一个很好的动机,也可以说是价值观,就是程序第一是写给人看的,而不是写给机器看的。
根据这一价值观,其他多种利益纷至沓来,比如当程序有了良好的可读性和可理解性,程序中隐藏的Bug便很容易浮出水面,开发进度也更加顺畅,并且对于系统将来的结构变更和扩展,程序也更加具有灵活性。
2、《重构》与《设计模式》的关系,在《设计模式》和《重构》中都有提出“设计模式为重构提供了目标”,在之前对这句话的理解总是朦朦胧胧,觉得有道理但又不是很深刻,现在觉得有两个词非常的关键:目标和目的。
设计模式为重构提供了目标,但不是目的。
设计模式是经过证实的在一定场景下解决一般设计问题的解决方案的核心,通过设计模式我们很好得解决了某种问题,并且便于我们思考和交流,降低沟通之间的理解误差,此外同样重要的,设计模式增强了可复用性,便于将来扩展和维护。
而重构是对程序内部结构的一种调整,其目的是在不改变“软件之可察行为”的前提下,提高其可理解性,降低其修改成本(《重构》的名词性定义)。
所以如果我们把软件开发比作在大海中航行,设计模式就是遍布在大海中的航标,他可以引导我们驶向目的地——高可读性、可理解性、可扩展性、可维护性。所以设计模式是重构的目标(航标)而不是目的,设计模式可以帮助我们更好更快的抵达目的地(准确地说是无止境的),而避免触礁或偏离航向
3、重构和优化,在之前的开发中,优化的意识要比现在(看完《重构》之后)强的多,如果遇到在一个循环中可以做多个事情的时候,决定把每件事情分开放到单独的循环中是要鼓起很大的勇气的,而现在便可以轻松的决定,因为清晰的代码在需要性能优化时有更宽的道路可以选择,并且往往这种决定不会造成真正的性能影响。
《实时UML与Rational Rose RealTime建模案例剖析 》
将实时系统、实时统一建模语言、实时系统的统一开发过程和Rational Rose RealTime建模环境有机地结合起来,以案例为基础,系统地介绍了实时系统的设计与实现。全书分为3部分,第1部分为基础篇,主要介绍实时系统的基本概念、实时统一建模语言、实时对象约束语言和Rational Rose RealTime建模环境。第2部分为建模篇,结合实时统一建模语言和Rational Rose RealTime建模工具,介绍了实时系统的需求分析、系统设计和实现与部署。第3部分为案例篇,分析了4个典型的实时系统案例:纸牌游戏、咖啡机控制系统、ATM自动取款机控制系统和电梯控制系统的设计与实现。案例是针对不同层次的实时系统开发人员进行设计的,同时也涵盖了实时系统设计的主要特性。