目录

一、有意义的命名

1.名副其实、见名知意

2.避免误导

(1)应当避免使用与本意相悖的词

(2)提防使用不同之处较小的名称

3.做有意义的区分

4.使用读得出来的名称

5.使用可搜索的名称

6.避免使用编码

(1)匈牙利语标记法

(2)成员前缀

(3)接口和实现

7.避免思维映射

8.类名

9.方法名

10.每个概念对应一个词

11.别用双关语

12.使用解决方案领域名称

13.使用源自所涉问题领域的名称

14.添加有意义的语境

15.不要添加没用的语境

16.最后的话

二、函数

1.短小

2.只做一件事

3.每个函数一个抽象层级

(1)确保函数只做一件事

(2)自顶向下读代码:向下规则

4.switch 语句

5.使用描述性的名称

(1)别害怕长名称

(2)别害怕花时间取名字

(3)命名方式要保持一致

6.函数参数

7.一元函数的普遍形式

8.标识参数

9.二元函数

10.三元函数

11.参数对象

12.参数列表

13.动词与关键字

14.无副作用

15.分隔指令与询问

16.使用异常替代返回错误码

(1)抽离 Try/Catch 代码块

(2)错误处理就是一件事。

(3)Error.java 依赖磁铁

17.别重复自己

18.结构化编程

19.如何写出这样的函数

20.小结


一、有意义的命名


软件中随处可见命名。我们给变量、函数、参数、类和包命名。我们给源代码及源代码所在目录命名。这么多命名要做,不妨做好它。


1.名副其实、见名知意


(1)变量名不要太随意,haha、list1、ok、theList 这些都没啥意义。
(2)如果名称需要注释来补充,那就不算是名副其实。


2.避免误导


(1)应当避免使用与本意相悖的词

包含List、import、java等类名、关键字或特殊字;字母o与数字0,字母l与数字1等。


(2)提防使用不同之处较小的名称

比如:XYZControllerForEfficientHandlingOfStringsXYZControllerForEfficientStorageOfStrings。


3.做有意义的区分


反面教材,变量名:a1、a2、a3。
避免冗余,不要出现Variable、表字段中避免出现table、字符串避免出现nameString,直接name就行,知道是字符串类型。
再比如:定义了两个类:Customer类和CustomerObject类,如何区分?

              定义了三个方法:getActiveAccount()、getActiveAccounts()、getActiveAccountInfo(),如何区分?
(1)如果名称必须相异,那其意思也应该不同才对。
(2)要区分名称,就要以读者能鉴别不同之处的方式来区分。


4.使用读得出来的名称


不要使用自己拼凑出来的单词,比如:xsxm(学生姓名);genymdhms(生成日期,年、月、日、时、分、秒)。
所谓的驼峰命名法,尽量使用完整的单词。如果名称读不出来,讨论的时候就会像个傻鸟。
这不是小事,因为编程本就是一种社会活动。命名应当为恰当的英语词,而非傻乎乎的自造词。


5.使用可搜索的名称


一些常量,最好不直接使用数字,而指定一个变量名,这个变量名可以便于搜索到。比如:找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦了。
长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。
若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。


6.避免使用编码


(1)匈牙利语标记法


即变量名表明该变量数据类型的小写字母开始。例如,szCmdLine的前缀sz表示“以零结束的字符串”。


(2)成员前缀


避免使用前缀,但是Android中一个比较好的喜欢用m表示私有等,个人感觉比较好


(3)接口和实现


作者不喜欢把接口使用大写字母I来开头,实现也希望只是在后面添加Imp。
编码已经太多,无谓再自找麻烦。把类型或作用域编进名称里面,徒然增加了解码的负担。带编码的名称通常也不便发音,容易打错。


7.避免思维映射


不应当让读者在脑中把你的名称翻译为他们熟知的名称。
比如传统上惯用单字母名称做循环计数器。所以就不要给一些非计数器的变量命名为:i、j、k等。


8.类名


类名与对象名应该是名词与名词短语。
如Customer、WikiPage、Account和AddressParser。避免使用Data或Info这样的类名。
不能使动词。比如:Manage、Process


9.方法名


方法名应当是动词或动词短语。如postPayment、deletePage或save


10.每个概念对应一个词


项目中同时出现controllers与managers,为什么不统一使用其中一种?对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。
函数名称应当独一无二,而且要保持一致,这样你才能不借助多余的浏览就找到正确的方法。


11.别用双关语


避免将同一单词用于不同目的。例如有时可能使用add并不合适,比例insert、append。add表示完整的新添加的含义。

    
12.使用解决方案领域名称


只有程序员才会读你的代码,所以,依据问题所涉领域来命名可不算是聪明的做法,程序员要做太多技术性工作,给这些事取个技术性的名称,通常是最靠谱的做法,所以尽量用那些计算机科学术语、算法名、模式名、数学术语。


13.使用源自所涉问题领域的名称


如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。
至少,负责维护代码的程序员就能去请教领域专家了。
优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。


14.添加有意义的语境


很少有名称是能自我说明的—多数都不能。反之,你需要用有良好命名的类、函数或名称空
间来放置名称,给读者提供语境,可以把相关的变量放到一个类中,使用这个类来表明语境。
如果没这么做,给名称添加前缀就是最后一招了。


15.不要添加没用的语境


名字中带有项目的缩写,这样完全没有必要。比如有一个名为“加油站豪华版”(Gas Station Deluxe)的项目,在其中给每个类添加GSD前缀就不是什么好策略。
只要短名称足够清楚,就要比长名称好,别给名称添加不必要的语境,命名的要点是精确。


16.最后的话


取好名字最难的地方在于需要良好的描述技巧和共有文化背景。与其说这是一种技术、商业或管理问题,还不如说是一种教学问题。其结果是,这个领域内的许多人都没能学会做得很好。我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改得更好,那大家真的会感激你。多数时候我们并不记忆类名和方法名。我们使用现代工具对付这些细节,好让自己集中精力于把代码写得就像词句篇章、至少像是表和数据结构(词句并非总是呈现数据的最佳手段)。改名可能会让某人吃惊,就像你做到其他代码改善工作一样。别让这种事阻碍你的前进步伐。
不妨试试上面这些规则,看你的代码可读性是否有所提升。如果你是在维护别人写的代码,使用重构工具来解决问题。效果立竿见影,而且会持续下去。


二、函数


1.短小


函数的第一规则是要短小。第二条规则是还要更短小。我无法证明这个断言。我给不出任何证实了小函数更好的研究结果。经验告诉我,函数就该小。
每个函数都一目了然。每个函数都只说一件事。而且,每个函数都依序把你带到下一个函数。这就是函数应该达到的短小程度!
例如if 语句、else 语句、while 语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。
这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。
这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。
当然,这样的函数易于阅读和理解。


2.只做一件事


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


3.每个函数一个抽象层级


(1)确保函数只做一件事


要确保函数只做一件事,函数中的语句都要在同一抽象层级上。
函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。
更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。


(2)自顶向下读代码:向下规则


我们想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能偱抽象层级向下阅读了。我把这叫做向下规则。
换一种说法。我们想要这样读程序:程序就像是一系列 TO 起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续 TO 起头段落。


4.switch 语句


写出短小的 switch 语句很难。即便是只有两种条件的 switch 语句也要比我想要的单个代码块或函数大得多。写出只做一件事的 switch 语句也很难。Switch 天生要做 N 件事。不幸我们总无法避开 switch 语句,不过还是能够确保每个 switch 都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。


5.使用描述性的名称


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


(1)别害怕长名称


长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。


(2)别害怕花时间取名字


你当尝试不同的名称,实测其阅读效果。在 Eclipse 或 IntelliJ 等现代 IDE 中改名称易如反掌。
使用这些 IDE 测试不同名称,直至找到最具有描述性的那一个为止。
选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。


(3)命名方式要保持一致


使用与模块名一脉相承的短语、名词和动词给函数命名。


6.函数参数


最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)—所以无论如何也不要这么做。参数不易对付。它们带有太多概念性。所以我在代码范例中几乎不加参数。参数与函数名处在不同的抽象层级,它要求你了解目前并不特别重要的细节。从测试的角度看,参数甚至更叫人为难。想想看,要编写能确保参数的各种组合运行正常的测试用例,是多么困难的事。如果没有参数,就是小菜一碟。如果只有一个参数,也不太困难。有两个参数,问题就麻烦多了。如果参数多于两个,测试覆盖所有可能值的组合简直让人生畏。输出参数比输入参数还要难以理解。读函数时,我们惯于认为信息通过参数输入函数,通过返回值从函数中输出。我们不太期望信息通过参数输出。所以,输出参数往往让人苦思之后才恍然大悟。相较于没有参数,只有一个输入参数算是第二好的做法。也相当易于理解。


7.一元函数的普遍形式


向函数传入单个参数有两种极普遍的理由。
一种是是操作该参数,将其转换为其他什么东西,再输出之。
还有一种虽不那么普遍但仍极有用的单参数函数形式,那就是事件(event)。在这种形式中,
有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,例如 void passwordAttemptFailedNtimes(int attempts)。
小心使用这种形式。应该让读者很清楚地了解它是个事件。谨慎地选用名称和上下文语境。


8.标识参数


标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。如果标识为 true 将会这样做,标识为 false 则会那样做!


9.二元函数


有两个参数的函数要比一元函数难懂。当然,有些时候两个参数正好。
二元函数不算恶劣,而且你当然也会编写二元函数。不过,你得小心,使用二元函数要付出代价。你应该尽量利用一些机制将其转换成一元函数。


10.三元函数


有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问题都会加倍体现。建议你在写三元函数前一定要想清楚。

另一方面,这里有个并不那么险恶的三元函数:assertEquals(1.0, amount, .001)。虽然也要费点神,还是值得的。得到“浮点值的等值是相对而言”的提示总是好的。


11.参数对象


如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参数被共同传递,往往就是该有自己名称的某个概念的一部分。


12.参数列表


有时,我们想要向函数传入数量可变的参数。例如,String.format 方法:String.format("%s worked %.2f hours.", name, hours);

有可变参数的函数可能是一元、二元甚至三元。超过这个数量就可能要犯错了。


13.动词与关键字


给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。
把参数的名称编码成函数名,这大大减轻了记忆参数顺序的负担。


14.无副作用


副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。时序性耦合令人迷惑,特别是当它躲在副作用后面时。如果一定要时序性耦合,就应该在函数名称中说明。


15.分隔指令与询问


函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。


16.使用异常替代返回错误码


从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在 if 语句判断中把指令当作表达式使用。
if (deletePage(page) == E_OK)这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。
另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。


(1)抽离 Try/Catch 代码块


Try/catch 代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把 try 和 catch 代码块的主体部分抽离出来,另外形成函数。
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
} }
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
在上例中,delete 函数只与错误处理有关。很容易理解然后就忽略掉。deletePageAndAllReference 函数只与完全删除一个 page 有关。
错误处理可以忽略掉。有了这样美妙的区隔,代码就更易于理解和修改了。


(2)错误处理就是一件事。


函数应该只做一件事,因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字 try 在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally 代码块后面也不该有其他内容。


(3)Error.java 依赖磁铁


返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们就复用旧的错误码,而不添加新的。使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。


17.别重复自己


重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。


18.结构化编程


每个函数,函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个 return 语句,循环中不能有 break 或 continue 语句,而且永永远远不能有任何 goto 语句。我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益不大。只有在大函数中,这些规则才会有明显的好处。所以,只要函数保持短小,偶尔出现的 return、break 或 continue 语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,goto 只在大函数中才有道理,所以应该尽量避免使用。


19.如何写出这样的函数


写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。
最后,遵循以上列出的规则,我组装好这些函数。我并不从一开始就按照规则写函数。我想没人做得到。


20.小结


每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设计来描述那个系统的。函数是语言的动词,类是名词。
这并非是退回到那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可怕的旧观念。其实这是个历史更久的真理。
编程艺术是且一直就是语言设计的艺术。大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。
那种领域特定语言的一个部分,就是描述在系统中发生的各种行为的函数层级。
在一种狡猾的递归操作中,这些行为使用它们定义的与领域紧密相关的语言讲述自己那个小故事。
如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。
不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐