《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 zhuanlan.zhihu.com/p/99246269 「三省吾身丶丶」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

几个业务场景中的重构示例

请求顺序依赖

在这种场景中,首先还是业务的复杂度决定了代码的复杂度。首先我们来看一个在前端和node都有可能出现的一个简单的例子:

我们有 A, B, C, D 四个请求获取数据的函数(函数自己实现), C 依赖 B 的结果,D 依赖 ABC 的结果,最终输出 D 的结果。

错误示例

// 伪代码
function A(callbak) {
ajax(url, function(res) {
callbak(res);
});
}
// ... 剩下 同上
// 实现如下:
A(function(resa) {
B(function(resb) {
C(resb, function(resc) {
D(resa, resb, resc, function(resd) {
console.log("this is D result:", resd);
});
});
});
});

虽然这个代码是故意写成这样的,不过确实也有在一些初学者身上看到过。这份代码还是能正确给出结果的,但是写法丑陋,回调地狱。如果后来人不进行重构,还有请求依赖,得继续回调嵌套。性能太差,没有考虑 A 和 B 实际上是可以并发的。

这里介绍了一下最原始的 callback ... 中间大家可以去回顾一下 整个 ES2015+ ,callback (async.js) --> Promise --> generator + co --> async + await 的进化过程。其实是从原生的语法层面不断去简化和增强我们对于异步的控制能力。

下面直接给目前阶段原生提供的终极方案:基于 Promise + async/await

正确示例

function A() {
return new Promise(r =>
setTimeout(() => {
r("a");
}, 2000)
);
}
// ...剩下同上
async function asyncBC() {
const resb = await B();
const resc = await c(resb);
return { resb, resc };
}
async function asyncTask() {
const [resa, { resb, resc }] = await Promise.all([A(), asyncBC()]);
const resd = await D(resa, resb, resc);
return resd;
}
asyncTask().then(resd => {
console.log("this is D result:", resd);
});

我们重新思考了一下上面的问题,理清楚了逻辑顺序的依赖。并且用最新的语法。

使用 Promise.all 结合 async/await 的形式,考虑了并发和串行,写法简洁,达到了在示例要求下的最快方案。解决了无限嵌套的问题。这是跟随语言进化本身带给我们可以进行的优化。

但又不仅仅如此。我们将问题进行归类 将 B,C 有依赖顺序的请求,抽离出单独的函数。让他们去处理自身的逻辑。这个点我们稍后再提。

折磨人的 if else

可能存在下面一些问题

  1. 过多的嵌套
  2. 逻辑处理冗余
  3. 没有做好防御编程(错误处理

直接来一个代码例子,这是一个获取背景颜色的方法,但是随着业务的不断变化,背景颜色的来源越来越多,在一些业务人员的处理下可能是这样的:

错误示例

const _getPageBgColor = (pageInfo, pageUrlObj) => {
let bgColor = "";
if (window.__isMyCache) {
if (pageInfo && pageInfo.theme) {
bgColor = pageInfo.theme.backgroundColor;
} else {
if (window.__customMyConfig.backgroundMap) {
let queryParam = pageUrlObj.params;
if (queryParam.myPid) {
let pids = queryParam.myPid.split("-");
if (pids.length === 2) {
bgColor = window.__customMyConfig.backgroundMap[pids[1]];
} else {
bgColor = window.__customMyConfig.backgroundMap[pids[0]];
}
}
}
if (!bgColor && window.__customMyConfig.customBgMap) {
Object.keys(window.__customMyConfig.customBgMap).forEach(item => {
if (this.pageUrl.indexOf(item) > 0) {
bgColor = window.__customMyConfig.customBgMap[item];
}
});
}
}
} else {
if (window.__pageTheme) {
bgColor = window.__pageTheme.backgroundColor;
}
}
return bgColor;
};

相信你在读上面的代码的时候是极为痛苦的,想要一目了然的知道最终会进入哪个分支,基本不可能。

于是基于下面两个原则

  • 合理的抽取成函数
  • 错误优先返回

有了一个基础版本的重构:

正确示例

const getBackgroundMapBgColor = pageUrlObj => {
if (!window.__customMyConfig.backgroundMap) return "";
let queryParam = pageUrlObj.params;
if (!queryParam.myPid) return "";
const pids = queryParam.myPid.split("-");
if (pids.length === 2) {
return window.__customMyConfig.backgroundMap[pids[1]];
}
return window.__customMyConfig.backgroundMap[pids[0]];
};
const getCustomBgMapBgColor = () => {
let bgColor = "";
if (window.__customMyConfig.customBgMap) return "";
Object.keys(window.__customMyConfig.customBgMap).forEach(item => {
if (this.pageUrl.indexOf(item) !== -1) {
bgColor = window.__customMyConfig.customBgMap[item];
}
});
return bgColor;
};
const _getPageBgColor = (pageInfo, pageUrlObj) => {
if (!window.__isMyCache && !window.__pageTheme) return "";
if (window.__pageTheme) {
return window.__pageTheme.backgroundColor;
}
if (pageInfo && pageInfo.theme) {
return pageInfo.theme.backgroundColor;
}
let bgColor = getBackgroundMapBgColor(pageUrlObj);
if (!bgColor) bgColor = getCustomBgMapBgColor();
return bgColor;
};

可以看到整个逻辑,经过了重新梳理。拆分成了三个函数,子方法分别去处理对应层级的逻辑,由一个主方法负责调度。整体都变得一目了然了。

当然,在我们基于上面的原则进行重构之后,这个代码有没有问题呢?当然有。可以看到我们这三个函数,都依赖了全局变量。函数本身就不纯了。如果是全局的问题,还是不易于排查。

我们可以将其修改为纯函数,让这一份代码易于理解和测试。

以一个函数的修改为示例:我们将 全局变量变成了参数,只需要在调用的时候,将全局变量传入即可,但是这样,我们得到了一个纯函数。

const getBackgroundMapBgColor = (pageUrlObj, config) => {
if (!config.backgroundMap) return "";
let queryParam = pageUrlObj.params;
if (!queryParam.myPid) return "";
const pids = queryParam.myPid.split("-");
if (pids.length === 2) {
return config.backgroundMap[pids[1]];
}
return config.backgroundMap[pids[0]];
};

为什么会在这里特别强调这个点呢,其实在函数式编程中的一个最基础的问题那就是纯函数。只有这样输入输出才是可被观测的,一个输入一定会有一个输出。也只有通过这样的方式,才能让系统中非纯的函数越来越少。让代码变得更易于测试。

当然作为我们如果以重构的角度去思考的话,我们还需要关注到这个点:

Object.keys(window.__customMyConfig.customBgMap).forEach(item => {
if (this.pageUrl.indexOf(item) !== -1) {
bgColor = window.__customMyConfig.customBgMap[item];
}
});

这里的逻辑会将会 最后一个被匹配到的数据,设置为 bgColor 。(我们都知道 find indexOf 等基本都是从前匹配。)是否真的是业务的需求呢?

可以看到将业务代码写好/重构的过程中其实也是对业务逻辑和业务理解的再一次提升。不论是抽取成函数还是错误优先返回的设计,这其实也都是可以解决这样一个问题:能在不去读懂全局的情况下,了解某一个区域的细节逻辑,也就做到了让代码易于理解和修改。

... 这里的代码即便是经过这样的重构后,依然有可以考虑进一步优化的空间,比如函数与参数的命名,完整的测试用例等等,受限于文章篇幅,暂不展开说明。

一些代码中可能存在的其他问题

  1. 逻辑耦合在视图层。

    a === 'a' && b ==='b' && c==='c' && d ==='d'? <div>...</div>:null

  2. 组件复用,函数复用,不封装,代码重复。

  3. 函数功能不单一,一个函数处理太多职责。且这些职责没有任何关联,但是都耦合在同一个区块内。

  4. 参数列表混乱,有做好防御编程,不处理错误(接口错误,超时,重复提交等等

  5. 魔法数字,魔法字符串,且没说明。

  6. 糟糕数据结构 / 糟糕命名 (其实上面的具体代码示例也存在)

关于优化代码的思想准备

首先来说一下为什么会说需要优化代码?

  1. 技术追求。
  2. 公司要求,线上有系统在用。有用户在用,不写好出问题实际上苦的还是自己。
  3. 团队协作,我不好好写,团队成员其他人也不好好写,恶性循环苦的还是自己。
  4. 快速迭代。系统需要不断的增加新功能。必须要写好代码才能做到。
  5. 其他人的看法,怕别人觉得自己技术能力差... xxxx....

那么就会有下面这些要求:

  • 易于理解系统的架构
  • 易于理解系统的生命周期与执行流程
  • 易于理解每一个函数的作用
  • 易于理解函数之间是如何调用与传递的(输入输出)
  • 易于理解变量的含义,表达式的含义。
  • 易于扩展...

最终实际上又回到了写出来的代码应该是 整洁的代码,要使代码易于理解/修改/测试。(这里其实大部分时候,都隐含了一个人员协作的条件在里面,所以,既要写好代码,又不能过度封装,让团队其他成员看不懂(当然如果确实有些人经验不够,那么是他自身的问题,需要他自己去加强。))

一些建议

  1. 更加清晰的去了解业务,去思考可能的变化。思考和设计清楚再动手。
  2. 看一些开源项目与业界最佳实践,明白什么样的是好代码,什么样的是不好的代码。
  3. 建立明白代码虽然是给计算机运行的,但最终还是人看的。不仅仅是没有 bug 就行了,这样的心智模型。
  4. 建立业务与代码质量同等重要的思考模型。避免因为时间导致的不得不这么写的代码。
  5. 明白 code review 本身可能能发现和指出来一些问题,但最终的落实还的靠自己,不能变成形式,而是需要融合成自身的思考。
  6. 使用错误优先原则。尽可能的让出错的先返回, 这样后面就会得到干净的代码。(写代码的时候,不仅仅正向,反向的判断也需要思考)
  7. 合理的拆分成独立的函数。明确输入输出,错误处理等在函数内部的处理。(比如在一些场景中确实会存在大量逻辑判断,首先就要思考在判断内部的语句是否能被归类与拆分出去)
  8. 对于多种状态的判断与组合,可以使用 组合状态表 (map表)状态机等模式。
  9. 学习设计模式与重构等相关知识。
  10. 重构!!只要你觉得这个地方有问题了,那就不要等到以后。以后往往就是再也不。

结束

说到这可能会有一种戛然而止的感觉。在这一篇文章里面,我们首先以两个优化代码的具体实例为引子,让大家明白了一些业务代码的优化思路。在之后从列举了一些其他可能出现的错误,以及是优化代码的思想准备和理论指导。

其实都是希望大家能够在业务中去发现问题,再去思考如何解决问题,因为说了那么多,到底能不把代码写好。还是得靠自己。

文章目录
  1. 1. 几个业务场景中的重构示例
    1. 1.1. 请求顺序依赖
    2. 1.2. 错误示例
    3. 1.3. 正确示例
    4. 1.4. 折磨人的 if else
    5. 1.5. 错误示例
    6. 1.6. 正确示例
  2. 2. 一些代码中可能存在的其他问题
  3. 3. 关于优化代码的思想准备
  4. 4. 一些建议
  5. 5. 结束