读书笔记《代码整洁之道》味道与启发 中 (一般性问题)

261 阅读8分钟

接上篇

特性依恋:

类的方法应该只对它所属的类中的变量和函数感兴趣,而不应该去调用到其它类的变量,函数。如果有了这种写法,我们就应该消除掉,将这个方法写到另一个类中。有一些特殊情况 比如另一个类加这个方法会破坏好几种面向对象设计原则时,我们可以取舍一下。

选择算子参数

算子参数指的是,用来作为条件判断的参数。比如说传入一个布尔值,当值为true的时候写一段逻辑,值为false的时候 写另一种逻辑,这样就在一个函数里写了多种场景,不符合单一职责原则。我们应该把这种情况换成三个小函数。一个处理一种情况,然后再用一个函数把他们结合起来。

晦涩的意图

代码要有表达力,也就是说要让人看得懂,不要写很长一串 (从这个角度来说,promise的链式表达力就不如async await了)

位置错误的权责

 开发者做出的最重要的决定是在哪里放代码,比如pi常量的位置到底是放在Math类中,Circle类中 还是Trigonimetry类呢? 最小惊异原则说 我们应该把代码放在读者期待的位置。pi常量经常出现在声明三角函数的地方,所以应该放到Trigonimetry中。也就是说 我们要放在更利于阅读 还不是更利于开发的地方。

不恰当的静态方法

Math.max(a,b)是个很好的静态方法,因为不需要在单个实例操作,它只关心传入的参数a,b 并不需要关心 是哪个对象实例化了它。 这样的方法 就可以写成静态方法。 也就是说 如果一个函数与实例化对象无关,只与传入参数有关,那这个方法就可以写为静态方法。  反之,如果我们希望这个方法是多态的,那么就不应该写为静态方法。

使用解释性变量:

解释性变量指的是 把一些计算得来的值再取个有意义的名字 重新命名一下。

let key  = match.group(1);

let value = match.group(2);

直接看key会比match.group(1)更易懂,这样就能增加代码的可读性。(这是个好的用法)

函数名称应该表达其行为:

函数的名字应该能让人一眼看得出来它做了什么 举个反面例子

Date newDate = date.add(5

这个add就看不出来到底添加的是什么。这是有歧义的,可能是5天,5个小时,5个星期。而且加了5之后date本身是否会改变也看不出来。 

所以如果是想修改当前日期 给它加5天 应该命名为 addDaysTo() 或者increaseByDays() 。如果并不想修改当前日期,只是想获取5天后的日期 可以命名为 daysLater() 或者daysSince()

理解算法

很多if else的逻辑是因为开发者 不懂算法。所以需要开发者提高自己的算法水平。真正的去理解自己写这段代码的算法是否恰当。

把逻辑依赖改为物理依赖

逻辑依赖指的是 依赖模块对被依赖模块代码逻辑有假定。

举个例子来说 有个模块类是 HourlyReporter 是用来收集员工工作时长的数据的。还有个员工时长打印类HourlyReportFormatter。 代码流程是先调用HourlyReporter来处理数据,数据处理完毕后,调用HourlYReportFormatter的打印方法print. 此时HourlyReporter代码里写了这样一句 

if(page.size() == PAGE_SIZE){
  print()
}

这里就出现逻辑依赖了,因为他要求HourlyReporter知道 PAGE_SIZE 这是不合理的。 因为这种东西 应该属于HourlYReportFormatter管理,这里还有个错误叫 位置的错误权责。改正办法是给他变成物理依赖。 物理依赖指的是从代码里能直观看出来的依赖。 我们可以在  HourlyReporter中写一个getMaxSize() 在这个函数中去获取定义在HourlYReportFormatte里的pageSize 而不是 直接使用常量。

这个对前端来说也是很好借鉴的,将类理解为组件,每个组件各司其职,不要写一些逻辑上的依赖,如果有的话通过props传值的方式 物理化这种依赖.

用多态替代if/else 或者switch/case

我们在使用switch之前 就可以先考虑一下 是否可以用多态替换。对于前端来说 这个很难借鉴。因为前端主要靠组件化来分模块,每个组件都很少用到多态的思想。很少有重写基类的方法的场景,

遵循标准约定

每个团队都有一些通用的编码标准。包括在哪里声明变量,哪里导入文件,如何命名,何如换行等。每个成员应该都遵循这种约定。 这也要求团队的开发者要实时的生成,维护这样一份规范表。

用命名常量替代魔术数

魔术数也就是硬编码 指的是一些写死的值。比如说每页展示50条数据,这个50 最好不要写在代码里,而是用一个常量PAGE_SIZE定义一下。然后在代码逻辑里再比较这个常量。

当然有一些常量是大家都知道的不会改变的 就可以不用常量表示,比如说一天有24个小时。这个24就可以不用常量表示。

结构基于约定:

约定指的是通过一些文档之类的对变量的命名和一些写法制定约束,。结构指的是对一些代码组织上的约束。

举个例子 约定可以让开发者写出命名良好的switch/case 语句,结构可以通过抽离基类来实现代码的重新组织。结构构建好了开发者就会自动遵从了,而约定的话 却非常考验开发人员的严谨性了。

(前端方面 我还没想到有类似的例子。)

封装条件

对于条件判断语句 最好将括号内的内容进行封装。也就是最好if的()里面不要拼接太多的语句。如果有多个条件 可以把他们封装成一个函数,这样布尔值就有了上下文 比较便于理解。

例如

ifshouldBeDeleted(timer)){}

好于

if(timer.hasExpired && !timer.isRecurrent){}
避免否定性条件

否定式要比肯定难明白一些。所以尽量用肯定的

if(buffer.shouldCompact()){}

好于

if(!buffer.shouldNotCOmpact()){}

(但是我觉得对于一些边界条件 还是可以放在函数的头部去做一些否定性判断 比如下面这样)

function doSomething(){
  if(!buffer.shouldNotCOmpact()){
     return;
  }
  //其他逻辑 巴拉巴拉
}

好于

function doSomething(){
  if(buffer.shouldCompact()){

    //其他逻辑 巴拉巴拉

  }
}
函数只该做一件事

不要在一个函数里处理多件事情。如果已经有这样的函数了,记得把它们拆分成小函数。这样代码组织更为灵活。

掩蔽时序耦合

有时候函数需要按照时间顺序调用,我们可以通过创建函数的时候 来显示这种时序性,这样就能看出来他们前后调用的顺序是不可变更的,是有时序性的。比如 

function dive(){
  saturateGradient();
  retivulateSplines();
  diveForMoog(reason);
}

不如这样

function dive(){
  let gradient = saturateGradient();
  let splines = retivulateSplines(gradient);
  diveForMoog(splines,reason);
}

这样虽然稍微复杂了一点。但是下次再改这段代码的时候 就不太可能发生不小心变换了顺序 导致的问题。因为这样编译就会出错了。

封装边界条件

边界条件集中到一处,不要散落的代码中。比如判断好几个地方都判断了level+1 那就可以用额nextLevel来保存一下这个边界条件,不要在代码里写很多重复的level+1。

这里我认为更多的场景可能在判断数组长度上,如果好几个地方都判断同一个数组的长度arr.length。那就弄个变量定义一下。

函数应该只在一个抽象层级上

这里作者举了例子 是动态创建一个html的逻辑。 这个函数创建了hr标签,并且给他在标签里设置了长度。表面上就是生成字符串

<hr size =“size变量”/> 

表面上看是一个抽象层级。

但实际上是两个层级,一个是创建标签,一个是设置长度。可以分成两个函数来处理。先创建标签,然后再通过addAttribute(“size”,size变量)来添加高度设置。 这就需要我们在实践中去不断总结了。

在较高层层级放置可配置数据

在较高层级设置一些常量,可以往下不断的传递,不要在较低层级的函数里设置,这样比较好找。也就是说 如果你想设置一个常量,你不要在当前函数里直接写,首先先区分一下它是否还有较高层级的函数,如果有的话,考虑一下 放入高层级的函数中。

避免传递浏览

如果一个模块A需要依赖另一个模块B,那模块B就应该提供它能提供的值,而不是作为中介 让A再去找另一个模块C 。也就是不要写 a.getB().getC().dongSomething 之类的逻辑。(我理解就是 不要有中间商,如果有这个设计可能就有问题)因为万一还要要在B和C再加一个中间商的话 就需要全部改写重新A调用B的逻辑。