编写可读代码的艺术

349 阅读16分钟

本文作者: 姚泽源

相信大家都见过这样的代码:

// poj ac 题解 - 337d
// https://github.com/Hapoa/Accepted/blob/master/19 - 简单图论/003 - CodeForces 337D.md
int bfs(int start)
{
    queue <int> Q;
    memset(d1, -1, sizeof(d1));
    d1[start] = 0;
    Q.push(start);
    while(!Q.empty())
    {
        int now = Q.front();
        Q.pop();
        for(unsigned int i = 0, sz = vec[now].size(); i < sz; i++)
            if(d1[vec[now][i]] == -1)
                d1[vec[now][i]] = d1[now] + 1, Q.push(vec[now][i]);
    }

    int ret = 1;
    for(int i = 1; i <= m; i++)
        if(d1[p[i]] > d1[p[ret]]) ret = i;

    return p[ret];
}

这样的

// poj ac 题解 - 3176
// https://github.com/snuc/poj/blob/master/poj/3176.c
#include <stdio.h>
int main()
{
    int f[2][500],i,j,n,d=0;
    scanf("%d\n%d",&n,&f[0][0]);
    for (i=1;i<n;i++)
    {
        d=1-d;
        scanf("%d",&f[d][0]);
        f[d][0]+=f[1-d][0];
        for (j=1;j<i;j++)
        {
            scanf("%d",&f[d][j]);
            f[d][j]+=(f[1-d][j-1] > f[1-d][j]) ? f[1-d][j-1] : f[1-d][j];
        }
        scanf("%d",&f[d][i]);
        f[d][i]+=f[1-d][i-1];
    }
    j=f[d][0];
    for (i=1;i<n;i++)
    {
        if (j<f[d][i])
            j=f[d][i];
    }
    printf("%d\n",j);
    return 0;
}

运气好还能看到这样的

// 国际混乱代码大赛-2012-grothe
// http://www.ioccc.org/2012/grothe/grothe.c
#include<stdio.h>
#include<stdlib.h>
#include<ctype.h>

int u,z,q[0400],O[0x101],o[0401],I[257],w[258][0403],W[0x100],Z[0x103],c[0403],k
,i,j,n,l,p,m;const char*J[0416],*M[0400];FILE*K[280],*s[0x102];void f(char*n,int
a){char*e=n;while(*e!='\0'){if(tolower((int)*e)!=*e)fputc(040,stderr);fputc((*e)
-a,stderr);e+=1;}(void)fputc('\n',stderr);}int y(int a,int b);int t(int i,int j)
{int k=i&j,l=i^j,n,m=1;for(n=1;k>=n;n<<=1)if(k&n)m=y(m,1<<n|1<<(n-1));return m>1
?y(m,1<<l):1<<l;}int y(int a,int b){int n,i=0x0,j;if((n=w[a][b]))return n;for(;a
>>i;++i)for(j=0x0;b>>j;j++)if(((a>>i)&1)&&((b>>j)&1))n^=t(i,j);return w[a][b]=w[
b][a]=n;}void a(void){for(i=0;i<z;i++){n=0;if(!i[I]){for(j=0;j<u;++j)if(i[O]==q[
j])n=Z[j];}else for(j=0;j<u;j++)n^=w[Z[j]][w[I[i]][W[w[o[j]][O[i]^q[j]]]]];c[i]=
n;}}void X(int v,int u){char*y=0;v-=1;switch(v){case(0x2):y ="HckngfVqQrgpKprwv"
"Hkng"; BC(4):y="JempihXsStirMrtyxJmpi"; BC(0):y="PointValueTooLarge"; BC 0x1:y=
"EvqmjdbufJoqvuQpjou";BC(6):y="TuOtv{zLorky";BC(3):y="WrrPdq|RxwsxwSrlqwv";BC(5)
:y="GfiFwlzrjsyX~syf}"; BC(07):y="UvV|{w|{Mpslz";}if(u)exit(0); f(y,v);exit(1);}
int main(int t,const char*T[]){for(i=00;i<0x100;++i)for(j=0;j<=i;++j)if(1==y(i,j
))W[i]=j,W[j]=i;for(k=0x1;k<t;k++){p=0;for(l=0;(T[k][l]>=toupper('0'))&&(T[k][l]
<=tolower('9'));l++){p=p*10+(T[k][l]-'0');if(p>=256)X(1,0);}if(T[k][l]=='-'){for
(m=0;m<u;m++)if(q[m]==p)X(2,0);q[u]=p;J[u]=T[k]+l+1;K[u]=fopen(J[u],"r");if(!u[K
])X(3,0);u++;}else if(T[k][l]=='+'){if(z>=256)X(4,0); O[z]=p;M[z]=T[k]+l+1;s[z]=
fopen(M[z],"w");if(!s[z])X(5,0);z++;}else X(6,0);}if(!(u!=0))X(7,0);if(!(z!= 0))
X(8,0);for(i=0;i<u;i++){n=1;for(j=0;j<u; j+=1)if(j!=i)n=w[n][q[i]^q[j]];o[i]=n;}
for(i=0;i<z;i++){n=1;for(j=0;j<u;j++)n=w[n][O[i]^q[j]];I[i]=n;}while(!(0)){for(k
=0;k<u;k++){int n;n=getc(K[k]); if(n==EOF)X(42,1); Z[k]=n;}a();for(k=0;k<z;k++)(
void)putc(c[k],s[k]);}X(11,1);}

想想看, 如果在项目里有 1000 行这样的代码, 维护起来是什么感觉……

我们在日常工作中, 会用很多办法来增强代码的可读性. 比如, 我们会设定统一的代码格式(JS Standard), 强制要求写注释, 合并前先进行 Code Review, 等等等等. 其实呢, 除了遵循这些规范, 我们也可以看一些相关的书籍. 比如 —— 《编写可读代码的艺术》

image.png

让我们先从最基本的问题开始, 什么才是好的代码.

image.png

这里有两份遍历链表的代码

// version 1
Node* node = list->head;
if(node == NULL) {
    return;
}
while(node->next != NULL)
{
    print(node->data);
    node = node->next;
}
if(node != NULL) {
    print(node);
}
// version 2
for(Node* node = list->head; node != NULL;node = node->next)
    print(node->data);

这两份代码, 都是在从头到尾的遍历一份链表, 如果要评判优劣的话, 显然是下边的代码最好, 因为他又短, 又便于理解.

这也符合我们通常对优秀代码的印象, 一般我们会认为代码越短越好. 因为代码越短, 需要理解的元素就越少, 所以可读性也就越好

但,真的是越短越好吗?

image.png

来看这两段代码. 首先是常规的八皇后问题的解决方案

# 常规八皇后解决方案(601字)
# https://github.com/zhsj/nqueen/blob/master/N皇后问题.md
#-*- coding:utf-8 -*-
def isAvailable(row,col):
    """检查当前位置是否合法"""
    for k in range(row):
        if queen[k]==col or queen[k]-col == k - row or queen[k]-col == row - k:
            return False
    return True

def find(row):
    """当row == n时表明已放置了n个皇后,递归结束,记录一个解"""
    global count,n,queen
    if row == n:
        count += 1
    else:
        for col in range(n):
            if isAvailable(row,col):
                queen[row]=col
                find(row+1)

def main():
    global count,n,queen
    n = input()
    queen = [-1]*n
    count = 0
    find(0)
    print count

if __name__ == "__main__":
    main()

然后是 90 年混乱代码大赛的作品, c 语言一行搞定八皇后问题.

// 一行代码解决八皇后问题(232字)
// 国际混乱代码大赛1990年获奖作品
// http://www.ioccc.org/1990/baruch.c
v,i,j,k,l,s,a[99];
main()
{
    for(scanf("%d",&s);*a-s;v=a[j*=v]-a[i],k=i<s,j+=(v=j<s&&(!k&&!!printf(2+"\n\n%c"-(!l<<!j)," #Q"[l^v?(l^j)&1:2])&&++l||a[i]<s&&v&&v-i+j&&v+i-j))&&!(l%=s),v||(i==j?a[i+=k]=0:++a[i])>=s*k&&++a[--i])
    ;
}

坦诚的说, 虽然第二段代码比正常版本简练的多, 但如果是实际项目, 而且代码里真的出现了 bug 的话, 我相信在座的同学宁肯把第二段代码重写一遍, 也不会去调试里边的 bug.

所以, 再想想, 代码真的是越短越好吗?

显然不是.

在日常工作中, 我们所谓的优化代码, 提升代码质量, 本质上其实是在让代码变得更容易理解. 而度量代码可读性最好的指标, 有且只有一个, 我们把它称作:

『可读性基本定理』

简单来说, 就一句话:

好的代码, 应该是使别人理解它所需的时间----最小化

image.png

其实检测代码质量的方法很简单. 对于我们编写的任何代码, 在写完它之后, 我们就可以估算一下让身边的同事把代码通读一遍并达到理解的水平所需要的时间, 这个时间的长短, 就是我们评判代码可读性的尺度.

而且需要特别点出的是, 当我们说『理解』时, 我们对『理解』这个词有着很高的要求. 我们所说的理解, 是指当一个人真的『理解』了这些代码之后, 他应该就能直接去改动它, 找出缺陷并能明白这些代码是怎么和代码的其他部分交互的. 而让这个时间最小化, 是评判代码可读性的核心标准.

所以, 如何编写可读代码这个问题就变成了: 『怎样编写代码, 才能让别人理解它的时间最小化』

让我们从命名开始.

命名之法: 把信息装进名字里

首先, 在为方法、变量命名的时候, 我们要尽量起一个有意义的名字.

比如说食人花这个名字, 真的很贴切……

image.png

然后来看几条起名时的原则.

首先, 要使用专业的词语.

一般来说, 专业的词语总是最有表现力的. 比如在下边这个方法中. getPage 是一个很模糊的词, 只看名字很难知道它想做些什么.

如果是想从本地的缓存中获取一个页面的话, 应该叫 loadPage;如果是想从数据库中获取一个页面的话, 应该叫 queryPage;如果是想在互联网上抓取一张页面, 那应该叫 fetchPage 或者 downloadPage.

这几个名字, 都比 getPage 更有表现力.

同样, 假定我们有一个二叉树类, 类里有个 size 方法, 只看这个方法名也是很难知道它想表达什么意思.

如果是想知道树的高度的话, 应该用 height;如果是想知道这个二叉树的节点数的话, 应该叫 countNodes;如果是想知道这个二叉树在内存中所占的空间的话, memSize 会更合适一点.而这些名字也都比只有一个简单的 size 要好.

然后看这个. Thread 类里的 stop 方法. 这个方法看起来就很不错了. 简洁明了, 一眼就能知道它在做什么.

但, 还是有改进空间.

比如说, 如果这是一个重量级操作, 线程停止之后就不能再恢复, 那它应该叫 kill. 如果还有方法可以继续这个线程, 那它应该叫 pause

这样就贴切多了.

image.png

然后继续.

在中文环境中, 如果我们想要去拿一个东西的话, 我们是可以用『拿』,『取』,『抓』,『提』这些同义词的. 根据环境不同, 用不同的词汇可以让文章更有表现力. 和中文一样, 英语里有很多同义词, 如果能记住这些词, 在写代码的时候也可以让代码的含义变得更直观.

比如表格里的这些词语.

image.png

当然, 过分了就不好了.

比如 PHP 里有一个 explode 函数, 这个函数的名字很形象, 一看就知道是要把字符串炸碎成块. 但是, 问题来了, PHP5.3 之前还有一个内置的函数叫 split. 这两个函数都是在切割字符串, 如果不看说明的话, 根本就不知道这两个函数有什么区别. 这就很尴尬了……

不过有个好消息是, split 方法从 5.3 起开始被声明为废弃函数, 在 PHP7 里被正式移除. 也算是比较好的结果了.

image.png

然后. 避免空泛的名字.

有一个很经典的问题: 什么是世界上最糟糕的变量名? data 第二糟糕的呢? data2 第三呢? data_2 如果代码中有 data, a, str, i1, i2, i3, i4, i5 这样的变量, 维护起来是很糟心的.

举个例子啊. 在我们平常写循环的时候, 经常会用i、j、k这样没有意义的名字做循环变量. 但这样就需要让读者经常去回看上下文才能明白变量的内容, 延长了理解代码的时间, 是一个很不好的习惯.

而且, 有时候还会出现问题.

比如, 看下边这段代码. 在这段代码的最后,users 和 members 使用了错误的索引, 但因为用了无意义变量, 所以即使是知道用错了, 也很难看出来错在了那儿. 这在后期维护的时候就是一个大坑

// bad version
for (int i = 0; i < clubs.size(); i++)
    for (int j = 0; j < clubs[i].members.size(); j++)
        for (int k = 0; k < users.size(); k++)
            if(clubs[i].members[k] == users[j])
                cout << "user[" << j << "] is in club[" << i << "]" << endl;

但是, 如果换成有意义的名字就好多了, 基本上一眼就能看出来 user 和 member 用的索引不对, 一目了然.

// good version
for (int club_i = 0; i < clubs.size(); club_i++)
    for (int member_j = 0; member_j < clubs[club_i].members.size(); member_j++)
        for (int user_k = 0; user_k < users.size(); user_k++)
            if(clubs[club_i].members[user_k] == users[member_j])
                cout << "user[" << member_j << "] is in club[" << club_i << "]" << endl;

image.png

image.png

然后下一条, 在变量名中展示信息.

如果有些信息非常重要的话, 我们应该考虑把它嵌到变量名里.

比如, start 方法需要一个延迟启动参数, 我们可以在后边附上 second, 来说明是按秒来进行延迟启动

createCache 方法需要设定 size 大小, 如果没有单位的话很难知道这个大小是 b, 还是 kb, 还是 mb,所以可以附上单位 mb, 一目了然.

throttleDownload 也一样, 把 limit 换成 max_kbps, 这样很容易知道这是要把最大网速限制为每秒 max kb

这里需要注意, 我们虽然把 second 缩写成了 s, 但一般来说, 我们还是要尽量减少缩写的使用. 因为除了一些约定俗成的缩写外, 其他缩写实际上会增加我们的理解成本. 比如, 突然出现一个 res, 我们要想想这是 response 还是 resource. 如果只有一个 M, 我们就得想想这是内存(mb), 还是网速(M/s), 还是一个 moment 对象. 如果这些缩写不能让刚加入项目的新人明白是什么意思的话, 那就不应该让它出现在代码里.

image.png

然后继续, 除了在变量名里加单位, 我们也可以在变量名里加信息.

如果是纯文本密码的话, 可以在后边加上 plaintext

如果是需要转义的注释, 可以附注上 unescaped

在 Python 里的字符串变量经常会有编码问题, 所以如果是 html 字符串的话可以考虑加上 utf8 后缀

image.png

不过, 加信息也不是什么都往里边加. 如果变量名里有没用的单词的话, 完全可以直接拿掉.

比如, coverToString 不如直接换成 toString.

同样, serveLoop 和 doServeLoop 一样清楚.

减少冗余信息是一种美德.

image.png

还有一点是, 要让变量名不会被误解.

假如我们有一个这样的 clip 函数(clip, 修剪)

def clip(text, length):
    pass

显然, 只看 clip 这个名字, 它可能会有两种行为:

1.从尾部删掉 length 的长度

2.截取最大长度为 length 的一段

第二种可能性的概率最大, 但只看函数名的话, 没办法完全肯定.

与其让读者乱猜, 不如直接把函数名称改成 truncate, 直接就是截短的意思, 简单明了.

参数名 length 也不好, 不如直接改成 max_length

但 max_length 还不够好, 因为 length 也有很多意思: 它可能是字节数, 也可能是字符数, 还可能是字数. 如果只有一个孤零零的 length 的话, 读者还是没法判断到底会以什么为单位去截取字符串.

所以, 这实际上是前面所说的需要把单位附在名字后边的情况. 在这里, 我们假定是按字符数截取文本, 所以, 应该用 max_chars, 而不是 max_length

image.png

在分页展示数据的时候, 我们经常会遇到为范围变量命名的问题, 这里有几个通用的命名原则

首先, 我们可以使用 min 和 max 来表达包含极限.

在需要表达极限含义的时候, 之前大家可能会用 limit 这个词. 但是 limit 有少于和少于且包括这两种含义, 不符合清晰明了的原则. 所以命名极限最清楚的方式还是在限制前加上 min 和 max.

同样, 在表达一段区间时, 可以用 first/last 表示包含的含义. 而且, 我们也可以用 begin/end 表示 包含/排除 范围, 就像这张图中所展示的一样

image.png

在编程过程中我们经常遇到的就是要为布尔值进行命名, 布尔值的命名也有一些原则

对于那些返回布尔类型的函数, 要确保他们返回true或false的含义非常明确.

比如这个变量, read_password = true, 这就有两个含义: 已经读取过密码, 或者需要读取密码. 在实际看代码的时候就会很困惑.

但是, 只要在布尔值前面加上is, has, can或者should这样的定语, 就能让变量含义的变得更明确

另外一点就是尽量避免用反义名字. 用反义名字会明显的增加我们理解代码时的负担.

比如这个, disable_ssl = false, 这种变量名出现在代码里简直反人类

换成use_ssl = true就好多了.

image.png

然后, 命名的最后一条要求就是: 命名时一定要符合用户的预期. 举一个实际案例.

let startAtMoment = moment();
for (
  let currentAtMoment = startAtMoment;
  currentAtMoment.isBefore(startAtMoment.add(1, "months"));
  currentAtMoment = currentAtMoment.add(1, "days")
) {
  // do something
}

moment 是 JS 时间函数的事实标准, 上面的代码其实就是从当前时间开始, 循环执行操作一直到一个月之后. 看起来没啥问题, 但实际上, 这个代码运行之后, 根本不会停下来.

原因很简单, 在我们看来, 执行完startAtMoment.add(1, 'months')这个方法之后应该直接返回下个月的时间给我们, 但事实上, 这个方法并不会直接返回一个月之后的时间, 而是直接对startAtMoment变量进行操作. 所以每执行一次 add 方法,startAtMoment都会往后移一个月, 我们的程序当然停不下来.

然后一个不幸的消息是 moment 官方 14 年就发现这个问题了, 并且还承诺在 moment3.0 里把 add 接口改成通常的操作方式. 但是, 到现在为止, moment 版本号还是 2.x, 大家工作的时候还是要注意一下.

image.png

讲完变量命名, 然后来看一下代码整体的规范.

首先是审美.

我们来看这两段代码

class StatsKeeper {
public:
// A class for keeping track of a series of doubles
    void Add(double d);  // and methods for quick statistics about them
   private:   int count;        /* how many so    far
*/ public:
        double Average();
private:   double minimum;
list<double>
  past_items
      ;double maximum;
};
// A class for keeping track of a series of doubles
// and methods for quick statistics about them
class StatsKeeper {
public:
    void Add(double d);
    double Average();
private:
    list<double> past_items;
    int count; // how many so far

    double maximum;
    double minimum;
};

上下两段代码完全一致, 只调整了一下下方代码的格式, 理解时间就可以缩短很多. 显然, 美观本身就可以提高代码可读性. 而让代码美观, 具体来说, 就是达到下边的两条标准:

使用一致的布局 相关代码形成代码块 我们分开讲下.

先说一下布局, 以下边这段代码为例

let body = _.get(req, ["body"], {});
let projectId = _.get(body, ["projectId"], "");
let displayName = _.get(body, ["displayName"], "");
let projectName = _.get(body, ["projectName"], "");
let createUid = _.get(body, ["createUid"], "");
let email = _.get(body, ["email"], "");
let nickname = _.get(body, ["nickname"], "");
let cDesc = _.get(body, ["cDesc"], "");
let record = {
  body,
  projectId,
  displayName,
  projectName,
  createUid,
  email,
  nickname,
  cDesc
};

首先, 我们可以用列对齐的方式, 把它排列开

let body = _.get(req, ["body"], {});
let projectId = _.get(body, ["projectId"], "");
let displayName = _.get(body, ["displayName"], "");
let projectName = _.get(body, ["projectName"], "");
let createUid = _.get(body, ["createUid"], "");
let email = _.get(body, ["email"], "");
let nickname = _.get(body, ["nickname"], "");
let cDesc = _.get(body, ["cDesc"], "");

let record = {
  body,
  projectId,
  displayName,
  projectName,
  createUid,
  email,
  nickname,
  cDesc
};

其次, 我们可以通过抽取方法, 把相同操作抽象成公共方法, 进一步增强代码的可读性和可维护性

let record = {};
let body = _.get(req, ["body"], {});
for (let column of [  "projectId",  "displayName",  "projectName",  "createUid",  "email",  "nickname",  "cDesc"]) {
  if (_.has(body, column)) {
    record[column] = body[column];
  }
}

当然, 像这样手工操作太累了, 最简单的办法就是使用 lint 工具, 引入代码规范, 直接在全局上保持布局一致. 比如 PHP 的PSR-2, JS 的Standard. 当然有些规范下的代码风格你可能觉得比较难看, 但是, 从全局上说, 一致的风格比"正确"的风格更重要.

另外一个让代码变美观的办法是形成代码块, 让相似代码的布局也相似, 参考下边的代码, 乍看上去很乱,

class FrontendServer {
    public:
      FrontendServer();
      void ViewProfile(HttpRequest* request);
      void OpenDatabase(string location, string user);
      void SaveProfile(HttpRequest* request);
      string ExtractQueryParam(HttpRequest* request, string param);
      void ReplyOK(HttpRequest* request, string html);
      void FindFriends(HttpRequest* request);
      void ReplyNotFound(HttpRequest* request, string error);
      void CloseDatabase(string location);
      ~FrontendServer();
};

其实只要简单整理一下, 调整顺序, 填点空行, 拆分成段落, 感受就会好很多.

class FrontendServer {
    public:
      FrontendServer();
      ~FrontendServer();

    // Handlers
    void ViewProfile(HttpRequest* request);
    void SaveProfile(HttpRequest* request);
    void FindFriends(HttpRequest* request);

    // Request/Reply
    string ExtractQueryParam(HttpRequest* request, string param);
    void ReplyOK(HttpRequest* request, string html);
    void ReplyNotFound(HttpRequest* request, string error);

    // Database Helpers
    void OpenDatabase(string location, string user);
    void CloseDatabase(string location);
};

然后是注释:

注释显然可以加速对代码的理解.但是, 阅读注释本身也是需要消耗时间的, 所以并不是所有代码都需要注释. 如果一行注释既没能提供新信息, 又不能帮助后人更好更快的理解代码的话, 那就不需要写.

// 坏注释的例子
/**
 *  get方法
 */
function get() {}

而且, 如果代码质量本身就很差, 这时候需要做的也不是给代码加注释, 而应该直接提升代码质量.

// 坏注释的例子
/**
 *  警告, 这个不是get方法, 他实际上会直接删除整个数据库!
 */
function get() {}

好代码 > 坏代码 + 好注释

所以, 究竟什么时候需要注释呢, 有这么几种情况:

首先, 你可以利用注释, 在代码中添加你的见解, 避免后人踩坑

// 好注释的例子

// Moment对象的时间操作方法会修改变量自身, 因此需要先clone一下再操作
let nextDayAtMoment = currentAtMoment.clone().add(1, "days");

其次, 如果代码中有瑕疵, 也可以用标记记录下来, 比如说这四个标记:todo/fixme/hack/warn.

image.png

用的时候需要加上括号, 指明任务所有者, 这样将来才好维护.

对于常量, 我们也需要加上必要的注释, 这样会方便理解. 比如下边的订单常量列表, 只看这些常量的话基本看不懂,

const ORDER_STATUS_WAIT_PAY_ORDER_FAILED = 1;
const ORDER_STATUS_WAIT_PAY_WAIT_CONFIRM = 2;
const ORDER_STATUS_WAIT_PAY_HAS_CONFIRM = 3;
const ORDER_STATUS_WAIT_PAY_HAS_REFUSE = 4;
const ORDER_STATUS_WAIT_PAY_HAS_EXPIRE = 5;
const ORDER_STATUS_HAS_PAY_WAIT_CONFIRM = 6;
const ORDER_STATUS_HAS_PAY_HAS_CONFIRM = 7;
const ORDER_STATUS_WAIT_REFUND_ORDER_FAILED = 8;
const ORDER_STATUS_WAIT_REFUND_ORDER_FAILED = 9;
const ORDER_STATUS_WAIT_REFUND_ORDER_REFUSE = 10;

但加上注释会好很多

const ORDER_STATUS_WAIT_PAY_ORDER_FAILED = 1; // 待支付下单失败
const ORDER_STATUS_WAIT_PAY_WAIT_CONFIRM = 2; // 待支付待确认
const ORDER_STATUS_WAIT_PAY_HAS_CONFIRM = 3; // 待支付已确认
const ORDER_STATUS_WAIT_PAY_HAS_REFUSE = 4; // 待支付确认失败(拒单)
const ORDER_STATUS_WAIT_PAY_HAS_EXPIRE = 5; // 待支付确认失败(超时)
const ORDER_STATUS_HAS_PAY_WAIT_CONFIRM = 6; // 已支付待确认
const ORDER_STATUS_HAS_PAY_HAS_CONFIRM = 7; // 已支付已确认
const ORDER_STATUS_WAIT_REFUND_ORDER_FAILED = 8; // 待退款下单失败
const ORDER_STATUS_WAIT_REFUND_ORDER_FAILED = 9; // 待退款下单失败
const ORDER_STATUS_WAIT_REFUND_ORDER_REFUSE = 10; // 待退款已拒单

当然, 如果常量非常多, 而且彼此之间有很接近, 对于这种特殊情况, 最好的办法是直接用中文命名, 真正做到把信息写到名字里

const ORDER_STATUS_待支付_下单失败 = 1;
const ORDER_STATUS_待支付_待确认 = 2;
const ORDER_STATUS_待支付_已确认 = 3;
const ORDER_STATUS_待支付_确认失败_拒单 = 4;
const ORDER_STATUS_待支付_确认失败_超时 = 5;
const ORDER_STATUS_已支付_待确认 = 6;
const ORDER_STATUS_已支付_已确认 = 7;
const ORDER_STATUS_待退款_下单失败 = 8;
const ORDER_STATUS_待退款_下单失败 = 9;
const ORDER_STATUS_待退款_已拒单 = 10;

写注释时要站在读者的角度思考问题. 比如: 如果有些接口数据很难拿到, 那可以把返回值 json 之后放到注释里, 方便后人维护, 如果预料到后人会在这个坑里栽到, 也应该添加一行注释进行提醒. 比如说, 这样的注释, 就不如下边的注释更好

// 登录接口
let loginResponse = await http.postForm(ucConfig.api + "/passport/login", {});
// wiki地址: http://wiki.ke.com/page?pageId=3322111
// 登录成功返回值 => {"code":0,"data":"23111112233"}
let loginResponse = await http.postForm(ucConfig.api + "/passport/login", {});

注释要说明代码的意图, 而不是描述程序行为, 在下面案例里, "反向展示列表"这个注释显然不如"从高到底展示商品价格"好

// 反向展示列表
for (
  let itemIndex = productList.length - 1;
  itemIndex >= 0;
  itemIndex = itemIndex - 1
) {
  DisplayPrice(productList[itemIndex]["price"]);
}
// 从高到底展示商品价格
for (
  let itemIndex = productList.length - 1;
  itemIndex >= 0;
  itemIndex = itemIndex - 1
) {
  DisplayPrice(productList[itemIndex]["price"]);
}

然后来讲一下程序中的循环和逻辑.

image.png

在编写程序的时候, 如果循环和没有条件判断的话, 整个代码还是相对比较好看的. 但一旦加上了控制语句, 每多一层if/else, 结构就会复杂一倍. 如果控制语句一直堆叠下去的话, 整个代码就会像漫画里的蛇那样. 可读性…… 几乎为 0

然后这里是简化控制流的几个常见方法

比如, 调整if/else的顺序, 先处理正逻辑, 简单情况, 或者先处理有趣或者可疑的情况. 比如, if(isNotDebug) 就显然比 if(!isNotDebug) 要好很多, 当然, isNotDebugb 并不是什么好名字, 这里只是拿来占位

if(isNotDebug)
/******** vs ******/
if(!isNotDebug)

然后就是最小化嵌套. 这个很好理解, 因为对我们来说, 每层嵌套都是在为我们的"思维栈"加一个条件. 而当嵌套很深时, 代码会非常难以理解. 比如向下边这样

let msg = "";
if (isNumber(projectId) === false) {
  msg = "projectId不是数字\n";
} else {
  if (isString(name) === false) {
    msg = msg + "name不是字符串\n";
  } else {
    if (name.length > 10) {
      msg = msg + "name长度超过10个字符\n";
    } else {
      if (name.length < 3) {
        msg = msg + "name长度不足3个字符\n";
      }
    }
  }
}

对于这种情况我们可以通过使用提前返回的方式来减少嵌套. 比如处理问题前先判断参数是否正确, 如果存在问题直接报错返回不再向下运行. 像这种提前返回的语句被称为"卫语句", 我们可以通过卫语句来有效的减少嵌套.

let msg = "";
if (isNumber(projectId) === false) {
  msg = "projectId不是数字\n";
  return msg;
}
if (isString(name) === false) {
  msg = msg + "name不是字符串\n";
  return msg;
}
if (name.length > 10) {
  msg = msg + "name长度超过10个字符\n";
  return msg;
}
if (name.length < 3) {
  msg = msg + "name长度不足3个字符\n";
  return msg;
}

最后就是尽量避免使用三目运算符?:. 因为所有的三目运算符其实都可以被转换为if/else语句, 而且跟if语句相比, 三目运算符除了节约代码行数之外并没有其他优势, 而且在大部分情况下都会让代码变得更加难以理解. 所以, 如果没有特别的理由, 要尽量避免使用三目运算符.

image.png

然后, 除了 n 层嵌套的循环之外, 代码中另一个很折磨人的就是那些超长的表达式.

image.png

// 把单词全部变为小写并统计单词出现的次数
var content = "hello World";
var tally = content
  .split(/[\s.,\/:\n]+/)
  .map(word => word.toLowerCase())
  .reduce((pre, cur) => (pre[cur]++ || (pre[cur] = 1), pre), {});

这里介绍几个把他们拆分成容易理解的小块代码的方法.

首先, 是使用解释变量.

比如我们可以用变量名去解释子表达式的含义.

先看这行代码.

if (line.split(":")[0].trim() == "root") {
  // do something
}

如果没有注释帮助的话, 理解代码的功能恐怕要花上一段时间.

但加一个中间变量就会好很多

let username = line.split(":")[0].strip();
if (username === "root") {
}

或者, 我们也可以用总结变量来解释一大块代码.

if (request.user.id == document.owner_id) {
  // document可编辑
}
if (request.user.id != document.owner_id) {
  // document只读
}

比如上边的request.user.id == document.owner_id, 这行代码很长, 而且出现了两次. 但它实际上只是要判断一下当前用户是不是文档的所有者而已. 所以我们可以用一个总结变量把这个值记下来.

let is_user_owns_document = request.user.id == document.owner_id;

if (is_user_owns_document) {
  // do something
}

if (is_user_owns_document === false) {
  // do something
}

这样的代码理解起来就容易多了.

image.png

另外一点要说的就是德摩根定理, 这个在我们简化条件判断的时候很有用.

只有一句话: 分别取反, 转换与或.

就像下边这样.

not (a  or  b  or  c ) == (not a) and (not b) and (not c)

not (a  and  b  and  c) ==(not a) or (not b) or (not c)

image.png

最后是一些零散的建议.

image.png

比如, 如果我们在两个地方用到了同一处代码, 就应该考虑把代码独立出来, 做成通用函数, 而不是之恶复制粘贴.

再比如, 如果有可能的话, 每一个函数应该只完成一个功能. 即使不能做到这么小的粒度, 也要尽量把代码按功能拆分到不同的段落中

然后就是当我们编写代码之前, 可以先试着用自然语言把逻辑或者问题描述一遍. 这样可以让代码写的更流畅. 在写代码的过程中也要考虑网上有没有更好的解决方案. 比如, 对前端同学来说, 最常遇见的就是这种代码:

// 获取b.c.d.e
let a = b && b.c && b.c.d && b.c.d.e;

但实际上, 如果我们用 lodash 的话, 只要一行代码就够了

// 利用lodash
let a = _.get(b, ["c", "d", "e"], "");

另外代码里最好不要出现被注释掉的代码. 在有版本控制系统的情况下, 应该用代码库来记录代码, 而不是把代码加到注释里. 无用的代码应该被直接删除, 这样才能减少新人理解代码所花的时间.

image.png

每隔一段时间我们都应该去看下代码库里的函数. 这个不是为了记下来, 只是去看看有什么可以直接拿来用的代码, 避免重复造轮子.

对于错误消息, 我们也要尽量把失败消息放在返回值里或者打印出来, 而不是直接丢掉. 在有错误消息的情况下, 会让 debug 工作简单很多.

最后, 过犹不及. 上边说的这些, 其实都是建议. 真正在做的时候, 还是要根据具体情况具体对待, 避免出现过度优化的情况.

image.png

我的分享就到这里了, 谢谢大家!

image.png