我重構了一遍第一份工做寫的代碼

Jacque Schrag 原做,受權 New Frontend 翻譯。數組

#DevDiscuss 聊「開發者懺悔」這個話題時,我認可 3 年前開始個人第一份開發工做時,根本不知道本身在作什麼。題圖中的代碼是一個例子,展現了我當時是如何寫代碼的。瀏覽器

我收到太多分享相似經歷的迴應。咱們中的大多數人都寫過讓本身羞愧的糟糕代碼(硬寫一些愚蠢代碼,雖然能夠完成任務所需,但本能夠寫得更高效)。可是當咱們回顧過去的代碼時,若是能意識到咱們能夠如何寫得更好,乃至以爲當初作的選擇很好笑,那麼這是一個成長的標誌。秉承持續學習的精神,我想要分享一些如今的我會怎麼來寫這段代碼的方式。安全

上下文和目標

在重構任何陳年代碼以前,評估當初寫代碼時的上下文是極爲關鍵的一步。開發者當初作的某個瘋狂決策背後可能有一個重要的緣由,源自你不瞭解的上下文(或者源自你不記得的上下文,若是代碼是你寫的)。個人這個例子,則單純是由於缺少經驗,因此我能夠安全地重構代碼。ide

這段代碼是爲兩張數據可視化圖表寫的。它們的數據和功能類似,主要目標是讓用戶能夠根據類型、年份、區域過濾查看數據集。性能

數據集可視化

咱們假定改變過濾器會返回如下數值:學習

let currentType = 'in' // 或 'out'
let currentYear = 2017
let currentRegions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']
複製代碼

最後,下面是一個從 CSV 加載數據的簡化例子:ui

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,每一個類別一次。spa

咱們首先聲明類別常量。這樣將來咱們只需在 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 McCrearyBaseCode,編寫更簡單、更可讀代碼的指南,其中包含不少真實世界的例子。

最重要的是,承認這一事實,有時你會寫下糟糕的代碼,重構(讓它變得更好)是成長的標誌,值得慶祝。

相關文章
相關標籤/搜索