良好的代码习惯

来源:互联网 发布:js格式化php时间戳 编辑:程序博客网 时间:2024/05/16 12:13

本文转载自: http://blog.csdn.net/poem_qianmo/article/details/52268975

作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442



我们就是一群代码猴子,上蹿下跳,自以为领略了编程的真谛。可惜当我们抓着几个酸桃子,得意洋洋坐到树枝上,却对自己造成的混乱熟视无睹。那堆“可以运行”的乱麻程序,就在我们的眼皮底下慢慢腐坏。

 

——《代码整洁之道》作者 RobertC.Martin,于SD West 2007技术大会

 

 

 

一、系列文章前言

 

 

敲完上面这段文字的时候,心里在想,一个刚踏入编程生涯的新人,要经历多少的淬炼,才能领略到Bob大叔所谓的编程的真谛。

有人说,这个过程会很漫长,大概至少是在读完N本编程领域的经典著作,并经过大量的思考与实践之后。

 

而写这个系列的起因,正是因为最近闲暇时一直在阅读一些之前一直想看的经典著作,并有将阅读过程中一些思考和总结记录下来。为了不枉费这些阅读、思考与总结的过程,决定将这些零散的内容整理成文,沉淀下来。过些年后再回首,也许会觉得当时的一些思考,弥足珍贵。

 

这个系列的文章,不仅仅是读书笔记,而是对一本书核心内容的全新演绎,内容解刨,与重组。希望自己的这些文字,能对各位想进一步了解这些经典著作的读者们有所帮助。

 

 

 

 

二、《代码整洁之道》其书

 

 

《代码整洁之道》(《Clean Code》)是几乎每一个对编程境界有追求、有志于改善代码质量的编程者,都应该阅读的一本好书。

这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。

书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

 

 

 

三、本文涉及知识点思维导图

 

 

先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。

 

 

 

 

四、糟糕代码,是如何产生的

 

 

最初的问题来了,我们都不喜欢坏代码,为何坏代码还总会产生?

抛开编程者本身技艺的问题,答案也许是想要快点完成任务,项目进度赶着时间,或许我们只是不耐烦再继续做这个需求,渴望它能早点结束。

很多时候,会有一堆事情压着你去完成,你觉得自己需要赶紧弄完手上的东西,好接着做下一件工作,所以,为了走捷径,为了特定实现功能而强加的糟糕的代码,就悄悄地诞生了。我们都曾经瞟一眼自己亲手造成的混乱,决定弃之不顾,走向新的一天,等有朝一日再来清理。

然而中国古训有云,“明日复明日,明日何其多”,勒布朗(LeBlanc)法则也表示,“稍后等于永不。”你会发现,因为各种各样的原因,很多时候你根本都不会(没时间)去整理之前的代码。所以,正如本书作者Robert C.Martin(Uncle Bob),在SD West 2007技术大会上所说的,“那堆“可以运行”的乱麻程序,就在我们的眼皮底下慢慢腐坏。”

 

我们知道,坏代码会污染环境,最后会坏掉整个项目。保持整洁的习惯,发现脏代码就要及时纠正。花时间保持代码代码整洁,不但有关效率,还有关项目的生存。

 

 

 

五、为什么好代码会很快变质?

 

 

为什么好代码会很快变质?一般情况下,需求一直在变更、需求变化违背了初期设计、进度太紧是导致好代码变质的诱因。

多数的产品经理想要好的代码,即便他们总是痴缠于进度,他们会奋力的维护进度和需求。而程序员们则当以同等的热情捍卫代码的整洁性和可扩展性。

举个栗子,如果你是医生,病人在请求给给他们做手术前别洗手,因为那会花太多时间,你会照办吗?本该是病人说了算,但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病个感染的风险。医生如果按病人说的办,就是一种不专业的态度。

同理,程序员遵从不了解混乱代码风险的产品经理(策划)的意愿,都是不专业的做法。

 

 

 

 

六、程序员的基础价值谜题

 

 

程序员都面临着一种基础价值的谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后腿,但开发者背负期限的压力,只好制造混乱。简言之,他们没花时间让自己做得更快。

而其实真正专业的人士明白,这道谜题第二部分说错了,制造混乱无助于赶上期限,混乱只会立刻拖慢你,叫你错过期限,赶上期限的唯一方法——做得更快的唯一方法——就是始终尽可能保持代码的整洁。

 

 

 

七、大师们眼中的整洁代码

 

 

到底什么是整洁的代码?有多少程序员,就有多少定义。 “大师级程序员把系统当故事来讲,而不是当做程序来写”。就让我们一起看看经验丰富的大师级程序们都是如何定义整洁代码的。

 

Bjarne Stroustrup ,C++语言之父, The C++Programming Language(中译版《C++程序设计语言》)一书作者:

我喜欢优雅和高效的代码,代码逻辑应直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;一句某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。

 

 

Grady Booch,Object Orient Analysisand Design with Application(中译版《面向对象程序分析与设计》)一书作者:

整洁的代码简单直接,整洁的代码如优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。

 

Michael Feathers,Working EffectivelywithLegacy programming(中译版《修改代码的一书》)一书的作者:

整洁的代码总是看起来想是某位特别在意它的人写的,几乎没有改进的余地,代码作者署名都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入到某人留下的代码。

 

Ron Jeffries,Extreme ProgrammingInstalled(中译版《极限编程实施》)以及Extreme Programming Adventures in(C#中译版《C#极限编程探险》)作者:

减少重复的代码,提高表达力,提早构建简单抽象,这就是我写整洁代码的方法。

 

 

 

 

八、编写代码的难度,取决于周边代码的阅读难度

 

 

编写代码的难度,取决于周边代码的阅读难度。何出此言?因为各种实践与统计表明,在项目里开发新功能的过程中,阅读之前代码与书写新的代码,花费的时间比例超过10:1,新写代码时,我们一直在读旧代码。既然比例如此之高,我们就应该让读的过程变得轻松,即便那会使得编写过程更难。

所以说,编写代码的难度,取决于周边代码的阅读难度,想要快速实现需求,想要快速完成任务,想要轻松的写代码,先让你书写的代码整洁易读吧。

 

 

 

九、让代码比你来时更干净

 

 

我们知道,光把代码写好可不够,必须时时保持代码整洁,我们都见过代码随着时间的流逝而腐坏。我们应该更积极地阻止腐坏的发生。

借用美国童子军的一条简单的军规,应用到我们的专业领域:

“让营地比你来时更干净。”

那么可以同样对编程领域这样说:

“让代码比你来时更干净。”

 

也就是说,如果我们每次签入时,代码都比签出时干净,那么代码就不会腐坏。这就是我们需要遵从的代码整洁之道。

 

 

 

十、本文涉及知识点提炼整理

 

 

文章开头部分已经用思维导图的方式展现了本文的知识点,这边再贴出一个文字列表版,方便大家整理:

 

1.编写代码的难度,取决于周边代码的阅读难度。想要快速实现需求,想要快速完成任务,想要轻松的写代码,请先让你书写的代码整洁易读。

2.保持整洁的习惯,发现脏代码就要及时纠正。花时间保持代码代码整洁,这不但有关效率,还有关项目的生存。

3.程序员遵从不了解混乱风险的产品经理(策划)的意愿,都是不专业的做法。

4.让代码比你来时更干净:如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。

5.赶上期限的唯一方法,做得更快的唯一方法,就是始终尽可能保持代码的整洁。


 

本文与大家聊一聊编程中非常关键的一个点,如何更好的对代码命名。

 

 

一、引言

 

 

《代码整洁之道》这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

 

但我们知道,很多时候,理想很丰满,现实很骨感,也知道人在江湖,身不由己。因为项目的紧迫性,需求的多样性,我们无法时时刻刻都写出整洁的代码,保持自己输出的都是高质量、优雅的代码。

 

但若我们理解了代码整洁之道的精髓,我们会知道怎样让自己的代码更加优雅、整洁、易读、易扩展,知道真正整洁的代码应该是怎么样的,也许就会渐渐养成持续输出整洁代码的习惯。

 

而且或许你会发现,若你一直保持输出整洁代码的习惯,长期来看,会让你的整体效率和代码质量大大提升。

 

 

 

 

二、本文涉及知识点思维导图

 

 

先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。

 

 

 

 

 

 

三、高质量代码的命名法则

 

 

名副其实

 

名副其实说起来貌似很简单,但真正做起来,似乎没那么容易。选个好名字要花一些时间,但其实选好名字之后省下来的时间,要比之前选名字时花掉的时间多得多。

我们一旦发现有更好的名称时,就应该换掉之前的旧名称,这样做读你代码的人(包括你自己),都会很开心。

一个好的变量、函数或类的名称应该已经几乎答复了所有的大问题。它应该告诉你,这个名称所代表的内容,为什么会存在,做了什么事情,应该如何用等。

如果一个名称需要注释来补充才能让大家明白其真正含义,那其实就不算是名副其实。(并不是说不需要注释,恰如其分的注释是程序员让自己代码锦上添花的好方法,关于注释的一些注意事项,稍后会有文章专门涉及。)

举个栗子:

 

以下的这句代码里的d就不算是个好命名。名称d什么都没说,它没引起我们对时间消逝的感觉,更别说单位是天了:

[cpp] view plain copy

 

1. int d; // elapsed time in days||经过了几天时间  

[cpp] view plain copy

 

1. int d; // elapsed time in days||经过了几天时间  

我们应该选择这样的指明了计量对象和计量单位的名称:

[cpp] view plain copy

 

1. int elapsedTimeInDays;  

2. int daysSinceCreation;  

3. int daysSinceModification;  

4. int fileAgeInDays;  

[cpp] view plain copy

 

1. int elapsedTimeInDays;  

2. int daysSinceCreation;  

3. int daysSinceModification;  

4. int fileAgeInDays;  

 

 

避免造成误导

 

我们应该避免留下隐藏代码本意的错误线索,也应该避免使用与本意相悖的词。例如,别用accountList来指一组账号,除非它真的是List类型,用accountGroup、bunchOfAccounts,或者直接用accounts,都是更好的选择。

尽量提防长得太像的名称。想区分XYZControllerForEfficientHandlingOfStrings和XYZControllerForEfficientStorageOfStrings,会花费我们太多的时间。因为这两个词,实在太相似了。

 以同样的方式拼写出同样的概念才是信息,拼写前后不一致就是误导。

 

 

尽量做有意义的区分

 

1.尽量避免使用数字系列命名(a1、a2…….aN)。这样的名称纯属误导,因为很多时候完全没有提供正确的信息,没有提供导向作者意图的线索。

2.废话是另一种没有意义的区分。如果我们有一个Product类,还有一个ProductInfo或ProductData类,那么他们的名称虽然不同,但意思却无区别。这里的Info、Data就像a、an和the一样,是意义含混的废话。

注意,只要体现出有意义的区分,使用a、the这样的前缀就没错。例如,将a用在域内变量,把the用于函数参数。

 

 

尽量使用读得出来的名称

 

我们要使用读得出来的名称。如果名称读不出来,讨论的时候就会不方便且很尴尬,甚至让旁人觉得很蠢。

例如,变量名称是beeceearrthreecee,讨论的时候读起来简直像没吃药。

 

 

尽量使用可搜索的名称

 

单字母和数字常量有个问题,就是很难再一大篇文字中找出来。

找MAX_CLASSED_PER_STUDENT很容易,但想找数字7,就很麻烦。

同样,字母e也不是个便于搜索的好变量名。因为作为英文中最常用的字母,在每个程序、每段代码中都有可能出现。

 名称长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,应赋予其以便于搜索的名称。

举个栗子,比较如下两段代码:

[cpp] view plain copy

 

1. for (int j=0; j<34; j++)  

2. {  

3.      s += (t[j]*4)/5;  

4. }  

[cpp] view plain copy

 

1. for (int j=0; j<34; j++)  

2. {  

3.      s += (t[j]*4)/5;  

4. }  

[cpp] view plain copy

 

1. const int WORK_DAYS_PER_WEEK = 5;  

2. int sum = 0;  

3. for (int j=0; j < NUMBER_OF_TASKS; j++)  

4. {  

5.      int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;  

6.     int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);  

7.      sum += realTaskWeeks;  

8. }  

[cpp] view plain copy

 

1. const int WORK_DAYS_PER_WEEK = 5;  

2. int sum = 0;  

3. for (int j=0; j < NUMBER_OF_TASKS; j++)  

4. {  

5.      int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;  

6.     int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);  

7.      sum += realTaskWeeks;  

8. }  

按整洁代码的要求来评判,第一段代码会让读者不知所云,第二段代码比第一段好太多。第二段代码中,sum并非特别有用的名称,但至少他搜索得到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比数字5好找得多,而列表中也只剩下了体现我们意图的名称。

 

 

取名不要绕弯子

 

我们取名的时候要避免思维映射,不应当让读者在脑中把你的名称翻译为他们熟知的名称,也就是说取名不要绕弯子,而是要直白,直截了当。

 在多数情况下,单字母不是个好的命名选择,除非是在作用域小、没有名称冲突的地方,比如循环。循环计数器自然有可能被命名为i,j或k(最好别用字母l),这是因为传统上我们惯用单字母名称做循环计数器。

 程序员通常都是聪明人,聪明人有时会借助脑筋急转弯炫耀其聪明。而聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确就是王道。专业的程序员善用其能,能编写出其他人能理解的代码。

 

 

类名尽量用名词

 

类名尽量用名词或名词短语,比如Customer, WikiPage,Account, 或 AddressParser。

类名最好不要是动词。

 

 

方法名尽量用动词

 

方法名尽量用动词或动词短语。比如postPayment, deletePage, 或者save。

属性访问器、修改器和断言应该根据其value来命名,并根据标准加上get、set和is前缀。

举个栗子,这里的getName、setName等命名都很OK:

[cpp] view plain copy

 

1. string name = employee.getName();  

2. customer.setName("mike");  

3. if (paycheck.isPosted())...  

[cpp] view plain copy

 

1. string name = employee.getName();  

2. customer.setName("mike");  

3. if (paycheck.isPosted())...  

而重载构造器时,使用描述了参数的静态工厂方法名。如:

[cpp] view plain copy

 

1. Complex fulcrumPoint =Complex.FromRealNumber(666.0);  

[cpp] view plain copy

 

1. Complex fulcrumPoint =Complex.FromRealNumber(666.0);  

通常好于:

[cpp] view plain copy

 

1. Complex fulcrumPoint = new Complex(666.0);  

[cpp] view plain copy

 

1. Complex fulcrumPoint = new Complex(666.0);  

我们也可以考虑将相应的构造器设置为private,强制使用这种命名手段。

 

 

每个概念对应一词,并一以贯之

 

我们需给每个概念选一个词,并且一以贯之。

例如,使用fetch、retrieve和get来给在多个类中的同种方法命名,你怎么记得住哪个类中是哪个方法呢?

同样,在同一堆代码中混用controller、manager,driver,就会令人困惑。DeviceManager和Protocol-Controller之间有何根本区别?为什么不全用controller或者manager?他们都是Driver吗?这就会让读者以为这两个对象是不同的类型,也分属不同的类。

所以,对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。

 

 

10 通俗易懂

 

我们应尽力写出易于理解的变量名,把代码写得让别人能一目了然,而不必让人去非常费力地去揣摩其含义。我们想要那种大众化的作者尽责地写清楚的通俗易懂的畅销书风格,而不是那种学者学院风的晦涩论文写作风格。

 

 

 

11 尽情使用解决方案领域专业术语

 

记住,只有程序员才会读你写的代码。所以,尽管去用那些计算机科学(ComputerScience,CS)领域的专业术语、算法名、模式名、数学术语。

对于熟悉访问者(Visitor)模式的程序来说,名称AccountVisitor富有意义。给技术性的事物取个恰如其分的技术性名称,通常就是最靠谱的做法。

 

 

 

12 添加有意义的语境

 

很少有名称是可以自我说明的。所以,我们需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境。若没能提供放置的地方,还可以给名称添加前缀。

 

举个栗子,假如我们有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量。当他们搁一块儿的时候,的确是构成了一个地址。不过,假如只是在某个方法中看到一个孤零零的state呢?我们会推断这个变量是地址的一部分吗?

我们可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境。至少,读者可以知道这些变量是某个更大变量的一部分。当然,更好的方案是创建名为Address的类。这样,即便是编译器也会知道这些变量是隶属于某个更大的概念了。

 

另外,只要短名称足够好,对含义的表达足够清除,就要比长名称更合适。添加有意义的语境甚好,别给名称添加不必要的语境。

 

 

 

四、小结

 

 

其实,取一个好名字最难的地方在于需要良好的描述技巧和共有的文化背景。与其说这是一种技术、商业或管理问题,还不如说这是一种教学问题。

不妨试试上面列出的这十二条规则与要点,看看你的代码可读性是否有所提升。而如果你是在维护别人的代码,或者是在重构,效果应该会是立竿见影的。

 

 

 

五、本文涉及知识点提炼整理

 

 

文章开头部分已经用思维导图的方式展现了本文的知识点,这边再贴出一个文字列表版,方便大家整理:

 

要点一:要名副其实。一个好的变量、函数或类的名称应该已经答复了所有的大问题。一个好名称可以大概告诉你这个名称所代表的内容,为什么会存在,做了什么事情,应该如何用等。

要点二:要避免误导。我们应该避免留下隐藏代码本意的错误线索,也应该避免使用与本意相悖的词。

要点三:尽量做有意义的区分。尽量避免使用数字系列命名(a1、a2…….aN)和没有意义的区分。

要点四:尽量使用读得出来的名称。如名称读不出来,讨论的时候会不方便且很尴尬。

要点五:尽量使用可搜索的名称。名称长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,应赋予其以便于搜索的名称。

要点六:取名不要绕弯子。取名要直白,要直截了当,明确就是王道。

要点七:类名尽量用名词。类名尽量用名词或名词短语,最好不要是动词。

要点八:方法名尽量用动词。方法名尽量用动词或动词短语。

要点九:每个概念对应一词,并一以贯之。对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。

要点十:通俗易懂。应尽力写出易于理解的变量名,要把代码写得让别人能一目了然,而不必让人去非常费力地去揣摩其含义。

要点十一:尽情使用解决方案领域专业术语。尽管去用那些计算机科学领域的专业术语、算法名、模式名、数学术语。

要点十二:要添加有意义的语境。需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境。若没能提供放置的地方,还可以给名称添加前缀。

本文与大家聊一聊编程中非常关键的一个点,如何更好的对代码命名。

 

 

一、引言

 

 

《代码整洁之道》这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

 

但我们知道,很多时候,理想很丰满,现实很骨感,也知道人在江湖,身不由己。因为项目的紧迫性,需求的多样性,我们无法时时刻刻都写出整洁的代码,保持自己输出的都是高质量、优雅的代码。

 

但若我们理解了代码整洁之道的精髓,我们会知道怎样让自己的代码更加优雅、整洁、易读、易扩展,知道真正整洁的代码应该是怎么样的,也许就会渐渐养成持续输出整洁代码的习惯。

 

而且或许你会发现,若你一直保持输出整洁代码的习惯,长期来看,会让你的整体效率和代码质量大大提升。

 

 

 

 

二、本文涉及知识点思维导图

 

 

先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。

 

 

 

 

 

 

三、高质量代码的命名法则

 

 

名副其实

 

名副其实说起来貌似很简单,但真正做起来,似乎没那么容易。选个好名字要花一些时间,但其实选好名字之后省下来的时间,要比之前选名字时花掉的时间多得多。

我们一旦发现有更好的名称时,就应该换掉之前的旧名称,这样做读你代码的人(包括你自己),都会很开心。

一个好的变量、函数或类的名称应该已经几乎答复了所有的大问题。它应该告诉你,这个名称所代表的内容,为什么会存在,做了什么事情,应该如何用等。

如果一个名称需要注释来补充才能让大家明白其真正含义,那其实就不算是名副其实。(并不是说不需要注释,恰如其分的注释是程序员让自己代码锦上添花的好方法,关于注释的一些注意事项,稍后会有文章专门涉及。)

举个栗子:

 

以下的这句代码里的d就不算是个好命名。名称d什么都没说,它没引起我们对时间消逝的感觉,更别说单位是天了:

[cpp] view plain copy

 

1. int d; // elapsed time in days||经过了几天时间  

[cpp] view plain copy

 

1. int d; // elapsed time in days||经过了几天时间  

我们应该选择这样的指明了计量对象和计量单位的名称:

[cpp] view plain copy

 

1. int elapsedTimeInDays;  

2. int daysSinceCreation;  

3. int daysSinceModification;  

4. int fileAgeInDays;  

[cpp] view plain copy

 

1. int elapsedTimeInDays;  

2. int daysSinceCreation;  

3. int daysSinceModification;  

4. int fileAgeInDays;  

 

 

避免造成误导

 

我们应该避免留下隐藏代码本意的错误线索,也应该避免使用与本意相悖的词。例如,别用accountList来指一组账号,除非它真的是List类型,用accountGroup、bunchOfAccounts,或者直接用accounts,都是更好的选择。

尽量提防长得太像的名称。想区分XYZControllerForEfficientHandlingOfStrings和XYZControllerForEfficientStorageOfStrings,会花费我们太多的时间。因为这两个词,实在太相似了。

 以同样的方式拼写出同样的概念才是信息,拼写前后不一致就是误导。

 

 

尽量做有意义的区分

 

1.尽量避免使用数字系列命名(a1、a2…….aN)。这样的名称纯属误导,因为很多时候完全没有提供正确的信息,没有提供导向作者意图的线索。

2.废话是另一种没有意义的区分。如果我们有一个Product类,还有一个ProductInfo或ProductData类,那么他们的名称虽然不同,但意思却无区别。这里的Info、Data就像a、an和the一样,是意义含混的废话。

注意,只要体现出有意义的区分,使用a、the这样的前缀就没错。例如,将a用在域内变量,把the用于函数参数。

 

 

尽量使用读得出来的名称

 

我们要使用读得出来的名称。如果名称读不出来,讨论的时候就会不方便且很尴尬,甚至让旁人觉得很蠢。

例如,变量名称是beeceearrthreecee,讨论的时候读起来简直像没吃药。

 

 

尽量使用可搜索的名称

 

单字母和数字常量有个问题,就是很难再一大篇文字中找出来。

找MAX_CLASSED_PER_STUDENT很容易,但想找数字7,就很麻烦。

同样,字母e也不是个便于搜索的好变量名。因为作为英文中最常用的字母,在每个程序、每段代码中都有可能出现。

 名称长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,应赋予其以便于搜索的名称。

举个栗子,比较如下两段代码:

[cpp] view plain copy

 

1. for (int j=0; j<34; j++)  

2. {  

3.      s += (t[j]*4)/5;  

4. }  

[cpp] view plain copy

 

1. for (int j=0; j<34; j++)  

2. {  

3.      s += (t[j]*4)/5;  

4. }  

[cpp] view plain copy

 

1. const int WORK_DAYS_PER_WEEK = 5;  

2. int sum = 0;  

3. for (int j=0; j < NUMBER_OF_TASKS; j++)  

4. {  

5.      int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;  

6.     int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);  

7.      sum += realTaskWeeks;  

8. }  

[cpp] view plain copy

 

1. const int WORK_DAYS_PER_WEEK = 5;  

2. int sum = 0;  

3. for (int j=0; j < NUMBER_OF_TASKS; j++)  

4. {  

5.      int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;  

6.     int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);  

7.      sum += realTaskWeeks;  

8. }  

按整洁代码的要求来评判,第一段代码会让读者不知所云,第二段代码比第一段好太多。第二段代码中,sum并非特别有用的名称,但至少他搜索得到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比数字5好找得多,而列表中也只剩下了体现我们意图的名称。

 

 

取名不要绕弯子

 

我们取名的时候要避免思维映射,不应当让读者在脑中把你的名称翻译为他们熟知的名称,也就是说取名不要绕弯子,而是要直白,直截了当。

 在多数情况下,单字母不是个好的命名选择,除非是在作用域小、没有名称冲突的地方,比如循环。循环计数器自然有可能被命名为i,j或k(最好别用字母l),这是因为传统上我们惯用单字母名称做循环计数器。

 程序员通常都是聪明人,聪明人有时会借助脑筋急转弯炫耀其聪明。而聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确就是王道。专业的程序员善用其能,能编写出其他人能理解的代码。

 

 

类名尽量用名词

 

类名尽量用名词或名词短语,比如Customer, WikiPage,Account, 或 AddressParser。

类名最好不要是动词。

 

 

方法名尽量用动词

 

方法名尽量用动词或动词短语。比如postPayment, deletePage, 或者save。

属性访问器、修改器和断言应该根据其value来命名,并根据标准加上get、set和is前缀。

举个栗子,这里的getName、setName等命名都很OK:

[cpp] view plain copy

 

1. string name = employee.getName();  

2. customer.setName("mike");  

3. if (paycheck.isPosted())...  

[cpp] view plain copy

 

1. string name = employee.getName();  

2. customer.setName("mike");  

3. if (paycheck.isPosted())...  

而重载构造器时,使用描述了参数的静态工厂方法名。如:

[cpp] view plain copy

 

1. Complex fulcrumPoint =Complex.FromRealNumber(666.0);  

[cpp] view plain copy

 

1. Complex fulcrumPoint =Complex.FromRealNumber(666.0);  

通常好于:

[cpp] view plain copy

 

1. Complex fulcrumPoint = new Complex(666.0);  

[cpp] view plain copy

 

1. Complex fulcrumPoint = new Complex(666.0);  

我们也可以考虑将相应的构造器设置为private,强制使用这种命名手段。

 

 

每个概念对应一词,并一以贯之

 

我们需给每个概念选一个词,并且一以贯之。

例如,使用fetch、retrieve和get来给在多个类中的同种方法命名,你怎么记得住哪个类中是哪个方法呢?

同样,在同一堆代码中混用controller、manager,driver,就会令人困惑。DeviceManager和Protocol-Controller之间有何根本区别?为什么不全用controller或者manager?他们都是Driver吗?这就会让读者以为这两个对象是不同的类型,也分属不同的类。

所以,对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。

 

 

10 通俗易懂

 

我们应尽力写出易于理解的变量名,把代码写得让别人能一目了然,而不必让人去非常费力地去揣摩其含义。我们想要那种大众化的作者尽责地写清楚的通俗易懂的畅销书风格,而不是那种学者学院风的晦涩论文写作风格。

 

 

 

11 尽情使用解决方案领域专业术语

 

记住,只有程序员才会读你写的代码。所以,尽管去用那些计算机科学(ComputerScience,CS)领域的专业术语、算法名、模式名、数学术语。

对于熟悉访问者(Visitor)模式的程序来说,名称AccountVisitor富有意义。给技术性的事物取个恰如其分的技术性名称,通常就是最靠谱的做法。

 

 

 

12 添加有意义的语境

 

很少有名称是可以自我说明的。所以,我们需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境。若没能提供放置的地方,还可以给名称添加前缀。

 

举个栗子,假如我们有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量。当他们搁一块儿的时候,的确是构成了一个地址。不过,假如只是在某个方法中看到一个孤零零的state呢?我们会推断这个变量是地址的一部分吗?

我们可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境。至少,读者可以知道这些变量是某个更大变量的一部分。当然,更好的方案是创建名为Address的类。这样,即便是编译器也会知道这些变量是隶属于某个更大的概念了。

 

另外,只要短名称足够好,对含义的表达足够清除,就要比长名称更合适。添加有意义的语境甚好,别给名称添加不必要的语境。

 

 

 

四、小结

 

 

其实,取一个好名字最难的地方在于需要良好的描述技巧和共有的文化背景。与其说这是一种技术、商业或管理问题,还不如说这是一种教学问题。

不妨试试上面列出的这十二条规则与要点,看看你的代码可读性是否有所提升。而如果你是在维护别人的代码,或者是在重构,效果应该会是立竿见影的。

 

 

 

五、本文涉及知识点提炼整理

 

 

文章开头部分已经用思维导图的方式展现了本文的知识点,这边再贴出一个文字列表版,方便大家整理:

 

要点一:要名副其实。一个好的变量、函数或类的名称应该已经答复了所有的大问题。一个好名称可以大概告诉你这个名称所代表的内容,为什么会存在,做了什么事情,应该如何用等。

要点二:要避免误导。我们应该避免留下隐藏代码本意的错误线索,也应该避免使用与本意相悖的词。

要点三:尽量做有意义的区分。尽量避免使用数字系列命名(a1、a2…….aN)和没有意义的区分。

要点四:尽量使用读得出来的名称。如名称读不出来,讨论的时候会不方便且很尴尬。

要点五:尽量使用可搜索的名称。名称长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,应赋予其以便于搜索的名称。

要点六:取名不要绕弯子。取名要直白,要直截了当,明确就是王道。

要点七:类名尽量用名词。类名尽量用名词或名词短语,最好不要是动词。

要点八:方法名尽量用动词。方法名尽量用动词或动词短语。

要点九:每个概念对应一词,并一以贯之。对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。

要点十:通俗易懂。应尽力写出易于理解的变量名,要把代码写得让别人能一目了然,而不必让人去非常费力地去揣摩其含义。

要点十一:尽情使用解决方案领域专业术语。尽管去用那些计算机科学领域的专业术语、算法名、模式名、数学术语。

要点十二:要添加有意义的语境。需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境。若没能提供放置的地方,还可以给名称添加前缀。

 

 

 

 




 

这篇文章,是关于整洁代码函数书写的一些准则。

 

 

 

一、引言

 

 

以下引言的内容,有必要伴随这个系列的每一次更新,这次也不例外。

 

《代码整洁之道》这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

 

但我们知道,很多时候,理想很丰满,现实很骨感,也知道人在江湖,身不由己。因为项目的紧迫性,需求的多样性,我们无法时时刻刻都写出整洁的代码,保持自己输出的都是高质量、优雅的代码。

 

但若我们理解了代码整洁之道的精髓,我们会知道怎样让自己的代码更加优雅、整洁、易读、易扩展,知道真正整洁的代码应该是怎么样的,也许就会渐渐养成持续输出整洁代码的习惯。

 

而且或许你会发现,若你一直保持输出整洁代码的习惯,长期来看,会让你的整体效率和代码质量大大提升。

 

 

 

 

二、本文涉及知识点思维导图

 

 

国际惯例,先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。

 

 

 

 

 

 

三、整洁代码的函数书写准则

 

 

1短小

 

函数的第一规则是要短小。第二规则还是要短小。

《代码整洁之道》一书作者Bob大叔写道,“近40年来,我写过各种长度不同的函数。我写过令人憎恶的长达3000行的厌物,也写过许多100行到300行的函数,还写过20行到30行的。经过漫长的试错,经验告诉我,函数就该短小”。

那么函数应该有多短小合适呢?通常来说,应该短于如下这个函数:

[cpp] view plain copy

 

1. public static StringrenderPageWithSetupsAndTeardowns  

2. (PageData pageData, boolean isSuite  

3.         )throws Exception  

4. {  

5.         booleanisTestPage = pageData.hasAttribute("Test");  

6.        if(isTestPage) {  

7.                WikiPagetestPage = pageData.getWikiPage( );  

8.               StringBuffernewPageContent = new StringBuffer( );  

9.                includeSetupPages(testPage,newPageContent, isSuite);  

10.              newPageContent.append(pageData.getContent());  

11.               includeTeardownPages(testPage,newPageContent, isSuite);  

12.              pageData.setContent(newPageContent.toString());  

13.        }  

14.       returnpageData.getHtml( );  

15. }  

[cpp] view plain copy

 

1. public static StringrenderPageWithSetupsAndTeardowns  

2. (PageData pageData, boolean isSuite  

3.         )throws Exception  

4. {  

5.         booleanisTestPage = pageData.hasAttribute("Test");  

6.        if(isTestPage) {  

7.                WikiPagetestPage = pageData.getWikiPage( );  

8.               StringBuffernewPageContent = new StringBuffer( );  

9.                includeSetupPages(testPage,newPageContent, isSuite);  

10.              newPageContent.append(pageData.getContent());  

11.               includeTeardownPages(testPage,newPageContent, isSuite);  

12.              pageData.setContent(newPageContent.toString());  

13.        }  

14.       returnpageData.getHtml( );  

15. }  

而其实,最好应该缩短成如下的样子:

[csharp] view plain copy

 

1. public static StringrenderPageWithSetupsAndTeardowns(  

2.        PageDatapageData, boolean isSuite) throws Exception   

3.  {  

4.        if(isTestPage(pageData))  

5.                includeSetupAndTeardownPages(pageData,isSuite);  

6.        returnpageData.getHtml( );  

7.  }  

[csharp] view plain copy

 

1. public static StringrenderPageWithSetupsAndTeardowns(  

2.        PageDatapageData, boolean isSuite) throws Exception   

3.  {  

4.        if(isTestPage(pageData))  

5.                includeSetupAndTeardownPages(pageData,isSuite);  

6.        returnpageData.getHtml( );  

7.  }  

总之,十行以内是整洁的函数比较合适的长度,若没有特殊情况,我们最好将单个函数控制在十行以内。

 

评论区有一些讨论,也放到正文来吧。

“函数是否应该足够短小,算是《代码整洁之道》中最具争议的议题之一。

书写短小函数的时候,其实我们不要忽略一点,那就是,函数名名称本身就具描述性。短小的函数构成,如果要追根溯源了解内部实现,自然需要一层层找到最终的实现。但若是想大致知道这个函数到底做了什么,结合这个短小函数体内具描述性的一些函数名,应该也就一目了然了。试想,当你眼前的这个函数是几十上百上千行的庞然大物的时候,你能做到一眼就一目了然,将其大概做了什么了然于心吗?函数短小的一方面优点,在这里就体现出来了。

函数应该短小这个议题,仁者见仁智者见智,在实际编码过程中任何人都很难做到严格遵守,但大的方向,若想写出整洁的代码,应该去向短小的函数靠拢,对吧?”

 

 

 

 

2 单一职责

 

函数应该只做一件事情。只做一件事,做好这件事。

设计模式中有单一职责原则,我们可以把这条原则理解为代码整洁之道中的函数单一职责原则。

要判断函数是不是只做了一件事情,还有一个方法,就是看能否再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。

 

 

 

3命名合适且具描述性

 

“如果每个例程都让你感到深合己意,那就是整洁的代码。”要遵循这一原则,大半工作都在于为只做一件事的小函数取个好名字。函数越短小,功能越集中,就越便于取个好名字。

 

别害怕长名称。长而具有描述性的名称,比短而令人费解的名称好。而且长而具有描述性的名称,比描述性的长注释要好。且选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。当然,如果短的名称已经足够说明问题,还是越短越好。

 

命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。比如,includeSetupAndTeardownPages,includeSetupPages,includeSuiteSetupPage, and includeSetupPage等。这些名词使用了类似的措辞,依序讲述一个故事,就是是比较推崇的命名方式了。

 

 

 

4参数尽可能少

 

最理想的函数参数形态是零参数,其次是单参数,再次是双参数,应尽量避免三参数及以上参数的函数,有足够的理由才能用三个以上参数(多参数函数)。

 

函数参数中出现标识符参数是非常不推崇的做法。有标识符参数的函数,很有可能不止在做一件事,标示如果标识符为true将这样做,标识符为false将那样做。正确的做法应该将有标识符参数的函数一分为二,对标识符为true和false分别开一个函数来处理。

 

 

 

5 避免重复

 

重复的代码会导致模块的臃肿,整个模块的可读性可能会应该重复的消除而得到提升。

 

其实可以这样说,重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为控制与消除重复而创建的。仔细想一想,面向对象编程是如何将代码集中到基类,从而避免了冗余的。而面向方面编程(Aspect Oriented Programming)、面向组件编程(ComponentOrientedProgramming)多少也是消除重复的一种策略。这样看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复。

 

重复而啰嗦的代码,乃万恶之源,我们要尽力避免。

 

 

 

 

四、范例

 

 

有必要贴出一段书中推崇的整洁代码作为本次函数书写准则的范例。

[csharp] view plain copy

 

1. using System;  

2.   

3. public class SetupTeardownIncluder  

4. {  

5.      private PageData pageData;  

6.     private boolean isSuite;  

7.      private WikiPage testPage;  

8.     private StringBuffer newPageContent;  

9.      private PageCrawler pageCrawler;  

10.  

11.     public static String render(PageData pageData) throws Exception  

12.    {  

13.         return render(pageData, false);  

14.    }  

15.     public static String render(PageData pageData, boolean isSuite)throws Exception  

16.    {  

17.         return new SetupTeardownIncluder(pageData).render(isSuite);  

18.    }  

19.     private SetupTeardownIncluder(PageData pageData)  

20.    {  

21.         this.pageData = pageData;  

22.        testPage = pageData.getWikiPage();  

23.         pageCrawler = testPage.getPageCrawler();  

24.        newPageContent = new StringBuffer();  

25.     }  

26.  

27.     private String render(boolean isSuite) throws Exception  

28.    {  

29.         this.isSuite = isSuite;  

30.        if (isTestPage())  

31.         includeSetupAndTeardownPages();  

32.        return pageData.getHtml();  

33.     }  

34.  

35.     private boolean isTestPage() throws Exception  

36.    {  

37.         return pageData.hasAttribute("Test");  

38.    }  

39.   

40.    private void includeSetupAndTeardownPages() throws Exception  

41.     {  

42.        includeSetupPages();  

43.         includePageContent();  

44.        includeTeardownPages();  

45.         updatePageContent();  

46.    }  

47.   

48.    private void includeSetupPages() throws Exception  

49.     {  

50.        if (isSuite)  

51.             includeSuiteSetupPage();  

52.        includeSetupPage();  

53.     }  

54.  

55.     private void includeSuiteSetupPage() throws Exception  

56.    {  

57.         include(SuiteResponder.SUITE_SETUP_NAME, "-setup");  

58.    }  

59.   

60.    private void includeSetupPage() throws Exception  

61.     {  

62.        include("SetUp""-setup");  

63.     }  

64.  

65.     private void includePageContent() throws Exception  

66.    {  

67.         newPageContent.append(pageData.getContent());  

68.    }  

69.   

70.    private void includeTeardownPages() throws Exception  

71.     {  

72.        includeTeardownPage();  

73.         if (isSuite)  

74.        includeSuiteTeardownPage();  

75.     }  

76.  

77.     private void includeTeardownPage() throws Exception  

78.    {  

79.         include("TearDown""-teardown");  

80.    }  

81.   

82.    private void includeSuiteTeardownPage() throws Exception  

83.     {  

84.        include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");  

85.     }  

86.  

87.     private void updatePageContent() throws Exception  

88.    {  

89.         pageData.setContent(newPageContent.toString());  

90.    }  

91.   

92.    private void include(String pageName, String arg) throws Exception  

93.     {  

94.        WikiPage inheritedPage = findInheritedPage(pageName);  

95.         if (inheritedPage != null)   

96.        {  

97.             String pagePathName = getPathNameForPage(inheritedPage);  

98.            buildIncludeDirective(pagePathName, arg);  

99.         }  

100.    }  

101.   

102.    private WikiPage findInheritedPage(String pageName) throws Exception  

103.     {  

104.        return PageCrawlerImpl.getInheritedPage(pageName, testPage);  

105.     }  

106.  

107.     private String getPathNameForPage(WikiPage page) throws Exception  

108.    {  

109.         WikiPagePath pagePath = pageCrawler.getFullPath(page);  

110.        return PathParser.render(pagePath);  

111.     }  

112.  

113.     private void buildIncludeDirective(String pagePathName, String arg)  

114.    {  

115.         newPageContent  

116.        .append("\n!include ")  

117.         .append(arg)  

118.        .append(" .")  

119.         .append(pagePathName)  

120.        .append("\n");  

121.     }  

122.}  

[csharp] view plain copy

 

1. using System;  

2.   

3. public class SetupTeardownIncluder  

4. {  

5.      private PageData pageData;  

6.     private boolean isSuite;  

7.      private WikiPage testPage;  

8.     private StringBuffer newPageContent;  

9.      private PageCrawler pageCrawler;  

10.  

11.     public static String render(PageData pageData) throws Exception  

12.    {  

13.         return render(pageData, false);  

14.    }  

15.     public static String render(PageData pageData, boolean isSuite)throws Exception  

16.    {  

17.         return new SetupTeardownIncluder(pageData).render(isSuite);  

18.    }  

19.     private SetupTeardownIncluder(PageData pageData)  

20.    {  

21.         this.pageData = pageData;  

22.        testPage = pageData.getWikiPage();  

23.         pageCrawler = testPage.getPageCrawler();  

24.        newPageContent = new StringBuffer();  

25.     }  

26.  

27.     private String render(boolean isSuite) throws Exception  

28.    {  

29.         this.isSuite = isSuite;  

30.        if (isTestPage())  

31.         includeSetupAndTeardownPages();  

32.        return pageData.getHtml();  

33.     }  

34.  

35.     private boolean isTestPage() throws Exception  

36.    {  

37.         return pageData.hasAttribute("Test");  

38.    }  

39.   

40.    private void includeSetupAndTeardownPages() throws Exception  

41.     {  

42.        includeSetupPages();  

43.         includePageContent();  

44.        includeTeardownPages();  

45.         updatePageContent();  

46.    }  

47.   

48.    private void includeSetupPages() throws Exception  

49.     {  

50.        if (isSuite)  

51.             includeSuiteSetupPage();  

52.        includeSetupPage();  

53.     }  

54.  

55.     private void includeSuiteSetupPage() throws Exception  

56.    {  

57.         include(SuiteResponder.SUITE_SETUP_NAME, "-setup");  

58.    }  

59.   

60.    private void includeSetupPage() throws Exception  

61.     {  

62.        include("SetUp""-setup");  

63.     }  

64.  

65.     private void includePageContent() throws Exception  

66.    {  

67.         newPageContent.append(pageData.getContent());  

68.    }  

69.   

70.    private void includeTeardownPages() throws Exception  

71.     {  

72.        includeTeardownPage();  

73.         if (isSuite)  

74.        includeSuiteTeardownPage();  

75.     }  

76.  

77.     private void includeTeardownPage() throws Exception  

78.    {  

79.         include("TearDown""-teardown");  

80.    }  

81.   

82.    private void includeSuiteTeardownPage() throws Exception  

83.     {  

84.        include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");  

85.     }  

86.  

87.     private void updatePageContent() throws Exception  

88.    {  

89.         pageData.setContent(newPageContent.toString());  

90.    }  

91.   

92.    private void include(String pageName, String arg) throws Exception  

93.     {  

94.        WikiPage inheritedPage = findInheritedPage(pageName);  

95.         if (inheritedPage != null)   

96.        {  

97.             String pagePathName = getPathNameForPage(inheritedPage);  

98.            buildIncludeDirective(pagePathName, arg);  

99.         }  

100.    }  

101.   

102.    private WikiPage findInheritedPage(String pageName) throws Exception  

103.     {  

104.        return PageCrawlerImpl.getInheritedPage(pageName, testPage);  

105.     }  

106.  

107.     private String getPathNameForPage(WikiPage page) throws Exception  

108.    {  

109.         WikiPagePath pagePath = pageCrawler.getFullPath(page);  

110.        return PathParser.render(pagePath);  

111.     }  

112.  

113.     private void buildIncludeDirective(String pagePathName, String arg)  

114.    {  

115.         newPageContent  

116.        .append("\n!include ")  

117.         .append(arg)  

118.        .append(" .")  

119.         .append(pagePathName)  

120.        .append("\n");  

121.     }  

122.}  

上面这段代码,满足了函数书写短小、单一职责、命名合适、参数尽可能少、不重复啰嗦这几条准则。整洁的函数代码大致如此。

 

 

 

 

五、小结

 

 

大师级程序员把系统当作故事来讲,而不是当做程序来写。这是之前已经提到过的一个观点。

 

本文讲述了如何编写良好函数的一些准则,如果你遵循这些准则,函数就会短小,有个好名字,而且被很好的归置。不过永远不要忘记,我们真正的目标在于讲述系统的故事,而你编写的函数必须干净利落的拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。

 

程序员,其实是故事家。

 

 

 

 

六、本文涉及知识点提炼整理

 

 

整洁代码的函数书写,可以遵从如下几个原则:

·        第一原则:短小。若没有特殊情况,最好将单个函数控制在十行以内。

·        第二原则:单一职责。函数应该只做一件事情。只做一件事,做好这件事。

·        第三原则:命名合适且具描述性。长而具有描述性的名称,比短而令人费解的名称好。当然,如果短的名称已经足够说明问题,还是越短越好。

·        第四原则:参数尽可能少。最理想的函数参数形态是零参数,其次是单参数,再次是双参数,应尽量避免三参数及以上参数的函数,有足够的理由才能用三个以上参数。

·        第五原则:尽力避免重复。重复的代码会导致模块的臃肿,整个模块的可读性可能会应该重复的消除而得到提升。


 

这篇文章将与大家一起聊一聊,书写代码过程中一些良好的格式规范。

 

 

 

一、引言

 

 

以下引言的内容,有必要伴随这个系列的每一次更新,这次也不例外。

 

《代码整洁之道》这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

 

但我们知道,很多时候,理想很丰满,现实很骨感,也知道人在江湖,身不由己。因为项目的紧迫性,需求的多样性,我们无法时时刻刻都写出整洁的代码,保持自己输出的都是高质量、优雅的代码。

 

但若我们理解了代码整洁之道的精髓,我们会知道怎样让自己的代码更加优雅、整洁、易读、易扩展,知道真正整洁的代码应该是怎么样的,也许就会渐渐养成持续输出整洁代码的习惯。

 

而且或许你会发现,若你一直保持输出整洁代码的习惯,长期来看,会让你的整体效率和代码质量大大提升。

 

 

 

 

二、本文涉及知识点思维导图

 

 

 

国际惯例,先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。

 

 

 

 

 

 

三、优秀代码的书写格式准则

 

 

 

1 像报纸一样一目了然

 

想想那些阅读量巨大的报纸文章。你从上到下阅读。在顶部,你希望有个头条,告诉你故事主题,好让你决定是否要读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说话及其他细节。

 

优秀的源文件也要像报纸文章一样。名称应当简单并且一目了然,名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。

 

 

 

恰如其分的注释

 

带有少量注释的整洁而有力的代码,比带有大量注释的零碎而复杂的代码更加优秀。

我们知道,注释是为代码服务的,注释的存在大多数原因是为了代码更加易读,但注释并不能美化糟糕的代码。

 

另外,注意一点。注释存在的时间越久,就会离其所描述的代码的意义越远,越来越变得全然错误,因为大多数程序员们不能坚持(或者因为忘了)去维护注释。

 

当然,教学性质的代码,多半是注释越详细越好。

 

 

 

合适的单文件行数

 

尽可能用几百行以内的单文件来构造出出色的系统,因为短文件通常比长文件更易于理解。当然,和之前的一些准则一样,只是提供大方向,并非不可违背。

 

例如,《代码整洁之道》第五章中提到的FitNess系统,就是由大多数为200行、最长500行的单个文件来构造出总长约5万行的出色系统。

 

 

 

4合理地利用空白行

 

古诗中有留白,代码的书写中也要有适当的留白,也就是空白行。

在每个命名空间、类、函数之间,都需用空白行隔开(应该大家在学编程之初,就早有遵守)。这条极其简单的规则极大地影响到了代码的视觉外观。每个空白行都是一条线索,标识出新的独立概念。

其实,在往下读代码时,你会发现你的目光总停留于空白行之后的那一行。用空白行隔开每个命名空间、类、函数,代码的可读性会大大提升。

 

 

 

5让紧密相关的代码相互靠近

 

如果说空白行隔开了概念,那么靠近的代码行则暗示了他们之间的紧密联系。所以,紧密相关的代码应该相互靠近。

 

举个反例(代码段1):

[csharp] view plain copy

 

1. public class ReporterConfig  

2. {  

3.      /** 

4.     * The class name of the reporter listener 

5.     */  

6.     private String m_className;  

7.    

8.     /** 

9.     * The properties of the reporter listener 

10.    */  

11.     private List<Property> m_properties = new ArrayList<Property>();  

12.  

13.     public void addProperty(Property property)  

14.    {  

15.         m_properties.add(property);  

16.    }  

17. }  

[csharp] view plain copy

 

1. public class ReporterConfig  

2. {  

3.      /** 

4.     * The class name of the reporter listener 

5.     */  

6.     private String m_className;  

7.    

8.     /** 

9.     * The properties of the reporter listener 

10.    */  

11.     private List<Property> m_properties = new ArrayList<Property>();  

12.  

13.     public void addProperty(Property property)  

14.    {  

15.         m_properties.add(property);  

16.    }  

17. }  

再看个正面示例(代码段2):

[cpp] view plain copy

 

1. public class ReporterConfig  

2. {  

3.      private String m_className;  

4.     private List<Property> m_properties = new ArrayList<Property>();  

5.    

6.     public void addProperty(Property property)  

7.      {  

8.         m_properties.add(property);  

9.      }  

10.}  

[cpp] view plain copy

 

1. public class ReporterConfig  

2. {  

3.      private String m_className;  

4.     private List<Property> m_properties = new ArrayList<Property>();  

5.    

6.     public void addProperty(Property property)  

7.      {  

8.         m_properties.add(property);  

9.      }  

10.}  

以上这段正面示例(代码段2)比反例(代码段1)中的代码好太多,它正好一览无遗,一眼就能看这个是有两个变量和一个方法的类。而再看看反例,注释简直画蛇添足,隔断了两个实体变量间的联系,我们不得不移动头部和眼球,才能获得相同的理解度。

 

 

 

 

基于关联的代码分布

 

关系密切的概念应该相互靠近。对于那些关系密切、放置于同一源文件中的概念,他们之间的区隔应该成为对相互的易懂度有多重要的衡量标准。应该避免迫使读者在源文件和类中跳来跳去。变量的声明应尽可能靠近其使用位置。

对于大多数短函数,函数中的本地变量应当在函数的顶部出现。例如如下代码中的is变量的声明:

[cpp] view plain copy

 

1. private static void readPreferences()  

2. {  

3.      InputStream is= null;  

4.     try  

5.      {  

6.         is= new FileInputStream(getPreferencesFile());  

7.          setPreferences(new Properties(getPreferences()));  

8.         getPreferences().load(is);  

9.      }  

10.    catch (IOException e)  

11.     {  

12.        DoSomeThing();  

13.     }  

14.}  

[cpp] view plain copy

 

1. private static void readPreferences()  

2. {  

3.      InputStream is= null;  

4.     try  

5.      {  

6.         is= new FileInputStream(getPreferencesFile());  

7.          setPreferences(new Properties(getPreferences()));  

8.         getPreferences().load(is);  

9.      }  

10.    catch (IOException e)  

11.     {  

12.        DoSomeThing();  

13.     }  

14.}  

 

而循环中的控制变量应该总在循环语句中声明,例如如下代码中each变量的声明:

[cpp] view plain copy

 

1. public int countTestCases()  

2. {  

3.      int count = 0;  

4.     for (Test each : tests)  

5.          count += each.countTestCases();  

6.     return count;  

7.  }  

[cpp] view plain copy

 

1. public int countTestCases()  

2. {  

3.      int count = 0;  

4.     for (Test each : tests)  

5.          count += each.countTestCases();  

6.     return count;  

7.  }  

在某些较长的函数中,变量也可能在某代码块的顶部,或在循环之前声明。例如如下代码中tr变量的声明:

[cpp] view plain copy

 

1. ...  

2. for (XmlTest test : m_suite.getTests())   

3.  {  

4.     TestRunner tr = m_runnerFactory.newTestRunner(this, test);  

5.      tr.addListener(m_textReporter);  

6.     m_testRunners.add(tr);  

7.      invoker = tr.getInvoker();  

8.     for (ITestNGMethod m : tr.getBeforeSuiteMethods())   

9.      {  

10.        beforeSuiteMethods.put(m.getMethod(), m);  

11.     }  

12.    for (ITestNGMethod m : tr.getAfterSuiteMethods())  

13.     {  

14.        afterSuiteMethods.put(m.getMethod(), m);  

15.     }  

16.}  

17. ...  

[cpp] view plain copy

 

1. ...  

2. for (XmlTest test : m_suite.getTests())   

3.  {  

4.     TestRunner tr = m_runnerFactory.newTestRunner(this, test);  

5.      tr.addListener(m_textReporter);  

6.     m_testRunners.add(tr);  

7.      invoker = tr.getInvoker();  

8.     for (ITestNGMethod m : tr.getBeforeSuiteMethods())   

9.      {  

10.        beforeSuiteMethods.put(m.getMethod(), m);  

11.     }  

12.    for (ITestNGMethod m : tr.getAfterSuiteMethods())  

13.     {  

14.        afterSuiteMethods.put(m.getMethod(), m);  

15.     }  

16.}  

17. ...  

另外,实体变量应当在类的顶部声明(也有一些流派喜欢将实体变量放到类的底部)。

若某个函数调用了另一个,就应该把它们放到一起,而且调用者应该尽量放到被调用者上面。这样,程序就有自然的顺序。若坚定地遵守这条约定,读者将能够确信函数声明总会在其调用后很快出现。

概念相关的代码应该放到一起。相关性越强,则彼此之间的距离就该越短。

 

这一节的要点整理一下,大致就是:

  • 变量的声明应尽可能靠近其使用位置。
  • 循环中的控制变量应该在循环语句中声明。
  • 短函数中的本地变量应当在函数的顶部声明。
  • 而对于某些长函数,变量也可以在某代码块的顶部,或在循环之前声明。
  • 实体变量应当在类的顶部声明。
  • 若某个函数调用了另一个,就应该把它们放到一起,而且调用者应该尽量放到被调用者上面。
  • 概念相关的代码应该放到一起。相关性越强,则彼此之间的距离就该越短。

 

 

 

7团队遵从同一套代码规范

 

一个好的团队应当约定与遵从一套代码规范,并且每个成员都应当采用此风格。我们希望一个项目中的代码拥有相似甚至相同的风格,像默契无间的团队所完成的艺术品,而不是像一大票意见相左的个人所堆砌起来的残次品。

 

定制一套编码与格式风格不需要太多时间,但对整个团队代码风格统一性的提升,却是立竿见影的。

 

记住,好的软件系统是由一系列风格一致的代码文件组成的。尽量不要用各种不同的风格来构成一个项目的各个部分,这样会增加项目本身的复杂度与混乱程度。

 

 

 

 

 

四、范例代码

 

 

 

和上篇文章一样,有必要贴出一段书中推崇的整洁代码作为本次代码书写格式的范例。书中的这段代码采用Java语言,但丝毫不影响使用C++和C#的朋友们阅读。

[cpp] view plain copy

 

1. public class CodeAnalyzer implements JavaFileAnalysis  

2. {  

3.      private int lineCount;  

4.     private int maxLineWidth;  

5.      private int widestLineNumber;  

6.     private LineWidthHistogram lineWidthHistogram;  

7.      private int totalChars;  

8.   

9.      public CodeAnalyzer()  

10.    {  

11.         lineWidthHistogram = new LineWidthHistogram();  

12.    }  

13.   

14.    public static List<File> findJavaFiles(File parentDirectory)  

15.     {  

16.        List<File> files = new ArrayList<File>();  

17.         findJavaFiles(parentDirectory, files);  

18.        return files;  

19.     }  

20.  

21.     private static void findJavaFiles(File parentDirectory, List<File> files)  

22.    {  

23.         for (File file : parentDirectory.listFiles())  

24.        {  

25.             if (file.getName().endsWith(".java"))  

26.                files.add(file);  

27.             else if (file.isDirectory())  

28.                findJavaFiles(file, files);  

29.         }  

30.    }  

31.   

32.    public void analyzeFile(File javaFile) throws Exception  

33.     {  

34.        BufferedReader br = new BufferedReader(new FileReader(javaFile));  

35.         String line;  

36.        while ((line = br.readLine()) != null)  

37.         measureLine(line);  

38.    }  

39.   

40.    private void measureLine(String line)  

41.     {  

42.        lineCount++;  

43.         int lineSize = line.length();  

44.        totalChars += lineSize;  

45.         lineWidthHistogram.addLine(lineSize, lineCount);  

46.        recordWidestLine(lineSize);  

47.     }  

48.  

49.     private void recordWidestLine(int lineSize)  

50.    {  

51.         if (lineSize > maxLineWidth)  

52.        {  

53.             maxLineWidth = lineSize;  

54.            widestLineNumber = lineCount;  

55.         }  

56.    }  

57.   

58.    public int getLineCount()  

59.     {  

60.        return lineCount;  

61.     }  

62.  

63.     public int getMaxLineWidth()  

64.    {  

65.         return maxLineWidth;  

66.    }  

67.   

68.    public int getWidestLineNumber()  

69.     {  

70.        return widestLineNumber;  

71.     }  

72.  

73.     public LineWidthHistogram getLineWidthHistogram()  

74.    {  

75.         return lineWidthHistogram;  

76.    }  

77.   

78.    public double getMeanLineWidth()  

79.     {  

80.        return (double)totalChars / lineCount;  

81.     }  

82.  

83.     public int getMedianLineWidth()  

84.    {  

85.         Integer[] sortedWidths = getSortedWidths();  

86.        int cumulativeLineCount = 0;  

87.         for (int width : sortedWidths)  

88.        {  

89.             cumulativeLineCount += lineCountForWidth(width);  

90.            if (cumulativeLineCount > lineCount / 2)  

91.                 return width;  

92.        }  

93.         throw new Error("Cannot get here");  

94.    }  

95.   

96.    private int lineCountForWidth(int width)  

97.     {  

98.        return lineWidthHistogram.getLinesforWidth(width).size();  

99.     }  

100.  

101.     private Integer[] getSortedWidths()  

102.    {  

103.         Set<Integer> widths = lineWidthHistogram.getWidths();  

104.        Integer[] sortedWidths = (widths.toArray(new Integer[0]));  

105.         Arrays.sort(sortedWidths);  

106.        return sortedWidths;  

107.     }  

108.}  

[cpp] view plain copy

 

1. public class CodeAnalyzer implements JavaFileAnalysis  

2. {  

3.      private int lineCount;  

4.     private int maxLineWidth;  

5.      private int widestLineNumber;  

6.     private LineWidthHistogram lineWidthHistogram;  

7.      private int totalChars;  

8.   

9.      public CodeAnalyzer()  

10.    {  

11.         lineWidthHistogram = new LineWidthHistogram();  

12.    }  

13.   

14.    public static List<File> findJavaFiles(File parentDirectory)  

15.     {  

16.        List<File> files = new ArrayList<File>();  

17.         findJavaFiles(parentDirectory, files);  

18.        return files;  

19.     }  

20.  

21.     private static void findJavaFiles(File parentDirectory, List<File> files)  

22.    {  

23.         for (File file : parentDirectory.listFiles())  

24.        {  

25.             if (file.getName().endsWith(".java"))  

26.                files.add(file);  

27.             else if (file.isDirectory())  

28.                findJavaFiles(file, files);  

29.         }  

30.    }  

31.   

32.    public void analyzeFile(File javaFile) throws Exception  

33.     {  

34.        BufferedReader br = new BufferedReader(new FileReader(javaFile));  

35.         String line;  

36.        while ((line = br.readLine()) != null)  

37.         measureLine(line);  

38.    }  

39.   

40.    private void measureLine(String line)  

41.     {  

42.        lineCount++;  

43.         int lineSize = line.length();  

44.        totalChars += lineSize;  

45.         lineWidthHistogram.addLine(lineSize, lineCount);  

46.        recordWidestLine(lineSize);  

47.     }  

48.  

49.     private void recordWidestLine(int lineSize)  

50.    {  

51.         if (lineSize > maxLineWidth)  

52.        {  

53.             maxLineWidth = lineSize;  

54.            widestLineNumber = lineCount;  

55.         }  

56.    }  

57.   

58.    public int getLineCount()  

59.     {  

60.        return lineCount;  

61.     }  

62.  

63.     public int getMaxLineWidth()  

64.    {  

65.         return maxLineWidth;  

66.    }  

67.   

68.    public int getWidestLineNumber()  

69.     {  

70.        return widestLineNumber;  

71.     }  

72.  

73.     public LineWidthHistogram getLineWidthHistogram()  

74.    {  

75.         return lineWidthHistogram;  

76.    }  

77.   

78.    public double getMeanLineWidth()  

79.     {  

80.        return (double)totalChars / lineCount;  

81.     }  

82.  

83.     public int getMedianLineWidth()  

84.    {  

85.         Integer[] sortedWidths = getSortedWidths();  

86.        int cumulativeLineCount = 0;  

87.         for (int width : sortedWidths)  

88.        {  

89.             cumulativeLineCount += lineCountForWidth(width);  

90.            if (cumulativeLineCount > lineCount / 2)  

91.                 return width;  

92.        }  

93.         throw new Error("Cannot get here");  

94.    }  

95.   

96.    private int lineCountForWidth(int width)  

97.     {  

98.        return lineWidthHistogram.getLinesforWidth(width).size();  

99.     }  

100.  

101.     private Integer[] getSortedWidths()  

102.    {  

103.         Set<Integer> widths = lineWidthHistogram.getWidths();  

104.        Integer[] sortedWidths = (widths.toArray(new Integer[0]));  

105.         Arrays.sort(sortedWidths);  

106.        return sortedWidths;  

107.     }  

108.}  




 

 

 

 

五、小结:让代码不仅仅是能工作

 

 

代码的格式关乎沟通,而沟通是专业开发者的头等大事,所以良好代码的格式至关重要。

 

或许之前我们认为“让代码能工作”才是专业开发者的头等大事。然而,《代码整洁之道》这本书,希望我们能抛弃这个观点。

 

让代码能工作确实是代码存在的首要意义,但作为一名有追求的程序员,请你想一想,今天你编写的功能,极可能在下一版中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。原始代码修改之后很久,其代码风格和可读性仍会影响到可维护性和扩展性。即便代码已不复存在,你的风格和律条仍影响深远。

 

“当有人在阅读我们的代码时,我们希望他们能为其整洁性、一致性和优秀的细节处理而震惊。我们希望他们高高扬起眉毛,一路看下去,希望他们感受能到那些为之劳作的专业人士们的优秀职业素养。但若他们看到的只是一堆由酒醉的水手写出的鬼画符,那他们多半会得出结论——这个项目的其他部分应该也是混乱不堪的。”

 

所以,各位,在开发过程中请不要仅仅是停留在“让代码可以工作”的层面,而更要注重自身输出代码的可维护性与扩展性。请做一个更有追求的程序员。



 

 

 

六、本文涉及知识点提炼整理

 

 

整洁代码的书写格式,可以遵从如下几个原则:

 

第一原则:像报纸一样一目了然。优秀的源文件也要像报纸文章一样,名称应当简单并且一目了然,名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。

第二原则:恰如其分的注释。带有少量注释的整洁而有力的代码,比带有大量注释的零碎而复杂的代码更加优秀。

第三原则:合适的单文件行数。尽可能用几百行以内的单文件来构造出出色的系统,因为短文件通常比长文件更易于理解。

第四原则:合理地利用空白行。在每个命名空间、类、函数之间,都需用空白行隔开。

第五原则:让紧密相关的代码相互靠近。靠近的代码行暗示着他们之间的紧密联系。所以,紧密相关的代码应该相互靠近。

第六原则:基于关联的代码分布。

  • 变量的声明应尽可能靠近其使用位置。
  • 循环中的控制变量应该在循环语句中声明。
  • 短函数中的本地变量应当在函数的顶部声明。
  • 对于某些长函数,变量也可以在某代码块的顶部,或在循环之前声明。
  • 实体变量应当在类的顶部声明。
  • 若某个函数调用了另一个,就应该把它们放到一起,而且调用者应该尽量放到被调用者上面。
  • 概念相关的代码应该放到一起。相关性越强,则彼此之间的距离就该越短。

 

第七原则:团队遵从同一套代码规范。一个好的团队应当约定与遵从一套代码规范,并且每个成员都应当采用此风格。


 

这篇文章将与大家一起聊一聊,书写整洁类的一些法则。

 

 

 

一、引言

 

 

以下引言的内容,有必要伴随这个系列的每一次更新,这次也不例外。

 

《代码整洁之道》这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

 

但我们知道,很多时候,理想很丰满,现实很骨感,也知道人在江湖,身不由己。因为项目的紧迫性,需求的多样性,我们无法时时刻刻都写出整洁的代码,保持自己输出的都是高质量、优雅的代码。

 

但若我们理解了代码整洁之道的精髓,我们会知道怎样让自己的代码更加优雅、整洁、易读、易扩展,知道真正整洁的代码应该是怎么样的,也许就会渐渐养成持续输出整洁代码的习惯。

 

而且或许你会发现,若你一直保持输出整洁代码的习惯,长期来看,会让你的整体效率和代码质量大大提升。

 

 

 

 

二、本文涉及知识点思维导图

 

 

国际惯例,先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。

 

 

 

 

 

三、整洁类的书写准则

 

 

 

1合理地分布类中的代码

 

一般情况下,我们遵循变量列表在前,函数在后的原则。

类应该从一组变量列表开始。若有公有静态常量,应该最先出现,然后是私有静态变量,以及公有变量,私有变量。尽可能少的出现公有变量。

公共函数应该出现在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧跟在公共函数后面。

这样是符合自定向下的原则,让程序读起来像一篇报纸文章。

 

2尽可能保持类的封装

 

我们喜欢保持变量和工具函数的私有性,但不执著于此。有时,我们需要用到protected变量或者工具,比如让测试可以访问到。然而,我们会尽可能使函数或变量保持私有,不对外暴露太多细节。放松封装,总是下策。

 

 

3类应该短小

 

正如之前关于函数书写的论调。类的一条规则是短小,第二条规则还是要短小。

和函数一样,马上有个问题要出现,那就是,多小合适呢?

对于函数,我们通过计算代码行数来衡量大小,对于类,我们采用不同的衡量方法,那就是权责(responsibility)。

 

3.1单一权责原则

 

单一权责(Single Responsibility Principle,SRP)认为,类或模块应有且只有一条加以修改的理由。

举个栗子,下面这个类足够短小了吗?

[cpp] view plain copy

 

1. public class SuperDashboard extends JFrameimplements MetaDataUser  

2. {  

3.      public Component getLastFocusedComponent()  

4.     public void setLastFocused(Component lastFocused)  

5.      public int getMajorVersionNumber()  

6.     public int getMinorVersionNumber()  

7.      public int getBuildNumber()  

8. }  

[cpp] view plain copy

 

1. public class SuperDashboard extends JFrameimplements MetaDataUser  

2. {  

3.      public Component getLastFocusedComponent()  

4.     public void setLastFocused(Component lastFocused)  

5.      public int getMajorVersionNumber()  

6.     public int getMinorVersionNumber()  

7.      public int getBuildNumber()  

8. }  

答案是否定的,这个类不够“短小”。5个方法不算多,但是这个类虽方法少,但还是拥有太多权责。这个貌似很小的SuperDashboard类,却有两条关联度并不大的加以修改的理由:

第一, 它跟踪会随着软件每次发布而更新的版本信息(含有getMajorVersionNumber等方法)。

第二,它还在管理组件(含有getLastFocusedComponent方法)。

其实,鉴别权责(修改的理由)常常帮助我们在代码中认识到并创建出更好的抽象。

我们可以轻易地将SuperDashboard拆解成名为Version的类中,而这个名为Version的类,极可能在其他应用程序中得到复用:

[cpp] view plain copy

 

1. public class Version  

2. {  

3.      public int getMajorVersionNumber()  

4.     public int getMinorVersionNumber()  

5.      public int getBuildNumber()  

6. }  

[cpp] view plain copy

 

1. public class Version  

2. {  

3.      public int getMajorVersionNumber()  

4.     public int getMinorVersionNumber()  

5.      public int getBuildNumber()  

6. }  

这样,这个类就大致做到了单一权责。

 

 

4合理提高类的内聚性

 

我们希望类的内聚性保持在较高的水平。

何为类的内聚性?类的内聚性就是类中变量与方法之间的依赖关系。类中方法操作的变量越多,就越黏聚到类上,就代表类的内聚性高。

类应该只有少量的实体变量,类中的每个方法都应该操作一个或者多个这种变量。通常而言,如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。一般来说,创建这种极大化的内聚类不可取,也不可能。

我们只希望内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。

举个高内聚的例子:

[cpp] view plain copy

 

1. public class Stack   

2. {  

3.      private int topOfStack = 0;  

4.     List<Integer> elements = new LinkedList<Integer>();  

5.    

6.     public int size()   

7.      {  

8.         return topOfStack;  

9.      }  

10.  

11.     public void push(int element)  

12.    {  

13.         topOfStack++;  

14.        elements.add(element);  

15.     }  

16.  

17.     public int pop() throws PoppedWhenEmpty   

18.    {  

19.         if (topOfStack == 0)  

20.            throw new PoppedWhenEmpty();  

21.         int element = elements.get(--topOfStack);  

22.        elements.remove(topOfStack);  

23.             return element;  

24.    }  

25. }  

[cpp] view plain copy

 

1. public class Stack   

2. {  

3.      private int topOfStack = 0;  

4.     List<Integer> elements = new LinkedList<Integer>();  

5.    

6.     public int size()   

7.      {  

8.         return topOfStack;  

9.      }  

10.  

11.     public void push(int element)  

12.    {  

13.         topOfStack++;  

14.        elements.add(element);  

15.     }  

16.  

17.     public int pop() throws PoppedWhenEmpty   

18.    {  

19.         if (topOfStack == 0)  

20.            throw new PoppedWhenEmpty();  

21.         int element = elements.get(--topOfStack);  

22.        elements.remove(topOfStack);  

23.             return element;  

24.    }  

25. }  

这个类非常内聚,在三个方法中,仅有size()方法没有使用所有的两个变量。

注意,保持函数和参数短小的策略,有时候会导致为一组子集方法所用的实体变量增加。我们应该尝试将这些方法拆分到两个或者多个类中,让新的类更为内聚。

 

 

 

5有效地隔离修改

 

需求会改变,所以代码也会改变。在面向对象入门知识中我们学习到,具体类包含实现细节(代码),而抽象类则呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。

 

举个栗子,在一个设计场景下,我们以其设计直接依赖于TokyoStockExchange的Protfolio类,不如创建StockExchange接口,里面只声明一个方法:

[cpp] view plain copy

 

1. public interface StockExchange  

2. {  

3.      MoneycurrentPrice(String symbol);  

4. }  

[cpp] view plain copy

 

1. public interface StockExchange  

2. {  

3.      MoneycurrentPrice(String symbol);  

4. }  

接着设计TokyoStockExchange类来实现这个接口:

[cpp] view plain copy

 

1. public class TokyoStockExchange extends StockExchange  

2. {  

3.         //…  

4. }  

[cpp] view plain copy

 

1. public class TokyoStockExchange extends StockExchange  

2. {  

3.         //…  

4. }  

我们还要确保Portfolio的构造器接受作为参数StickExchange引用:

[cpp] view plain copy

 

1. public Portfolio  

2. {  

3.      private StockExchange exchange;  

4.     public Portfolio(StockExchange exchange)  

5.      {  

6.         this.exchange = exchange;  

7.      }  

8.     // ...  

9.  }  

[cpp] view plain copy

 

1. public Portfolio  

2. {  

3.      private StockExchange exchange;  

4.     public Portfolio(StockExchange exchange)  

5.      {  

6.         this.exchange = exchange;  

7.      }  

8.     // ...  

9.  }  

那么现在就可以为StockExchange接口创建可以测试的实现了,例如返回固定的股票现值。比如测试购买5股微软股票,我们下面的实现代码返回100美元的现值,然后再实现一个总投资价值为500美元的测试,那么大概代码则是:

[cpp] view plain copy

 

1. public class PortfolioTest  

2. {  

3.      privateFixedStockExchangeStub exchange;  

4.     privatePortfolio portfolio;  

5.     

6.     @Before  

7.      protected void setUp() throws Exception  

8.     {  

9.          exchange = new FixedStockExchangeStub();  

10.        exchange.fix("MSFT", 100);  

11.         portfolio = new Portfolio(exchange);  

12.    }  

13.    

14.    @Test  

15.     public void GivenFiveMSFTTotalShouldBe500() throws Exception  

16.    {  

17.         portfolio.add(5, "MSFT");  

18.        Assert.assertEquals(500,portfolio.value());  

19.     }  

20.}  

[cpp] view plain copy

 

1. public class PortfolioTest  

2. {  

3.      privateFixedStockExchangeStub exchange;  

4.     privatePortfolio portfolio;  

5.     

6.     @Before  

7.      protected void setUp() throws Exception  

8.     {  

9.          exchange = new FixedStockExchangeStub();  

10.        exchange.fix("MSFT", 100);  

11.         portfolio = new Portfolio(exchange);  

12.    }  

13.    

14.    @Test  

15.     public void GivenFiveMSFTTotalShouldBe500() throws Exception  

16.    {  

17.         portfolio.add(5, "MSFT");  

18.        Assert.assertEquals(500,portfolio.value());  

19.     }  

20.}  

如果系统解耦到足以这样测试的程度,也就更加灵活,更加可复用。部件之间的解耦代表着系统中的元素相互隔离得很好。隔离也让对系统每个元素的理解变得更加容易。

 

我们的Portfolio类不再是依赖于TokyoStockExchange类的实现细节,而是依赖于StockExchange接口这个抽象的概念,这样就隔离了特定的细节。而其实我们的类就遵循了另一条类的设计原则,依赖倒置原则(Dependency Inversion Principle , DIP),因为依赖倒置原则的本质,实际上就是认为类应该依赖于抽象,而不是依赖于具体细节。

 

 

 

 

四、一些思考与总结

 

 

让软件能够保持工作和让软件代码整洁,是两种截然不同的工作。我们中大多数人脑力有限,只能更多把更多精力放在让代码能够工作上,而不是放在保持代码有组织和整洁上。

问题是太多人在程序能够正常工作时就以为万事大吉了。我们没能把思维转向有关代码组织和整洁的部分,我们只是一直在做新的需求,而不是回头将臃肿的类切分为只有单一权责的去耦式单元。

 

与此同时,许多开发者害怕数量巨大的短小单一目的的类会导致难以一目了然抓住全局。他们认为,要搞清楚一件较大的工作如果完成,就得在类与类之间找来找去。其实,有大量短小的类的系统并不比有少量庞大类的系统更难掌控。问题是:你是想把工具归置于有许多抽屉、每个抽屉中装有定义和标记的良好组件的工具箱中呢,还是想要少数几个能随便把所有东西都扔进去的抽屉呢?大概我们都更趋向于选择前者。

 

每个达到一定规模的系统都包含大量逻辑和复杂性。管理这种复杂性的首要目标就是加以组织,以便开发者能知道在哪里找到需要的内容,专注于当下工作直接相关的具体模块。反之,拥有巨大、多目的类的系统,总是让我们在目前并不需要了解的一大堆东西中艰难跋涉。

 

最终再强调一下:系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

 

 

 

 

 

五、本文涉及知识点提炼整理

 

 

原则一:合理地分布类中的代码。 类中代码的分布顺序大致是:

1.公有静态常量

2.私有静态变量

3.公有普通变量

4.私有普通变量

5.公共函数

6.私有函数

 

原则二:尽可能地保持类的封装。尽可能使函数或变量保持私有,不对外暴露太多细节。

原则三:类应该短小,尽量保持单一权责原则。类或模块应有且只有一条加以修改的理由。

原则四:合理提高类的内聚性。我们希望类的内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。

原则五:有效地隔离修改。类应该依赖于抽象,而不是依赖于具体细节。尽量对设计解耦,做好系统中的元素的相互隔离,做到更加灵活与可复用。

 

 

本文就此结束。

至此,《代码整洁之道》的精读与演绎系列文章,已经完结。


0 0
原创粉丝点击