clean code

149 阅读14分钟

前段时间刚好有这样的学习任务,又加上刚进入新的环境,我需要一些改变,改变之前的不好的习惯,再网络四处闲逛的时候刚好再B站看到了有关于lean Code的课程个人感觉非常不错,再加上自己的一些个人见解做了一些总结。

课程链接 clean code

关于clean code

Clean Code 是什么?

Clean Code通常具备以下一些特点

  • 可读的、有意义的、理解成本低的
  • 精确的,不会存在歧义
  • 避免不直观的命名、复杂的嵌套、超大的代码块
  • 遵循最佳的实践和设计原则

Clean Code 的意义在哪里

  • Clean Code可读性问题,大量的时间浪费在重新读一遍代码上
  • 一个代码库的持久活性,代码可读性是关键。

有时候业务压力等多种因素的影响下,我们会写一些Quick Code。这种方式在短期的产出会比较高,但是随着时间发展,越来越难以维护,也就会越来越影响产出。下面这张图也就描述了这种情况,横轴是时间,纵轴是产出情况。

image.png

下面我将对于 这两个方面进行总结

Clean Code

  • 更侧重于如何写代码
  • 更强调代码的可读性和可理解性
  • 更着眼于单个问题和文件

Pattern&Principle(设计模式和设计原则)

  • 更强调代码可维护性和可扩展性

如何编写Clean Code

关于如何编写CleanCode,这里主要有以下几个方面,命名、注释、函数、流程控制。

命名

自己总结的一些常用的命名规范,好的命名不仅可以使代码看起来简洁,并且维护起来也方便了许多。

不要嫌弃函数名过长
const getLocationPermission = () => {}

这是一个用来获取定位权限的函数。虽然这个名称很长,但是语义化清晰,看一眼就知道他是用来做什么的。这里可以拆分成为 3 部分 get 获取的意思 location 定位的意思 permission 权限的意思, 这样一个十分具有语义化的函数命名就完成了。

变量 / 函数 命名头部

一般为动词,后面加上具体要做什么的名词

前缀前缀 + 命名大意
getgetListInfo获取列表信息
del/deletedelListInfo删除列表信息
set / addsetListInfo / addListInfo修改列表信息 / 增加列表信息
isisShow是否展示
hashasListInfo有没有列表信息
什么时候用 has , 什么时候用 is

has 常用于表示有没有或者是否包含 / 而 is 常用于表示是不是,是否

has的使用场景,例如

// 有没有定位权限
const hasLocationPermission = ?;

// 有没有列表信息
const hasListInfo = ?;

is 的使用场景

// 是否(展示/显示)
const isShow = true

// 是否超时
const isTiemout = true

总结一下: has 是 "有没有" 包含的关系,而 is 则是 "是不是?"这个意思

一个好的命名需要遵循的规则

首先,你要清楚知道你这个函数是用来干什么的。比如我需要写一个函数用来处理对象、数组等数据是否为空。那么我可以这样写 isEmpty 是否为空。例如我需要一个函数来获取本地保存的列表信息,另一个是需要通过网络请求来获取列表信息那么我可以这样来编写

const getLocalListInfo = ?;
const getNetWorkListInfo = ?;

仔细拆分,获取关键的字眼。 '获取本地保存的列表信息' => get(获取)local(本地)ListInfo(列表信息) 这样,一个十分具有语义化的函数命名就完成了

函数变量 使用小驼峰命名规则 / 组件构造函数使用大驼峰 / 组件文件名使用下划线开头

小驼峰

const getListInfo

大驼峰

const GetListInfo

下划线

const _getListInfo
使用缩写
  • 通用性,不能随便拉出来一个单词就使用缩写
  • 保证统一性 既然某个单词使用了缩写, 那么最好都用缩写,不能有的写,有的不写
  • 缩写是作为一个单词存在,也就是这样的规则去命名的
  • 例如: typeScript 缩写 ts这里第一个是小写,那么就是小写,后面的 Script 不再是单独的一个单词,应该是与前面是属于一个单词。转换规则 typeScript => tscript => ts , 同理如果 TypeScript => Ts , 这是只在命名的情况下的转换

注意点4:不要通过删除单词中的字母来达到缩写的目的

一些不好的命名:

const n = ?							            // 无意义的命名
const nError = ?				                            // 不明确的命名
const wgcComponents = ?             // 不明确的命名,或者就你能看懂,一旦有人员变动维护就会困难
比较通用的缩写
源单词缩写
messagemsg
informationinfo
buttonbtn
backgroundbg
responseres
requestreq
imageimg
utilityutil
prropertyprop
sourcesrc
booleanbool
errorerr
settingsset

以上有很多其实在平时已经有使用到,也还有很多没有写进去的,使用缩写命名的时候一定要注意规范。

常量命名

关于常量的命名,一般不会改变的变量,这类变量比较固定(例如:一天有多少毫秒,180deg 或者 xxx deg的选择角度,再就是和其他人约定好的魔鬼数字等等)他们的共同点是我们无法使其变化,也可以说我们不希望他会被改变。

这种常量的话一般是使用全大写,每一个单词使用 _ 下划线分开。 例如

一天毫秒数综合

const DAY_MILLI_SECOND_SUM = ?

注释

为什么要用到注释,用简单单行注释不香吗?下面就来看看下面的代码:
// xxx函数
const myFunction = ({ id, name, avatar, list, type }) => {
  // 此处省略 30 行代码
};

一个传入五个参数,内部数行代码的函数竟然只有短短的一行注释,
也许你开发的时候能记住这个函数的用途以及参数的类型以及是否必传等,
但是如果你隔了一段时间再回头看之前的代码,那么简短的注释就可能变成你的困扰。
更不用说没有注释,不写注释一时爽,回看代码火葬场。
写注释的目的在于提高代码的可读性。相比之下,下面的注释就清晰的多:

/**
 * 调整滚动距离
 * 用于显示给定 id 元素
 * @param    id        string  必传    元素 id
 * @param    distance  number  非必传  距离视口最顶部距离(避免被顶部固定定位元素遮挡)
 * @returns  null
 */
export const scrollToShowElement = (id = "", distance = 0) => {
  return () => {
    if (!id) {
      return;
    };

    const element = document.getElementById(id);
    if (!element) {
      return;
    };

    const top = element?.offsetTop || 0;
    window.scroll(0, top - distance);
  };
};

对于复杂的函数,函数声明上面要加上统一格式的多行注释,同时内部的复杂逻辑和重要变量也需要加上单行注释,两者相互配合,相辅相成。函数声明的多行注释格式一般为:

/**
 * 函数名称
 * 函数简介
 * @param    参数1    参数1数据类型  是否必传  参数1描述
 * @param    参数2    参数2数据类型  是否必传  参数2描述
 * @param    ...
 * @returns  返回值
 */

多行注释的优点是清晰明了,缺点是较为繁琐(可以借助编辑器生成 JavaScript 函数注释模板)。建议逻辑简单的函数使用单行注释,逻辑复杂的函数和公共/工具函数使用多行注释, 建议在我们的平台上的编辑器里面也能引入注释模板。

无论是 css 还是 JavaScript 中,当代码越来越多的时候,也使得寻找要改动的代码时变得越来越麻烦。所以我们有必要对代码按模块进行整理,并在每个模块的顶部用注释,结束时使用空行进行分割。

 /* 以下代码仅为示例 */

 /* 模块1 */
 /* 类名1 */
 .class-a {}

 /* 类名2 */
 .class-b {}

 /* 类名3 */
 .class-c {}

 /* 模块2 */
 /* 类名4 */
 .class-d {}

 /* 类名5 */
 .class-e {}

 /* ... */
复制代码
// 以下代码仅为示例

// 模块1
// 变量1
const value1 = "";
// 变量2
const value2 = "";
// 变量3
const value3 = "";
// 函数1
const myFunction1 = () => {};

// 模块2
// 变量4
const value4 = "";
// 变量5
const value5 = "";
// 变量6
const value6 = "";
// 函数2
const myFunction2 = () => {};

// ...
复制代码

效果有了,但是似乎不太明显,因此我们在注释中增加 - 或者 = 来进行分割试试:

 /* ------------------------ 模块1 ------------------------ */
 /* 类名1 */
 .class-a {}

 /* 类名2 */
 .class-b {}

 /* 类名3 */
 .class-c {}

 /* ------------------------ 模块2 ------------------------ */
 /* 类名4 */
 .class-d {}

 /* 类名5 */
 .class-e {}

 /* ... */
复制代码
// 以下代码仅为示例

/* ======================== 模块1 ======================== */
// 变量1
const value1 = "";
// 变量2
const value2 = "";
// 变量3
const value3 = "";
// 函数1
const myFunction1 = () => {};

/* ======================== 模块2 ======================== */
// 变量4
const value4 = "";
// 变量5
const value5 = "";
// 变量6
const value6 = "";
// 函数2
const myFunction2 = () => {};

// ...
复制代码

能直观的看出,加长版的注释分割效果更好,区分度更高。高质量的代码往往需要最朴实无华的注释进行分割。 其中 JavaScript 的注释“分割线”建议使用多行注释。

函数

组成部分
  • 参数:针对方法调用者来说,对于它们而言,方法参数的个数、类型、顺序,返回值等都应该是易于理解的。
  • 函数体:相比较而言,函数体更偏向方法提供者,需要控制方法体的长度,便于后续的维护和阅读。

首先是参数部分

最小化入参个数

过多的参数,对于调用者来说会非常有难度,该传什么值,顺序是什么样子。

参数个数说明示例
0最好,容易理解和调用new Date();
1非常好, 比较容易理解和调用String.valueOf(10);
2调用时就需要参数顺序和类型(a,b)=>{console.log(a+b)}// 常识情况下第一个参数是a,第二个参数是b
多于3个难度增加,适当情况下,可以借助对象作为参数Math.min( 2 , 30 ,1 , 200-10 , 300*22 , 20-30 )// 如果调用该函数,容易赋值错误

这里有一个特例,就是es6的一些新特性,让函数的可读性增加。

// 计算一堆数字的累加和
function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42
避免output参数,尤其是意料之外的output参数

output参数是指,我们对于输入的参数会进行一些修改。我不推荐对入参进行修改,但是某些业务场景下,不可避免的会有需要修改的情况,这种情况下,需要通过函数命名提醒调用者函数会对入参进行怎样的修改。

在该函数中,对输入参数进行了修改,但是我们没有办法直接通过函数名知道具体的参数是什么。

function iterate (personList) {
    personList.forEach(item => {
        let name = item.name;
        name = name.toUpperCase()
    })
}
function personNameToUpperCase (personList) {
    personList.forEach(item => {
        let name = item.name;
        name = name.toUpperCase()
    })
}

而对于函数体,同样有一些建议

尽可能小且仅完成一件事情

一个函数应该只负责一件事情,并尽可能地减少代码量,提高可读性

不要将抽象层级不同的代码放置到一起

这里up介绍了一个抽象层级的概念,还是非常有帮助的。

High Level:

这里我们知道email会被校验,它不会控制email是如何被校验的

isEmail(email)

Low Level:

需要明确控制email是如何被校验的

email != null && email.indexOf("@") != -1

相比较而言,High Level的抽象更容易理解,而Low Level的代码是需要一些额外解释工作的。
当然我们代码中一定会包含HighLevel和LowLevel的抽象,但是最好的做法是不要在一个方法中同时包含两种抽象。

Not Good:

function saveUser(user) {
    // 低级别的抽象代码
    if (user?.email != null && user?.email.indexOf("@") != -1) {
        console.log("电子邮箱填写错误")
        return false
    }   
    
    // 高级别的抽象代码
    saveUser(user);
}

Better:

function saveUser(user) {
    validateUser(user) ? saveUser(user) : ''
}

function validateUser(user) {
    if (user?.email != null && user?.email.indexOf("@") != -1) {
        console.log("电子邮箱填写错误")
        return false
    } 
}
合理拆分
  • 如果一些代码的作用比较类似,考虑将其提取为一个函数
  • 避免过度拆分,当你发现拆分完之后,发现很难给新的函数起一个名字,或者拆分后发现并没有提升可读性,那就先保持不动
DRY(Do not repeat yourself)
  • 如果发现存在复制粘贴的情况,大概率存在重复代码,可以考虑将代码提取复用。
  • 如果发现当要修改某个功能时,需有多处地方需要做同样的修改,这个时候也可能存在重复代码,可以考虑提取复用。
避免函数产生意外影响

对于同样的输入,应该总会得到一样的输出。

易测试

相比于冗长的方法,没有副作用且短小的方法更容易被测试,我们可以通过给函数写测试,来判断是否需要对函数进行拆分。

流程控制

避免过深的代码嵌套,对于代码嵌套特别深的代码,学到了一个名字,飞机代码,真的好像一个飞机。

这里嵌套比较深的主要是指if-else语句,对于这里的解决有一下几点。

优先正向判断

对于判断语句,更倾向于选择正向的,因为正向的比较符合大众的认知。

Not Good:

function dummyCode(user) {
    let isRich = isRich(user);
    if (!isRich) {
        processPoorUser(user);
    }
}

function isRich(user) {
    return user?.totalMoney > 1000;
}

Better:

function dummyCode(user) {
    let isRich = isRich(user);
    if (isRich) {
        processPoorUser(user);
    }
}

function isRich(user) {
    return user?.totalMoney <= 1000;
}
合理应用守护和快速失败

Not Good:

function dummyProcess(data) {
    if (data != null && data.length > 0) {
        data.forEach(item => {
            console.log(item)
        })
    }
}

Better:

public void dummyProcess(List<String> data) {
    if (data == null || data.length == 0) {
        return false;
    }
    
    data.forEach(item => {
        console.log(item)
    })
}
合理利用多态

多态在设计模式中应用得比较广泛,比如 组合模式 / 策略模式等等
虽然我们在平时的开发中不太用的到,但是一旦我们涉及到一些设计模式的话,多态还是很有用的

合理使用一些变量中的初始化

Not Good:

let arr = [0,0,0,0,0,0,0,0,0,0]

Better:

let arr = Array(10).fill(0);

SOLID

设计原则对于编写Clean Code是有一定帮助的,特别强调了S和O对于Clean Code的重要性。

单一职责(Single Responsibilty Principle)

说明:不要因为多个原因对类进行修改,这里的单一职责并非意味着只有一个方法,而是同一个业务领域的,这条原则有助于保证类可以专注于提供一类职责。

开闭原则(Open and Close Principle)

说明:面向扩展开放,面向修改关闭,这个原则有助于保证类的规范,因为我们需要扩展出新的类,而不是对原有类上进行修改。

里氏替换原则(Liskvo substitution Principle)

说明:对象可以被他们的子类所替换,而且这种替换不会改变类的行为,也就是子类对象即是父类对象。这条原则强制子类必须满足一定的约束,不会改变父类的行为。

接口隔离原则(Interface Segregate Principle)

说明:相比于提供宽泛的、复合的接口,面向特定client的、小的接口反而更好,因为某些情况下,client并不需要那么多的接口。

依赖倒转原则(Dependency Inverson)

说明:依赖抽象,而不是依赖具体,避免依赖变动时,必须同步变动。

总结

对于CleanCode,视频和文章都是自己再根据现有的代码环境结合产生的。而在实际业务中,每个人遇到的情况不尽相同,还需要根据自己的实际情况进行优化。

命名

  • 有意义的命名
  • 命名的规则
  • 通用命名缩写

注释

  • 尽量多写有意义的注释,一个项目的好与坏从代码层面上看,主要要看注释是否令人看到的懂看的明白

函数

  • 控制函数的参数
  • keep function small & do only one thing
  • 不要在一个函数中融合多个抽象层级的代码

流程控制

  • 优先正向判断
  • 合理应用守护和快速失败
  • 利用面向对象的多态特性

除了以上的这些,还有一些建议

  1. 尽管我们强调SRP,但并非意味着要将类拆分成很小,一定要避免粒度过细的拆分。
  2. 对于设计模式的引入,一定要有实际的收益,否则的话很有可能增加代码复杂度,降低可读性。
  3. 优化和重构一定要进行,但是切记不要过早优化,毕竟过早优化是一切问题的源泉。此外,如果进行重构,可以考虑将重构的代码和正在开发的功能代码做一定的隔离,比如通过不同的commit提交,这样对主程或者后续接手人都有帮助。