Day 26 - Clean Code 迈向更好读、好维护的程序

前言

今天的主题会参考这本非常有名的书 Clean Code

写程序到最後,除了最基本的,商业功能要能正常运作以外,其实大部分的时候都是在追求,如何让 code 好读、好维护、好重构,而这也是我这个系列文的核心目标。

而这本书也是朝向这个目标,是需要对自己有要求,期待朝向「更好」的 developer 迈进。因此,今天来好好讨论,有哪些小细节可以优化。

命名的艺术

名字就是认识这个变数最快的途径,只要名字取得好,光是把所有变数看过一次,就可以大致了解这段程序的目的。

Naming 一直都不是一件容易的事情,因为英文真的不好,有时候要表达一个抽象的概念时,也不容易用英文去形容,而且命名即便取不好,程序仍然可以正常执行,所以就更不容易好好看待这件事

命名的最高指导原则:好猜

没错就是这麽简单两个字!我都做不到QQ

无论是变数命名或函式命名,都要遵循这个原则,更精确来说,是要让人「容易理解意图」,那要怎麽衡量有没有符合这个原则呢?

  • 找同事来看一遍你的 code(并计算他每分钟说 WTF 的次数)
  • 三个月後再看一次还要看得懂

Clean Code

命名的要领

基本上在 Day 7 的时候讨论过一次了,这边简单复习一下:

  • 驼峰式命名(camelCase)
  • 函式命名要用:动词 + 名词,不同动词用於不同意义

常见但意义不明的命名

很多时候程序写得正火热(?),逻辑已经想到第 30 步了,但连第 1 个变数都还没想好名字,很容易就随便给了个「好像有意义但其实没意义」的命名,大致分成下列几类:

  • dataelementvaluetempitem:完全不知道是什麽
  • numarrlistfunc:只看得出变数类型
  • iindex:十分泛滥

是不是很常见?

没错我在铁人赛赶稿的时候,就是这个死样XD:

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 }
];
const displayArr = arr.map(item => item.name).join('、');

但如果是在那种意图单纯、用一次就结束的 scope 内,还算是比较可以接受(好啦是我自己可以接受啦QQ),比如上例第 6 行的 item

const displayArr = arr.map(item => item.name).join('、');

item 只有在 map function 内的 scope 使用,所以只要使用一次的情况下,其实本身就比较容易理解,很多时候我甚至会直接用物件解构赋值,省掉一次命名:

const displayArr = arr.map(({ name }) => name).join('、');

要改进的话,array 的变数可以在後面加个 slist 复数型来表示,这样如果跑 map,里面的 iterator 就可以变成单数型:

const products = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 }
];
const displayArr = products.map(product => product.name).join('、');

但规则尽量是统一的,不要偶尔 s 偶尔 list

注解的用意

注解也是写在程序码里面的一部分,只是不会被执行,那注解跟一般的程序语法比起来,差别、用意究竟是什麽?

程序语法是写给电脑看的,而注解是写给人看的。

当这个区分出来之後,我们的问题就更清晰了一点:

如果注解是写给人看的,那人们期待在注解上面看到什麽?

注解用来解释意图

毕竟语法是写给电脑看的,很多时候在不同的情境下,我们其实想表达不同的意思,但语法就是那几个 ifforswitch 在排列组合,有点像是只用 500 个英文单字过生活一样(?)

因此,很自然就会衍生出,使用注解来解释这段 code 的意图,这个用意。

这一点虽然很直觉,但别忘记,变数名称也是可以自由命名的,所以其实不是只有 500 个单字能用,你仍然可以用变数名称来解释变数的意图,用函式名称来解释整段 code 的意图

正是基於这一点,网路上看到有些 developer 对於「把注解拿来解释程序意图」这件事,是抱有稍微贬意的,因为他们认为「就是因为你变数命名没做好,才会需要仰赖注解」

关於这种批评我是觉得大可不必,原因如下:

  • 对於新手的尊重友善包容
  • 英文语法与中文的隔阂
  • 命名难以解释更抽象的意图

对於新手的尊重友善包容

不可否认大部分的时候,JavaScript 的新手的确是因为对於命名不够熟悉,或者单纯就是函式拆得不够细微,再加上可能词汇量有限(比如只会用 if/else 不会用 switch),所以呈现出来的程序也比较冗杂与单一。

const color = 'red';
let colorCode;

// 根据 color 对应的颜色转成色码
if (color === 'blue') {
    colorCode = '#0000FF';
} else if (color === 'green') {
    colorCode = '#00FF00';
} else if (color === 'red') {
    colorCode = '#FF0000';
}

console.log(colorCode); // '#FF0000'

比较适合改成:

const color = 'red';
let colorCode;

switch (color) {
    case 'blue':
        colorCode = '#0000FF';
        break;
    case 'green':
        colorCode = '#00FF00';
        break;
    case 'red':
        colorCode = '#FF0000';
        break;
}

console.log(colorCode); // '#FF0000'

但基本上肯学习的新手都会自己找学习资源,如果这时候在网路上又看到有人说:「啊写注解就是因为菜」,有些人会不小心得到一个结论:即便不是很擅长命名,也不要去写注解。

当然我没有要一竿子打翻一船人,也是会有人因此而认真去练习命名,减少注解的数量。

但我个人比较倾向从鼓励、优化的角度来看这件事,毕竟程序语言也是语言的一种,没有人刚开始学语言就很溜的。

英文语法与中文的隔阂

因为变数命名都是用英文,所以外国人当然会觉得不用写注解啊((误

其实最主要还是因为,英文跟中文有一些概念没有对应的词汇,所以要对应就没那麽方便。

再加上,台湾人天生看中文就比较快啊,一些比较难懂的概念,使用熟悉的中文来写注解,会比用不熟的英文来帮变数命名,还容易理解。

如果是站在学习的角度,的确可以自己好好练习在没有注解的情况下,用命名来解释 code;但公司不是请人来学习的,真的用英文表达不清的情况下,请直接写中文注解,下一个接手的人会感谢你的!

命名难以解释更抽象的意图

通常命名取得好,大多也是在解释「这个变数」或者「这个函式」所做的事情,很像是在帮每一片拼图命名,但很多片拼图拼出来的一个局部,整体在做什麽,就很难用命名来解决。

比如下面这段程序码:

const studentList = [
    { name: 'Joey', grade: 25 },
    { name: 'Susan', grade: 49 },
    { name: 'Allen', grade: 64 },
    { name: 'Alice', grade: 100 }
];
const passList = [];

studentList.forEach(student => {
    const adjustedScore = Math.sqrt(student.grade) * 10;
    const isHigherThan60 = adjustedScore > 60;
    if (isHigherThan60) {
        passList.push(student);
    }
});

const displayPassList = passList.map(student => student.name).join('、');
console.log(`恭喜以下同学:${displayPassList}`);

执行结果

恭喜以下同学:Susan、Allen、Alice

如果多了注解来说明就会清楚得多:

const studentList = [
    { name: 'Joey', grade: 25 },
    { name: 'Susan', grade: 49 },
    { name: 'Allen', grade: 64 },
    { name: 'Alice', grade: 100 }
];
const passList = [];

// 计算每个学生调分後是否及格,并列出及格名单
studentList.forEach(student => {
    const adjustedScore = Math.sqrt(student.grade) * 10;
    const isHigherThan60 = adjustedScore > 60;
    if (isHigherThan60) {
        passList.push(student);
    }
});

const displayPassList = passList.map(student => student.name).join('、');
console.log(`恭喜以下同学:${displayPassList}`);

结语

今天讲的都是偏向变数命名、注解写法,这系列的讨论其实都是很小细节的东西,但如果真的有做好,看程序码也能够让人感到如沐春风!

期许每天都在每个细节上更进步一点,每天多懂一点,才赶得上忘记的速度((误

名字的背後
藏着浅显的暗示
与深层的灵魂

参考资料

Clean Code


<<:  [Day31] 再访碰撞侦测与解析(三) - 解析很复杂!

>>:  [Day 26] 建立table

Day17 Redis应用实战-GEO/HyperLogLog/Transaction操作

Redis 资料型态GEO 储存地理空间资讯. 可用指令 GEOADD GEOPOS GEODIST...

[DAY 15] getValues 与 setValues

上一次说到了getRange 与 getDataRange 但是这只是「取得资料的位置与范围」 如果...

MLOps在金融产业:模型的可解释性与公平性

有一些模型像是逻辑回归和决策树,背後运作的原因相当简单明了,容易解释模型是如何得出其输出的。但随着更...

【Day 13】Google Apps Script - API 篇 - Drive Service - 云端硬碟服务范例

Google Drive(云端硬碟) Service 使用到的 API 程序介绍 今日要点: 》D...

JS Library 学习笔记:首先当然来试试 jQuery (二)

//jQuery 使用Id指定 $('#my-div') //jQuery 使用Class指定 $(...