Browsing Category

技術債

技術債 軟體開發

💸還技術債的藝術 II – 一魚多吃,除技術債同時提升效能

本篇文章是 💸還技術債的藝術 I 的續集,沒看過的觀眾建議先連過去看看。

漫無目的從 Code base 找技術債並不是個聰明的作法,雖然這麼做你一定能馬上找到數十個技術債的嫌疑犯,但很可能他們都是清白的,就像是在路上隨機抓人就懷疑他犯法一樣,我不會說這個方法完全無效,只是它相對會花上更多時間。

怎麼做才是有效率的呢?

這個時機很簡單,你開發新功能的時候。

當你開發新功能時多多少少會接觸到原有的程式碼,為了要使用它們你會更仔細去審視實作細節,此時你很可能會發現原有程式碼的缺陷,但這個時候請先不要急著解決它,因為你現在的首要任務是完成新功能的開發,先把它們放到待辦事項中,等功能做一個段落時提出來與團隊成員討論,並隨著開發時程的步調來調整決定還哪個債及何時還債。

如果每遇到一個技術債就開支線任務的話你會遇到許多問題:

大量的 Context switching

這對生產力非常有害,人腦跟電腦不一樣的地方是我們每次 Context swtich 都會耗費巨量的能量,而每個人每天的注意力能量是非常有限的。相信你也過這樣的經驗,當你同處理多件事情時很容易覺得累,每次回到另一個 Context 都會覺得『我剛剛處理到哪了?』

重複的解決相同的問題

假如你總共遇到三個相似的技術債,但因為你的方法是來一個打一個,因此會犧牲了有可能批次處理的機會,相當可惜。

解決不了的技術債

也許這個技術債是 NP 問題,一時之間是無法解決的,不能解決頂多就是浪費了時間去試,怕就怕你以為你解決了,但其實產生了的未知問題,更糟的情況是你為此耽誤太多時間而延誤新功能開發時程。

你沒解決技術債,也沒完成功能開發,開了太多支線任務,導致你主線任務根本破不完或是熬夜才能破完,你不會想要落此下場的。

實際案例

故事是這樣的,我在開發某個跟會員相關的功能過程中發現一個問題,因為電話號碼在會員系統中有唯一性,也就是不會有同兩個人擁有同一個電話號碼,所以在新增(修改)會員電話時必須做檢查,確認該電話在系統中還沒被使用,才能讓這個修改成立。

這個需求簡單明瞭但舊有的程式碼卻大有問題,其資料結構是把所有會員資料存在一個 Array 當中進行操作。

也就是說,當進行電話號碼欄位比對時會循序檢查 Array 中的每一筆會員的電話號碼是否與目標值相同,全部會員資料有多少筆就比對多少次,這是一個沒有效率的實作方法。而其方法同時也影響了「查找此會員是否存在」,或是「其電話號碼是否已被使用」的功能,以演算法的術語來說就是時間複雜度是 O(N)

不僅如此,也因為把會員資料全部倒入記憶體中佔據大量空間,而大多時候它們是閒置的資料,只有在操作會員相關資訊時才有作用,而造成無謂的記憶體浪費。

這個誤用的資料結構造成了「搜尋效能低」及「濫用記憶體」的問題。

由於隨著該功能上線,我們預期將會大大的提升各餐廳的會員數量,進而將放大其問題的嚴重性。這個技術債會隨著公司的 Business scale 升級而被放大,而現在正是還它的好時機。

在幾次 Stand up 會議討論後主管也認為該問題很嚴重值得花時間解決。

技術債考古

做任何決定前,先確保擁有足夠多的資訊來輔助判斷,避免在資訊不足的情況下做出錯誤決定。

於是我向同事請教當時的時空背景,先前之所以把所有資料放入 Array 之中是當時常常遇到資料庫毀損,在找不到原因的情況下先加了一層快取來避免直接對資料庫操作。

「不過現在我們很少資料庫毀損了,這段應該可以改。」他說。

衡量解決方案

在做之前先衡量有哪些解決方案,並衡量其優缺點、實作難度、你所擁有的時間等維度來選擇最佳的解決方案。

方案一:建立 HashMap 加速搜尋

於是很直覺的想把 Array 改成 HashMap ,但這麼做的缺點很明顯,需要維護多個 HashMap,像是 UUID to 會員,電話 to 會員等等。而且這麼做並沒有解決記憶體使用量的問題,反而有可能上升。

*實作複雜度較高,且問題只解決一半。

方案二:回歸 SQL 正途

後來想也許該用更簡單的方式,也許根本不需要 Cache,直接用資料庫現 SQL query 來實作電話比對,但我不確定自己是否夠熟悉 SQL。(因為還有另一個技術債,我們把會員的資料是壓成 JSON 字串,直接存入某個欄位,藉此來設計一種通用的 Table)

*實作複雜度較低,問題有機會可以一次解決。
*不確定因素是:對 SQL 不熟

基於「不要過早最佳化」及「不要過度設計」的想法,再加上方案二才能同時加快搜尋以及減少記憶體使用量,於是答案就很明顯了。

值得一提的是這裡請你以結果導向來做決策,無論實作方法的困難度最終我們在乎的是成果。很多時候我們會落入過度設計的陷阱裡,使用了多複雜的設計最後發現別人看不懂難以維護,這樣一來反而本末倒置。

衡量成果

有了初步的假設與解決方案,接著要來試著驗證它們。

如果少了這個步驟,將難以確認修改到底改善多少?值不值得我們花時間修改?於是簡單做了個實驗來看看修改前後的差異。

搜尋時間

在 200k 筆會員資料的情況下,用原方法確認「單筆電話號碼是否被使用」的時間是 2.05 秒

原方法耗時 2.05 秒

在同樣會員數情境下,新的方法只需要 0.051 秒

新方法耗時 0.051 秒

在效能方面結果相當顯著,該方法從 2 秒下降到 0.05 秒。
效能提升大約 40 倍。
記憶體使用量
在會員數 28k 的情境下最高記憶體使用量從 450MB 降到 344MB 。
大約降低 100MB

記憶體使用量差異示意圖

結語

以前我也是看到技術債就馬上動手去修改,恨不得把房間整理的一塵不染。

但我後來了解時間是有限的,而程式碼改善是無限的。隨著時間的流逝你永遠可以找到更好的方式還債(又或者不用還)。永遠有新出的 framework 或者是 Open source 幫你解決了過去某個問題。

用聰明的方法還債,將會使你工作輕鬆很多,也對公司有更多貢獻。
用健康的心態去面對技術債,會讓你的人生好過很多。

把心思與力氣放在最重要的事情上,才能在有限的人生裡發揮最大效益。