敏捷编码
代码要清晰地表达意图
开发代码时,应该更注重可读性,而不是只图自己方便。代码阅读的次数要远远超过编写的次数,所以在编写的时候值得花点功夫让它读起来更加简单。
PIE原则(Program Intently and Expressively)
代码必须明确说出你的意图,而且必须富有表达力。这样可以让代码更易于被别人阅读和理解。代码不让人迷惑,也就减少了发生潜在错误的可能。一言以蔽之,代码应意图清晰,表达明确。
在编写代码时,应该使用语言特性来提升表现力。使用方法名来传达意向,对方法参数的命名要帮助读者理解背后的想法。异常传达的信息是哪些可能会出问题,以及如何进行防御式编程,要正确地使用和命名异常。好的编码规范可以让代码变得易于理解,同时减少不必要的注释和文档。
具体技巧
- 现在对你显而易见的事情,对别人可能并非如此,对于一年以后的你来说,也不一定显而易见。
用代码沟通
通常程序员都很讨厌写文档,这是因为大部分文档都与代码没有关系,并且越来越难以保证其符合目前的最新状况。这不只违反了DRY原则(Don't Repeat Yourself),还会产生使人误解的文档,这还不如没有文档。
建立代码文档无外乎两种方式:利用代码本身;利用注释来沟通代码之外的问题。
Don't comment to cover up.
如果必须通读一个方法的代码才能了解它做了什么,那么开发人员先要投入大量的时间和精力才能使用它。反过来讲,只需短短几行注释说明方法行为,就可以让生活变得轻松许多。开发人员可以很快了解到它的意图、它的期待结果,以及应该注意之处——这可省了你不少劲儿。
应该文档化你所有的代码吗?在某种程度上说,是的。但这并不意味着要注释绝大部分代码,特别是在方法体内部。源代码可以被读懂,不是因为其中的注释,而应该是由于它本身优雅而清晰——变量名运用正确、空格使用得到、逻辑分离清晰,以及表达式非常简洁。
如何命名很重要。程序元素的命名是代码读者必读的部分。通过使用细心挑选的名称,可以向阅读者传递大量的意图和信息。反过来讲,使用人造的命名范式(比如现在已经无人问津的匈牙利表示法)会让代码难以阅读和理解。这些范式中包括的底层数据类型信息,会硬变吗在变量名和方法名中,形成脆弱、僵化的代码,并会在将来造成麻烦。使用细心挑选的名称和清晰的执行路径,代码几乎不需要注释。
对于显而易见的代码增加注释,也会有同样的问题。这种注释很常见——通常是由过于热心的IDE插入的。许多注释没有传递任何有意义的信息,这种注释只会分散注意力,而且很容易失去时效性。
对于类中的每个方法可能要说明下列信息:
- 目的:为什么需要这个方法?
- 需求(前置条件):方法需要什么样的输入,对象必须处于何种状态,才能让这方法工作?
- 承诺(后置条件):方法成功执行后,对象现在处于什么状态,有哪些返回值?
- 异常:可能会发生什么样的问题?会抛出什么样的异常?
要感谢如RDoc、javadoc和ndoc这样的工具,使用它们可以很方便地直接从代码注释创建有用的、格式优美的文档。这些工具抽取注释,并生成样式漂亮且带有超链接的HTML输出。
具体技巧
- 在代码可以传递意图的地方不要使用注释
- 解释代码做了什么的注释用处不那么大。相反,注释要说明为什么会这样写代码。
- 当重写方法时,保留描述原有方法意图和约束的注释。
动态评估取舍
对任何单个因素如此独断地强调,而不考虑它是否是项目成功的必要因素,必然导致灾难的发生。如果应用的性能已经足够好了,还有必要继续投入精力让其运行的更快一点吗?大概不用了吧。一个应用还有许多其他方面的因素同样重要。与其花费时间去提升千分之一的性能表现,也许减少开发投入,降低成本,并尽快让应用程序上市销售更有价值。
问题的关键是要多长个心眼儿,而不是总按照习惯的思路去解决问题。如果团队认为性能上还有提升的空间,那么就去咨询一下利益相关者,让他们决定应将重点放在哪里。没有适宜所有状况的最佳解决方案。你必须对手上的问题进行评估,并选出最合适的解决方案。每个设计都是针对特定问题的一一只有明确地进行评估和权衡,才能得到更好的解决方案。
具体技巧
- 如果现在投入额外的资源和精力,是为了将来可能得到的好处,要确认投入一定要得到回报(大部分情况下,是不会有回报的)
- 真正的高性能系统,从一开始设计时就在向这个方向努力。
- 过早的优化是万恶之源。
- 过去用过的解决方案对当前的问题可能适用,也可能不适用。不要事先预设结论,先看看现在是什么状况。
增量式编程
如果不对自己编写的代码进行测试,保证没有问题,就不要连续几个小时,甚至连续几分钟进行编程。相反,应该采用增量式的编程方式。增量式编程可以精炼并结构化你的代码。代码被复杂化、变成一团乱麻的几率减少了。所开发的代码基于即时的反馈,这些反馈来自以小步幅方式编写代码和测试的过程。
采取增量式编程和测试,会倾向于创建更小的方法和更具内聚性的类。你不是在埋头盲目地编写一大堆代码。相反,你会经常评估代码质量,并不时地进行许多小调整,而不是一次修改许多东西。
在编写代码的时候,要经常留心可以改进的微小方面。这可能会改善代码的可读性。也许你会发现可以把一个方法拆成几个更小的方法,使其变得更易于测试。在重构的原则指导下,可以做出许多细微改善(《重构:改善既有代码的设计》)。可以使用测试优先开发方式,作为强制进行增量式编程的方式。关键在于持续做一些细小而有用的事情,而不是做一段长时间的编程或重构。
这就是敏捷的方式。
具体技巧
- 如果构建和测试循环话费的时间过长,你就不会希望经常运行它们了。要保证测试可以快速运行。
- 在编译和测试运行中,停下来想一想,并暂时远离代码细节,这是保证不会偏离正确方向的好办法。
- 要休息的话就要好好休息,请远离键盘。
- 要像重构你的代码那样,重构你的测试,而且要经常重构测试。
保持简单
不要让自己被迫进行过分设计,也不要将代码过分复杂化。
Simple is not simplistic.
简单这个词汇被人们大大误解了(在软件开发工作以及人们的日常生活中,皆是如此)。它并不意味着简陋、业余或是能力不足。
优雅的代码第一眼看上去,就知道它的用处,而且很简洁。但是这样的解决方案不是那么容易想出来的。这就是说,优雅说易于理解和辨识的,但是要想创建出来就困难得多了。
除非有不可辩驳的原因,否则不要使用模式、原则和高难度技术之类的东西。
具体技巧
- 代码几乎总是可以得到进一步精炼,但是到了某个点之后,再做改进就不会带来任何实质性的好处了。这时开发人员就该停下来,去做其他方面的工作了。
- 要将目标牢记在心:简单、可读性高的代码。强行让代码变得优雅与过早优化类似,同样会产生恶劣的影响。
- 当然,简单的解决方案必须要满足功能需求。
- 泰国简洁不等于简单,那样无法达到沟通的目的。
- 一个人认为简单的东西,可能对另一个人就意味着复杂。
编写内聚的代码
内聚性用来评估一个组件(包、模块或配件)中成员的功能性。内聚程度高,表明各个成员共同完成了一个功能特性或是一组功能特性。内聚程度低的话,表明各个成员提供的功能是互不相干的。
如何组织一个组件中的代码,会对开发人员的生产力和全部代码的可维护性产生重要影响。在决定创建一个类的时候,问问自己,这个类的功能是不是与组件中其他某个类的功能类似,而且功能紧密相关。这就是组件级的内聚性。
类也要遵循内聚性。如果一个类的方法和属性共同完成了一个功能或是一系列紧密相关的功能,这个类就是内聚的。
一些设计技巧可以起到帮助作用。举例来说,我们常常使用模型-视图-控制器(MVC)模式来分离展示层逻辑、控制器和模型。这个模式非常有效,因为它可以让开发人员获得更高的内聚性。模型中的类包含一种功能,在控制器中的类包含另外的功能,而视图中的类则只关心UI。
内聚性会影响一个组件的可重用性。
具体技巧
- 有可能会把一些东西拆分成很多微小的部分,而使其失去了实用价值。当你需要一只袜子的时候,一盒棉线不能带给你任何帮助。
- 具有良好内聚性的代码,可能会根据需求的变化,而成比例地进行变更。考虑一下,实现一个简单的功能变化需要变更多少代码。
告知,不要询问
“面向过程的代码取得信息,然后做出决策;面向对象的代码让别的对象去做事情。”
作为某段代码的调用者,开发人员绝对不应该基于被调用对象的状态来做出任何决策,更不能去改变该对象的状态。这样的逻辑应该是被调用对象的责任,而不是你的。在该对象之外替它做决策,就违反了它的封装原则,而且为bug提供了滋生的土壤。
将命令与查询分离开来
Keep commands separate from queries.
一个常规的命令可能会改变对象的状态,而且有可能反馈一些有用的值,以方便使用。
一个查询仅仅提供给开发人员对象的状态,并不会对其外部的可见状态进行修改。
像命令这种会产生内部影响的方法,强化了告知,不要询问的建议。此外,保证查询没有副作用,也是很好的编码实践,因为开发人员可以在单元测试中自由使用它们,在断言或者调试器中调用它们,而不会改变应用的状态。
具体技巧
- 一个对象如果只是用作大量数据容器,这样的做法很可疑。有些情况下会需要这样的东西,但并不像想象的那么频繁。
- 一个命令返回数据以方便使用时没有问题的(如果需要的话,创建单独读取数据的方法也是可以的)。
- 绝对不允许一个看起来无辜的查询去修改对象的状态。
根据契约进行替换
保持系统灵活性的关键方式,是当新代码取代原有代码之后,其他已有的代码不会意识到任何差别。
Liskov替换原则告诉我们:任何继承后得到的派生类对象,必须可以替换任何被使用的基类对象,而且使用者不必知道任何差异。要遵守Liskov替换原则,相对基类的对应方法,派生类方法应该不要求更多,不承诺更少;要可以进行自由的替换。在设计类的继承层次时,这是一个非常重要的考虑因素。
如果违反了Liskov替换原则,继承层次可能仍然可以提供代码的可重用性,但是将会失去可扩展性。类继承关系的使用者现在必须要检查给定对象的类型,以确定如何针对其进行处理。当引入新的类之后,调用代码必须经常评估并修正。这不是敏捷的方式。
针对is-a关系使用继承;针对has-a或uses-a关系使用委托。
聚合时指在类中包含一个对象,并且该对象是其他类的实例,开发人员将责任委托给所包含的对象来完成。
具体技巧
- 相对继承来说,委托更加灵活,适应力也更强。
- 继承不是魔鬼,只是长久以来被大家误解了。
- 如果你不确定一个接口做出了什么样的承诺,或是有什么样的需求,那就很难提供一个对其有意义的实现了。