相信很多同窗在维护老项目时,都遇到过在深深的 if else 之间纠缠的业务逻辑。面对这样的一团乱麻,简单粗暴地继续增量修改经常只会让复杂度愈来愈高,可读性愈来愈差,有没有固定的套路来梳理它呢?这里分享三种简单通用的重构方式。编程
所谓的【面条代码】,常见于对复杂业务流程的处理中。它通常会知足这么几个特色:设计模式
咱们知道,主流的编程语言均有函数或方法来组织代码。对于面条代码,不妨认为它就是知足这几个特征的函数吧。根据语言语义的区别,能够将它区分为两种基本类型:数组
if...if
型这种类型的代码结构形如:框架
function demo (a, b, c) {
if (f(a, b, c)) {
if (g(a, b, c)) {
// ...
}
// ...
if (h(a, b, c)) {
// ...
}
}
if (j(a, b, c)) {
// ...
}
if (k(a, b, c)) {
// ...
}
}复制代码
流程图形如:编程语言
它经过从上到下嵌套的 if
,让单个函数内的控制流不停增加。不要觉得控制流增加时,复杂度只会线性增长。咱们知道函数处理的是数据,而每一个 if
内通常都会有对数据的处理逻辑。那么,即使在不存在嵌套的情形下,若是有 3 段这样的 if
,那么根据每一个 if
是否执行,数据状态就有 2 ^ 3 = 8 种。若是有 6 段,那么状态就有 2 ^ 6 = 64 种。从而在项目规模扩大时,函数的调试难度会指数级上升!这在数量级上,与《人月神话》的经验一致。函数
else if...else if
型这个类型的代码控制流,一样是很是常见的。形如:spa
function demo (a, b, c) {
if (f(a, b, c)) {
if (g(a, b, c)) {
// ...
}
// ...
else if (h(a, b, c)) {
// ...
}
// ...
} else if (j(a, b, c)) {
// ...
} else if (k(a, b, c)) {
// ...
}
}复制代码
流程图形如:设计
else if
最终只会走入其中的某一个分支,所以并不会出现上面组合爆炸的情形。可是,在深度嵌套时,复杂度一样不低。假设嵌套 3 层,每层存在 3 个 else if
,那么这时就会出现 3 ^ 3 = 27 个出口。若是每种出口对应一种处理数据的方式,那么一个函数内封装这么多逻辑,也显然是违背单一职责原则的。而且,上述两种类型能够无缝组合,进一步增长复杂度,下降可读性。3d
但为何在这个有了各类先进的框架和类库的时代,仍是常常会出现这样的代码呢?我的的观点是,复用的模块确实可以让咱们少写【模板代码】,但业务自己不管再怎么封装,也是须要开发者去编写逻辑的。而即使是简单的 if else
,也能让控制流的复杂度指数级上升。从这个角度上说,若是没有基本的编程素养,不论速成掌握再优秀的框架与类库,一样会把项目写得一团糟。调试
上文中,咱们已经讨论了面条代码的两种类型,并量化地论证了它们是如何让控制流复杂度指数级激增的。然而,在现代的编程语言中,这种复杂度实际上是彻底可控的。下面分几种情形,列出改善面条代码的编程技巧。
对看起来复杂度增加最快的 if...if
型面条代码,经过基本的函数便可将其拆分。下图中每一个绿框表明拆分出的一个新函数:
因为现代编程语言摒弃了 goto
,所以不论控制流再复杂,函数体内代码的执行顺序也都是从上而下的。所以,咱们彻底有能力在不改变控制流逻辑的前提下,将一个单体的大函数,自上而下拆逐步分为多个小函数,然后逐个调用之。这是有经验的同窗常用的技巧,具体代码实如今此不作赘述了。
须要注意的是,这种作法中所谓的不改变控制流逻辑,意味着改动并不须要更改业务逻辑的执行方式,只是简单地【把代码移出去,而后用函数包一层】而已。有些同窗可能会认为这种方式治标不治本,不过是把一大段面条切成了几小段,并无本质的区别。
然而真的是这样吗?经过这种方式,咱们可以把一个有 64 种状态的大函数,拆分为 6 个只返回 2 种不一样状态的小函数,以及一个逐个调用它们的 main 函数。这样一来,每一个函数复杂度的增加速度,就从指数级下降到了线性级。
这样一来,咱们就解决了 if...if
类型面条代码了,那么对于 else if...else if
类型的呢?
对于 else if...else if
类型的面条代码,一种最简单的重构策略是使用所谓的查找表。它经过键值对的形式来封装每一个 else if
中的逻辑:
const rules = {
x: function (a, b, c) { /* ... */ },
y: function (a, b, c) { /* ... */ },
z: function (a, b, c) { /* ... */ }
}
function demo (a, b, c) {
const action = determineAction(a, b, c)
return rules[action](a, b, c)
}复制代码
每一个 else if
中的逻辑都被改写为一个独立的函数,这时咱们就可以将流程按照以下所示的方式拆分了:
对于先天支持反射的脚本语言来讲,这也算是较为 trivial 的技巧了。但对于更复杂的 else if
条件,这种方式会从新把控制流的复杂度集中处处理【该走哪一个分支】问题的 determineAction
中。有没有更好的处理方式呢?
在上文中,查找表是用键值对实现的,对于每一个分支都是 else if (x === 'foo')
这样简单判断的情形时,'foo'
就能够做为重构后集合的键了。但若是每一个 else if
分支都包含了复杂的条件判断,且其对执行的前后顺序有所要求,那么咱们能够用职责链模式来更好地重构这样的逻辑。
对 else if
而言,注意到每一个分支实际上是从上到下依次判断,最后仅走入其中一个的。这就意味着,咱们能够经过存储【断定规则】的数组,来实现这种行为。若是规则匹配,那么就执行这条规则对应的分支。咱们把这样的数组称为【职责链】,这种模式下的执行流程以下图:
在代码实现上,咱们能够经过一个职责链数组来定义与 else if
彻底等效的规则:
const rules = [
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
},
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
},
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
}
// ...
]复制代码
rules
中的每一项都具备 match
与 action
属性。这时咱们能够将原有函数的 else if
改写对职责链数组的遍历:
function demo (a, b, c) {
for (let i = 0; i < rules.length; i++) {
if (rules[i].match(a, b, c)) {
return rules[i].action(a, b, c)
}
}
}复制代码
这时每一个职责一旦匹配,原函数就会直接返回,这也彻底符合 else if
的语义。经过这种方式,咱们就实现了对单体复杂 else if
逻辑的拆分了。
面条代码其实容易出如今不加思考的【糙快猛】式开发中。不少简单粗暴地【在这里加个 if
,在那里多个 return
】的 bug 修复方式,再加上注释的匮乏,很容易让代码可读性愈来愈差,复杂度愈来愈高。
但解决这个问题的几种方案都并不复杂。这些示例之因此简单,本质上是由于高级编程语言强大的表达能力已经可以不依赖于各类模式的模板代码,为需求提供直接的语义支持,而无需套用各类设计模式的八股文。
固然,你能够用模式来归纳一些下降业务逻辑复杂度的技巧,但若是生搬硬套地记忆并使用模式,一样可能会走进过分设计的歧途。在实现常见业务功能时,掌握好编程语言,梳理好需求,用最简单的代码将其实现,就已是最优解了。