接上篇
特性依恋:
类的方法应该只对它所属的类中的变量和函数感兴趣,而不应该去调用到其它类的变量,函数。如果有了这种写法,我们就应该消除掉,将这个方法写到另一个类中。有一些特殊情况 比如另一个类加这个方法会破坏好几种面向对象设计原则时,我们可以取舍一下。
选择算子参数
算子参数指的是,用来作为条件判断的参数。比如说传入一个布尔值,当值为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的()里面不要拼接太多的语句。如果有多个条件 可以把他们封装成一个函数,这样布尔值就有了上下文 比较便于理解。
例如
if(shouldBeDeleted(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的逻辑。