Hardog's blog

trace forever

Group: 572218159
Email: 1273203953@qq.com
Location: hangzhou·zhejiang
GitHub: https://github.com/hardog

原文链接: The art of writing small and plain functions

The complexity of software applications is growing. The code quality is important in order to make the application stable and easily extensible.
软件应用正变的越来越复杂. 要想使得应用变的稳定和易扩展, 代码质量起到至关重要的作用.

Unfortunately almost every developer, including myself, in his career faced with bad quality code. And it’s a swamp.
Such code has the following harmful characteristics:
不幸的是, 包括我自己在内几乎所有的软件开发者在他的职业生涯中都遇到过质量差的代码, 它带给我们很多的麻烦. 质量差的代码大致有如下几个特点:

1
1. 一个函数职责太多, 函数体很长.
2. 这样的函数有很大的副作用导致很难理解和调试.
3. 含糊不清的函数和变量命名.
4. 不健壮代码, 小小的修改就可能导致程序内其他功能无法正常工作.
5. 缺少或者没有代码覆盖率测试.

It sounds very common: “I don’t understand how this code works”, “this code is a mess”, “it’s hard to modify this code” and the like.
经常听到类似这样的抱怨: '我根本不能理解这代码是怎么工作的','这代码太凌乱了','这代码根本无法修改'

Once I had a situation when my colleague quit his job because he dealt with a REST API on Ruby that was hard to maintain. He received this project from previous team of developers.
Fixing current bugs creates new ones, adding new features creates a new series of bugs and so on (fragile code). The client didn’t want to rebuild the application with a better design, and the developer made the correct decision to quit.
曾经遇到同事停止维护一个应用程序情况, 因为他接手了一个用REST API编写的Ruby应用很难维护. 他从先前的开发团队接手了这个项目. 解决了一个BUG时又产生了另外一个BUG, 当增加新的功能时又引入了一系列新的BUG.客户并不想用一个更好的设计来重新构建应用程序, 开发者也就停止维护了.

mac-sick

Ok, such situations happen often and are sad. But what do to?
这种情况经常发生, 让人很悲伤. 针对这种情况我们应该做些什么呢?

The first to keep in mind: simply making the application run and taking care of the code quality are different tasks.
首先应该记住: 让程序跑起来和保持良好的代码质量是不同的方面的任务.

On one side you implement the app requirements. But on the other side you should take time to verify if any function doesn’t have too much responsibility, write comprehensive variable and function names, avoid functions with side effects and so on.
一方面你需要实现app需求. 另一方面你需要花时间来确保你的函数没拥有太多的职责. 编写易理解的变量和函数名, 避免函数产生副作用等.

The functions (including object methods) are the little gears that make the application run. First you should concentrate on their structure and composition. The current article covers best practices how to write plain, understandable and easy to test functions.
函数(包括对象方法)是使应用程序跑起来必不可少的部分.首先, 你应该集中注意力在函数的结构和组合上. 这篇文章介绍了一些最佳实践关于如何编写简单可理解、易测试的函数.

1. Functions should be small. Really small

函数应该尽可能保持简短

Big functions that have a lot of responsibility should be avoided and split into small ones. Big black box functions are difficult to understand, modify and especially test.
大函数一般都拥有太多的职责, 应该尽量将大函数拆解. 因为大的黑盒函数很难理解、修改和测试.

Suppose a scenario when a function should return the weight of an array, map or plain JavaScript object. The weight is calculated by summing the property values:
现在假设一种场景用来返回数组、map、简单对象的权重. 按以下方式计算权重:

1
- null、undefined被计算为1个点
- 原始数据类型被计算为2个点
- 对象和函数被计算为4个点

For example the weight of an array [null, ‘Hello World’, {}] is calculated this way: 1 (for null) + 2 (for string primitive type) + 4 (for an object) = 7.
例如数组 [null, 'Hello World', {}] 按照以下方式计算: 1(null) + 2('hello world' primitive type) + 4({} object) = 7

Step 0: The initial big function

第0步: 一开始编写的大函数

Let’s start with the worst practice. The logic is coded within a single big function getCollectionWeight():
让我们以最糟糕的实践开始. 整个逻辑都被塞进getCollectionWeight()函数里.

1
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

The problem is clearly visible. getCollectionWeight() function is too big and looks like a black box full of surprises.
这里问题很明显. getCollectionWeight()这个函数太长, 看起来像一个黑盒充满了令人意想不到的结果.

You probably find it difficult to understand what it does from the first sight. And imagine a bunch of such functions in an application.
咋一看你可能发现这个函数非常难理解. 想象一下如果一个应用里面有非常多的这样的函数会怎么样?

When you work with such code, you waste time and effort. On the other side the quality code doesn’t make you feel uncomfortable. Quality code with small and self-explanatory functions is a pleasure to read and easy to follow.
当你需要与这样的代码周旋时, 只能是让你的时间和心血白费. 另一方面, 这样差质量的代码会让你觉得很不舒服. 高质量、简洁、能够自解释的函数是非常容易阅读和维护的.

1

Step 1: Extract weight by type and drop magic numbers

第一步:弃用魔法数字, 按照类型抽取权重计算函数

Now the goal is to split the big function into smaller, independent and reusable ones. The first step is to extract the code that determines the weight of a value by its type. This new function will be named getWeight().
现在, 我们的目标是将一长段函数分成简洁、独立和可重用的代码. 第一步是抽取按照类型计算权重的代码. 该函数将被命名为getWeight().

Also take a look at the magic weight numbers: 1, 2 and 4. Simply reading these numbers without knowing the whole story does not provide useful information. Fortunately ES2015 allows to declare const read-only references, so you can easily create constants with meaningful names to knockout the magic numbers.
同样, 让我们来看一下魔法数字1、2、4. 如果仅仅单看这些数字, 并不能得到更多有用的信息. 幸运的是, ES2015允许申明只读引用. 因此, 你可以创建富有意义的常量来淘汰那些魔法数字.

Let’s create the small function getWeightByType() and improve getCollectionWeight() accordingly:
让我们来创建短函数getWeightByType(), 根据情况改善getCollectionWeight()函数:

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

Looks better, right?
getWeightByType() function is an independent component that simply determines the weight by type. And reusable, as you can execute it in any other function.
现在看起来更好了, 对吧? 现在getWeightByType()函数是一个独立的组件, 按照类型计算权重. 函数也要是可重用的, 以便在其他函数中重复使用.

getCollectionWeight() becomes a bit lighter.
现在getCollectionWeight()函数更轻量了.

WEIGHT_NULL_UNDEFINED, WEIGHT_PRIMITIVE and WEIGHT_OBJECT_FUNCTION are self-explanatory constants that describe the type weights. You don’t have to guess what 1, 2 and 4 numbers mean.
WEIGHT_NULL_UNDEFINED、WEIGHT_PRIMITIVE以及WEIGHT_OBJECT_FUNCTION都是含义非常明确能描述权重的常量, 不用猜1,2,4到底是什么意思了.

Step 2: Continue splitting and make it extensible

第二步:继续分解和扩展函数

However the updated version still has drawbacks.
Imagine that you have the plan to implement the weight evaluation of a Set or even other custom collection. getCollectionWeight() will grow fast in size, because it contains the logic of collecting the values.
经过改造的函数仍然是有缺点的. 想象一下你需要实现其他Set, 甚至是自定义集合的权重计算. getCollectionWeight()这个函数将会增长非常快, 因为它包含收集值的逻辑.

Let’s extract into separated functions the code that gathers values from maps getMapValues() and plain JavaScript objects getPlainObjectValues(). Take a look at the improved version:
让我们为收集值抽取出单独的函数, maps抽取成getMapValues(), 简单对象抽取成getPlainObjectValues(). 接下来让我们看看简化后的版本:

1
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

If you read getCollectionWeight() now, you find much easier figure out what it does. It looks like an interesting story.
现在你回过头来看getCollectionWeight()函数, 你可以更容易的发现这个函数做了什么事, 就像看故事一样有趣.

Every function is obvious and straightforward. You don’t waste time digging to realize what the code does. That’s how the clean code should be.
现在每个函数的功能都很明显和直接, 你不必耗费时间去理解函数的执行原理. 这就是简洁代码存在的理由.

Step 3: Never stop to improve

第三步: 不要停止改进

Even at this step you have a lot of space for improvement!
即使到现在例子函数仍然有很多改进的空间!

You can create getCollectionValues() as a separated function, which contains the if/else statements to differentiate the collection types:
你可以创建一个单独的getCollectionValues()函数, 该函数包含了if/else函数来区分集合类型.

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

Then getCollectionWeight() would become truly plain, because the only thing it needs to do is: get the collection values getCollectionValues() and apply the sum reducer on it.
现在getCollectionWeight()该函数变得真正简洁了, 它所需要做的唯一事就是:获取集合值然后在它上面运用reducer函数.

You can also create a separated reducer function:
你也可以创建一个单独的reducer函数:

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

Because ideally getCollectionWeight() should not define functions.
理想情况下, getCollectionWeight()不应该定义任何函数.

In the end the initial big function is transformed into the following small functions:
最后, 一开始的长函数被分割成以下的小函数:

1
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

That’s the art of writing small and plain functions!
以上就是编写小而美函数的艺术!

After all these code quality optimizations, you get a bunch of nice benefits:
在优化完之后, 你可以获取以下好处:

1
- getCollectionWeight()函数因自解释代码而变的更易读
- getCollectionWeight()函数的大小也变小了
- 函数getCollectionWeight()现在也不会随着所要计算的集合类型快速增长
- 被抽取的函数现在解耦和可重用了. 你的同事可能要求你把这些函数导入另一个工程里去使用, 而这很容易做到.
- 一旦程序出现了错误, 由于函数名字的原因调用栈变的更精确. 你几乎能够立即找到问题所在.
- 被分割的函数更容易测试能够拥有更高的测试覆盖率. 你可以通过结构化你的测试、单独验证每一个小函数, 而不必去考虑一个很长函数的各种可能测试场景.
- 你可以享受CommonJS和ES2015模块化所带来的好处. 从分离出的函数中抽离出单独的模块, 这能够使你的工程文件更轻量和结构化.

These advantages help you survive in the complexity of the applications.
这些优点能够帮助你摆脱复杂应用程序的困境

2

As a general rule, your functions should not be longer than 20 lines of code. Smaller - better.
一般来说你的函数不应该超过20行. 越小越好.

I think now you want to ask me a reasonable question: “I don’t want to create functions for each line of code. Is there a criteria when I should stop splitting?” This is a subject of the next chapter.
现在, 我想你可能要问我一个问题了:'我并不想为每一行代码都创建一个函数, 抽离函数是否有一个标准我们应该遵循.'这是下一章节的主题

2. Functions should be plain

2. 函数应该是清晰的

Let’s relax a bit and think what is actually a software application?
让我们停下来放松, 思考下软件应用的本质是什么?

Every application is implementing a list of requirements. The role of developer is to divide these requirements into small executable components (namespaces, classes, functions, code blocks) that do a well determined task.
每一个应用程序都是为了实现一系列的需求. 开发者的角色是把这些需求分割成小的可执行的单元(命令空间、类、函数、代码块)能给很好的执行任务.

A component consists of other smaller components. If you want to code a component, you need to create it from components at only one level down in abstraction.
一个组件又又另一系列小的组件组成.当你想要编码创建一个组件时, 你需要在同一个抽象级别去抽离

In other words, what you need is to decompose a function into smaller steps, but keep these steps at the same, one step down, level of abstraction. This is important because it makes the function plain and implies to “do one thing and do it well”.
换句话说, 你所需要做的就是将一个函数解耦分离成若干小步骤, 同时需要保持这些小的函数在同一抽象层次上.这很重要, 因为它能使函数更简单, 同时也表达出'做一件事并且把它做好'的理念

Why is this necessary? Because plain functions are obvious. Obvious means easy to read and modify.
为什么这是必须的?因为简洁的函数阅读起来非常清晰, 清晰意味着易读和易修改

Let’s follow an example. Suppose you want to implement a function that keeps only prime numbers (2, 3, 5, 7, 11, etc) in an array, removing non prime ones (1, 4, 6, 8, etc). The function is invoked this way:
让我们举个例子.假设你想实现一个函数, 删除非奇数使得数组中仅仅包含奇数. 函数将按以下方式调用

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

What are steps at one level down in abstraction to implement the function getOnlyPrime()? Let’s formulate this way:
在同一个层次上抽象这个函数的步骤是什么?让我们来公理化该这个步骤

1
To implement getOnlyPrime() function, filter the array of numbers using isPrime() function.
1
为了实现getOnlyPrime()函数, 使用isPrime()来过滤该函数.

Simply, just apply a filter function isPrime() over the array of numbers.
简单地, 在数组上运用isPrimse()函数即可

Do you need to implement the details of isPrime() at this level? No, because getOnlyPrime() function would have steps from different level of abstractions. The function would take too much responsibility.
你需要在getOnlyPrime()这个函数级别上实现isPrime()的详情嘛? 不必的, 因为getOnlyPrime()函数还拥有其它层次上的抽象.该函数将会拥有太多职责

Having the plain idea in mind, let’s implement the body of getOnlyPrime() function:
在头脑中时刻保持简洁的想法, 接下来让我们来实现getOnlyPrimse()函数体

1
function getOnlyPrime(numbers) {  
  return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]

As you can see, getOnlyPrime() is plain and simple. It contains steps from a single level of abstraction: .filter() array method and isPrime().
正如你能看到的, getOnlyPrime()函数现在变的很清晰和简单. 它拥有同一层次上的抽象.

Now is the time move one level down in abstraction.
现在是时候在一个抽象级别上来抽离出函数了

The array method .filter() is provided by JavaScript engine and use it as is. Of course the standard describes exactly what it does.
.filter过滤方法由原生Javascript引擎提供. 当然也提供了该函数具体的功能说明

Now you can detail into how isPrime() should be implemented:
现在你可以看下isPrimse()具体实现原理

1
To implement isPrime() function that checks if a number n is prime, verify if any number from 2 to Math.sqrt(n) evenly divides n.
1
为了实现isPrimse()函数检查一个数字是否为奇数, 检验是否有任何从2到Math.sqrt(n)能整除n.

Having this algorithm (yet not efficient, but used for simplicity), let’s code isPrime() function:
使用以下的算法来实现isPrimse()函数(虽然不是很高效, 但是很简单)

1
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() is small and plain. It has only strictly necessary steps from one level down in abstraction.
现在getOnlyPrime()函数很清晰短小. 它仅仅包含在一个实现层次上的抽象步骤

The readability of complex functions can be much improved if you follow the rule of making them plain. Having each level of abstraction coded precisely prevents the creation of big chunks of unmaintainable code.
遵循使函数清晰的规则能够大大改善函数的可读性.确保在每一个抽象级别上编写代码能够阻止不可维护代码的产生

3. Use concise function names

使用简洁的函数名字

Function names should be concise: no more and no less. Ideally the name suggests clearly what the function does, without the necessity to dive into the implementation details.
函数名字应该保持简洁: 恰如其分的命名. 理想情况下, 函数名字应该能够清晰的表达出函数的功能, 而不用表达函数的实现细节

For function names use camel case format that starts with a lowercase letter: addItem(), saveToStore() or getFirstName().
使用小写字母开头的驼峰命名法来命名函数.

Because functions are actions, the name should contain at least one verb. For example deletePage(), verifyCredentials(). To get or set a property, use the standard set and get prefixes: getLastName() or setLastName().
因为函数是执行的一个动作, 函数名字应该至少有一个动词. 为了设置一个属性, 使用标准的get, set前缀

Avoid in the production code misleading names like foo(), bar(), a(), fun(), etc. Such names have no meaning.
避免在生产环境中使用容易误导人的名字例如foo(), bar(), a()等等. 这些名字毫无意义.

If functions are small and plain, names are concise: the code is read as a wonderful prose.
如果函数名字清晰简短, 名字简洁, 代码将能够像阅读散文一样随心自如

4. Conclusion

总结

Certainly the provided examples are quite simple. Real world applications are more complex. You may complain that writing plain functions, with only one level down in abstraction, is a tedious task. But it’s not that complicated if your practice right from the start of the project.
虽然上面的例子非常简单. 实际的例子是非常复杂的.你可能抱怨编写在一个抽象级别, 清晰的函数是一个沉闷的任务. 但是如果你从工程开始就遵循这样的实践, 编写这样的函数并不会太复杂.

If an application already has functions with too much responsibility, you may find hard to reorganize the code. And in many cases impossible to do in a reasonable amount of time. At least start with small: extract something you can.
如果一个应用程序的函数拥有太多的职责, 你可能发现很难组织你的代码. 在大多数情况下这不可能在一个合理的时间去完成这样的工作. 至少以编写清晰简短的函数开始.

Of course the correct solution is to implement the application correctly from the start. And dedicate time not only to implementation, but also to a correct structure of your functions: as suggested make them small and plain.
当然, 正确的解决方案是要从一开始就正确的的遵循本文的方式去实现应用. 不仅要花时间去实现函数功能, 也要花时间在如何正确的编写小而清晰的函数上面.

Measure seven times, cut once.

HappyMac

ES2015 implements a nice module system, that clearly suggest that small functions are a good practice.
ES2015实现了一个模块系统, 清晰的表达出小函数是一个好的实践.

Just remember that clean and organized code always deserves investing time. You may find it hard to do. You may need a lot of practice. You may come back and modify a function multiple times.
记住简洁和可重新组织的代码是值得花费精力的.你可能发现这很难.需要很多的实践.可能需要多次频繁的修改一个函数

Nothing can be worse than messy code.
没有什么比凌乱的代码更糟糕的了