Copyright © 2008 ~ 2024 MUKI space* / omegaBook theme All Rights Reserved.

前言

在程式開發的領域中,重構也是重要的一環,透過重構,可以改善程式碼的結構和品質,也可能加強系統、功能的可維護性和可擴展性。

而我最近重構的部分,是公司的圖表功能,他是一個已經上線,且規格成熟運作良好的功能。為什麼我要重構?我在重構前準備了什麼?我重構了哪些部分?想透過這篇文章將我的心路歷程記錄下來,希望能讓以後的重構之路愈發順利 👍

重構的時機

我先列舉幾個比較常見的重構原因:

  1. 功能不穩定:如果已上線的功能時好時壞,或是容易踩雷觸發 bug,的確需要重構以修復這些問題,並提高穩定性
  2. 程式碼過於複雜:有時候因為新需求不斷,或邏輯太繞,導致程式碼變得太複雜也不好維護,這時候就可以靠重構簡化程式碼結構,並重新梳理自己的邏輯,提高可讀性和可維護性。
  3. 效能問題:效能是部分的前端工程師容易忽略的部分,卻是佔整個程式開發很重要的一環。如果執行速度慢,網站容易當機,也是需要重構以優化效能的。
  4. 技術債:通常發生在「功能要快速上線,但開發時間短」的情況,趕工時很容易降低程式碼的品質以及設計原則。正所謂欠債總是要還,我們可以透過重構來償還技術債,降低未來維護的成本。

而我這次重構的主要原因,就是程式碼過於複雜,我希望精簡化程式碼,以提高可讀性和可維護性。

重構前要準備什麼

對功能的熟悉程度

鑑於這次重構的是「已上線」且「規格成熟完整」的功能,因此要先確認,自己是否能掌握該功能的所有規格以及資料結構

▼ 以我重構的圖表為例,畫面上的各種設定,都會影響圖表的呈現,有的互相獨立,有的卻息息相關。

當功能增多時,如果對其不夠熟悉,或者隔了一段時間才添加新的功能,就可能會出現「使用重複的變數來實現新功能」的情況。然而,先前可能已經存在可用的變數,只是我們可能不知道或者擔心更改這些變數會對舊功能造成影響。

因此,當該功能已經趨近於成熟,也不太會再加入新的功能時,就可以考慮重構了。前提是,我們能清楚的了解圖表的架構、功能,與相關程式碼,重構時才能避免出現重複的問題。

備份與讀取舊檔案

✭ 步驟一:

從主要的 branch (例:dev)拉出一條分支,我的分支名稱為 refactor/chart

✭ 步驟二・備份檔案:

我的圖表元件是 chart.vue,所以我將原檔案複製一份叫做 _chart.vue,以做為備份

✭ 步驟三:

接下來是我很推薦的一個功能,使用 git worktree 指令,讓我們在不需要切換 branch 的情況下,再開一個工作站以瀏覽網站。

例如我現在在 refeactor/chart 的分支,但我想要瀏覽 dev 分支,以看到重構前的網站畫面,這時我就可以用 git worktree 把 dev 分支建成一個新的工作站:

$ git worktree add -b <new_branch_name> <folder_path> <source_branch>

詳細的介紹可以參考以下文章:

✭ 步驟四:

我重構時喜歡從頭來過,所以我會在 refactor/chart 分支底下,豪邁的將 chart.vue 的內容全部清空,然後重新撰寫內容。

現在,我會有兩個 VSCode 視窗

▼ 一個是 refactor/chart 的分支,會放 _chart.vue(原檔) 以及 chart.vue(重構的新檔) 兩個檔案,方便我了解新舊差異

▼ 另外一個視窗是我用 git worktree 做的工作站,可以直接看重構前的網站畫面,如果要印 console.log 找舊資料也很方便

重構了哪些部分

我用的圖表套件是 Apache ECharts,跟大部分的圖表套件類似,他用 option{} 來設定圖表的所有資料。而我這次是針對 Apache ECharts 做程式碼的重構與精簡,以提高可讀性和可維護性。

從初始化圖表開始

✭ 重構前:

初始化圖表後,直接呼叫 fetchOption(),把一大串的 option 寫在同個 function 一併處理。另外把兩個有過多條件的 yAxis: {}series: [] 拉成 function 額外處理。

<script>
  
const fetchOption = async () => {
  const setYAxis = () => { }
  const fetchSeriesData = () => { }

  const getOption = async () => {
    option.value = {
      ...
    }
  }

  await getOption()
  
  chartInstance.setOption(option.value)
}

const initChart = async () => {
  chartInstance = useChart(chartRef.value, 'shine')
  await fetchOption()
}
</script>

✭ 重構後:

option{} 所有的設定拆成獨立的 function,未來如果資料有變動,可以直接呼叫對應的 function,以及我封裝過的 initChar()updateChart(),降低彼此的耦合

<script>
  
class ChartGenerator{
  initChart() {
    if (this.chartRef === null) return
    this.chartInstance = useChart(this.chartRef, 'shine')
    const { setOption } = this.chartInstance
    setOption(this.chartOptions)
    return this
  }
  
  updateChart() {
    if (this.chartInstance === null || this.chartInstance === undefined) return
    const { setOption } = this.chartInstance
    setOption(this.chartOptions)
    return this
  }
}
  
  
const fetchChart = async () => {
  chart.value = new ChartGenerator(chartRef.value)
  await chart.value
    .setGrid()
    .setToolTip()
    .setLegend()
    .setXAxis()
    .setYAxis()
    .setDatazoom()
    .setSeries()
    .initChart()
  await chart.value.setDimension()
}
</script>

不要在 <template> 放太多的邏輯

✭ 重構前:

在 <template> 裡面有一大段的 if ... else 邏輯,用來顯示不同的資料

<template>
  <div>
    <span v-if="datePickerStore.type === 'single'">
      ...
    </span>
    <span v-else-if="datePickerStore.type === 'multi'">
      ...
    </span>
    <div v-if="datePickerStore.type === 'year' && dimensionSelect === '1month'">
      ...
    </div>
    <div v-if="datePickerStore.type === 'year' && dimensionSelect === '1week'">
      ...
    </div>
  </div>
</template>

✭ 重構後:

當需求很簡單,邏輯也不複雜時,我選擇改用 function 來處理資料,再回傳要顯示的文字即可。

<template>
  <div>
    {{ getDateDimension() }}
  </div>
</template>

<script>
const getDateDimension = () => {
  switch (type) {
    case 'single': {
      return '...'
    }
    case 'multi' : {
      return '...'
    }
    case 'year': {
      switch (value) {
        case '1week':
          return '...'
        case '1month':
          return '...'
      }
      break
    }
    default:
      break
  }
}
</script>

避免太多的 if...else

✭ 重構前:

當我切換不同的時間維度時,圖表對應的 X 軸也會有所不同,正因為有太多的條件,所以一開始用 if...else 快速處理

<script>
  const axisLabelSetting = () => {
  if (datePickerStore.type === 'single') {
    if (dimensionSelect.value === '3min') {
      result = {
        interval: (num) => {
          ...
        }
      }
    } else if (dimensionSelect.value === '30min') {
      result = {
        interval: (num) => {
          ...
        }
      }
    } else if (dimensionSelect.value === '60min') {
      result = {
        interval: (num) => {
          ...
        }
      }
    }
  } else if (datePickerStore.type === 'year') {
    if (dimensionSelect.value === '1week') {
      result = {
        ...
      }
    }
  }
}
</script>

✭ 重構後:

將每個時間維度的設定檔獨立成一個物件,再用索引的時候取出物件的值,不只易擴充,且清晰又美觀

<script>
const AXIS_INTERVAL_CONFIG = {
  single: {
    '3min': { value: 1, points: [0, 10], limit: 10 },
    '30min': { value: 1, points: [0, 20], limit: 20 },
    '60min': { value: 1, points: [0, 30], limit: 30 }
  },
  year: {
    '1week': { value: 1, points: [0, 10], limit: 10 },
    '1month': { value: 1, points: [0, 20], limit: 20 }
  }
}

axisLabelSetting() {
  const config = AXIS_INTERVAL_CONFIG[datePickerStore.type][dimensionSelect.value]
  let result = {}
  result = {
    interval: (num) => {
      for (let i = 1; i <= config.limit; i++) {
        if (num === config.value * i - 1) return true
      }
    }
  }
  return result
}
</script>

避免多個 Function() 做同一件事

✭ 重構前:

當圖表做完的一、兩個月後,PM 說要再加入新功能,此時可能存在可重複利用的變數 / 功能,但我可能忘了?或是我不確定使用後會不會對舊功能產生影響?所以又另外寫了新的 function 來做同樣的事情。

這樣就會造成多餘且重複的 Function 不斷增生,讓整份檔案愈來愈肥大。

✭ 重構後:

所以當一個功能已經趨近於成熟,且幾乎不會再增加新功能時,就必須要回頭審視,是否有能合併的功能或變數,讓資料保持乾淨與一致性

結語

即使寫程式寫了那麼久,也還是會因為時程的壓力,或者其他因素,導致髒 code 的誕生 😂。

為了自己與下一代的健康,請把程式碼當成自己的小孩,請細心呵護,不要射後不理。我們要隨時檢視過往的自己,才能創造更好的未來唷!XXD

歡迎給我點鼓勵,讓我知道你來過 :)

34
3
1
Subscribe
Notify of
guest

0 則留言
Inline Feedbacks
View all comments