通过代码自文档,getCollectionWeight() 函数的可读性得到很大提升。
getCollectionWeight() 函数的长度大幅减少。
如果你打算计算其他类型的权重值,getCollectionWeight() 的代码不会再剧烈膨胀了。
这些拆分出来的函数都是低耦合、高可复用的组件,你的同事可能希望将他们导入其他项目中,而你可以轻而易举的实现这个要求。
当函数偶发错误的时候,调用栈会更加详细,因为栈中包含函数的名称,甚至你可以立马发现出错的函数。
这些小函数更简单、易测试,可以达到很高的代码覆盖率。与其穷尽各种场景来测试一个大函数,你可以进行结构化测试,分别测试每一个小函数。
你可以参照CommonJS 或 ES2015 模块格式,将拆分出的函数创建为独立的模块。这将使得你的项目文件更轻、更结构化。
这些建议可以帮助你,战胜应用的复杂性。
原则上,你的函数不应当超过 20 行——越小越好。
现在,我觉得你可能会问我这样的问题:“我可不想将每一行代码都写为函数。有没有什么准则,告诉我何时应当停止拆分?”。这就是接下来的议题了。
2. 函数应当是简单的让我们稍微放松一下,思考下应用的定义到底是什么?
每一个应用都需要实现一系列需求。开发人员的准则在于,将这些需求拆分为一些列较小的可执行组件(命名空间、类、函数、代码块等),分别完成指定的工作。
一个组件又由其他更小的组件构成。如果你希望编写一个组件,你只能从抽象层中低一级的组件中,选取需要的组件用于创建自己的组件。
换言之,你需要将一个函数分解为若干较小的步骤,并且保证这些步骤都在抽象上,处于同一级别,而且只向下抽象一级。这非常重要,因为这将使得函数变得简单,做到“做且只做好一件事”。
为什么这是必要的?因为简单的函数非常清晰。清晰就意味着易于理解和修改。
我们来举个例子。假设你需要实现一个函数,使数组仅保留素数(2, 3, 5, 7, 11 等等),移除非素数(1, 4, 6, 8 等等)。函数的调用方式如下:
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
如何用低一级抽象的若干步骤实现getOnlyPrime() 函数呢?我们这样做:
为了实现 getOnlyPrime() 函数, 我们用 isPrime() 函数来过滤数组中的数字。
非常简单,只需要对数字数组执行一个过滤函数isPrime() 即可。
你需要在当前抽象层实现isPrime() 的细节吗?不,因为getOnlyPrime() 函数会在不同的抽象层实现一些列步骤。否则,getOnlyPrime() 会包含过多的功能。
在头脑中谨记简单函数的理念,我们来实现getOnlyPrime() 函数的函数体:
[JavaScript] 纯文本查看 复制代码
function getOnlyPrime(numbers) {
return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
如你所见,getOnlyPrime() 非常简单,它仅仅包含低一级抽象层的步骤:数组的.filter() 方法和isPrime()函数。
现在该进入下一级抽象。
数组的.filter()方法由 JavaScript 引擎提供,我们直接使用即可。当然,标准已经准确描述了它的行为。
现在你可以深入如何实现isPrime() 的细节中了:
为了实现 isPrime() 函数检查一个数字 n 是否为素数,只需要检查 2 到Math.sqrt(n) 之间的所有整数是否均不能整除n。
有了这个算法(不算高效,但是为了简单起见,就用这个吧),我们来为isPrime() 函数编码:
[JavaScript] 纯文本查看 复制代码
function isPrime(number) {
if (number === 3 || number === 2) {
return true;
}
if (number === 1) {
return false;
}
for (let divisor = 2; divisor <= Math.sqrt(number); divisor++) {
if (number % divisor === 0) {
return false;
}
}
return true;
}
function getOnlyPrime(numbers) {
return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
getOnlyPrime() 很小也很清晰。它只从更低一级抽象中获得必要的一组步骤。
只要你按照这些规则,将函数变的简洁清晰,复杂函数的可读性将得到很大提升。将代码进行精确的抽象分级,可以避免出现大块的、难以维护的代码。
3. 使用简练的函数名称函数名称应该非常简练:长短适中。理想情况下,名称应当清楚的概括函数的功用,而不需要读者深入了解函数的实现细节。
对于使用骆驼风格的函数名称,以小写字母开始:addItem(),saveToStore()或者getFirstName()之类。
由于函数都是某种操作,因此名称中至少应当包含一个动词。例如deletePage(),verifyCredentials()。需要get 或 set 属性的时候,请使用 标准的set和get前缀:getLastName()或setLastName()。
避免在生产代码中出现有误导性的名称,例如foo(),bar(),a(),fun()等等。这样的名称没有任何意义。
如果函数都短小清晰,命名简练:代码读起来就会像诗一样迷人。
4. 总结当然了,这里假定的例子都非常简单。现实中的代码更加复杂。你可能要抱怨,编写清晰的函数,只在抽象上一级一级下降,实在太没劲了。但是如果从项目一开始就开始你的实践,就远没有想象中复杂。
如果应用中已经存在一些功能繁杂的函数,希望对它们进行重构,你可能会发现困难重重。而且在很多情况下,在合理的时间内是不可能完成的。但千里之行始于足下:在力所能及的前提下,先拆分一部分出来。
当然,最正确的解决方案应该是,从项目一开始就以正确的方式实现应用。除了花一些时间在实现上,也应该花一些精力在组建合理的函数结构上:如我们所建议的——让它们保持短小、清晰。
成竹在胸,落笔有神.
ES2015 实现了一个非常棒的模块系统,它明确建议,小函数是优秀的工程实践。
记住,干净、组织良好的代码通常需要投入大量时间。你会发现这做起来有难度。可能需要很多尝试,可能会迭代、修改一个函数很多次。
然而,没有什么比乱麻一样的代码更让人痛心的了,那么这一切都是值得的!