如何消除掉那種實際上可以避免的狀況(或者說是「使用上的錯誤」,像是在關閉檔案之後又想要寫入檔案,或是在物件完成初始化之前就去調用那個物件裡的方法)
比如說,你寫了某個函式,其中有兩個參數是陣列值,或許你會希望這兩個陣列,總是具有相同的大小
如果相關參數有彼此對不上的問題 ,最好的情況下也必須等到執行程式碼時 才能發現問題,因為編譯階段根本看不出這類的問題
其實最好的做法,就是把界面設計成不可能接受錯誤的用法
你可以把多個陣列合併成一個,這樣就能消除掉多個陣列大小 對不上的問題
如果想建立出不會出問題的介面,其中一個很關鍵的重點,就是儘早偵測出問題
執行階段偵測出問題 - 還可以接受
編譯階段偵測出問題 - 太棒了
讓你無法寫出錯誤的東西 - 完美
如果我們想在某個角色的頭上畫一個簡單的球體(就像我們之前想標示出有哪些 NPC 看到了玩家一樣),或許可以把程式碼寫成下面這樣
問題
最常見的解法就是把構建過程分解成好幾個步驟
但這個設計又引入了另一個新的問題點,因為有好幾個執行步驟,就會產生執行順序的問題。
如果我們並沒有按照順序來調用這些函式 (例如 commit 之後 又去調用 setXRay,或是在 commit 之前調用 drawsphere),由於我們並沒有定義這樣會發生什麼事,所以編輯器和編譯器全都幫不上什麼忙。實際上一直要等到執行階段,才能偵測出這樣的錯誤
你或許可以用某種約定慣例的做法,來協助大家避免掉這種執行順序上的錯誤,不過這並不是我們所能採用的最佳做法
其中一種做法就是把兩個階段切分成兩個物件,先構建參數,再利用參數來進行繪製。
在這樣的結構下,執行的順序就被隱含進來了。你一定要先有一個 Params 物件,才能去建立Draw物件,所以當然會先建立 Params
由於所有 Params 相關的操作,全都是在我們這一整串方法中進行的,所以我們可以利用 C++ 的 const 關鍵字,讓這個 Params物件變成一個常量。這也就表示,一旦構建完成之後,編譯器就不會再讓我們去對它進行修改了。
如果不只是變得更清楚,而是真的變成不可能這麼做,那就更好了
它根本沒機會讓我們不小心又改動到參數。這確 實是非常緊湊的程式碼,像這樣寫的話,我們就等於是透過了設計,排除掉會出問題的情況了
需求:讓角色進入「無敵」的狀態
這樣的設計很容易導致我們犯下使用上的錯誤
如果在進入遊戲過場動畫時,玩家正好也打開了無敵藥水的軟木塞,那我們可就麻煩了
嘗試解法,讓玩家回到進入無敵前的狀態
這個解法同樣會出問題
舉例來說,如果玩家在過場動畫開始之前,先喝了無敵藥水,後來藥水在播放過場動畫期間失去作用 ,這時候還原到喝藥水之前的狀態就不對了。
我們可以考慮把每一段無敵相關的程式碼,各自切開來處理 。如果我們針對每一段相關的程式碼,個別去維護相應的無敵標記,這些程式碼就不會糾纏在一起了
如果採用這樣的做法,我們就必須去檢查所有的無敵標記,而不能只檢查單 一個標記。如果其中有任何一個標記被設為無敵,玩家就是處於無敵的狀 態
這種做法是可行的,不過還是需要遵守一定的紀律。如果有人太懶惰,在不 同段的程式碼裡重複使用同一個 InvulnerabilityReason ,還是有可能會把程式搞掛掉
我們也可以考慮透過持續追蹤無敵計數值的方式,來消除掉糾纏不清的問 題。只要有任何程式碼把角色設為無敵,那就是無敵。這樣就可以得出一個非常簡單的「推入- 彈出 」(push-pop )模型
像這樣的「推入-彈出」模型,確實是可行的。一旦你習慣這種慣用的做法就很容易理解了。這樣的做法也很容易進行擴展,每次要寫新的程式碼來進行無敵相關的操作,都不會影響到其他的程式碼,因為不同的程式碼都可以獨立去推入和彈出無敵的計數值,而不會破壞到任何重要的東西。
不過如果你的程式碼忘了去調用 popInvulnerability,這個角色就會永遠保持無敵狀態
像這種使用上的錯誤,最好還是要完全消除掉。最簡單的方式,就是把「推入-彈出」的操作,包裝在構建函式和解構函式內。然後編譯器就會再次成為我們的好朋友:
雖然我們無法讓設計完全防呆,但我們所擋下的每一件蠢事,確實可以讓我們的系統更加可靠。因此,你打從一開始就要盡可能找機會,從你的設計裡消除掉那些會出問題的狀況。