最近有朋友推荐了clean-code, 简单阅读之后,发现是一些好的编程实践,决定在阅读过程中记录下自己没做到的或做的不好的,便于后面翻阅反思
文档在这:clean-code-js
使用说明变量:
- 文档中例子:
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);
// =>
saveCityState(city, state);
- 该例子个人理解:使用说明变量的目的是提高代码可读性,但该例子是否需要遵循可以完全看个人意愿
- 原因:对于阅读代码的人来说,提出参数固然能提升代码可读性,但对于使用者来说,函数本身在定义时,已经为其两个参数赋予了意义,这种情况下,阅读代码的人只要懂这个函数,肯定能明白参数的含义,多使用两个变量反而有点浪费
避免无意义的条件判断
- 这一点就是代码简化的一部分,在不影响代码可读性的情况下,可以使用 || 或者 三目运算符等来简化代码,个人平时有注意到,但是有些地方还做的不好
函数
函数参数(理想情况下应不超过2个)
- 参数数量这块没有注意过,不过对于分离函数功能这块,自己确实做的不好,后面需要深入了解一些SOLID之类的东西
函数功能的单一性
- 即是SOLID中的S,但自己对于功能单一的粒度难以掌控,平时拆分函数都是看到函数过长,操作过多,或者函数中的某一部分功能需要复用,才会去进行拆分,比如例子中:
function emailClients(clients) {
clients.forEach(client => {
let clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
// =>
function emailClients(clients) {
clients.forEach(client => {
emailClientIfNeeded(client);
});
}
function emailClientIfNeeded(client) {
if (isClientActive(client)) {
email(client);
}
}
function isClientActive(client) {
let clientRecord = database.lookup(client);
return clientRecord.isActive();
}
- 如果让我个人来做的话, 会觉得原本的函数已经足够简单,无需再简化, 后面需要请教一下朋友关于功能单一的理解
不要使用标记(Flag)作为函数参数
这通常意味着函数的功能的单一性已经被破坏。此时应考虑对函数进行再次划分。
- 个人经常做这样的操作,比如正在做的毕设里就有这样的代码:
<el-input
class="username-input"
@focus="() => {changeLoginImg('drawn')}"
></el-input>
<el-input
class="password-input"
@focus="() => {changeLoginImg('muffle')}"
></el-input>
changeLoginImg(signal) {
const img = signal === 'drawn'?drawn:muffle;
this.$refs['login-img'].setAttribute('src', img);
}
- 但个人感觉这种做法的也并未破坏函数单一性,反而若是将两个操作分开,倒是使得代码更加冗余?
- 考虑一下,这个点应该是不需硬性要求的,像我上面的情况,个人感觉现在的处理反而更合理
避免副作用
当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。
程序在某些情况下确实需要副作用这一行为,如先前例子中的写文件。这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。
- 这个点的意思即是代码中应该总是使用纯函数,并将所有的非纯函数集合在一起,这一点与redux三大原则中的使用纯函数来执行修改是同一个理念
不要写全局函数
- 例子给出了一个为全局Array扩充自己函数的一个方式,不同于定义在Array.prototype的做法,这样可以避免污染全局变量:
class SuperArray extends Array {
constructor(...args) {
super(...args)
}
myFun() {
// ...
}
}
- ES6的class写法自己早就了解过了,但平时却并不会去使用它,究其原因,便是自己对面向对象思想并不熟悉,思维方式还是面向过程的,没有类和对象的概念,后面需要学一下
封装判断条件
- 书中只给了一个例子:
// 反例:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
/// ...
}
// 正例:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
- 个人经验,在读代码时候确实会有花几分钟去读一个复杂的“&& || ===”这种复杂的判断条件的时候,这种写法对写的人来说很清晰,但对于读代码来说确实意义不明
- 因此,将判断条件封装一下是个比较好的实践,后面编码时需要注意
避免条件判断
这看起来似乎不太可能。
大多人听到这的第一反应是:“怎么可能不用 if 完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。
第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。
// 反例:
class Airplane {
getCruisingAltitude() {
switch (this.type) {
case '777':
return getMaxAltitude() - getPassengerCount();
case 'Air Force One':
return getMaxAltitude();
case 'Cessna':
return getMaxAltitude() - getFuelExpenditure();
}
}
}
// 正例:
class Airplane {
//...
}
class Boeing777 extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getPassengerCount();
}
}
class AirForceOne extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude();
}
}
class Cessna extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getFuelExpenditure();
}
}
- 个人并不能理解这种做法,这种不使用条件判断的做法本质上只是把判断向上提了一层而已,这样修改之后,也会需要使用判断来决定调用哪个类的实例,只要程序的逻辑上存在分支,总是需要判断的
- 而保持功能的单一性这个功能来说,这样的改动确实合理,将原本函数的三个功能抽离到各自的类中去
- 因此,个人感觉这个例子也应该作为功能单一性的实践,至于标题所说的避免条件判断,感觉不必这样考虑
对象和数据结构
使用getters和setters
- 文档中说明了这种方式相对于点操作符的好处,加些自己的理解:
- 当需要对获取的对象属性执行额外操作时。
- 执行 set 时可以增加规则对要变量的合法性进行判断。
- 封装了内部逻辑。
- 在存取时可以方便的增加日志和错误处理。
- 继承该类时可以重载默认行为。
- 从服务器获取数据时可以进行懒加载。
- 这些优点对比的是类的点操作和gettters,放在Object类型数据上并不一定适用,这点需要注意
- 第4点可以类比vuex中的getter和mutation&action,只要将数据的存取集合在一起,就能方便数据的跟踪和调试
Class
SOLID
单一职责原则(SRP):
最小化对一个类需要修改的次数是非常有必要的。如果一个类具有太多太杂的功能,当你对其中一小部分进行修改时,将很难想象到这一修够对代码库中依赖该类的其他模块会带来什么样的影响。
- 个人对于单一职责的范围并不是很理解,但上面这句话有些经验:
- 编码过程中,修改一个类的功能之后,往往需要检查其它使用该类的地方,检查修改是否会造成一些意外的影响,当一个类的功能过于复杂时,就代表它会在多个完全不同的地方使用,我们修改后的影响范围也就越大,修改后检查的工作量也就越大.单一职责的必要性就源于此
开/闭原则(OCP):
“代码实体(类,模块,函数等)应该易于扩展,难于修改。”
这一原则指的是我们应允许用户方便的扩展我们代码模块的功能,而不需要打开 js 文件源码手动对其进行修改。
// 反例:
class AjaxRequester {
constructor() {
// What if we wanted another HTTP Method, like DELETE? We would have to
// open this file up and modify this and put it in manually.
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}
}
// 正例:
class AjaxRequester {
constructor() {
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}
addHTTPMethod(method) {
this.HTTP_METHODS.push(method);
}
}
- 这个例子展示的是一个扩展代码的过程,当这个类需要一个DELETE的方法时,不去修改源码,而是在设计类时,就设计了这样一个扩展方法,可以在使用的过程中随时添加方法
- 感觉这个例子有些不太恰当,封装的ajax工具应当是一个通用的工具类,它内部的
HTTP_METHODS
应该是可以预知(请求的methods就那么几种),且在程序中无论何时都适用的(不需要频繁的增加或删除methods), 因此个人觉得addHTTPMethod的设计并无必要 - 个人经验:平时代码过程中,是否需要这样设计类(设计扩展方法)是需要根据具体的功能需求来设计的,这样的设计虽然避免了直接修改源文件,但却也有一些坏处:
- 搞清楚两个代码分别的含义 :
- 构造器中的
HTTP_METHODS
是初始化的方法,每次打开程序,或者new一个AjaxRequester
实例,它们都是存在的;- 而使用addHTTPMethod创建的方法,只会在程序运行过程中存在,且只存在于当前实例中。
- 因此:
对于前者,缺点即是不能随时使用代码去增加修改,必须去修改代码
而后者,虽然我们在使用Instance1.addHTTPMethod('DELETE')
后,当前实例确实添加了这个method
,但若是从该类中再实例化一个Instance2
,Instance2
上还是没有DELETE方法,仍旧需要我们去手动调用方法,再添加一次
- 因此,这样的设计适合于当初始化的数据并不能满足程序需求,以及我们对数据有频繁的修改需求时使用, 如:
class SuperMarket{
constructor() {
this.goodsStore = ['candy', 'cookie']
}
addGoods(goods) {
this.goodsStore.push(goods)
}
}
利斯科夫替代原则(LSP)
“子类对象应该能够替换其超类对象被使用”。
也就是说,如果有一个父类和一个子类,当采用子类替换父类时不应该产生错误的结果。
接口隔离原则(ISP)
“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。”
在 JS 中,当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量的需求是有益的。
依赖反转原则 (DIP)
该原则有两个核心点:
- 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口。
- 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。
错误处理
代码中 try/catch 的意味着你认为这里可能出现一些错误,你应该对这些可能的错误存在相应的处理方案
书中没有自己体会的部分:
采用函数式编程
- 个人对于函数式编程还是一知半解,找到了一本书JS函数式编程, 定在我下一本读书计划吧
使用方法链 & 优先使用组合模式而非继承 & 测试
- 这块个人没有经验,也没有自己的理解,照着文档抄并没有任何意义,等后期有所了解后再补充
避免类型判断
- 这个点是为弥补js弱类型带来的问题,个人打算后面抽时间学习TS