1. 什么是整洁代码
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某些分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事情。 --Bjarne Stroustrup,C++语言发明者,C++ Programming Language(中译版《C++程序设计语言》)一书作者
整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直接了当的控制语句。 --Grady Booch,Object Oriented Analysis and Design with Applications(中译版《面向对象分析与设计》)一书作者
整洁的代码应可由作者之外的开发者阅读和增补。 它应有单元测试和验收测试。它使用又意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系, 而且要明确地定义和提供清晰、尽量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。--“老大”Dave Thomas,OTI公司创始人,Eclipse战略教父
我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本性的。整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码。 --Michael Feathers,Working Effectively with Legacy Code(中译版《修改代码的艺术》)一书作者。--
近年来,我开始研究贝克的简单代码规则,差不多也都琢磨透了。简单代码,依其重要顺序:
- 能通过所有测试
- 没有重复代码
- 体现系统中的全部设计理念
- 包括尽量少的实体, 比如类、方法、函数等。
在以上诸项中,我最在意代码重复。如果同一段代码反复出现,就表示某种想法未在代码中得到良好的体现。我尽力去找出到底那是什么,然后再尽力更清晰地表达出来。
在我看来,有意义的命名是体现表达力的一种方式,我往往会修改好几次才会定下名字来。借助Eclipse这样的现代编码工具,重命名代价极低,所以我无所顾忌。然而,表达力还不只体现在命名上。我也会检查对象或方法是否想做的事太多。如果对象功能太多,最好是切分为两个或多个对象。如果方法功能太多,我总是使用抽取手段(Extract Method)重构之,从而得到一个能较为清晰地说明自身功能的方法,以及另外数个说明如何实现这些功能的方法。
消除重复和提高表达力让我在整洁代码方面获益良多,只要铭记这两点,改进脏代码时就会大有不同。不过,我时常关注的另一规则就不太好解释了。
这么多年下来,我发现所有程序都由极为相似的元素构成。例如“在集合中查找某物”。不管是雇员记录数据库还是名-值对哈希表,或者某类条目的数组,我们都会发现自己想要从集合中找到某一特定条目。一旦出现这种情况,我通常会把实现手段封装到更抽象的方法或类中。这样做好处多多。
可以先用某种简单的手段,比如哈希表来实现这一功能,由于对搜索功能的引用指向了我那个小小的抽象,就能随需应变,修改实现手段。这样就既能快速前进,又能为未来的修改预留余地。
另外,该集合抽象常常提醒我留意“真正”在发生的事,避免随意实现集合行为,因为我真正需要的不过是某种简单的查找手段。
减少重复代码,提高表达力,提早构建简单抽象。这就是我写整洁代码的方法。
--Ron Jeffries,Extreme Programming Installed(中译版《极限编程实施》)以及Extreme Programming Adventures in C#(中译版《C#极限编程探险》)作者。
Ron以寥寥数段文字概括了本书的全部内容。不要重复代码,只做一件事,表达力,小规模抽象。该有的都有了。
如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。 --Ward Cunningham,Wiki发明者,eXtreme Programming(极限编程)的创始人之一,Smalltalk语言和面向对象的思想领袖。所有在意代码者的教父--
编程中的读写比例
在写新代码的时候, 我们会一直读旧代码。 读与写花费时间的比例超过10:1!既然比例如此之高, 我们想让读的过程变得轻松, 即便那会使得编写过程更难。没有可能光写不读,所以使之易读实际也使之易写。
童子军军规
让营地比你来时更干净。
如果每次嵌入时,代码都比签出时干净,那么代码就不会腐坏。 清理并不一定要花多少功夫, 也许只是改好一个变量名, 拆分一个有点过长的函数, 消除一点点重复代码, 清理一个嵌套的if语句。
持续改进是专业性的内在组成部分。
2. 有意义的命名
名副其实
bad:
int d; // 消逝的时间,以日记
good:
int elapsedTimeInDays;
用一个名副其实的函数,掩盖住魔术数,例如:
good:
if ( cell.IsFlagged() )
bad:
if (cell[STATUS_VALUE] == FLAGGED)
避免误导
程序员必须避免留下掩藏代码本意的错误线索。
别用accountList来指定一组账号,除非它真的是List类型。 用accountGroup或bunchOfAccounts, 甚至直接用accounts都会好一些。
做有意义的区分
good:
public static void copyChars(char source[], char destination[]) {
for (int i = 0; i < source.length; i++ ) {
destination[i] = source[i];
}
}
bad:
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++ ) {
a2[i] = a1[i];
}
}
废话是另一种没有意义的区分。 假设你有一个Product类型。 如果还有一个ProductInfo 或 ProductData类, 那它们的名称虽然不同, 意思却无区别。Info 和 Data 就像 a、an和 the 一样。
bad:
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
// 程序员怎么能知道该调用哪个函数呢。
要区分名称,就要以读者能鉴别不同之处的方式来区分。
使用读得出来的名称
不要使用非恰当的英语词(不用读出傻乎乎的自造词)
bad:
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqpint = "102";
/* ... */
};
good:
class Customer {
private Date generationTimeStamp;
private Date modificationTimestamp;
private final String recordId = "102";
/* ... */
}
使用可搜索的名称
bad:
for (int j=0; j<34; j++) {
s += (t[j] * 4)/5;
}
good:
int realDaysRerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for ( int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimatej] * realDaysRerIdealDay ;
int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK );
sum += realTaskWeeks;
}
成员前缀
也不必使用 m_ 前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。
public class Part {
private String m_desc; // The testual description
void setName(String name) {
m_desc = name;
}
}
----------------------------------
public class Part{
String description;
void setDescription(String descripthion) {
this.description = description;
}
}
人们会很快学会无视前缀(或后缀), 只看到名称中有意义的部分。 代码读得越多,眼中就越没有前缀。
接口和实现
如果你在做一个创建形状用的抽象工厂(Abstract Factory)。 该工厂是个接口(Interface), 要用具体类来实现。 你怎么命名工厂和具体类呢?
如何写出好的函数
写代码和写别的东西很像,在写论文或者文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称时随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。 然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。
最后,遵循本章列出的规则,我组装好这些函数。我并不从一开始就按照规则写函数。我想没人做得到。
函数
1、短小 < 15行
函数的第一规则是短小,第二规则是更短小。每个函数都只说一件事,每个函数都一目了然。每个函数都依序把你带到下一个函数。
2、代码块和缩进(不超过三层)
if else while语句等,其中的代码块应该只有一行,即函数调用语句,以保持函数短小。
3、只做一件事
函数应该只做一件事,做好这件事。只做这一件事。判断函数是否不止做了一件事的方法,看是否能再抽出一个函数。函数中如果存在多个区段,表明函数做的事情不止一件。
4、每个函数一个抽像层级。
每个函数一个抽像层级。确保函数只做一件事,函数中所有的语句都在一个抽象层级上。函数中混杂不同抽象层级,往往让人迷惑,更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
5、自顶向下阅读代码:向下规则
让每个函数后面都跟着位于下一抽象层级的函数,在查看函数列表时,就能依抽象层级向下阅读。
6、确保每个switch都埋藏在较低的抽象层级,而且永远不重复
7、使用描述性的名称
使用描述性的名称:函数越短小,功能越集中,就越便于取个好名字
长而具有描述性的名称,比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。别害怕花时间取名字,尝试不同的名称,实测阅读效果。选择描述性的名称能理清你关于模块的设计思路,并帮你改进。追索好名称,往往导致对代码的改善重构。命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。使用类似措辞,依序讲出一个故事。
8、函数参数,最好没有,尽量避免三个。
足够特殊理由才能用三个以上参数。从测试角度看,参数更叫人为难。
9、标识参数 ,这是标识函数不止做一件事情的明确信号,应该拆分。
10、二元函数,尽量改写为一元函数
利用一些机制将其转换为一元函数。(多个参数封装成临时类,不是一个好的解决方式)
11、函数和参数应当形成一种非常良好的动/、名词对形式
如assertEqual 改成assertExpectedEqualsActual(expected,actual)大大减轻记忆参数顺序的负担。
12避免使用输出参数
如果函数必须要修改某种状态,就修改所属对象的状态。
13、分隔指令与询问
函数要么做什么事 要么回答什么事,但二者不可得兼。
函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。
14、使用异常替代返回错误码。
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。鼓励了在if语句判断中把指令当做表达式使用。另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径径代码中分离出来,得到简化。
15、抽离Try/Catch代码块。
Try/Catch代码丑陋不堪。他们搞乱了代码结构,把错误处理与正常流程混为一谈。把try和catch代码块的主体部分抽离出来,另外形成函数。
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不应该做其他的事,如果关键字try在某个函数中存在,它就应该是这个函数的第一个单词,而且在catch/fianlly代码块后面也不应该有其他内容。
16、别重复自己
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。如面向对象将代码集中到积累,避免冗余。