代码的主意,编写小而美函数的法门

作者:亚搏app官网    发布时间:2020-02-09 19:41    浏览:167 次

[返回]

时间: 2018-01-27阅读: 776标签: 函数原文链接:译者:阿里云-也树

本文由 伯乐在线 - alvendarthy 翻译。未经许可,禁止转载!
英文出处:Dmitri Pavlutin。欢迎加入翻译组。

随着软件应用的复杂度不断上升,为了确保应用稳定且易拓展,代码质量就变的越来越重要。不幸的是,包括我在内的几乎每个开发者在职业生涯中都会面对质量很差的代码。这些代码通常有以下特征:

本文以 JavaScript 为例,介绍了该如何优化函数,使函数清晰易读,且更加高效稳定。

函数冗长,做了太多事情函数有副作用并且很难理解和调试排错含糊的函数/变量命名代码脆弱,一个小改动会意外地破坏应用的其它组件缺乏测试的覆盖

软件的复杂度一直在持续增长。代码质量对于保证应用的可靠性、易扩展性非常重要。

这些话听起来非常常见:“我不明白这部分代码怎么工作的”,“这代码太烂了”,“这代码太难改了”等等。

然而,几乎每一个开发者,包括我自己,在职业生涯中都见过低质量的代码。这东西就是个坑。低质量代码具备以下极具杀伤力的特点:

有一次我现在的同事因为在之前的团队处理过难以维护的Ruby 编写的 REST API 而辞职,他是接手了之前开发团队的工作。在修复现有的 bug 时会创造新的 bug,添加新的特性也会创造一系列新的 bug,而客户也不想以更好的设计去重构应用,因而我的同事做了辞职这个正确的决定。

  • 函数超级长,而且塞满了各种乱七八糟的功能。
  • 函数通常有一些副作用,不仅难以理解,甚至根本没法调试。
  • 含糊的函数、变量命名。
  • 脆弱的代码:一个小的变更,就有可能出乎意料的破坏其他应用组件。
  • 代码覆盖率缺失。

这样的场景时有发生,我们能做些什么呢?

它们听起来基本都是:“我根本没法理解这段代码是如何工作的”,“这段代码就是一堆乱麻”,“要修改这一段代码实在太难了” 等等。

需要牢记于心的是:仅仅让应用可以运行和关注代码质量是不同的。一方面你需要满足应用的功能,另一方面你需要花时间确认是否任意的函数没有包含太多职责、是否所有函数都使用了易理解的变量和函数名并且是否避免了函数的副作用。

我就曾遇到过这样的情况,我的一个同事由于无法继续将一个基于Ruby 的 REST API 做下去,继而离职。这个项目是他从之前的开发团队接手的。

函数(包括对象的方法)是让应用运行的小齿轮。首先你应该专注于它们的结构和编写,而下面这篇文章阐述了编写清晰易懂且容易测试的函数的最佳实践。

修复现有的 bug ,然后引入了新的 bug,添加新的特性,就增加了一连串新 bug,如此循环(所谓的脆弱代码)。客户不希望以更好的设计重构整个应用,开发人员也做出明智的选择——维持现状。

函数需要“小”

图片 1

要避免编写职责冗杂的庞大函数,而需要将它们分离成很多小函数。庞大的函数就像黑盒子一样,很难理解和修改,尤其在测试时更加捉襟见肘。

好吧,这种事儿经常发生,而且挺糟糕的。那我们能做点什么呢?

想象一个场景:一个函数需要返回一个数组、map 或者普通对象的“重量”。“重量”由属性值计算得到。规则如下:

首先,需要谨记于心:只是让应用运转起来,和尽心保证代码质量是两个完全不同的事。一方面,你需要实现产品需求。但是另一方面,你应该花点时间,确保函数功能简单、使用易读的变量和函数命名,避免函数的副作用等等。

null或者undefined计为1基础类型的数据计为2对象或者函数类型的数据计为4

函数(包括对象方法)是让应用运转起来的齿轮。首先你应当将注意力集中在他们的结构和整体布局上。这篇文章包括了一些非常好的示例,展示如何编写清晰、易于理解和测试的函数。

举个例子:数组[null, 'Hello World', {}]的重量计算为:1(null) +2(字符串类型) +4(对象) =7

1. 函数应当很小,非常小

避免使用包含大量的功能的大函数,应当将其功能分割为若干较小的函数。大的黑盒函数难于理解、修改,特别是很难测试。

假设这样一个场景,需要实现一个函数,用于计算 array、map 或 普通 JavaScript 对象的权重。总权重可通过计算各成员权重获得:

  • null 或者 未定义变量计 1 点。
  • 基本类型计 2 点。
  • 对象或函数计 4 点。

例如,数组 [null, ‘Hello World’, {}] 的权重这样计算:1(null) + 2(string 是基本类型) + 4(对象) = 7。

Step 0: 最初的庞大函数

Step 0: 最初的大函数

我们从最糟的实例开始。所有的逻辑都被编码在函数 getCollectionWeight()中:

在 repl.it 中运行一下``

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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() 函数超级长,而且看起来像一个装满“意外”的黑盒子。可能你也发现了,第一眼根本就搞不明白它要干什么。再试想一下,应用里有大把这样的函数。

在工作中遇到这样的代码,就是在浪费你的时间和精力。反之,高质量的代码不会令人不适。高质量代码中,那些精巧、自文档极好的函数非常易于阅读和理解。

图片 2

让我们从最坏的情况开始,所有的逻辑都写在一个庞大的getCollectionWeight()函数里。

Step 1:根据类型计算权重,抛弃那些“迷之数字”。

现在,我们的目标是:把这个巨型函数,拆分为较小的、独立的、可重用的一组函数。第一步,将根据类型计算权重的代码提取出来。这个新的函数命名为 getWeight()。

我们再看看这几个“迷之数字”: 1, 2, 4。在不知道整个故事背景的前提下,仅靠这几个数字提供不了任何有用的信息。幸好 ES2015 允许定义静态只读引用,那你就能简单的创造几个常量,用有意义的名称,替换掉那几个“迷之数字”。(我特别喜欢“迷之数字”这个说法:D)

我们来新建一个较小的函数 getWeightByType(),并用它来改进 getCollectionWeight():

在 repl.it 中运行一下

JavaScript

// Code extracted into getWeightByType() 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Code extracted into getWeightByType()
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 这些数字的意义。

在 repl.it 中尝试运行

Step 2: 继续切分,使之具备扩展性

然而,这个升级版依然有不足的地方。假如你打算对一个 Set,甚至其他用户自定义集合来实现权值计算。getCollectionWeight() 会快速膨胀,因为它包含了一组获得权值的具体逻辑。

让我们将获得 maps 权重的代码提取到 getMapValues(),将获得基本 JavaScript 对象权值的代码则放到 getPlainObjectValues() 中。看看改进后的版本吧。

在 repl.it 中运行一下

JavaScript

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; } // Code extracted into getMapValues() function getMapValues(map) { return [...map.values()]; } // Code extracted into getPlainObjectValues() 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
}
// Code extracted into getMapValues()
function getMapValues(map) {  
  return [...map.values()];
}
// Code extracted into getPlainObjectValues()
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() 函数,你会发现已经比较容易明白它的机理,看起来就像一段有趣的故事。

每一个函数的简单明了。你不需要花费时间去挖掘代码,理解代码的工作。这就是清新版代码该有的样子。

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 

Step 3: 优化永无止境

就算到了现在这种程度,依然有很大优化的空间!

你可以创建一个独立的函数  getCollectionValues(),使用 if/else 语句区分集合中的类型:

JavaScript

function getCollectionValues(collection) { if (collection instanceof Array) { return collection; } if (collection instanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); }

1
2
3
4
5
6
7
8
9
function getCollectionValues(collection) {  
  if (collection instanceof Array) {
    return collection;
  }
  if (collection instanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
}

那么, getCollectionWeight() 应该会变得异常纯粹,因为它唯一的工作:用 getCollectionValues() 获得集合中的值,然后依次调用求和累加器。

你也可以创建一个独立的累加器函数:

JavaScript

function reduceWeightSum(sum, item) { return sum + getWeightByType(item); }

1
2
3
function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
}

理想情况下 getCollectionWeight() 函数中不应该定义函数。

最后,最初的巨型函数,已经被转换为如下一组小函数:

在 repl.it 中运行一下

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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() 的代码不会再剧烈膨胀了。
  • 这些拆分出来的函数都是低耦合、高可复用的组件,你的同事可能希望将他们导入其他项目中,而你可以轻而易举的实现这个要求。
  • 当函数偶发错误的时候,调用栈会更加详细,因为栈中包含函数的名称,甚至你可以立马发现出错的函数。
  • 这些小函数更简单、易测试,可以达到很高的代码覆盖率。与其穷尽各种场景来测试一个大函数,你可以进行结构化测试,分别测试每一个小函数。
  • 你可以参照 CommonJS 或 ES2015 模块格式,将拆分出的函数创建为独立的模块。这将使得你的项目文件更轻、更结构化。

这些建议可以帮助你,战胜应用的复杂性。

图片 3

原则上,你的函数不应当超过 20 行——越小越好。

现在,我觉得你可能会问我这样的问题:“我可不想将每一行代码都写为函数。有没有什么准则,告诉我何时应当停止拆分?”。这就是接下来的议题了。

问题显而易见。getCollectionWeight()函数过于庞大,看起来像个装有很多惊喜的黑盒子。你很难第一眼理解它是做什么的,再想象一下你的应用里有一堆这样的函数是什么光景。

2. 函数应当是简单的

让我们稍微放松一下,思考下应用的定义到底是什么?

每一个应用都需要实现一系列需求。开发人员的准则在于,将这些需求拆分为一些列较小的可执行组件(命名空间、类、函数、代码块等),分别完成指定的工作。

一个组件又由其他更小的组件构成。如果你希望编写一个组件,你只能从抽象层中低一级的组件中,选取需要的组件用于创建自己的组件。

换言之,你需要将一个函数分解为若干较小的步骤,并且保证这些步骤都在抽象上,处于同一级别,而且只向下抽象一级。这非常重要,因为这将使得函数变得简单,做到“做且只做好一件事”。

为什么这是必要的?因为简单的函数非常清晰。清晰就意味着易于理解和修改。

我们来举个例子。假设你需要实现一个函数,使数组仅保留素数(2, 3, 5, 7, 11 等等),移除非素数(1, 4, 6, 8 等等)。函数的调用方式如下:

JavaScript

getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]

1
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]

1
2
3
4
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() 函数编码:

在 repl.it 中运行一下

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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 属性的时候,请使用 标准的 setget 前缀:getLastName() 或 setLastName()

避免在生产代码中出现有误导性的名称,例如 foo(),``bar(),``a(),``fun() 等等。这样的名称没有任何意义。

如果函数都短小清晰,命名简练:代码读起来就会像诗一样迷人。

Step 1: 通过数据类型提取“重量”并且去除魔数

4. 总结

当然了,这里假定的例子都非常简单。现实中的代码更加复杂。你可能要抱怨,编写清晰的函数,只在抽象上一级一级下降,实在太没劲了。但是如果从项目一开始就开始你的实践,就远没有想象中复杂。

如果应用中已经存在一些功能繁杂的函数,希望对它们进行重构,你可能会发现困难重重。而且在很多情况下,在合理的时间内是不可能完成的。但千里之行始于足下:在力所能及的前提下,先拆分一部分出来。

当然,最正确的解决方案应该是,从项目一开始就以正确的方式实现应用。除了花一些时间在实现上,也应该花一些精力在组建合理的函数结构上:如我们所建议的——让它们保持短小、清晰。

成竹在胸,落笔有神.

图片 4

ES2015 实现了一个非常棒的模块系统,它明确建议,小函数是优秀的工程实践。

记住,干净、组织良好的代码通常需要投入大量时间。你会发现这做起来有难度。可能需要很多尝试,可能会迭代、修改一个函数很多次。

然而,没有什么比乱麻一样的代码更让人痛心的了,那么这一切都是值得的!

1 赞 11 收藏 4 评论

现在我们的目标是把庞大的函数分解成更小的不耦合且可重用的函数。第一步是通过不同的类型,抽象出决定“重量”值的代码。这个新函数是getWeight()。

关于作者:alvendarthy

图片 5

一个热爱生活的家伙! 个人主页 · 我的文章 · 16

仅仅看到1、2和4这三个魔数而不了解上下文的情况下根本搞不清楚他们的含义。幸运的是 ES2015 允许我们利用const来定义只读的的变量,所以可以创建有含义的常量来取代魔数。

让我们创建getWeightByType()函数并且改善一下getCollectionWeight()函数:

在 repl.it 中尝试

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代表什么。

Step 2: 继续分割函数并且增加拓展性

上面的改进版仍然有瑕疵。想象一下你想要将“重量”的计算应用在Set或者其它定制的数据集合时,由于getCollectionWeight()函数包含了收集值的逻辑,它的代码量会快速增长。

让我们从代码中抽象出一些函数,比如获取 map 类型的数据的函数getMapValues()和获取普通对象类型数据的函数getPlainObjectValues()。再看看新的改进版:

在 repl.it 中尝试

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()函数,你会很容易的弄清楚它实现的功能,现在的函数看起来像一个有趣的故事。每个函数都很清晰并且直截了当,你不会在思考代码的含义上浪费时间。简洁的代码理应如此。

Step 3: 永远不要停止改进

现在依然有很多可以改进的地方。

你可以创建一个独立的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()中不应该定义匿名函数。

最终我们最初的庞大函数被拆分成下面这些函数:

在 repl.it 中尝试

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()函数代码量会过于迅速地增长。抽象出的函数是独立可重用的。你的同事可能想要引入你这些实用的函数到另一个项目中,你可以轻易的让他们做到这一点。如果某个函数意外报错,函数的调用栈信息会更加清晰,因为它包含了函数名称,你立刻就能确定出问题的函数在哪里。分割开的函数更容易编写测试和实现更高的测试覆盖率。相比于测试一个庞大函数的所有场景,更好的办法是独立构造测试并且独立核对每一个函数。你可以利用 CommonJS 或者 ES2015 模块标准使代码模块化。把函数抽象成独立的模块,这样会让你的项目文件更轻量和结构化。

这些优势会让你在复杂的应用中如鱼得水。

有条通用的准则:一个函数不应该超过20行,小则优。

你现在可能会问我一个合情合理的问题:“我不想为每一行代码都创建函数,有没有一个标准让我不再继续拆分函数?”这就是下一章节的主题。

  1. 函数应该是简练的

让我们稍作休息,思考一个问题:软件应用究竟是什么?

每个应用都是为了完成一系列的需求。作为开发者,需要把这些需求分解为可以正确运行特定任务的小组件(命名空间,类,函数,代码块)。

一个组件包含了其它更小的组件。如果你想要编写一个组件,需要通过抽象程度比它低一层级的组件来创建。

换句话讲:你需要把一个函数分解为多个步骤,这些步骤的抽象程度需要保持在同一层级或者低一层级。这样可以在保证函数简练的同时践行“做一件事,并且做好”的原则。

为什么分解是必要的?因为简练的函数含义更加明确,也就意味着易读和易改。

让我们看一个例子。假设你想要编写函数实现只保存数组中的素数,移除非素数。函数通过以下方式执行:

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()函数的代码:

在 repl.it 中尝试

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()函数小而精炼。它仅仅保留了必需的低一层级的抽象。

如果你遵照让函数简练化的原则,复杂函数的可读性可以大大提升。每一层级的精确抽象和编码可以防止编写出一大堆难以维护的代码。

  1. 使用简明扼要的函数名称

函数名称应该简明扼要,不应过于冗长或者简短。理想情况下,函数名称应该在不对代码刨根问底的情况下清楚反映出函数的功能。

函数名称应该使用驼峰式命名法,以小写字母开头:addItem(),saveToStore()或者getFirstName()。

因为函数代表了动作,函数名称应该至少包含一个动词。比如:deletePage(),verifyCredentials()。获取或者设置属性值时,使用标准的set和get前缀:getLastName()或者setLastName()。

避免编写含混的函数名,比如foo(),bar(),a(),fun()等等。这些名称没有意义。

如果函数小而清晰,名称简明扼要,代码就可以像散文一样阅读。

  1. 结论

当然,上面提供的示例十分简单。真实的应用中会更加复杂。你可能会抱怨仅仅为了抽象出一个层级而编写简练的函数是沉闷乏味的任务。但是如果从项目开始之初就正确实践的话就不会是一件困难的事。

如果应用已经有很多函数拥有太多职责,你会发现很难理解这些代码。在很多情况下,不大可能在合理的时间完成重构的工作。但是至少从点滴做起:尽你所能抽象一些东西。

最好的解决办法当然是从一开始就正确的实现应用。不仅要在实现需求上花费时间,同样应该像我建议的那样:正确组织你的函数,让它们小而简练。

三思而后行。(Measure seven times, cut once)

ES2015 实现了一个很棒的模块系统,清晰地建议出分割函数是好的实践。

记住永远值得投资时间让代码变得简练有组织。在这个过程中,你可能觉得实践起来很难,可能需要很多练习,也可能回过头来修改一个函数很多次。

但没有比一团乱麻的代码更糟的了。

  1. 译者注

文章作者提出的small function的观点可能会让初学者产生一点误解,在我的理解里,更准确的表述应该是从代码实现功能的逻辑层面抽象出更小的功能点,将抽象出的功能点转化为函数来为最后的业务提供组装的零件。最终的目的依然是通过解耦逻辑来提高代码的拓展性和复用性,而不能仅仅停留在视觉层面的”小“,单纯为了让函数代码行数变少是没有意义的。

搜索