原书中引用 Bjarne Stroustrup 的话给整洁代码下了定义:
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略而完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做一件事
而且:
每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染
影响整洁代码的因素有:敷衍了事的错误处理代码、内存泄漏、静态条件代码、前后不一致的命名方式等。
另外 Dave Thomas 还推崇测试:
没有测试的代码不干净。不管它有多优雅,不管有多可读、多易理解,微乎测试,其不洁亦可知也。
一个整洁的代码的重要顺序应当是:
能通过所有测试
没有重复代码
体现系统中的全部设计理念
包括尽量少的实体。比如类、方法、函数等
而保持代码整洁的方式是:
让营地比你来时更干净
如果每次签入时,代码都比签出时干净,代码就会保持整洁。每次只需要更改一些小问题,比如一个变量名、拆分一个过长的函数、消除一点重复代码、清理一个嵌套 if 语句
有意义的命名
命名应当遵循以下条目:
变量、函数或者类的名称应该已经回复了它为什么存在、做什么事、应该怎么用。如果名称需要注释来补充,那就不算是名副其实。另外,代码中还要消除魔法数字[1]
例如代码:public List<int[]> getThem(){ List<int[]> list1 = new ArrayList<int[]>(); for (int[] x: theList) if(x[0] == 4) list1.add(x) return list1 }
这段代码的问题有:
theList 中是什么类型的东西?
theList 零下标条目的意义是什么?
4 的含义是什么?
返回的列表怎么使用?
而一个好的代码是:
public List<int[]> getFlaggedCells(){ List<int[]> flaggedCells = new ArrayList<int[]>(); for (int[] cell: gameBoard) if(cell[STATUS_VALUE] == FLAGGED) flaggedCells.add(cell) return flaggedCells }
另外,还可以令写一个类来掩盖此魔法数字:
public List<int[]> getFlaggedCells(){ List<int[]> flaggedCells = new ArrayList<int[]>(); for (int[] cell: gameBoard) if(cell.isFlagged()) flaggedCells.add(cell) return flaggedCells }
同样不要使用带有误导性的名字。例如 List 在命令中通常指的是一中数据结构而不是一个列表。除非底层类型真的是 List,否则 accountGroup, bunchOfAccounts, accounts 要比 accountList 更好。
即使容器类型真的是 List 类型,最好也别在名称中暴露类型名
此外,缩写、小写的字母 l 和 大写的字母 O 在命名中都应当被谨慎使用
在命名中也要进行有意义的区分。Variable 不应当出现在变量名中,Table 不应当出现在表名中。NameString 这样的也是无意义的。像下面这样的命名如果出现在同一作用域中也会引起混淆:
Customer, CustomerObject
moneyAmount, money
customerInfo, customer
theMessage, message
同样的,名字也应当使用可以读出来的名字,而不是 genymdhms(生成日期、年月日、十分秒)这样的名字
单字母名字和数字常量的一个问题是很难从一大段代码中找出来,更别提一个项目了。名字的长短应当与其作用域大小相对应,如果变量可能在代码中多次使用,那么就赋予其易于搜索的名字 . 将类型或者作用域写到名字里面是毫无意义的事情。而且不写明类型还可以自由更换变量的类型而无需担心引起混淆
对成员变量的前缀也不是必须的。类应当设计地尽可能小来减小对成员变量前缀的需求
我觉得成员变量前缀还是有必要的,尤其是对于代码补全来说,就不需要先把 this 打出来了
对实现添加额外标识要比对接口添加标识更好。比如 IShapeFactory/ShapeFactory 要劣于 ShapeFactory/ShapeFactoryImpl
命名还需要避免思维映射,明确的名字要优于聪明的名字。作者这里举了一个例子:
如果你记得 r 代表不包含主机名和 scheme 的小写字母版 url 的话。那你真是太聪明了
类名应当是名词或者名词短语。比如 Customer, WikiPage, Account。避免 Manager, Processor, Data, Info 这样的东西。尤其不能是动词
方法名应当是动词或者动词短语。getter、setter、断言应当根据其值命名。并依据 JavaBeans 标准加上 get, set, is 前缀
重载构造函数时,使用静态函数更好,例如:
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
要优于:
Complex fulcrumPoint = new Complex(23.0);
名字还应当避免与俗语或者俚语相关联,例如 kill 优于 whack,abort 优于 eatMyShorts
一个抽象概念对应一个词,并一以贯之。比如单使用 get 要优于混合使用 fetch, retrieve, get
名字不要使用双关语。只有抽象概念完全相同的才使用同一个名字,否则不要起同样的名字。例如列表使用 insert/append 优于 add(通常集合使用 add)
名字的命名可以大胆地使用术语。毕竟只有程序员才会读你的代码。所谓的术语既可以来自计算机科学中的术语,也可以使用所涉领域的术语
很少有名称能过自我说明,因此,应当使用命名良好的类、函数或者名称空间来给变量提供有意义的语境。但是不要添加一些毫无意义的语境,譬如给所有的类或变量加上项目的缩写。
函数
函数的重要性不言而喻,函数是所有程序中的第一组代码。在 Golang 中,函数是一等公民。因此,写一个好函数是很重要的。
函数应当尽量短小。函数的缩进层级不应该高于两层,函数的行数应当尽可能少,譬如少于 80 行
为了减少缩进层级,一个很有效的技巧是让函数尽可能早地返回。例如使用判断语句判断错误情况,然后直接返回,然后在后续写上非错误情况下的代码。
一个函数只做一件事
一个函数一个抽象等级。要确保函数只做一件事,就要保证函数中的所有函数都在一个抽象等级上。譬如 getHtml 的抽象等级要高于 list.append。而 buildRequest 和 parseResponse 则位于同一层次上
使用抽象工厂模式替换 switch 代码块。switch 天生要做 N 件事,它违反了单一职责原则,开闭原则等等。
另一种替换 switch 的方式是使用字典。例如:
level_int = 0 if level == 'Info': level_int = logging.INFO elif level == 'Debug': level_int = logging.DEBUG elif level == 'Warning': level_int = logging.WARNING elif level == 'Error': level_int = logging.ERROR elif level == 'Critical': level_int = logging.CRITICAL
替换为:
def setLogger(level: str): log_level = { 'Debug': logging.DEBUG, 'Info': logging.INFO, 'Warning': logging.WARNING, 'Error': logging.ERROR, 'Critical': logging.CRITICAL } logging.basicConfig( level=log_level[level], format="%(levelname)s: %(message)s" )
尽管第一段代码是 if,但是效果是一样的
取一个具有描述性的好名字。一个长但具有描述性的名字远比一个断但令人费解的名字好
函数的参数应当越少越好。一个函数的参数应当尽量小于三个。如果函数有三个或三个以上的参数,那就说明一些参数应当被封装为类了
如果一个函数只有输入参数而没有输出参数,且这个函数使用来修改系统状态的。那么这些参数被称为事件,这种函数被称为事件函数。事件函数的名字应当清晰明了地表达它的意图
尽可能不要使用输出参数
尽可能不要使用布尔参数。布尔参数会给阅读代码带来困难
函数应当是无副作用的,初始化函数只做初始化相关的事情,不要在其它函数中尝试初始化
分离 getter 和 setter。函数要么回答一些事情,要么做一些事情,不要让一个函数同时做两件事情。
例如:
if (attributeExists("username")) { setAttribute("username", "unclebob") }
将查询属性是否存在和设置属性分开
使用异常代替返回错误码。
Golang 使用了返回错误码的形式。这种形式需要谨慎考虑代码的书写方式以免代码嵌套很深。但是相比异常而言,我觉得返回错误码要好得多。毕竟异常会引入新的执行流
抽离异常处理块。try/catch 代码丑陋不看,搞乱了代码结构。更好的方法是将 try/catch 的主体部分抽离出来另外形成函数
错误处理就是一件事。函数应该只做一件事,错误处理就是一件事,因此,处理错误的函数不应当做其它事
依赖磁铁。返回错误码通常暗示某处有个类或者枚举,定义了所有的错误码。这样的类每次修改都会导致所有依赖它的类都被重新编译,而新的异常可以从旧的异常派生出来。
Golang 的异常可以实时派生(尽管不推荐)。在 Golang 中,拓展了 C 语言中错误码的功能,并用其顶替了异常的职责
消除重复。显而易见,重复是万恶之源
结构化编程。结构化编程认为每个代码块都应该有一个入口、一个出口。根据这些原则,意味着一个函数只有一个 return 语句,循环中不能有 break 或 continue 语句,而且永远不能有 goto 语句。但是这些原则对于小函数收益不大,而大函数又不推荐写
好的函数是经过打磨的。一个函数很难一个开始就遵循这些规则,因此需要不断地修改优化。
注释
好的代码应该可以自我解释。带有少量注释的整洁而有表达里的代码要比带有大量注释的零碎而复杂的代码要好的多。注释并不总是好的:
陈旧的注释往往提供过时的信息
不准确的注释还不如没注释
乱七八糟的注释会搞乱代码
例如:
if ((employee.flags & HOURLY_FLAG) &&
(employee.age > 65)
)
要劣于:
if (employee.isEligibleForFullBenefits())
好的注释应该是:
每个代码文件中放置的法律信息
提供信息的注释。例如解释代码的返回值,当然,更好的是让函数名来解释
对意图的解释。例如说明一个解决方案
解释一些晦涩代码。这些代码应该是由于引用的外部的和不可更改的库导致的(比如使用了 C 库)
警告某些后果
TODO 注释
另外,不要因为循规蹈矩或者是感觉应该添加注释就添加注释。一旦添加注释,就应该花必要的时间来写出最好的注释
不要写多余的注释
不要写误导性注释
不要循规蹈矩地解释每一部分
不要添加一些 CHANGLOG。有 Git 就够了!
不要添加一些废话(比如指明某个函数是默认构造函数)
不要添加签名。有 Git 就够了!
不用尝试用很长的注释来分隔代码。比如用一长串等号分隔 getter 和 setter
不要保留注释掉的代码。不用就赶快删掉。反正有 Git!
不要添加 HTML 注释
格式
注
现在又很多格式检查工具,只需要关注 linter 给出的信息就行了