你可能不需要那麼多變數
探討 bug 的真正起因
前言
我們在寫程式時,或多或少都遇到 bug 過,
我們把遇到的 bug 給 patch 起來,寫個測試, 認為這就是修好了。
然後之後遇到新的bug,再次修復,再次寫測試。
不斷往復,重複這個輪迴,
提出了更多的 design pattern,用了更複雜的測試方法。
然而,修得更多,生得更多。
這究竟是為什麼讓我們一直在這個輪迴受難?
為什麼
讓我們從最簡單的一個程式開始
var a = 0
var b = 0
function setA(value) {
a = value
}
function setB(value) {
b = value
}
function print() {
console.log(`a is ${a}, b is ${b}`)
}
我們有兩個 setter
分別設定 a
, b
變數,以及一個 print
function 輸出 a
, b
目前的值。
簡單的試一下
setA(1)
setB(0)
print()
// 輸出: a is 1, b is 0
看起來一切正常
目前這個程式有四種狀態
a | b |
---|---|
0 | 0 |
1 | 0 |
0 | 1 |
1 | 1 |
這些狀態都對嗎?答案是都對,畢竟目前就只是單純輸出而已
接下來我們加一個變數表示表示 a
xor b
,
然後在設定完 b
時更新他
var a = 0
var b = 0
var aXorB = 0
function setA(value) {
a = value
}
function setB(value) {
b = value
aXorB = a ^ b
}
function print() {
console.log(`a is ${a}, b is ${b}, a xor b is ${aXorB}`)
}
再試一下
setA(1)
setB(0)
print()
// 輸出: a is 1, b is 0, a xor b is 1
看起來輸出是對的。
然而眼尖的你應該也發現哪裡不對勁了
要是我們改完 b
後再改 a
呢?
setB(0)
setA(1)
print()
// 輸出: a is 1, b is 0, a xor b is 0
歐不,程式出 bug 了
接下來我們再把 setA
也 patch 一下
var a = 0
var b = 0
var aXorB = 0
function setA(value) {
a = value
aXorB = a ^ b
}
function setB(value) {
b = value
aXorB = a ^ b
}
function print() {
console.log(`a is ${a}, b is ${b}, a xor b is ${aXorB}`)
}
然後再寫一堆測試確定全部順序組合都沒錯
it('should work when setA then setB', function () {
setA(1)
setB(0)
})
it('should work when setA then setB', function () {
setB(0)
setA(1)
})
// 其他高達幾十種組合...
這些聽起來某種意義上很愚蠢對吧?
然而我們差不多每天都在做,雖然不會這麼明顯,但事情都差不多。
是不是哪裡跑偏了?有沒有更恰當的做法?
原因
讓我們回到源頭,什麼是 bug?bug 就是程式變成了不該出現的狀態,我們再做一次表格來看我們現在的程式所有的狀態組合
a | b | AXorB | 是bug嗎? |
---|---|---|---|
0 | 0 | 0 | |
1 | 0 | 0 | bug * |
0 | 1 | 0 | bug |
1 | 1 | 0 | |
0 | 0 | 1 | bug |
1 | 0 | 1 | |
0 | 1 | 1 | |
1 | 1 | 1 | bug |
我們剛剛炸掉的程式在第二個組合,而這個是不該出現的組合。
所謂的除錯就是避免程式意外進到錯誤的狀態組合中。
組合數量隨著狀態的變多,呈現 級數增長,直到某個時間點後, 就再也沒有任何一個人能把哪些狀態是該出現的,哪些是不該出現的給搞清楚。 只能出了 bug 後把出錯的路徑封死、寫個測試確保以後一定不會再出現。
然而問題的核心並不在這裡。
而是, 為什麼我們的程式要能表達出 bug 的狀態?
我們最開始的程式是不會有 bug 的,因為每一個狀態都是正確的,不存在能表達程式遇到 bug 的狀態。
為什麼加了一個新 field 後就爛了
因為你一開始就不該為一個完全依賴其他變數的東西加 field。
那怎麼辦
請改用 getter
讓我們用 getter 改寫剛剛出 bug 的程式
var a = 0
var b = 0
// var aXorB = 0
function setA(value) {
a = value
}
function setB(value) {
b = value
// aXorB = a ^ b
}
function getAXorB() {
return a ^ b
}
function print() {
console.log(`a is ${a}, b is ${b}, a xor b is ${getAXorB()}`)
}
這個程式有 bug 嗎?
答案是 不可能有,因為它並沒辦法表達出 bug 的狀態。
它的狀態表格跟最開始一模一樣,所有組合都是正確的,所以它 不能 出bug。
你不可能得到 getAXorB()
不等於 a xor b 的結果。
也就是說:getter 是不會增加額外的程式狀態的
不像加一個 field 後更新時可能因為順序出問題, 如果 getter 的轉換規則是正確的,那它永遠對應到正確的數值,不可能意外出錯。 而更少的狀態組合也代表你的程式測試起來會更簡單,更難以出錯
結語
與其寫出 測試完沒出 bug 的程式
,
我想,寫出 不能出 bug 的程式
或許才是我們真正該做的。
monkey patch 修正每一個 bug 真的不太對,而且很可能只在專案還小時行得通,
隨著專案規模上升,要 patch 掉每條錯誤的流程難度將會暴增。
10 個 boolean
就是 1024 種狀態,10個 int
或許就能達到恆河沙數,
暴漲的狀態數量最終都將會把程式帶入難以測試除錯的陷阱。
只有真正思索每一個變數/屬性是不是必要,是否完全依賴什麼數值可以用 getter 替代,思考狀態組合的正確與否,才能到達沒有 bug 的世界。