惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MyScale Blog
MyScale Blog
爱范儿
爱范儿
The Register - Security
The Register - Security
小众软件
小众软件
D
DataBreaches.Net
雷峰网
雷峰网
S
Secure Thoughts
L
LINUX DO - 最新话题
M
MIT News - Artificial intelligence
Application and Cybersecurity Blog
Application and Cybersecurity Blog
罗磊的独立博客
F
Fortinet All Blogs
博客园 - 叶小钗
TaoSecurity Blog
TaoSecurity Blog
博客园 - Franky
aimingoo的专栏
aimingoo的专栏
S
SegmentFault 最新的问题
Hacker News: Ask HN
Hacker News: Ask HN
Help Net Security
Help Net Security
N
News and Events Feed by Topic
S
Security Affairs
The Last Watchdog
The Last Watchdog
Forbes - Security
Forbes - Security
S
Security @ Cisco Blogs
Latest news
Latest news
O
OpenAI News
N
Netflix TechBlog - Medium
云风的 BLOG
云风的 BLOG
Blog — PlanetScale
Blog — PlanetScale
Google Online Security Blog
Google Online Security Blog
S
Securelist
博客园 - 司徒正美
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Palo Alto Networks Blog
T
The Blog of Author Tim Ferriss
Stack Overflow Blog
Stack Overflow Blog
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
AI
AI
SecWiki News
SecWiki News
G
GRAHAM CLULEY
Cloudbric
Cloudbric
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
LINUX DO - 热门话题
C
Cybersecurity and Infrastructure Security Agency CISA
B
Blog RSS Feed

一颗小树

周更的第 100 篇 改变阅读方式 宝贵的人生建议 小树的工具库 2023 读《重构》有感 仅需 10 分钟,用 GPTs 实现文章总结助手 高质量的需求交付 产品始于问题,而不是解决方案 如何更好地休息 最优解人生 与体重斗,其乐无穷 结构化 prompt = 数字员工? 持续创作的法门 如何降低知识焦虑 7 月思维碎片 N 倍生产力提升:我的 AI 助理 Indie Hacker,互联网打工人的下一个出路? 如何成就伟大事业 夏日碎片 小报编辑的自我修养 一颗小树 #49 投入真实生活 一颗小树 #48 过年杂记 一颗小树 #47 我的人生信念(2023) 一颗小树 #46 回本就卖 一颗小树 #45 规划和落地 小树的 2022 年终总结 一颗小树 #44 构建高质量信源 一颗小树 #43 我的 2022 书单 一颗小树 #42 阳了怎么办 一颗小树 #41 一人企业 一颗小树 #40 非必要别正常啦 一颗小树 #39 阅读和创造 一颗小树 #38 疫情生活碎片 一颗小树 #37 基于多维表格的可转债投资系统 一颗小树 #36 飞盘初体验 一颗小树 #35 雁栖湖半日游 一颗小树 #34 初探多维表格 一颗小树 #33 回不去的名字叫家乡 一颗小树 #32 生活需要留白 一颗小树 #31 Figma 的成功之道 一颗小树 #30 拓宽自己 拥抱善意 一颗小树 #29 有关下雨的记忆 一颗小树 #28 隐入尘烟 一颗小树 #27 找到并坚持自己的热爱 一颗小树 #26 好产品的“差”改版 一颗小树 #25 吹吹南澳岛的海风 一颗小树 #24 代码重构的一点心得 一颗小树 #23 和团队新同学的对话 一颗小树 #22 从《干法》中学到的 一颗小树 #21 阿里四年我的收获和感想 一颗小树 #20 周更半年我学到的事 一颗小树 #19 慢慢来比较快 一颗小树 #18 只规划每天最重要的三件事 一颗小树 #17 裁员潮下的反思 一颗小树 #16 纪录片的力量在于真实 一颗小树 #15 世界很小但很好 一颗小树 #14 让自己成为问题的终结者 一颗小树 #13 别忘记你的热爱 一颗小树 #12 聊聊写作这件事 一颗小树 #11 开始投资前更重要的事 一颗小树 #10 疫情囤货指南 一颗小树 #9 疫情之下 重建内心的秩序 一颗小树 #8 不要倒在黎明前 一颗小树 #7 从小孩子身上学到的事 一颗小树 #6 和自己的产品一起成长 一颗小树 #5 技术对生产力的的变革是不可阻挡的 一颗小树 #4 暴风雨的意义 一颗小树 #3 投资成功,是我们变成一个更好的人之后自然的结果 一颗小树 #2 持续学习来应对这个复杂多变的世界 一颗小树 #1 任天堂和上岭村 一颗小树 #0 这是写给你们的第一封信 游戏改变世界 把自己给开源了 构建自己的不为清单 有爱和被爱 做了一点微小的工作 小树的 2021 年终总结 小树的 2020 年终总结 与其感慨路难行,不如马上出发 阿里前端也切图?不,人工智能帮你做 如何写好简历,拿到大厂 offer 的敲门砖 任时光匆匆流去 | 2019 年终总结 聊聊在阿里远程办公那点事儿 在 D2 做主持人是怎样一种体验? 提高工作效率的几个方法 有限状态机在 CSS 动画中的应用 「译」React 项目结构和组件命名之道 使用纯粹的JS构建 Web Component 翻译计划-状态管理请三思 利用Dawn工程化工具实践MobX数据流管理方案 高性能 MobX 模式(part 3)- 用例教程 高性能 MobX 模式(part 2)- 响应变化 技术文章写法浅谈 翻译计划-用node.js开发一个可交互的命令行应用 无他,唯手熟尔-前端实习面试题篇 工欲善其事必先利其器-前端实习简历篇 手把手带你用原生js实现css属性的set和get 我们来翻翻元素样式的族谱-getComputedStyle 翻译计划-从sass到PostCSS 把简单做好也不简单-css水平垂直居中
编写小而美函数的艺术
也树 · 2018-01-26 · via 一颗小树

随着软件应用的复杂度不断上升,为了确保应用稳定且易拓展,代码质量就变的越来越重要。

原文链接: https://dmitripavlutin.com/the-art-of-writing-small-and-plain-functions 译者:阿里云-也树

不幸的是,包括我在内的几乎每个开发者在职业生涯中都会面对质量很差的代码。这些代码通常有以下特征:

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

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

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

Sad computer about bad code quality

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

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

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

函数需要“小”

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

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

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

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

Step 0: 最初的庞大函数

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

在 repl.it 中尝试运行

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() 函数过于庞大,看起来像个装有很多惊喜的黑盒子。你很难第一眼理解它是做什么的,再想象一下你的应用里有一堆这样的函数是什么光景。

当你在和这样的代码打交道时,是在浪费时间和精力。另一方面小而能够自解释的函数读起来也会让人愉悦,方便开展之后的工作。

避免庞大的黑盒函数

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

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

仅仅看到124 这三个魔数而不了解上下文的情况下根本搞不清楚他们的含义。幸运的是 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_PRIMITIVEWEIGHT_OBJECT_FUNCTION 从变量名就可以看出“重量”所描述的数据类型,而不需要再猜 1, 24 代表什么。

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 模块标准使代码模块化。把函数抽象成独立的模块,这样会让你的项目文件更轻量和结构化。

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

Favor transparent composite functions

有条通用的准则:一个函数不应该超过 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 是否为素数的功能,需要确认是否从 2Math.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() 函数小而精炼。它仅仅保留了必需的低一层级的抽象。

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

使用简明扼要的函数名称

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

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

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

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

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

结论

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

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

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

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

Happy computer about good code quality

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

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

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

译者注

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