我重构了一遍第一份工作写的代码

栏目: JavaScript · 发布时间: 5年前

内容简介:Jacque Schrag 原作,授权New Frontend 翻译。在#DevDiscuss 聊“开发者忏悔”这个话题时,我承认 3 年前开始我的第一份开发工作时,根本不知道自己在做什么。题图中的代码是一个例子,展示了我当时是如何写代码的。我收到太多分享类似经历的回应。我们中的大多数人都写过让自己羞愧的糟糕代码(硬写一些愚蠢代码,虽然可以完成任务所需,但本可以写得更高效)。但是当我们回顾过去的代码时,如果能意识到我们可以如何写得更好,乃至觉得当初做的选择很可笑,那么这是一个成长的标志。秉承持续学习的精神,

Jacque Schrag 原作,授权New Frontend 翻译。

在#DevDiscuss 聊“开发者忏悔”这个话题时,我承认 3 年前开始我的第一份开发工作时,根本不知道自己在做什么。题图中的代码是一个例子,展示了我当时是如何写代码的。

我收到太多分享类似经历的回应。我们中的大多数人都写过让自己羞愧的糟糕代码(硬写一些愚蠢代码,虽然可以完成任务所需,但本可以写得更高效)。但是当我们回顾过去的代码时,如果能意识到我们可以如何写得更好,乃至觉得当初做的选择很可笑,那么这是一个成长的标志。秉承持续学习的精神,我想要分享一些现在的我会怎么来写这段代码的方式。

上下文和目标

在重构任何陈年代码之前,评估当初写代码时的上下文是极为关键的一步。开发者当初做的某个 疯狂 决策背后可能有一个重要的原因,源自你不了解的上下文(或者源自你不记得的上下文,如果代码是你写的)。我的这个例子,则单纯是因为缺乏经验,所以我可以安全地重构代码。

这段代码是为两张数据可视化图表写的。它们的数据和功能相似,主要目标是让用户可以根据类型、年份、区域过滤查看数据集。

我重构了一遍第一份工作写的代码

我们假定改变过滤器会返回以下数值:

let currentType = 'in' // 或 'out'
let currentYear = 2017
let currentRegions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']
复制代码

最后,下面是一个从 CSV 加载数据的简化例子:

const data = [
  { country: "Name", type: "in", value: 100, region: "Asia", year: 2000 },
  { country: "Name", type: "out", value: 200, region: "Asia", year: 2000 },
  ...
]
// 数组中总共约有 2400 条数据
复制代码

选项一:初始化空对象

除了硬编码之外,我原本的代码完全违反了 DRY 原则。当然有些情况下重复是有意义的,但在这个不断重复同样属性的情况下,动态创建对象是更明智的选择。这还可以降低数据集新增年份的工作量,同时降低输入错误的风险。

这里有好几种选择: for.forEach.reduce 。我将使用 .reduce 方法处理数组,将数组转化为其他东西(在我们的例子中是对象)。我们使用三次 .reduce ,每个类别一次。

我们首先声明类别常量。这样未来我们只需在 years 数组中加上新的年份,我们将要编写的代码会处理剩下的部分。

const types = ['in', 'out']
const years = [2000, 2005, 2010, 2015, 2016, 2017]
const regions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']
复制代码

我们想要逆转 types → years → regions 的顺序,从 regions 开始。一旦 regions 转换为对象,就可以将它赋值给 years 属性。years 和 types 同理。尽管我们可以少写几行代码,但我选择更清晰而不是更聪明的写法。

const types = ['in', 'out']
const years = [2000, 2005, 2010, 2015, 2016, 2017]
const regions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']

// 将 regions 转换为对象,每个 region 是一个属性,值为一个空数组。
const regionsObj = regions.reduce((acc, region) => {
  acc[region] = []
  return acc
}, {}) // 累加器(`acc`)的初始值设为 `{}`

console.log(regionsObj)
// {Africa: [], Americas: [], Asia: [], Europe: [], Oceania: []}
复制代码

既然已经有了区域对象,年份和类型也可以照此处理。只不过它们的值不是像区域一样设为空数组,而是之前说的类别对象。

function copyObj(obj) {
  return JSON.parse(JSON.stringify(obj))
}

// 和 regions 一样处理 years,但将每个年份的值设为 region 对象。
const yearsObj = years.reduce((acc, year) => {
  acc[year] = copyObj(regionsObj)
  return acc
}, {})

// type 也一样。返回最终对象。
const dataset = types.reduce((acc, type) => {
  acc[type] = copyObj(yearsObj)
  return acc
}, {}

console.log(dataset)
// {
//  in: {2000: {Africa: [], Americas: [],...}, ...},
//  out: {2000: {Africa: [], Americas: [], ...}, ...}
// }
复制代码

我们现在得到的效果和我最初的代码是一致的,然而我们成功地将它重构成可读性更强、更容易重构的代码!需要在数据集中新增年份时再也不需要复制粘贴了!

不过还有一个问题:我们仍然需要手工更新年份列表。而且既然我们将在对象中加载数据,没理由单独初始化一个空对象。下面的两个重构选项完全脱离了我最早的代码,展示了如何直接使用数据。

附注:老实说,如果我在 3 年前尝试重构,我大概会用 3 层嵌套的 for 循环,并对此表示满意。就像Stephen Holdaway 在评论中给出的写法:

const types = ['in', 'out'];
const years = [2000, 2005, 2010, 2015, 2016, 2017];
const regions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania'];

var dataset = {};

for (let typ of types) {
  dataset[typ] = {};
  for (let year of years) {
    dataset[typ][year] = {};
    for (let region of regions) {
      dataset[typ][year][region] = [];
    }
  }
}

复制代码

我之前使用 reduce 的写法避免了过深的嵌套。

选项二:直接过滤

有些读者大概想知道为什么我们要把数据按类型分组。我们本可以使用 .filter 根据 currentType (当前类型)、 currentYear (当前年份)、 currentRegion (当前区域) 返回所需数据,就像这样:

/*
  `.filter` 会创建一个新数组,其中所有的成员均匹配 `currentType` 和 `currentYear`。
  `includes` 根据 `currentRegions` 是否包含条目的 region 返回真假。
*/
let currentData = data.filter(d => d.type === currentType && d.year === currentYear && currentRegion.includes(d.region))
复制代码

尽管这一行代码效果不错,但我不建议在我们的例子中使用它,原因有两个:

  1. 用户每次选择筛选条件都会运行该方法。取决于数据集的大小(别忘了数据集将随着年份增长),这可能影响性能。现代浏览器很高效,性能损失也许极小,但如果我们明知用户每次只能选择一种类型和一个年份,我们可以在一开始就分组数据,主动提升性能。
  2. 这一选项不会提供可供选择的类型、年份、区域列表。有了这些列表,我们可以使用它们动态生成用户界面的骨架,无需手动创建并更新。
我重构了一遍第一份工作写的代码

没错,我当年硬编码了选项。每次新增一个年份,我需要记住同时更新 JS 和 HTML。

选项三:由数据驱动的对象

我们可以将前两个选项组合一下,得到第三种重构方式。这种方式的目标是在更新数据集时完全不需要修改代码,直接根据数据确定类别。

同样,要做到这一点,技术上有多种方法。不过,我将继续使用 .reduce

const dataset = data.reduce((acc, curr) => {
  // 如果累加器的属性中已存在当前类型,将其设为自身,否则初始化为空对象。
  acc[curr.type] = acc[curr.type] || {}
  // 年份同理
  acc[curr.type][curr.year] = acc[curr.type][curr.year] || []
  acc[curr.type][curr.year].push(curr)
  return acc
}, {})
复制代码

注意上面的代码中不包括区域。这是因为,和类型、年份不同,可以同时选中多个区域。这使得预先根据区域分组毫无作用,要是这么做了,我们还得合并它们。

考虑到这一点,下面是新版的根据选定类型、年份、区域获取 currentData 的一行代码。由于我们将数据的查找范围限定于当前类型和当前年份,我们知道数组中数据项数目的最大值等于国家数(小于 200),这就比选项二中的 .filter 实现要高效很多。

let currentData = dataset[currentType][currentYear].filter(d => currentRegions.includes(d.region))
复制代码

最后一步是获取不同类型、年份、区域的数组。为此我将使用 .map 和集合。下面是一个例子,获取一个数组,包含数据中所有不同区域。

// `.map` 将提取特定对象属性值(例如,区域)到新数组
let regions = data.map(d => d.region)

// 根据定义,集合中的值是唯一的。重复值将被剔除。
regions = new Set(regions)

// Array.from 根据集合创建数组。
regions = Array.from(regions)

// 单行版本
regions = Array.from(new Set(data.map(d => d.region)))

// 或者使用 ... 操作符
regions = [...new Set(data.map(d => d.region))]
复制代码

使用同样的方法处理类型和年份。接着就可以根据数组的值动态创建过滤界面。

最终的重构代码

最终我们得到了如下的重构代码,未来数据集新增年份无需手工改动。

// 类型、年份、区域
const types = Array.from(new Set(data.map(d => d.type)))
const years = Array.from(new Set(data.map(d => d.year)))
const regions = Array.from(new Set(data.map(d => d.region)))

// 根据类型和年份分组数据
const dataset = data.reduce((acc, curr) => {
  acc[curr.type] = acc[curr.type] || {}
  acc[curr.type][curr.year] = acc[curr.type][curr.year] || []
  acc[curr.type][curr.year].push(curr)
  return acc
}, {})

// 根据选中内容获取数据
let currentData = dataset[currentType][currentYear].filter(d => currentRegions.includes(d.region))
复制代码

结语

调整格式仅仅是重构的一小部分,“重构代码”常常意味着重新构想实现和不同部分之间的关系。解决问题有多种方式,所以重构不容易。一旦找到有效的解决方案,可能不太容易去考虑不同做法。确定哪种解决方案更好并不总是显而易见的,可能很大程度上取决于代码的上下文,甚至,个人偏好。

想要更好地重构代码,我有一条简单的建议:阅读更多代码。如果你在团队里,积极参与代码审阅。如果有人让你重构代码,问下为什么并且尝试去理解其他人处理问题的方式。如果你单独工作(就像我刚开始工作时一样),留意同一问题的不同解决方案,同时搜寻最佳实践指南。我强烈推荐阅读Jason McCreary 的BaseCode,编写更简单、更可读代码的指南,其中包含很多真实世界的例子。

最重要的是,认可这一事实,有时你会写下糟糕的代码,重构(让它变得更好)是成长的标志,值得庆祝。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Blockchain Basics

Blockchain Basics

Daniel Drescher / Apress / 2017-3-16 / USD 20.99

In 25 concise steps, you will learn the basics of blockchain technology. No mathematical formulas, program code, or computer science jargon are used. No previous knowledge in computer science, mathema......一起来看看 《Blockchain Basics》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

SHA 加密
SHA 加密

SHA 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具