《clean-code》阅读整理

最近有朋友推荐了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

  • 文档中说明了这种方式相对于点操作符的好处,加些自己的理解:
  1. 当需要对获取的对象属性执行额外操作时。
  2. 执行 set 时可以增加规则对要变量的合法性进行判断。
  3. 封装了内部逻辑。
  4. 在存取时可以方便的增加日志和错误处理。
  5. 继承该类时可以重载默认行为。
  6. 从服务器获取数据时可以进行懒加载。
  • 这些优点对比的是类的点操作和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,但若是从该类中再实例化一个Instance2Instance2上还是没有DELETE方法,仍旧需要我们去手动调用方法,再添加一次
  • 因此,这样的设计适合于当初始化的数据并不能满足程序需求,以及我们对数据有频繁的修改需求时使用, 如:
class SuperMarket{
  constructor() {
    this.goodsStore = ['candy', 'cookie']
  }
  addGoods(goods) {
    this.goodsStore.push(goods)
  }
}

利斯科夫替代原则(LSP)

“子类对象应该能够替换其超类对象被使用”。
也就是说,如果有一个父类和一个子类,当采用子类替换父类时不应该产生错误的结果。

接口隔离原则(ISP)

“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。”
在 JS 中,当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量的需求是有益的。

依赖反转原则 (DIP)

该原则有两个核心点:

  1. 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口。
  2. 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。

错误处理

代码中 try/catch 的意味着你认为这里可能出现一些错误,你应该对这些可能的错误存在相应的处理方案


书中没有自己体会的部分:

采用函数式编程

  • 个人对于函数式编程还是一知半解,找到了一本书JS函数式编程, 定在我下一本读书计划吧

使用方法链 & 优先使用组合模式而非继承 & 测试

  • 这块个人没有经验,也没有自己的理解,照着文档抄并没有任何意义,等后期有所了解后再补充

避免类型判断

  • 这个点是为弥补js弱类型带来的问题,个人打算后面抽时间学习TS
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容