代码整洁之道

月伴飞鱼 2024-06-23 15:20:26
学习书籍 > 编程书籍
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!

书籍介绍:https://book.douban.com/subject/4199741/

整洁代码

整洁不会拖慢你的进度

程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法

混乱只会立刻拖慢你,叫你错过期限

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

何为好代码

代码逻辑应当直截了当,叫缺陷难以隐藏:

  • 尽量减少依赖关系,使之便于维护
  • 依据某种分层战略完善错误处理代码
  • 性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来

整洁的代码只做好一件事,力求集中:

  • 每个函数、每个类和每个模块都全神贯注于一事
  • 整洁的代码应当明确地展现出要解决问题的张力
  • 它应当将这种张力推至高潮,以某种显而易见的方案解决问题和张力
  • 代码应当讲述事实,不引人猜测
  • 它只该包含必需之物。读者应当感受到我们的果断决绝
  • 它应当有单元测试和验收测试。它使用有意义的命名
  • 它只提供一种而非多种做一件事的途径
  • 它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的API
  • 代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达

消除重复和提高表达力:

  • 如果同一段代码反复出现,就表示某种想法未在代码中得到良好的体现

  • 尽力去找出到底那是什么,然后再尽力更清晰地表达出来

  • 检查对象或方法是否想做的事太多。如果对象功能太多,最好是切分为两个或多个对象

  • 如果方法功能太多,我总是使用抽取手段(Extract Method)重构之

    • 从而得到一个能较为清晰地说明自身功能的方法,以及另外数个说明如何实现这些功能的方法
  • 消除重复和提高表达力让我在整洁代码方面获益良多,只要铭记这两点,改进脏代码时就会大有不同

你不会为整洁代码所震惊:

  • 你无需花太多力气:那代码就是深合你意
  • 它明确、简单、有力:每个模块都为下一个模块做好准备
  • 每个模块都告诉你下一个模块会是怎样的,整洁的程序好到你根本不会注意到它,设计者把它做得像一切其他设计般简单

语言是冥顽不化的:

  • 是程序员让语言显得简单

实际上,作者有责任与读者做良好沟通:

  • 下次你写代码的时候,记得自己是作者,要为评判你工作的读者写代码

有意义的命名

名副其实

一个好的变量、函数或类的名称应该已经几乎答复了所有的大问题

它应该告诉你,这个名称所代表的内容,为什么会存在,做了什么事情,应该如何用等

如果一个名称需要注释来补充才能让大家明白其真正含义,那其实就不算是名副其实

举个栗子:

以下的这句代码里的d就不算是个好命名

  • 名称d什么都没说,它没引起我们对时间消逝的感觉,更别说单位是天了
int d; // elapsed time in days||经过了几天时间
// 我们应该选择这样的指明了计量对象和计量单位的名称:

int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

避免造成误导

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

例如,别用accountList来指一组账号

  • 除非它真的是List类型,用accountGroup、bunchOfAccounts,或者直接用accounts,都是更好的选择

尽量提防长得太像的名称:

  • 想区分XYZControllerForEfficientHandlingOfStringsXYZControllerForEfficientStorageOfStrings

    • 会花费我们太多的时间
  • 因为这两个词,实在太相似了

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

尽量做有意义的区分

尽量避免使用数字系列命名(a1、a2…….aN)

  • 这样的名称纯属误导,因为很多时候完全没有提供正确的信息,没有提供导向作者意图的线索

废话是另一种没有意义的区分:

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

注意,只要体现出有意义的区分,使用a、the这样的前缀就没错

  • 例如,将a用在域内变量,把the用于函数参数

尽量使用读得出来的名称

我们要使用读得出来的名称

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

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

尽量使用可搜索的名称

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

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

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

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

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

for (int j=0; j<34; j++)
{
    s += (t[j]*4)/5;
}
和

const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++)
{
    int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
    int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
    sum += realTaskWeeks;
}

按整洁代码的要求来评判,第一段代码会让读者不知所云,第二段代码比第一段好太多

  • 第二段代码中,sum并非特别有用的名称,但至少他搜索得到

采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比数字5好找得多

  • 而列表中也只剩下了体现我们意图的名称

取名不要绕弯子

我们取名的时候要避免思维映射,不应当让读者在脑中把你的名称翻译为他们熟知的名称

  • 也就是说取名不要绕弯子,而是要直白,直截了当

在多数情况下,单字母不是个好的命名选择,除非是在作用域小、没有名称冲突的地方,比如循环

  • 循环计数器自然有可能被命名为i,j或k(最好别用字母l),这是因为传统上我们惯用单字母名称做循环计数器

程序员通常都是聪明人,聪明人有时会借助脑筋急转弯炫耀其聪明

而聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确就是王道

专业的程序员善用其能,能编写出其他人能理解的代码

类名尽量用名词

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

类名最好不要是动词

方法名尽量用动词

方法名尽量用动词或动词短语

  • 比如postPayment,deletePage或者save

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

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

string name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted())...

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

Complex fulcrumPoint =Complex.FromRealNumber(666.0);

通常好于:

Complex fulcrumPoint = new Complex(666.0);

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

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

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

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

同样,在同一堆代码中混用controller、manager,driver,就会令人困惑

DeviceManager和Protocol-Controller之间有何根本区别?

为什么不全用controller或者manager?他们都是Driver吗?

  • 这就会让读者以为这两个对象是不同的类型,也分属不同的类

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

通俗易懂

我们应尽力写出易于理解的变量名,把代码写得让别人能一目了然,而不必让人去非常费力地去揣摩其含义

我们想要那种大众化的作者尽责地写清楚的通俗易懂的畅销书风格

  • 而不是那种学者学院风的晦涩论文写作风格

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

记住,只有程序员才会读你写的代码

  • 所以,尽管去用那些计算机科学(Computer Science,CS)领域的专业术语、算法名、模式名、数学术语

对于熟悉访问者(Visitor)模式的程序来说,名称AccountVisitor富有意义

给技术性的事物取个恰如其分的技术性名称,通常就是最靠谱的做法

添加有意义的语境

很少有名称是可以自我说明的

  • 所以,我们需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境

若没能提供放置的地方,还可以给名称添加前缀

举个栗子,假如我们有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量

  • 当他们搁一块儿的时候,的确是构成了一个地址

  • 不过,假如只是在某个方法中看到一个孤零零的state呢?

  • 我们会推断这个变量是地址的一部分吗?

我们可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境

  • 至少,读者可以知道这些变量是某个更大变量的一部分

当然,更好的方案是创建名为Address的类

这样,即便是编译器也会知道这些变量是隶属于某个更大的概念了

  • 另外,只要短名称足够好,对含义的表达足够清除,就要比长名称更合适

添加有意义的语境甚好,别给名称添加不必要的语境

避免使用编码

使用编码 是一个不好的选择,把类型或者作用域添加的变量名中,无疑给变量名添加了额外的信息,增加了解码的负担

string m_name;

应该避免这样的命名方式,下面python语言为例:

m_list;
arr_list;

函数书写准则

短小

短小函数的第一规则

第二条规则还是要更短小

  • 每行都不应该有150个字符那么长,函数也不该有100行那么长

函数的缩进层级不该多于一层

单一职责 只做一件事

只做一件事,如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事

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

  • 只做一件事的函数无法被合理地切分为多个区段。

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

函数参数尽可能的少

每个函数一个且仅有抽象层级:

  • 我们想要让代码拥有自顶向下的阅读顺序
  • 让代码读起来像是一系列自顶向下的 起头段落是保持抽象层级协调一致的有效技巧
  • 把读代码想象成读报纸
  • 总分结构,或者总分总结构

switch语句:

  • 多态–将switch语句埋到抽象工厂底下,在系统的其他部分看不到,就还能容忍
    • 可以使用抽象工厂来进行改进

函数名称使用:描述性名称

  • testableHtml 改为SetupTeardownIncluder.render,好的名称价值怎么好评都不为过

  • 如果每个例程都让你感动深合已意,那就是简洁代码

函数参数:

  • 参数越少越好,参数越少越便于理解

一元函数的普遍形式:

  • 函数名称应能区分出来

问关于参数的问题,如 boolean fileExist("myFile");

将参数转换为其他什么东西,在输出之:

  • InputStream fileOpen("myFile");

事件:

  • 有输入参数而无输出参数,程序将函数看做一个事件,使用该参数修改系统状态

标识参数丑陋不堪,即如果标识为true将会这样做,标识为false将会这样做:

  • 向函数传入布尔值简直是骇人听闻的做法

如果出现了参数是Boolean 值,就要思考一下,函数是否只做了一件事情

  • 反复问自己 ,这个函数是不是做了一件事,或者承担了一项职责

二元函数:

  • 可以把某个参数转换成当前类的成员变量,从而无需再传递它
    • 当然有一些有两个参数更加合适Point(x,y) 这种就比较合理

三元函数:

  • 排序,琢磨,忽略的问题都会加倍体现,写三元函数之前一定要想清楚

参数列表:

  • 一个数量可变的参数等同于一个参数

函数命名应与参数形成动词/名词对,如writeField(name)

无副作用:

  • 函数承诺只做一件事,但还是会做其他被藏起来的事,例如时序性耦合

输出参数:

  • 面向对象语言中对输出参数的大部分需求已经消失了
  • 应避免使用输出参数,如果函数必须要修改某种状态,就修改所属对象的状态吧

看下这个例子:

appendFooter(s)

看到这个 首先就会 想把 s 添加什么东西,或者它把什么东西加到s 后面?

  • s 是输入 还是输出参数呢?
public void appendFooter(StringBuffer report);

看了函数的签名,我们大概明白了。

所以 在面向对象语言中,最好的做法

report.appendFooter() // 这样来调用。

分隔指令与询问

函数要么做什么事(do),要么回答什么事(boolean),但二者不可兼得

public boolean set(String attributer, String value);

用户调用:

if (set("username","frank")) {//...
}

看到这个代码,首先想 是否能够成功设置username 为frank, 还是要问,username 是否已经之前被设置过为frank了呢?

对看代码的人会比较困惑

所以 解决方案 就是 把做什么和是否成功分开

  • 防止发生混淆
if (exists("username")){
    setAttribute("username","frank")
}

使用异常替代返回错误码

使用异常替代错误码返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化

抽离try/catch代码块:

  • try/catch代码块丑陋不堪

  • 最好把try和catch代码块的主体部分抽离出来,另外形成函数

错误处理就是一件事:

  • 如果try在某个函数中存在,它就该是这个函数的第一个单词
    • 而且在catch/finally代码块后面也不该有其他内容

Error.java依赖磁铁:

  • 返回错误码通常暗示某处有个类或是枚举定义了所有错误码

  • 使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署

使用异常的好处,可以避免嵌套太深的层级

  • 代码可以写起来更加简化

来看一个例子:

  • 这里使用了很多的 嵌套的 if语句 这种很难让人理解,嵌套层级特别多,看起来很复杂
  • 对于这种代码 最好直接抛出异常就可以,直接 使用 try 语句 然后捕获具体的异常就可以了

而不是一层,一层的进行判断,嵌套起来,之后就很难维护这样的代码

对于try 代码块 丑陋不堪, 它们会搞乱代码结构,把错误处理和正常流程混为一谈

最好把 try 和 except 的主体部分抽离出来 单独形成函数

  • 这样以后维护也会方便一点,看起来代码清晰,代码结构简单
public void delete(Page page){
    try{

        deletePageAndAllReferences(page);
    }catch(Exception e){
        logError(e);
    }

}


private void deletePageAndAllReferences(Page page) throws Exception {
    deltePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKeys(page.name.makeKey());
}


public void logError(Exception e ){
    logger.log(e.getMessage)
}

别重复自己

别重复自己:重复可能是软件中一邪恶的根源

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

仔细想一想,面向对象编程是如何将代码集中到基类,从而避免了冗余的

而面向方面编程(Aspect Oriented Programming)

面向组件编程(Component Oriented Programming)多少也是消除重复的一种策略

  • 这样看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复

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

结构化编程:

  • 每个函数、函数中的每个代码块都应该有一个入口,一个出口,遵循这些规则
    • 意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句
  • 对于小函数,这些规则助益不大,只有在大函数中,这些规则才会有明显的好处

如何写出这样的函数

先想什么就写什么,然后再打磨它

  • 初稿也许粗陋无序,你就斟酌推敲,直到达到你心中的样子

最好的情况要配上一套单元测试,覆盖每一行丑陋的代码

然后开始打磨这些代码,分解函数,修改名称,消除重复。最后要保持测试可以顺利通过

我并不是一开始就按照规则写函数。 我想没有人做得到

代码注释准则

注释不能美化糟糕的代码

写注释的最常见的动机之一:

  • 就是 糟糕的代码存在

其实带有少量注释而有表达力的代码,要比大量注释的零碎而复杂的代码像样得多

与其花时间写注释,不如花时间清洁那堆糟糕的代码

用代码来阐述

如果一段代码本身就具有表现力,完全没有必要写注释

从变量名或者函数名本身就具有自描述性,这样的代码就好一些

好注释例子

法律信息:

  • 可指向一份标准许可或其他外部文档

提供信息的注释

对意图的解释

阐释

警示:

  • 例如为什么用某种设计模式

TODO 注释

放大

  • 注释可以用来放大某种不合理之物的重要性

公共API中的doc

坏注释例子

喃喃自语

多余注释

误导性注释

循轨式注释

日志式废话

废话注释:

  • 用整理代码的决心替代创造废话的冲动吧

可怕的废话

能用函数或变量时就别用注释

位置标记:

  • ///(少用,只在有价值的地方用)

括号后面的注释

归属与命名

注释掉的代码

html注释

非本地信息

信息过多

不明显的联系

函数头

非公共API中的javadoc

代码格式准则

像报纸一样一目了然

想想那些阅读量巨大的报纸文章

  • 你从上到下阅读

在顶部,你希望有个头条,告诉你故事主题,好让你决定是否要读下去

  • 第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节

接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说话及其他细节

优秀的源文件也要像报纸文章一样。名称应当简单并且一目了然,名称本身应该足够告诉我们是否在正确的模块中

  • 源文件最顶部应该给出高层次概念和算法

细节应该往下渐次展开,直至找到源文件中最底层的函数和细节

恰如其分的注释

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

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

另外,注意一点。

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

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

合适的单文件行数

尽可能用几百行以内的单文件来构造出出色的系统,因为短文件通常比长文件更易于理解

  • 当然,和之前的一些准则一样,只是提供大方向,并非不可违背

例如,《代码整洁之道》第五章中提到的FitNess系统

  • 就是由大多数为200行、最长500行的单个文件来构造出总长约5万行的出色系统

合理地利用空白行

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

  • 在每个命名空间、类、函数之间,都需用空白行隔开(应该大家在学编程之初,就早有遵守)

这条极其简单的规则极大地影响到了代码的视觉外观

  • 每个空白行都是一条线索,标识出新的独立概念

其实,在往下读代码时,你会发现你的目光总停留于空白行之后的那一行

  • 用空白行隔开每个命名空间、类、函数,代码的可读性会大大提升

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

如果说空白行隔开了概念,那么靠近的代码行则暗示了他们之间的紧密联系

  • 所以,紧密相关的代码应该相互靠近。

基于关联的代码分布

关系密切的概念应该相互靠近

对于那些关系密切、放置于同一源文件中的概念,他们之间的区隔应该成为对相互的易懂度有多重要的衡量标准

应该避免迫使读者在源文件和类中跳来跳去

  • 变量的声明应尽可能靠近其使用位置

对于大多数短函数,函数中的本地变量应当在函数的顶部出现

团队规则

每个程序员都有自己喜欢的格式规则,但如果在一个团队中工作,就是团队说了算

  • 一组开发者应该认同一中代码风格,每个成员应该遵守这个规则

  • 一个好的团队应当约定与遵从一套代码规范,并且每个成员都应当采用此风格

我们希望一个项目中的代码拥有相似甚至相同的风格,像默契无间的团队所完成的艺术品

  • 而不是像一大票意见相左的个人所堆砌起来的残次品

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

记住,好的软件系统是由一系列风格一致的代码文件组成的

  • 尽量不要用各种不同的风格来构成一个项目的各个部分,这样会增加项目本身的复杂度与混乱程度

错误处理

错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法

使用异常而非返回错误码:

  • 错误码搞乱了调用者代码

先写try-catch-finally语句:

  • 尝试编写强行抛出异常的测试,再往处理器中添加行为,使之满足测试要求
  • 结果就是你要先构造try代码块的事务范围,而且也会帮助你维护好该范围的事务特征

使用不可控异常:

  • 可控异常的代价就是违反开放/闭合原则

如果你在方法中抛出可控异常,而catch语句在三个层级之上

  • 你就得在catch语句和抛出异常之间的每个方法签名中声明该异常

给出异常发生的环境说明:

  • 你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所
  • 在java中,你可以从任何异常里得到堆栈踪迹,然而堆栈踪迹却无法告诉你该失败操作的初衷
  • 如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来

详细记录异常发生时,发生了什么,最好有详细的信息

依调用者需要定义异常类

定义常规流程:

  • 业务逻辑和错误处理代码之间就会有良好的间隔
  • 你来处理特例,客户代码就不用应付异常行为了,异常行为被封装到特例对象中

特例模式(Special Case Pattern):

  • 创建一个类或配置一个对象,用来处理特例

别返回null值:

  • 返回null 值会给调用者带来不方便,每次返回结果都要检查是否为null

别传递null值,不要给函数传递null 值

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!