【前端阅读】——《代码整洁之道》摘记之整洁代码、命名、函数、注释
这本书提出一种观念:代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。(作者认为书可以有另一个名字:《如何在意代码》)
读这本书,促使我思考代码中何谓正确,何谓错误。更重要的是,它还可以促使自己重新评估自己的专业价值观,以及对自己技艺的承诺。
1、整洁编程
- 混乱风险:制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限,赶上期限的唯一方法——做得快的唯一方法——就是始终尽可能的保持代码整洁。
- 代码感:写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。这种“代码感”就是关键所在。缺乏“代码感”的程序员,看混乱是混乱,无处着手。有“代码感”的程序员能从混乱中看出其他的可能与变化。“代码感”帮助程序员选出最好的方案,并指导程序员制订修改行动计划,按图索骥。
- “整洁的代码只做好一件事”——Bjarne Stroustrup(C++语言发明者)。软件设计的许多原则最终都会归结为这句警语。糟糕的代码想做太多事,它意图混乱、目的含混。整洁的代码力求集中。每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染。
整洁的代码应可由作者之外的开发者阅读和增补。它应当有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。——Dave Thomes(OTI公司创始人,Eclipse战略教父)
- 贝克的简单代码规则:消除重复并提高表达力,提早构建简单抽象
- 能通过所有测试
- 没有重复代码
- 体现系统中的全部设计理念
- 包括尽量少的实体,比如类、方法、函数等
2、有意义的命名
- 名副其实
- 避免误导
- 做有意义的区分
- 使用读的出来的名称
- 使用可搜索的名称
- 单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。
- 避免使用编码
- 不必用m_前缀来标明成员变量。应当把类和函数做的足够小,消除对成员前缀的需要。(人们会很快学会无视前缀或后缀,只看到名称中有意义的部分。代码读的越多,眼中就越没有前缀。最终,前缀变作了不入法眼的废料,变作了旧代码的标志物)
- 避免思维映射
- 单字母变量名就是个问题。在多数除循环计数器之外的其他情况下,单字母名称不是个好选择,读者必须在脑中将它映射为真实概念。(仅仅是因为有了a和b,就要取名为c,实在并非像样的理由。)
- 聪明程序员和专业程序员之间的区别在于:专业程序员了解,明确是王道。专业程序员善用其能,编写其他人能理解的代码。
- 类名
- 类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account和AddressParser
- 避免使用Mannager、Processor、Data、或Info这样的类名。
- 方法名
- 方法名应当是动词或动词短语,如postPayment、deletePage或save
- 属性访问器、修改器和断言应该根据其值命名,并依Javabean标准加上get、set和is前缀。
string name = employee.getname(); customer.setName("mike"); if (paycheck.isPosted())...
- 别扮可爱
- 言到意到,意到言到。
- 每个概念对应一个词
- 给每个抽象概念选一个词,并且一以贯之。(对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音)
- 别用双关语
- 避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。
- 比如,在多个类中都有add方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到群集(collection)中。如果把这个方法叫做add,貌似和其他add方法保持了一致,但实际上语义却不同,应该用insert或append之类词来命名才对。(把该方法命名为add,就是双关语了)
- 使用解决方案领域名称
- 记住,只有程序员才会读你的代码。所以,尽管去用那些计算机科学术语、算法名、模式名、数学术语吧
- 比如,对于熟悉访问者(VISITOR)模式的程序员来说,名称AccountVisitor富有意义。(程序员要做太多技术性工作,给这些事取个技术性的名称,通常是最靠谱的做法)
- 使用源自所涉问题领域的名称
- 如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。
- 优秀的程序员和设计师:其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。
- 添加有意义的语境
- 很少有名称是能自我说明的——多数都不能。反之,你需要用到良好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。
- 比如,对孤零零的一个state变量来说,可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为Address的类。这样,即便是编译器也会知道这些变量隶属某个更大的概念了。
- 语境的增强,也让算法能够通过分解为更小的函数而变得更为干净利落。
-
//语境不明确的变量 private void printGuessStatistics(char candidate,int count){ String number; String verb; String pluralModifier; ... } //有语境的变量 //创建GuessStaticsMessage类,把三个变量做成该类的成员字段 public class GuessStaticsMessage{ String number; String verb; String pluralModifier; ... }
- 不要添加没用的语境
- 设若有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在其中给每个类添加GSD前缀就不是什么好点子。
- 只要短命称足够清楚,就要比长名称要好。
- 对于Address类的实体来说,accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。Address是个好类名。如果需要与MAC地址、端口地址和Web地址相区别,我会考虑使用PostalAddress、MAC和URI。这样的名称更为准确,而精确正是命名的要点。
3、函数
- 短小
- 代码块和缩进:if语句、else语句、while语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。
- 这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层不该多于一层或两层。(这样的函数易于阅读和理解)
- 只做一件事
- 函数应该做一件事。做好这件事。只做这一件事。
- 问题在于很难知道那件该做的事是什么?(其实,有时候一件事也很容易被看作是三件事或很多具体细化的步骤)如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。
- 要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。
- 函数中的区段:只做一件事的函数无法被合理的切分为多个区段。(这也是函数做事太多的明显征兆)
- 每个函数一个抽象层级
- 函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
- 自项向下读代码:向下规则。这是保持函数短小、确保只做一件事的要诀。让代码读起来像是一系列自项向下的TO起头段落是保持抽象层级协调一致的有效技巧。
- switch语句
- 问题:写出短小的switch语句很难(包括if/else在内),写出只做一件事的switch语句也很难,Switch天生要做N件事。
- 解决:利用多态,确保每个switch都埋藏在较低的抽象层级,而且永远不重复。
- 如下代码:将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则藉由Emplyee接口多态地接受派遣。
- 对于switch语句,(作者的)规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍。当然也要就是论事,有时也会部分或全部违反这条规矩。
- 使用描述性的名称
- 沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码。”函数越短小、功能越集中,就越便于取个好名字。
- 别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。
- 选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。
- 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如:includeSTeardownPages、includeSetuoPages、includeSuiteSetupPage等
- 函数参数
- 最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这样做。
- 阅读模块所讲述的故事时,includeSetupPage()要比includeSetupPageInto(newPage-Content)易于理解。参数与函数名处在不同的抽象层级,它要求了解目前不是特别重要的细节(即那个Stringbuffer)
- 测试的角度:要编写能确保参数的各种组合运行正常的测试用例,是一件非常困难的事情。
- 输出参数比输入参数还要难以理解。
- 无副作用
- 副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。(如果一定要时序性耦合,就应该在函数名称里说明)
- 输出参数:参数多数会被自然而然地看作是函数的输入。普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。
- 分隔指令与询问
- 函数要么做什么事,要么回答什么事,但二者不可得兼。
- 函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。
- 使用异常替代返回错误码
- 从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在if语句判断中把指令当作表达式使用。另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。
- 抽离Try/Catch代码块:Try/Catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把它的主体部分抽离出来,另外形成函数。如图:
- 错误处理就是一件事:函数应该只做一件事,错误处理就是一件事,因此,处理错误的函数不该做其他事。
- 如何写出这样的函数
- 一开始都冗长而复杂。然后,会打磨,分解函数、修改名称、消除重复。缩短和重新安置方法。有时还拆散类。同时保持测试通过。
- 并不从一开始就按照规则写函数。一般没人做得到。
4、注释
- 注释会撒谎。
- 注释存在的越久,就离其所描述的代码越远,越来越变得全然错误。(原因很简单,程序员不能坚持维护注释)
- 注释不能美化糟糕的代码
- 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多。
- 用代码来阐述
- 很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可。
- 好注释——唯一真正好的注释是你想办法不去写的注释
- 法律信息
- 提供信息的注释
- 对意图的解释
- 阐释
- 警示
- TODO注释(一种程序员认为应该做,但由于某些原因目前还没做的工作)
- 放大
- 公共API中的Javadoc
- 坏注释
- 喃喃自语
- 多余的注释
- 误导性注释
- 循规式注释
- 日志性注释
- 废话注释
- 可怕的废话
- 能用函数或变量时就别用注释
- 位置标记
- 括号后面的注释(尽管这对于含有深度嵌套结构的长函数可能有意义,但只会给我们更愿意编写的短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实应该做的是缩短函数)
- 归属与署名
- 注释掉的代码
- HTML注释
- 非本地信息
- 信息过多
- 不明显的联系
- 函数头
- 非公共代码中的Javadoc
注:转载请注明出处