從前一天的故事及開發範例,我們發現隨著新需求的出現,我們必須回去修改既有的邏輯,這樣子的情形,並不符合開放封閉原則,也很容易漏思考一些東西。
由於程式碼的執行順序是從上至下,從外到裡。
理所當然的,排在前面 if / else statement
有命中,就會先進去裡面執行,而忽略 else
之後的內容。
然而這看起來卻只是問題的表象
“Fix the cause, not the symptom.” – Steve Maguire
或許你會說,錯誤有什麼關係、下次改版修回來就好了啊,QA 也應該要測到,或者 PM 新需求也應該想清楚或是要記得回去把舊規格書補好。
但假設你今天開發的是一個低容錯率的系統,如:
隨著不斷加入的新功能,我們或許會越改越沒信心,很怕改一些東西,其他地方會不會壞掉?(改 A 壞 B)
看醫生時,我們希望醫生不要治標不治本;
身為程式碼醫生,我們也要避免治標不治本。
所以追根溯源,究竟問題的本質是什麼?為什麼有這個現象發生?這是今天要來探討的
下面的舉例,或許是一些常見的組合?
if(isLogin && !isLoading && hasData) // 登入、資料載完、有資料,畫出特定 UI
if(isLogin && isBuyer && isPaid) // 登入、身份是買家、已付費,執行特定動作
透過
if/ else
中或是// From 舊功能
if(a && b)
// To 新功能
if(a && b && c)
if / else
及 else if
的開發特性// From 舊功能
if(a && b)
else
// To 新功能
if(a && b)
else if (新條件)
else if (超新條件)
else
就會造成新功能開發時,就要往上、往前去進行檢查
除了可讀性會越來越差之外,程式碼意義是由許多破碎的小單元組成,是隱含的、命令式的!
if(flagA && flagB && flagC && flagD)
? (背後代表的意義)if(flagA && flagB && flagC && flagD)
? (組合產生的原因,像昨天的一些防禦、防呆:跳躍不能直接進到匍匐)充滿命令式(imperative) 的邏輯越來越多(當你 flag 越多),發生錯誤時的除錯也會越來越困難。
if(flagA) doSomething()
舉例 flagA,遇到錯誤時,我們就是只有 2 種可能 flagA is true
or flagA is false
,此時我們只要去檢查 flagA 為啥會判斷錯誤即可
if(flagA || flagB) doSomething()
今天多了 1 個 flagB,發生錯誤時的檢查,變成 4 種可能
A | B | A or B |
---|---|---|
true |
true |
true |
true |
false |
true |
false |
true |
true |
false |
false |
false |
再多一個 flag 這裡的邏輯組合性,就變成 ,8 種可能。
再換一個角度想想,其實這些 flag 是不是都代表一些小狀態,我們想透過這些小狀態組合出一個大的狀態。我們可以說將這些小狀態堆疊出大狀態的過程是,由小至大,由下而上(抽象層次)。
隨著功能、需求不斷新增,我們需要的 flag 可能就越來越多,程式碼就越來越容易出 bug。
if(isLogin&&!isLoading&&hasData) // 登入、資料載完、有資料,畫出特定 UI
if(isLogin&&isBuyer&&isPaid) // 登入、身份是買家、已付費,執行特定動作
其實 isLogin&&!isLoading&&hasData 就只是想表達有「登入載完資料後的畫面」,
這已經是 happy path,但你還要去思考 not Login + loading + noData ... 這 2 x 2 x 2 總個 8 種可能。
扣掉第一種,其他七種狀態的意義是什麼?或是說你可能不需要那麼多種,但防呆判斷有沒有可能漏掉
這也代表著我們狀態的描述性是很差的
同樣以上面例子,或許在你的系統內 loading 就只有一種整頁是滿版轉圈圈的畫面,只要 isLoading === true
,其他 isLogin, hasData 都是多餘的。
(isLogin && !isLoading && hasData) ? <DataTable /> : <EmptyWrapper />
這樣是不是也增進未來閱讀的負擔,要多停下思考
比如 isLoading: true, isLogin: false, hasData: true 這個語句可能對你的系統是無意義的
沒看過的人可能還要思考在 isLoading: true, isLogin: false, hasData: true 的意義是什麼?
正在 Loading 但有資料、卻沒登入???這什麼意思?
但假設設計師今天僅來得及先出 2 種畫面,loading:<LoadingSpinner /> 及 loaded:<DataTable /> ,一時專案趕或是為求程式碼簡潔,直接抽換掉後面的元件
(isLogin && !isLoading && hasData) ? <DataTable /> : <LoadingSpinner />
有發現哪裡怪怪的嗎?
isLogin | isLoading | hasData | 意義 |
---|---|---|---|
true |
true |
true |
Loading |
true |
true |
false |
Loading |
true |
false |
true |
Loaded /(List View) |
true |
false |
false |
Loaded / (Empty View) |
... | ... | ... | 無意義 |
... | ... | ... | 無意義 |
如果依照這個邏輯寫下去,當登入後、拿回 API response 時,是空的沒有資料,現在這組邏輯就會永遠只看到轉圈圈 <LoadingSpinner />
,就變成是等設計師出完空資料畫面<EmptyWrapper />
之前,要拔掉 hasData
這個 flag ,因為這階段根本不需要它的存在。
有發現哪裡怪怪的嗎?
其實這個邏輯語句太強了,只關注到 3 個 flag 都是 true
的結果,當不是 true
時的意義沒有被正確定義時,用三元運算後面回傳什麼都很怪
(isLogin && !isLoading && hasData)
當 hasData===false
回傳 <LoadingSpinner />
... 怪
(isLogin && !isLoading && hasData)
當 isLogin===false
回傳 <LoadingSpinner />
或 <EmptyWrapper />
... 可能都怪 (應該要 redirect 到登入頁)
為了避免這個情形,如果又改使用 巢狀 if else if{if{}else{}}
,開發起來很難讀,隨著需求增刪也要來來回回檢查
同理在思考狀態轉換時,很容易 miss 小東西,比如說有個 isLogin, isAdmin 的 flag 組合,照理說當 isAdmin 是 true
時,isLogin 一定要是 true
,但我們在開發時,很有可能漏加到。
isLogin | isAdmin | 意義 |
---|---|---|
false |
true |
Buggy 不應該存在,但現實很容易就會漏改 |
上面一堆 flag 組合起來的結果,光是工程師們都要花一段時間消化跟思考...
試問如何快速跟新人交接?
或是當 bug 發生時,我們如何跟工程師以外的人解釋?
而且除錯時,也很難快速發生問題、或很難跟 PM 討論需求釐清是否存在邏輯矛盾或是漏處理到的 case。
新功能的開發,我們能不能避免建立過多的 flag、減低巢狀、一長串的邏輯判斷?
有沒有辦法更從使用者的角度出發?
(雖然是依據規格設計,但前面我們是以程式碼的邏輯出發,長出一串邏輯判斷)
先不管攻擊、防禦等等,以角色移動而言,仔細觀察我們的 站、跳躍、匍匐前進(左右負責控制移動的方向),其實跳躍、匍匐前進等等都不會同時存在,我們是不是可以視彼此為一種獨立存在的狀態。
isJumping
, isCrawling
其實也只是為了判斷以上的行為而存在的中介狀態(暫時存在的狀態)。
以一張電商的訂單而言,買家提交訂單(給賣家先確認庫存),賣家確認有貨可成立點選同意,訂單進入等待付款,付款完成收帳後進入等待發貨倉儲人員配貨、安排物流,送達目的地後等待取件,取件完成後訂單交易完成
等待發貨、等待取件不會同時存在(就像是跳躍跟俯臥不會同時存在),看起來我們需求中,訂單的每個階段,好像都能視為一種狀態。
還沒付款不能跳到等待發貨的狀態,
俯臥不能直接跳躍(要先站起來)
我們觀察到,狀態與狀態間、有明確的轉移路徑(或是你刻意要限制明確的路徑)
狀態轉移 | 合理性 |
---|---|
站 → 跳 → 站 | O合理 |
站 → 俯 → 站 | O合理 |
跳 → 俯 → 站 | X不合理 |
俯 → 跳 → 站 | X不合理 |
狀態轉移 | 合理性 |
---|---|
提交 → 待付款 → 待發貨→ 待取件→ 交易完成 | O合理 |
提交 → 待取件→ 交易完成 → 待付款 → 待發貨 | X不合理 |
提交 → 待發貨 → 待取件 → 交易完成 → 待付款 | X不合理 |
我們現在發現了這幾個有趣的現象,明天一起繼續往下探索吧!
解釋了昨天的解決方案帶來了什麼困擾及難題
if / else
判斷狀態、進行防呆,當新增、修改功能時