yusubond / yusubond.github.io

:cn: subond's blog
http://www.subond.com
1 stars 0 forks source link

[读书笔记]ARC_《编写可读代码的艺术》 #4

Closed yusubond closed 4 years ago

yusubond commented 4 years ago

核心思想

本书的核心思想就是代码应该写得容易理解,即让别人能用最短的时间理解你的代码

可读性定理

代码的写法应当使别人理解它的时间最小化,即为可续性定理。

第一部分:表面层次上的改进

原则一:把信息装到名字里

  1. 选择专业的词

举例: "get"这个词就非常不专业,没有表达出很多信息,既可以表示从本地缓存得到一个页面,也可以表示从数据库中,甚至可以表示从互联网中,更专业的词语应该是FetchPage()或DownloadPage()。

def getPage(url): ...

要用于找到更有表现力的词语,例如

单词 更多选择
send deliver, dispatch, announce, distribute, route
find search, extract, locate, recover
start launch, create, begin, open
make create, set up, build, generate, compose, add, new
  1. 避免泛泛的名字

像tmp, retval, foo这样的名字都是"我想不出名字的”托辞,不应该使用这样空洞的名字,应该挑选那些能够描述这个实体的值或目的的名字。举例,下面的例子中用sumres更好。

var res int
for _, v := range nums {
  res += v
}

i,j,k等名字常用作索引和迭代器,但是在多层循环中往往可能用错,可以通过增加前缀使其更精确。举例:

for (int i = 0; i < club.size(); i++)
  for (int j = 0; j < club[i].members.size(); j++)
    for (int k = 0; k < users.size(); k++)
      if (club[i].members[k] == users[j])   // 注意,这里有一个错误
        cout << "user[" << j << "] is in club[" << i << "]" << endl;

如果将i, j,k换成ci,mj,uk往往可以避免上面的错误。

  1. 用具体名字代替抽象的名字

给变量、函数或者其他元素命名时,要把它描述得更加具体,而不是更抽象。

举例:

我们的一个程序有个可选的命令行标志叫做--run_locally。这个标志会使得这个程序输 出额外的调试信息,但是会运行得更慢。这个标志一般用于在本地机器上测试,例如在 笔记本电脑上。但是当这个程序运行在远程服务器上时,性能是很重要的,因此不会使 用这个标志。

你能看出来为什么会有--run_locally这个名字,但是它有几个问题:

这里的问题是--run_locally是由它所使用的典型环境而得名。用像--extra_logging这样的名字来代换可能会更直接明了。

但是如果--r u n_l o c a l l y需要做比额外日志更多的事情怎么办?例如,假设它需要建立和使用一个特殊的本地数据库。现在--run_locally看上去更吸引人了,因为它可以同时控 制这两种情况。

但这样用的话就变成了因为一个名字含糊婉转而需要选择它,这可能不是一个好主意。 更好的办法是再创建一个标志叫--use_local_database。尽管你现在要用两个标志,但 这两个标志非常明确,不会混淆两个正交的含义,并且你可明确地选择一个。

  1. 用前缀或后缀给名字附带更多的信息

一个变量名就像是一个小小的注释。

如果关于一个变量有什么重要的需要读者必须知道,那么是值得把额外的“词”添加到名字中。举例,假设你有一个变量包含一个十六进制字符串:

var id string  // Example "123456789abcde"

如果让读者记住这个ID的格式很重要,那么这个变量名应该是hex_id。

同理,如果你的变量是一个度量的话,那么最好名字中带上它的单位。举例:

var start_ms = (new Date()).getTime(); // top of the page
  1. 决定名字的长度

当选择好名字的时候,有一个隐含的约束就是名字不能太长。

可以采用的一些小的直到原则: 1)在小的作用域可以使用短的名字; 2)首字母缩略词和缩写,这条使用的经验原则是团队的新成员是否能够理解这个名字的含义?如果能,那么就没问题。 3)去掉没用的词;举例:ConvertToString()不如ToString()好。

  1. 名字的格式表达含义

例如Google的C++代码格式规范Google C++ Style Guide

原则二:不会误解的名字

小心可能有歧义的名字。

举例,Filter(),filter是个二义性单词,无法清楚地表达是“挑出”还是”减去“,应避免使用这样的单词。

同样的单词还有limit,无法表达出”大于“还是“小于”。**命名极限最清楚的方式就是在要限定的东西前面加上max_min_

推荐用first和last来表示包含的范围;用begin和end来表示包含/排除的范围。

对于布尔变量,通常来讲,加上is,has,canshould这样的词,可以把布尔值变得更加明确。

不会误解的名字就是最好的名字——阅读你代码的人应该理解你的本意,并且不会有其他的理解。

原则三:审美与设计

好的源代码应当”看上去养眼“。

在讨论审美和设计的时候,我们所说的是以下三个原则: 1)使用一致的布局,让读者很快习惯这个风格; 2)让相似的代码看上去相似; 3)把相关的代码分组,形成代码块。

重新安排换行来保持一致和紧凑,注释也要对齐。

在需要的时候使用列对齐,列的边提供了“可见的栏杆”,阅读起来方便,这是个“让相似的代码看起来相似的好例子”。

选择一个有意义的顺序,始终一致地使用它。

按声明把块组织起来,不要把所有的方法都放到一个巨大的代码块中,应该按逻辑把它们分组。

核心思想就是,一致的风格比“正确”的风格更重要。

具体技巧包括,

原则四:什么样的注释

你可能认为注释的目的是“解释代码做什么”,但这只是其中很小一部分。

核心思想,注释的目的是尽量帮助读者和作者了解的一样多。

什么地方不需要注释: 1)能从代码本身中迅速地推断的事实; 2)“拐杖式的注释”——试图粉饰可读性查的代码的注释。好代码>坏代码+好注释

应该记录下来的想法包括: 1)对于为什么代码写成这样而不是那样的内在理由,即“指导性批注”; 2)代码中的缺陷,使用像TODO:或XXX:这样的标记; 3)常量背后的故事,为什么是这个值。

站在读者的立场去思考: 1)预料到代码中哪些部分会让读者说:“啊?”,并且给他们加上注释; 2)对普通读者意料之外的行为加上注释; 3)在文件/类的级别上使用“全局观”注释来解释所有的部分是如何一起工作的; 4)用注释来总结代码块,使读者不致迷失在细节中。

如果你要写注释,最好把它写得精确——越明确和细致越好。

注释应当由很高的信息/空间率。

写出言简意赅的注释的一些具体提示: 1)当像"it","this"这样的代词可能指代多个事物时,避免使用它们; 2)尽量精确地描述函数的行为; 3)在注释中用精心挑选的输入/输出例子进行说明; 4)声明代码的高层次意图,而非明显的细节; 5)用嵌入的注释来解释难以理解的函数参数; 6)用含义丰富的词来使注释简洁。

yusubond commented 4 years ago

第二部分:简化循环和逻辑

原则五:把控制流变得易读

关键思想就是,把条件、循环以及其他对控制流的改变做得越“自然”越好,运用一种方式使读者不用停下来重读你的代码。

条件语句中参数的顺序

指导原则

比较的左侧 比较的右侧
"被询问的”表达式,它的值更倾向于不断变化 用来作比较的值,它的值更倾向于常量

举例,下面的例子中第一段比第二段更易读,

if (length >= 10)
// 而不是
if (10 <= length)

if/else语句块的顺序,有些情况下有理由相信一种顺序比另一种顺序更好:

三目运算符最好只在最简单的情况下使用,相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需的时间。

避免do/while循环,因为do/while的奇怪之处是一个代码是否会执行是由其后一个条件决定。

从函数中提前返回,用保护语句来实现更自然。

大多数时候都应该避免使用goto

嵌套很深的代码是难以理解的。每个嵌套层次都在读者的”思维栈“上有增加了一个条件。举例:

if (user_result == SUCCESS) {
        if (permission_result != SUCCESS) {
                reply.WriteErrors("error reading permission");
                reply.Done();
                return;
        }
    reply.WriteErrors("");
} else {
    reply.WriteErrors(user_result);
}
reply.Done();
// 其中permission相关的代码是很可能后来加上去的,尽管想加入的代码很整洁,也很明确
// 但是,以后当其他人遇到这段代码时,所有的上下文早已经不在了,所以在你第一次读这段代码的时候不得不一下子全盘接受它
// 通过提前返回来减少嵌套,优化后
if (user_result != SUCCESS) {
    reply.WriteErrors(user_result);
    reply.Done();
    return;
}
if (permission != SUCCESS) {
    reply.WriteErrors(permission_result);
    reply.Done();
    return;
}
reply.WriteErrors("");
reply.Done();
return;

减少循环内的嵌套,举例:

for (int i = 0; i < result.size(); i++ {
  if (result[i] != NULL) {
    non_null_count++
    if (result[i]->name != "") {
      cout << "Considering candidate..." << endl;
    }
  }
}
// 在循环中提早返回的技术是continue,优化后
for (int i = 0; i < result.size(); i++ {
  if (result[i] == NULL) continue;
  non_null_count++;
  if (result[i].name == "") continue;
  cout << "Considering candidate..." << endl;
}

与if(...)return;在函数中所扮演的保护语句一样,这些if(...)continue;语句是循环中的保护语句。

原则六:拆分超长的表达式

关键思想,把你的超长表达式拆分成更容易理解的小块。

用作解释的变量

拆分表达式最简单的方法就是引入一个额外的变量,让它来表示一个小一点的子表达式。举例:

if line.split(':')[0].strip() == "root": 
   ...
// 优化后
username = line.split(':')[0].strip()
if username == "root":
   ...

总结变量,即使一个表达式不需要解释,把它装入一个新变量中仍然有用,它的目的只有一个,就是用一个短很多的名字来代替一大块代码,这个名字会更容易管理和思考。举例:

if (request.user_id == document.owner_id) {
  // user can edit this document...
}
if (request.user_id != document.owner_id) {
  // document is read-only...
}
// 用总结变量优化后
final boolean user_own_document = (request.user_id == documnet.owner_id)
if (user_own_document) {
// user can edit this document
}
if (!user_own_document) {
// document is read-only
}

使用德摩根定理,关于布尔表达式的两种等价写法: 1)not (a or b or c) <=> (not a) and (not b) and (not c) 2)not (a and b and c) <=> (not a) or (not b) or (not c) 即”分别取反,转换与/或“。

需要注意的是,很多编程语言中,布尔操作会做短路计算,即x = a | b | c|是用来找出a,b,c中第一个为“真”的值。

与复杂的逻辑做战斗,如果一段逻辑很复杂,可以适时停下来思考找到更优雅的方式,一种技术就是看看能否从”反方向“找到问题。

原则七:变量与可读性

关于变量和可读性,我们需要讨论三个问题: 1)变量越多,就越难以全部跟踪它们的动向; 2)变量的作用域越大,就越需要跟踪它们的动向越久; 3)变量改动得越频繁,就越难以跟踪它的当前值。

要避免没有价值的变量,举例:

now = datetime.datetime.now()
root_message.last_view_time = now
// 下面的更简洁
root_message.last_view_time = datetime.datetime.now()

减少中间结果,对于只是用来保存临时的结果,我们有必要通过得到它后立即处理它而消除。举例:

var remove_one = function (array, value_to_remove) {
  var index_to_remove = null;
  for (var i = 0; i < array.length; i += 1) {
    if (array[i] === value_to_remove) {
      index_to_remove = i;
      break;
    }
  }
  if (index_to_remove !== null {
    array.splice(index_to_remove, 1);
  }
}
// 优化后
var remove_one = function (array, value_to_remove) {
  for (var i = 0; i < array.length; i += 1) {
    if (array[i] === value_to_remove) {
      array.splice(i, 1);
      break;
    }
  }
}

缩小变量的作用域,其关键思想就是让你的变量对尽量少的代码可见。有时候可以通过”闭包“来实现”私有“变量,举例:


submitted = false
var submit_form = function(form_name) {
  if (submitted) {
    return;  // don't double-submit the form
  }
  ...
  submitted = true
};
// 优化后
var submit_form = (function () {
  var submitted = false;
   return function ( form_name) {
     if (submitted) {
        return;
     }
     ...
     submitted = true;
   };
}());

关于变量和可续性的总结就是**减少变量**,**减小每个变量的作用域**,**只写一次的变量更好**。
yusubond commented 4 years ago

第三部分:重新组织你的代码

原则八:抽取不相关的子问题

所谓工程学就是把大问题拆解成小问题再把这些问题的解决方案放回在一起。

积极地发现并抽取出不相关的子逻辑,我们是指: 1)看到某个函数或代码块,问问自己:这段代码高层次的目标是什么? 2)对于每一行代码,问一下:它是直接为高层次目标而工作吗?这行代码高层次的目标是什么? 3)如果足够的行数在解决不相关的子问题,抽取代码到独立的函数中。

自顶向下或自底向上式编程?

自顶向下编程是一种风格,先设计高层次模块和函数,然后根据支持它们的需要来实现低层次的函数。

自底向上编程尝试首先预料和解决所有的子问题,然后用这些代码段来建立更高层次的组件。

创建更加通用的代码,简化已有的接口,永远不要安于使用不理解的接口

关键思想,就是把一般代码和项目专有的代码分开。

原则九:一次只做一件事

关键思想,应该把代码组织得一次只做一件事。

同时在做几件事的代码很难理解。一个代码块可能初始化对象,清楚数据,解析输入,然后应用业务逻辑,所有这些都同时进行。如果所有的代码都纠缠在一起,对于每个”任务“都很难靠其自身来帮你理解它从哪里开始,到哪里结束。

关于一次只做一件事的具体建议和流程: 1)列出代码所做的所有”任务“。这里的”任务“没有很严格的定义——它可以小得如”确保这个对象有效“,或者含糊得如”遍历树中所有的结点”。 2)尽量把这件任务拆分到不同的函数中,或者至少是代码中不同的段落。

任务可以很小,举例:

var vote_value = function (vote) {
  if (vote === "up") {
    return +1;
   }
  if (vote === "down") {
     return -1;
   }
  return 0;
}

原则十:把想法变成代码

如果你不能把一件事解释给你祖母听的话说明你还没有真正理解它。——爱因斯坦

把一个想法用“自然语言”解释是个很有价值的能力,这需要把一个想法精炼成最重要的概念,这样做不仅可以帮助他人理解,而且也帮助你自己把这个想法想得更加清晰。

清楚地描述逻辑,用自然语言描述解决方案。

先用自然语言把逻辑,过程描述清楚,然后在写代码很有帮助。

通过复述,人们更容易找到问题的所在。

如果你不能把问题说明白或用词语来做设计,估计是缺少了什么东西或什么缺少定义。把一个问题(想法)变成语言真的可以让它更具体。

原则十一:少写代码

知道什么时候不写代码可能对于一个程序员来讲是他所要学习的最重要的技巧。

yusubond commented 4 years ago

本读书笔记已搬移至wiki https://github.com/yusubond/yusubond.github.io/wiki