程序是写给人读的,只是偶尔让计算机执行一下。 —— Donald Ervin Knuthgit
每次 review 过往写的代码,总有一种不忍直视的感受。想提升编码能力,故阅读了一些相关书籍及博文,并有所感悟,今将一些读书笔记及我的心得感悟梳理出来。抛转引玉,但愿这砖能抛得起来。程序员
开始阅读以前,你们能够快速思考一下,你们脑海里的好代码和坏代码都是怎么样的“形象”呢?github
若是看到这一段代码,如何评价呢?算法
if (a && d || b && c && !d || (!a || !b) && c) { // ... } else { // ... } 复制代码
上面这段代码,尽管是特地为举例而写的,要是真实遇到这种代码,想必你们都“一言难尽”吧。你们多多少少都有一些坏味道的代码的“印象”,坏味道的代码总有一些共性:编程
那坏味道的代码是怎样造成的呢?设计模式
对坏味道的代码有一个大概的了解后,或许读者心中有一个疑问:代码的好坏有没有一些量化的标准去评判呢?答案是确定的。bash
接下来,经过了解圈复杂度去衡量咱们写的代码。然而当代码的坏味道已经“弥漫”处处都是了,这时咱们应该了解一下重构。代码到了咱们手里,不能继续“发散”坏味道,这时应该了解如何编写 clean code。此外,咱们还应该掌握一些编码原则及设计模式,这样才能作到有的放矢。markdown
圈复杂度(Cyclomatic complexity,简写CC)也称为条件复杂度,是一种代码复杂度的衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度。架构
圈复杂度能够用来衡量一个模块断定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖全部的可能状况最少使用的测试用例数。app
圈复杂度能够经过程序控制流图计算,公式为:
V(G) = e + 2 - n
有一个简单的计算方法:圈复杂度实际上就是等于断定节点的数量再加上1。
注:
if else
、switch case
、for循环
、三元运算符
、||
、&&
等,都属于一个断定节点。
代码复杂度低,代码不必定好,但代码复杂度高,代码必定很差。
圈复杂度 | 代码情况 | 可测性 | 维护成本 |
---|---|---|---|
1 - 10 | 清晰、结构化 | 高 | 低 |
10 - 20 | 复杂 | 中 | 中 |
20 - 30 | 很是复杂 | 低 | 高 |
>30 | 不可读 | 不可测 | 很是高 |
ESLint 提供了检测代码圈复杂度的 rules。开启 rules 中的 complexity 规则,并将圈复杂度大于 0 的代码的 rule severity 设置为 warn 或 error 。
rules: { complexity: [ 'warn', { max: 0 } ] } 复制代码
借助 ESLint 的 CLIEngine ,在本地使用自定义的 ESLint 规则扫描代码,并获取扫描结果输出。
不少状况下,下降圈复杂度就能提升代码的可读性了。针对圈复杂度,结合例子给出一些改善的建议:
经过抽象配置将复杂的逻辑判断进行简化。
before:
// ... if (type === '扫描') { scan(args) } else if (type === '删除') { delete(args) } else if (type === '设置') { set(args) } else { // ... } 复制代码
after:
const ACTION_TYPE = { 扫描: scan, 删除: delete, 设置: set } ACTION_TYPE[type](args) 复制代码
将代码中的逻辑进行抽象提炼成单独的函数,有利于下降代码复杂度和下降维护成本。尤为是当一个函数的代码很长,读起来很费力的时候,就应该思考可否提炼成多个函数。
before:
function example(val) { if (val > MAX_VAL) { val = MAX_VAL } for (let i = 0; i < val; i++) { doSomething(i) } // ... } 复制代码
after:
function setMaxVal(val) { return val > MAX_VAL ? MAX_VAL : val } function getCircleArea(val) { for (let i = 0; i < val; i++) { doSomething(i) } } function example(val) { return getCircleArea(setMaxVal(val)) } 复制代码
某些复杂的条件判断可能逆向思考后会变的更简单,还能减小嵌套。
before:
function checkAuth(user){ if (user.auth) { if (user.name === 'admin') { // ... } else if (user.name === 'root') { // ... } } } 复制代码
after:
function checkAuth(user){ if (!user.auth) return if (user.name === 'admin') { // ... } else if (user.name === 'root') { // ... } } 复制代码
将冗余的条件合并,而后再进行判断。
before:
if (fruit === 'apple') { return true } else if (fruit === 'cherry') { return true } else if (fruit === 'peach') { return true } else { return true } 复制代码
after:
const redFruits = ['apple', 'cherry', 'peach'] if (redFruits.includes(fruit) { return true } 复制代码
对复杂难懂的条件进行提取并语义化。
before:
if ((age < 20 && gender === '女') || (age > 60 && gender === '男')) { // ... } else { // ... } 复制代码
after:
function isYoungGirl(age, gender) { return (age < 20 && gender === '女' } function isOldMan(age, gender) { return age > 60 && gender === '男' } if (isYoungGirl(age, gender) || isOldMan(age, gender)) { // ... } else { // ... } 复制代码
后文有简化条件表达式更全面的总结。
重构一词有名词和动词上的理解。名词:
对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提升其可理解性,下降其修改为本。
动词:
使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
若是遇到如下的状况,可能就要思考是否须要重构了:
为什么重构,不外乎如下几点:
本文讨论的内容只涉及第一点,仅限代码级别的重构。
第一次作某件事时只管去作;第二次作相似的事会产生反感,但不管如何仍是能够去作;第三次再作相似的事,你就应该重构。
关键思想:一致的风格比“正确”的风格更重要。
原则:
注释的目的是尽可能帮助读者了解得和做者同样多。所以注释应当有很高的信息/空间
率。
标记 | 一般的意义 |
---|---|
TODO: | 还没处理的事情 |
FIXME: | 已知的没法运行的代码 |
HACK: | 对一个问题不得不采用的比价粗糙的解决方案 |
关键思想:把信息装入名字中。
良好的命名是一种以“低代价”取得代码高可读性的途径。
“把信息装入名字中”包括要选择很是专业的词,而且避免使用“空洞”的词。
单词 | 更多选择 |
---|---|
send | deliver, despatch, announce, distribute, route |
find | search, extract, locate, recover |
start | launch, create, begin, open |
make | create, set up, build, generate, compose, add, new |
在给变量、函数或者其余元素命名时,要把它描述得更具体而不是更抽象。
若是关于一个变量有什么重要事情的读者必须知道,那么是值得把额外的“词”添加到名字中的。
正 | 反 |
---|---|
add | remove |
create | destory |
insert | delete |
get | set |
increment | decrement |
show | hide |
start | stop |
有一个复杂的条件(if-then-else)语句,从if、then、else三个段落中分别提炼出独立函数。根据每一个小块代码的用途,为分解而获得的新函数命名,并将原函数中对应的代码改成调用新建函数,从而更清楚地表达本身的意图。对于条件逻辑,能够突出条件逻辑,更清楚地代表每一个分支的做用,而且突出每一个分支的缘由。
有一系列条件测试,都获得相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
在条件表达式的每一个分支上有着相同的一段代码,将这段重复代码搬移到条件表达式以外。
函数中的条件逻辑令人难以看清正常的执行路径。使用卫语句表现全部特殊状况。
若是某个条件极其罕见,就应该单独检查该条件,并在该条件为真时马上从函数中返回。这样的单独检查经常被称为“卫语句”(guard clauses)。
经常能够将条件表达式反转,从而实以卫语句取代嵌套条件表达式,写成更加“线性”的代码来避免深嵌套。
变量存在的问题:
若是有一个临时变量,只是被简单表达式赋值一次,而将全部对该变量的引用动做,替换为对它赋值的那个表达式自身。
以一个临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中。将这个临时变量的全部引用点替换为对新函数的调用。此后,新函数就可被其余函数使用。
接上条,若是该表达式比较复杂,建议经过一个总结变量名来代替一大块代码,这个名字会更容易管理和思考。
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
在条件逻辑中,引入解释性变量特别有价值:能够将每一个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。使用这项重构的另外一种状况是,在较长算法中,能够运用临时变量来解释每一步运算的意义。
好处:
程序有某个临时变量被赋值超过一次,它既不是循环变量,也不是用于收集计算结果。针对每次赋值,创造一个独立、对应的临时变量。
临时变量有各类不一样用途:
若是临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每一个变量只承担一个责任。
有一个字面值,带有特别含义。创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
let done = false; while (condition && !done) { if (...) { done = true; continue; } } 复制代码
像done这样的变量,称为“控制流变量”。它们惟一的目的就是控制程序的执行,没有包含任何程序的数据。控制流变量一般能够经过更好地运用结构化编程而消除。
while (condition) { if (...) { break; } } 复制代码
若是有多个嵌套循环,一个简单的break不够用,一般解决方案包括把代码挪到一个新函数中。
当一个过长的函数或者一段须要注释才能让人理解用途的代码,能够将这段代码放进一个独立函数中。
一个函数过长才合适?长度不是问题,关键在于函数名称和函数本体之间的语义距离。
某个函数既返回对象状态值,又修改对象状态。创建两个不一样的函数,其中一个负责查询,另外一个负责修改。
有一个函数,其中彻底取决于参数值而采起不一样行为。针对该参数的每个可能值,创建一个独立函数。
某些参数老是很天然地同时出现,以一个对象取代这些参数。
能够经过立刻处理“特殊状况”,并从函数中提早返回。
若是有很难读的代码,尝试把它所作的全部任务列出来。其中一些任务能够很容易地变成单独的函数(或类)。其余的能够简单地成为一个函数中的逻辑“段落”。
在循环中,与提前返回相似的技术是continue
。与if...return;
在函数中所扮演的保护语句同样,if...continue;
语句是循环中的保护语句。(注意:JavaScript 中 forEach 的特殊性)
对于一个布尔表达式,有两种等价写法:
可使用这些法则让布尔表达式更具备可读性。例如:
// before if (!(file_exitsts && !is_protected)) Error("sorry, could not read file.") // after if (!file_exists || is_protected) Error("sorry, could not read file.") 复制代码
使用相关定律能优化开始举例的那段代码:
// before if (a && d || b && c && !d || (!a || !b) && c) { // ... } else { // ... } // after if (a && d || c) { // ... } else { // ... } 复制代码
具体简化过程及涉及相关定律能够参考这篇推文:你写这样的代码,不怕同事打你嘛?
所谓工程学就是关于把大问题拆分红小问题再把这些问题的解决方案放回一块儿。
把这条原则应用于代码会使代码更健壮而且更容易读。
积极地发现并抽取不相关的子逻辑,是指:
若是你不能把一件事解释给你祖母听的话说明你还没真正理解它。 --阿尔伯特·爱因斯坦
步骤以下:
有必要熟知前人总结的一些经典的编码原则及涉及模式,以此来改善咱们既有的编码习惯,所谓“站在巨人肩上编程”。
SOLID 是面向对象设计(OOD)的五大基本原则的首字母缩写组合,由俗称“鲍勃大叔”的Robert C.Martin在《敏捷软件开发:原则、模式与实践》一书中提出来。
A class should have only one reason to change.
一个类应该有且仅有一个缘由引发它的变动。
通俗来说:一个类只负责一项功能或一类类似的功能。固然这个“一”并非绝对的,应该理解为一个类只负责尽量独立的一项功能,尽量少的职责。
优势:
缺点:
这条定律一样适用于组织函数时的编码原则。
Software entities (classes,modules,functions,etc.)should be openfor extension,but closed for modification.
软件实体(如类、模块、函数等)应该对拓展开放,对修改封闭。
在一个软件产品的生命周期内,不可避免会有一些业务和需求的变化,咱们在设计代码的时候应该尽量地考虑这些变化。在增长一个功能时,应当尽量地不去改动已有的代码;当修改一个模块时不该该影响到其余模块。
const makeSound = function( animal ) { animal.sound(); }; const Duck = function(){}; Duck.prototype.sound = function(){ console.log( '嘎嘎嘎' ); }; const Chicken = function(){}; Chicken.prototype.sound = function(){ console.log( '咯咯咯' ); }; makeSound( new Duck() ); // 嘎嘎嘎 makeSound( new Chicken() ); // 咯咯咯 复制代码
Functions that use pointers to base classes must be able to useobjects of derived classes without knowing it.
全部能引用基类的地方必须能透明地使用其子类的对象。
只要父类能出现的地方子类就能出现(就能够用子类来替换它)。反之,子类能出现的地方父类不必定能出现(子类拥有父类的全部属性和行为,但子类拓展了更多的功能)。
Clients should not be forced to depend upon interfaces that they don't use.Instead of one fat interface many small interfaces arepreferred based on groups of methods,each one serving onesubmodule.
客户端不该该依赖它不须要的接口。用多个细粒度的接口来替代由多个方法组成的复杂接口,每个接口服务于一个子模块。
接口尽可能小,可是要有限度。当发现一个接口过于臃肿时,就要对这个接口进行适当的拆分。可是若是接口太小,则会形成接口数量过多,使设计复杂化。
High level modules should not depend on low level modules; bothshould depend on abstractions.Abstractions should not depend ondetails.Details should depend upon abstractions.
高层模块不该该依赖低层模块,两者都该依赖其抽象。抽象不该该依赖细节,细节应该依赖抽象。
把具备相同特征或类似功能的类,抽象成接口或抽象类,让具体的实现类继承这个抽象类(或实现对应的接口)。抽象类(接口)负责定义统一的方法,实现类负责具体功能的实现。
没有这么充足的时间遵循这些原则去设计,或遵循这些原则设计的实现成本太大。在受现实条件所限不能遵循五大原则来设计时,咱们还能够遵循下面这些更为简单、实用的原则。
Each unit should have only limited knowledge about other units: onlyunits "closely"related to the current unit.Only talk to your immediatefriends,don't talk to strangers.
每个逻辑单元应该对其余逻辑单元有最少的了解:也就是说只亲近当前的对象。只和直接(亲近)的朋友说话,不和陌生人说话。
这一原则又称为迪米特法则,简单地说就是:一个类对本身依赖的类知道的越少越好,这个类只须要和直接的对象进行交互,而不用在意这个对象的内部组成结构。
例如,类A中有类B的对象,类B中有类C的对象,调用方有一个类A的对象a,这时若是要访问C对象的属性,不要采用相似下面的写法:
a.getB().getC().getProperties()
复制代码
而应该是:
a.getProperties()
复制代码
Keep It Simple and Stupid.
保持简单和愚蠢。
DRY原则(Don't Repeat Yourself)
不要重复本身。
不要重复你的代码,即屡次遇到一样的问题,应该抽象出一个共同的解决方法,不要重复开发一样的功能。也就是要尽量地提升代码的复用率。
要遵循DRY原则,实现的方式很是多:
DRY原则在单人开发时比较容易遵照和实现,但在团队开发时不太容易作好,特别是对于大团队的项目,关键仍是团队内的沟通。
You aren't gonna need it,don't implement something until it isnecessary.
你不必那么着急,不要给你的类实现过多的功能,直到你须要它的时候再去实现。
Rule of three 称为“三次法则”,指的是当某个功能第三次出现时,再进行抽象化,即事不过三,三则重构。
保证方法的行为严格的是命令或者查询,这样查询方法不会改变对象的状态,没有反作用;而会改变对象的状态的方法不可能有返回值。
设计模式的开山鼻祖 GoF 在《设计模式:可复用面向对象软件的基础》一书中提出的23种经典设计模式被分红了三类,分别是建立型模式、结构型模式和行为型模式。
在面向对象软件设计过程当中针对特定问题的简洁而优雅的解决方案。
经常使用的设计模式有:策略模式、发布—订阅模式、职责链模式等。
好比策略模式使用的场景:
策略模式:定义一系列的算法,把它们一个个封装起来,而且使它们能够相互替换。
if (account === null || account === '') { alert('手机号不能为空'); return false; } if (pwd === null || pwd === '') { alert('密码不能为空'); return false; } if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(account)) { alert('手机号格式错误'); return false; } if(pwd.length<6){ alert('密码不能小于六位'); return false; } 复制代码
使用策略模式:
const strategies = { isNonEmpty: function (value, errorMsg) { if (value === '' || value === null) { return errorMsg; } }, isMobile: function (value, errorMsg) { // 手机号码格式 if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(value)) { return errorMsg; } }, minLength: function (value, length, errorMsg) { if (value.length < length) { return errorMsg; } } }; const accountIsMobile = strategies.isMobile(account,'手机号格式错误'); const pwdMinLength = strategies.minLength(pwd,8,'密码不能小于8位'); const errorMsg = accountIsMobile || pwdMinLength; if (errorMsg) { alert(errorMsg); return false; } 复制代码
又好比,发布—订阅模式具备的特色:
既能够用在异步编程中,也能够帮助咱们完成更松耦合的代码编写。
若是你们须要了解设计模式更多知识,建议另外找资料学习。
宋代禅宗大师青原行思提出参禅的三重境界:
参禅之初,看山是山,看水是水;禅有悟时,看山不是山,看水不是水;禅中彻悟,看山还是山,看水还是水。
同理,编程一样存在境界:编程的一重境界是照葫芦画瓢,二重境界是能够灵活运用,三重境界则是心中无模式。惟有多实践,多感悟,方能突破一重又一重的境界。
最后,愿你们终将能写出本身再也不讨厌的代码。
真的是最后了,有空会补上上述的示例代码,欢迎你们 Star & PR 呀:你所须要知道的代码整洁之道