MUKI AI Summary
重構程式碼能改善結構和品質,增強系統的可維護性和可擴展性。最近重構公司的圖表功能,目的是簡化程式碼,提高可讀性和可維護性。
重構前需要熟悉功能的所有規格和資料結構,做好備份並使用 git worktree 瀏覽舊檔案。重構過程中將設定拆成獨立的 function,避免在 template 放太多邏輯,減少 if...else 和重複的 function,保持程式碼乾淨和一致性。...
前言
在程式開發的領域中,重構也是重要的一環,透過重構,可以改善程式碼的結構和品質,也可能加強系統、功能的可維護性和可擴展性。
而我最近重構的部分,是公司的圖表功能,他是一個已經上線,且規格成熟運作良好的功能。為什麼我要重構?我在重構前準備了什麼?我重構了哪些部分?想透過這篇文章將我的心路歷程記錄下來,希望能讓以後的重構之路愈發順利 👍
重構的時機
我先列舉幾個比較常見的重構原因:
- 功能不穩定:如果已上線的功能時好時壞,或是容易踩雷觸發 bug,的確需要重構以修復這些問題,並提高穩定性
- 程式碼過於複雜:有時候因為新需求不斷,或邏輯太繞,導致程式碼變得太複雜也不好維護,這時候就可以靠重構簡化程式碼結構,並重新梳理自己的邏輯,提高可讀性和可維護性。
- 效能問題:效能是部分的前端工程師容易忽略的部分,卻是佔整個程式開發很重要的一環。如果執行速度慢,網站容易當機,也是需要重構以優化效能的。
- 技術債:通常發生在「功能要快速上線,但開發時間短」的情況,趕工時很容易降低程式碼的品質以及設計原則。正所謂
欠債總是要還,我們可以透過重構來償還技術債,降低未來維護的成本。
而我這次重構的主要原因,就是程式碼過於複雜,我希望精簡化程式碼,以提高可讀性和可維護性。
重構前要準備什麼
對功能的熟悉程度
鑑於這次重構的是「已上線」且「規格成熟完整」的功能,因此要先確認,自己是否能掌握該功能的所有規格以及資料結構。
▼ 以我重構的圖表為例,畫面上的各種設定,都會影響圖表的呈現,有的互相獨立,有的卻息息相關。
當功能增多時,如果對其不夠熟悉,或者隔了一段時間才添加新的功能,就可能會出現「使用重複的變數來實現新功能」的情況。然而,先前可能已經存在可用的變數,只是我們可能不知道或者擔心更改這些變數會對舊功能造成影響。
因此,當該功能已經趨近於成熟,也不太會再加入新的功能時,就可以考慮重構了。前提是,我們能清楚的了解圖表的架構、功能,與相關程式碼,重構時才能避免出現重複的問題。
備份與讀取舊檔案
✭ 步驟一:
從主要的 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