原文连接:https://dmitripavlutin.com/the-art-of-writing-small-and-plain-functions/?utm_source=codropscollective算法
译者:阿里云-也树数组
随着软件应用的复杂度不断上升,为了确保应用稳定且易拓展,代码质量就变的愈来愈重要。模块化
不幸的是,包括我在内的几乎每一个开发者在职业生涯中都会面对质量不好的代码。这些代码一般有如下特征:函数
这些话听起来很是常见:“我不明白这部分代码怎么工做的”,“这代码太烂了”,“这代码太难改了”等等。测试
有一次我如今的同事由于在以前的团队处理过难以维护的Ruby 编写的 REST API 而辞职,他是接手了以前开发团队的工做。在修复现有的 bug 时会创造新的 bug,添加新的特性也会创造一系列新的 bug,而客户也不想以更好的设计去重构应用,于是个人同事作了辞职这个正确的决定。优化
这样的场景时有发生,咱们能作些什么呢?ui
须要牢记于心的是:仅仅让应用能够运行和关注代码质量是不一样的。一方面你须要知足应用的功能,另外一方面你须要花时间确认是否任意的函数没有包含太多职责、是否全部函数都使用了易理解的变量和函数名而且是否避免了函数的反作用。阿里云
函数(包括对象的方法)是让应用运行的小齿轮。首先你应该专一于它们的结构和编写,而下面这篇文章阐述了编写清晰易懂且容易测试的函数的最佳实践。编码
要避免编写职责冗杂的庞大函数,而须要将它们分离成不少小函数。庞大的函数就像黑盒子同样,很难理解和修改,尤为在测试时更加捉襟见肘。spa
想象一个场景:一个函数须要返回一个数组、map 或者普通对象的“重量”。“重量”由属性值计算获得。规则以下:
null
或者 undefined
计为 1
2
4
举个例子:数组 [null, 'Hello World', {}]
的重量计算为: 1
(null
) + 2
(字符串类型) + 4
(对象) = 7
让咱们从最坏的状况开始,全部的逻辑都写在一个庞大的 getCollectionWeight()
函数里。
function getCollectionWeight(collection) {
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = [...collection.values()];
} else {
collectionValues = Object.keys(collection).map(function (key) {
return collection[key];
});
}
return collectionValues.reduce(function(sum, item) {
if (item == null) {
return sum + 1;
}
if (typeof item === 'object' || typeof item === 'function') {
return sum + 4;
}
return sum + 2;
}, 0);
}
let myArray = [null, { }, 15];
let myMap = new Map([ ['functionKey', function() {}] ]);
let myObject = { 'stringKey': 'Hello world' };
getCollectionWeight(myArray); // => 7 (1 + 4 + 2)
getCollectionWeight(myMap); // => 4
getCollectionWeight(myObject); // => 2
复制代码
问题显而易见。getCollectionWeight()
函数过于庞大,看起来像个装有不少惊喜的黑盒子。你很难第一眼理解它是作什么的,再想象一下你的应用里有一堆这样的函数是什么光景。
当你在和这样的代码打交道时,是在浪费时间和精力。另外一方面小而可以自解释的函数读起来也会让人愉悦,方便开展以后的工做。
如今咱们的目标是把庞大的函数分解成更小的不耦合且可重用的函数。第一步是经过不一样的类型,抽象出决定“重量”值的代码。这个新函数是 getWeight()
。
仅仅看到1
、2
和 4
这三个魔数而不了解上下文的状况下根本搞不清楚他们的含义。幸运的是 ES2015 容许咱们利用 const
来定义只读的的变量,因此能够建立有含义的常量来取代魔数。
让咱们建立 getWeightByType()
函数而且改善一下 getCollectionWeight()
函数:
function getWeightByType(value) {
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
}
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
function getCollectionWeight(collection) {
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = [...collection.values()];
} else {
collectionValues = Object.keys(collection).map(function (key) {
return collection[key];
});
}
return collectionValues.reduce(function(sum, item) {
return sum + getWeightByType(item);
}, 0);
}
let myArray = [null, { }, 15];
let myMap = new Map([ ['functionKey', function() {}] ]);
let myObject = { 'stringKey': 'Hello world' };
getCollectionWeight(myArray); // => 7 (1 + 4 + 2)
getCollectionWeight(myMap); // => 4
getCollectionWeight(myObject); // => 2
复制代码
是否是看起来好些了?getWeightByType()
函数是无依赖的,仅仅经过数据类型来决定数据的“重量”。你能够在任何一个函数中复用它。getCollectionWeight()
函数也变得简练了一些。
WEIGHT_NULL_UNDEFINED
, WEIGHT_PRIMITIVE
和 WEIGHT_OBJECT_FUNCTION
从变量名就能够看出“重量”所描述的数据类型,而不须要再猜 1
, 2
和 4
表明什么。
上面的改进版仍然有瑕疵。想象一下你想要将“重量”的计算应用在 Set
或者其它定制的数据集合时,因为 getCollectionWeight()
函数包含了收集值的逻辑,它的代码量会快速增加。
让咱们从代码中抽象出一些函数,好比获取 map 类型的数据的函数 getMapValues()
和获取普通对象类型数据的函数 getPlainObjectValues()
。再看看新的改进版:
function getWeightByType(value) {
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
}
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
function getMapValues(map) {
return [...map.values()];
}
function getPlainObjectValues(object) {
return Object.keys(object).map(function (key) {
return object[key];
});
}
function getCollectionWeight(collection) {
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = getMapValues(collection);
} else {
collectionValues = getPlainObjectValues(collection);
}
return collectionValues.reduce(function(sum, item) {
return sum + getWeightByType(item);
}, 0);
}
let myArray = [null, { }, 15];
let myMap = new Map([ ['functionKey', function() {}] ]);
let myObject = { 'stringKey': 'Hello world' };
getCollectionWeight(myArray); // => 7 (1 + 4 + 2)
getCollectionWeight(myMap); // => 4
getCollectionWeight(myObject); // => 2
复制代码
如今再读 getCollectionWeight()
函数,你会很容易的弄清楚它实现的功能,如今的函数看起来像一个有趣的故事。每一个函数都很清晰而且直截了当,你不会在思考代码的含义上浪费时间。简洁的代码理应如此。
如今依然有不少能够改进的地方。
你能够建立一个独立的 getCollectionValues()
函数,包含区分数据集合类型的判断逻辑:
function getCollectionValues(collection) {
if (collection instanceof Array) {
return collection;
}
if (collection instanceof Map) {
return getMapValues(collection);
}
return getPlainObjectValues(collection);
}
复制代码
getCollectionWeight()
函数会变得十分简单,由于它惟一要作的事情就是从 getCollectionValues()
中获取集合的值,而后执行累加操做。
你也能够建立一个独立的 reduce 函数:
function reduceWeightSum(sum, item) {
return sum + getWeightByType(item);
}
复制代码
由于理想状况下 getCollectionWeight()
中不该该定义匿名函数。
最终咱们最初的庞大函数被拆分红下面这些函数:
function getWeightByType(value) {
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
}
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
function getMapValues(map) {
return [...map.values()];
}
function getPlainObjectValues(object) {
return Object.keys(object).map(function (key) {
return object[key];
});
}
function getCollectionValues(collection) {
if (collection instanceof Array) {
return collection;
}
if (collection instanceof Map) {
return getMapValues(collection);
}
return getPlainObjectValues(collection);
}
function reduceWeightSum(sum, item) {
return sum + getWeightByType(item);
}
function getCollectionWeight(collection) {
return getCollectionValues(collection).reduce(reduceWeightSum, 0);
}
let myArray = [null, { }, 15];
let myMap = new Map([ ['functionKey', function() {}] ]);
let myObject = { 'stringKey': 'Hello world' };
getCollectionWeight(myArray); // => 7 (1 + 4 + 2)
getCollectionWeight(myMap); // => 4
getCollectionWeight(myObject); // => 2
复制代码
这就是编写小而美的函数的艺术。
通过一系列的代码质量优化,你得到了一连串的好处:
getCollectionWeight()
函数的可读性。getCollectionWeight()
函数的代码量。getCollectionWeight()
函数代码量会过于迅速地增加。这些优点会让你在复杂的应用中如鱼得水。
有条通用的准则:一个函数不该该超过20行,小则优。
你如今可能会问我一个合情合理的问题:“我不想为每一行代码都建立函数,有没有一个标准让我再也不继续拆分函数?”这就是下一章节的主题。
让咱们稍做休息,思考一个问题:软件应用到底是什么?
每一个应用都是为了完成一系列的需求。做为开发者,须要把这些需求分解为能够正确运行特定任务的小组件(命名空间,类,函数,代码块)。
一个组件包含了其它更小的组件。若是你想要编写一个组件,须要经过抽象程度比它低一层级的组件来建立。
换句话讲:你须要把一个函数分解为多个步骤,这些步骤的抽象程度须要保持在同一层级或者低一层级。这样能够在保证函数简练的同时践行“作一件事,而且作好”的原则。
为何分解是必要的?由于简练的函数含义更加明确,也就意味着易读和易改。
让咱们看一个例子。假设你想要编写函数实现只保存数组中的素数,移除非素数。函数经过如下方式执行:
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
复制代码
在 getOnlyPrime()
函数中有哪些低一层级的抽象步骤?接下来系统阐述:
使用
isPrime()
函数过滤数组中的数字。
须要在这个层级提供 isPrime()
函数的细节吗?答案是否认的。由于 getOnlyPrime()
函数会有不一样层级的抽象步骤,这个函数会包含许多的职责。
既然脑子里有了最基础的想法,让咱们先完成 getOnlyPrime()
函数的内容:
function getOnlyPrime(numbers) {
return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
复制代码
此时 getOnlyPrime()
函数很是简洁。它包含了一个独立层级的抽象:数组的 .filter()
方法和 isPrime()
函数。
如今是时候向更低的层级抽象了。
数组方法是 .filter()
直接由 JavaScript 引擎提供的,原样使用便可。ECMA标准中精确地描述了它的功能。
如今咱们来研究 isPrime()
函数的具体实现:
为了实现检查一个数字
n
是否为素数的功能,须要确认是否从2
到Math.sqrt(n)
的任意数字均可以整除n
。
理解了这个算法(效率不高,但简便起见)后,来完成 isPrime()
函数的代码:
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()
函数小而精炼。它仅仅保留了必需的低一层级的抽象。
若是你遵守让函数简练化的原则,复杂函数的可读性能够大大提高。每一层级的精确抽象和编码能够防止编写出一大堆难以维护的代码。
函数名称应该简明扼要,不该过于冗长或者简短。理想状况下,函数名称应该在不对代码刨根问底的状况下清楚反映出函数的功能。
函数名称应该使用驼峰式命名法,以小写字母开头:addItem()
, saveToStore()
或者 getFirstName()
。
由于函数表明了动做,函数名称应该至少包含一个动词。好比:deletePage()
, verifyCredentials()
。获取或者设置属性值时,使用标准的 set
和 get
前缀:getLastName()
或者 setLastName()
。
避免编写含混的函数名,好比 foo()
, bar()
, a()
, fun()
等等。这些名称没有意义。
若是函数小而清晰,名称简明扼要,代码就能够像散文同样阅读。
固然,上面提供的示例十分简单。真实的应用中会更加复杂。你可能会抱怨仅仅为了抽象出一个层级而编写简练的函数是沉闷乏味的任务。可是若是从项目开始之初就正确实践的话就不会是一件困难的事。
若是应用已经有不少函数拥有太多职责,你会发现很难理解这些代码。在不少状况下,不大可能在合理的时间完成重构的工做。可是至少从点滴作起:尽你所能抽象一些东西。
最好的解决办法固然是从一开始就正确的实现应用。不只要在实现需求上花费时间,一样应该像我建议的那样:正确组织你的函数,让它们小而简练。
三思然后行。(Measure seven times, cut once)
ES2015 实现了一个很棒的模块系统,清晰地建议出分割函数是好的实践。
记住永远值得投资时间让代码变得简练有组织。在这个过程当中,你可能以为实践起来很难,可能须要不少练习,也可能回过头来修改一个函数不少次。
但没有比一团乱麻的代码更糟的了。
文章做者提出的 small function
的观点可能会让初学者产生一点误解,在个人理解里,更准确的表述应该是从代码实现功能的逻辑层面抽象出更小的功能点,将抽象出的功能点转化为函数来为最后的业务提供组装的零件。最终的目的依然是经过解耦逻辑来提升代码的拓展性和复用性,而不能仅仅停留在视觉层面的”小“,单纯为了让函数代码行数变少是没有意义的。