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))
複製代碼
儘管這一行代碼效果不錯,但我不建議在咱們的例子中使用它,緣由有兩個:
沒錯,我當年硬編碼了選項。每次新增一個年份,我須要記住同時更新 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,編寫更簡單、更可讀代碼的指南,其中包含不少真實世界的例子。
最重要的是,承認這一事實,有時你會寫下糟糕的代碼,重構(讓它變得更好)是成長的標誌,值得慶祝。