# 消除掉各種會出問題的狀況
如何消除掉那種實際上可以避免的狀況(或者說是「使用上的錯誤」,像是在關閉檔案之後又想要寫入檔案,或是在物件完成初始化之前就去調用那個物件裡的方法)
## 設計出不容易誤用的程式(介面)
比如說,你寫了某個函式,其中有兩個參數是陣列值,或許你會希望這兩個陣列,總是具有相同的大小
```cpp=
void showAuthorRoyalties(
const vector<string> & titles,
const vector<double> & royalties)
{
assert(titles.size() == royalties.size());
for (int index = 0; index < titles.size(); ++index)
{
printf("%s,%f\n", titles[index].c_str(), royalties[index]);
}
}
```
如果相關參數有彼此對不上的問題 ,最好的情況下也必須等到執行程式碼時 才能發現問題,因為編譯階段根本看不出這類的問題
其實最好的做法,就是把界面設計成不可能接受錯誤的用法
你可以把多個陣列合併成一個,這樣就能消除掉多個陣列大小 對不上的問題
```cpp=
struct TitleInfo
{
string m_title;
float m_royalty;
};
void showAuthorRoyalties(const vector<TitleInfo> & titleInfos)
{
for (const TitleInfo & titleInfo : titleInfos)
{
printf("%s,%f\n", titleInfo.m_title.c_str(), titleInfo.m_royalty);
}
}
```
## 時間點最重要
如果想建立出不會出問題的介面,其中一個很關鍵的重點,就是儘早偵測出問題
執行階段偵測出問題 - 還可以接受
編譯階段偵測出問題 - 太棒了
讓你無法寫出錯誤的東西 - 完美
## 一個比較複雜的例子
```cpp=
struct Params
{
Params(
const Matrix & matrix,
const Sphere & sphereBounds,
ViewKind viewKind,
DrawStyle drawStyle,
TimeStyle timeStyle,
const Time & timeExpires,
string tagName,
const OffsetPolys & offsetPolys,
const LineWidth & lineWidth,
const CustomView & customView,
const BufferStrategy & bufferStrategy,
const XRay & xRay,
const HitTestContext * hitTestContext,
bool exclude,
bool pulse,
bool faceCamera);
void drawSphere(Point point, float radius, Color color);
};
```
如果我們想在某個角色的頭上畫一個簡單的球體(就像我們之前想標示出有哪些 NPC 看到了玩家一樣),或許可以把程式碼寫成下面這樣
```cpp=
void markCharacterPosition(const Character * character)
{
Params params(
Matrix(Identity),
Sphere(),
ViewKind::World,
DrawStyle::Wireframe,
TimeStyle::Update,
Time(),
string(),
OffsetPolys(),
LineWidth(),
CustomView(),
BufferStrategy(),
XRay(),
nullptr,
false,
false,
false);
params.drawSphere(
character->getPosition() + Vector(0.0, 0.0, 2.0),
0.015,
Color(Red));
}
```
問題
1. 你絕對記不住這16個參數的順序
2. 列表最後面的四個布林參數,究竟是什麼意思?
3. 要再添加或移除構建函式裡的參數會非常痛苦,而且很容易出錯
4. 使用起來很不方便,而且隨著時間的推移 ,還會越來越不方便
最常見的解法就是把構建過程分解成好幾個步驟
```cpp=
void markCharacterPosition(const Character * character)
{
Params params;
params.setXRay(0.5);
params.commit();
params.drawSphere(
character->getPosition() + Vector(0.0, 0.0, 2.0),
0.015,
Color(Red));
}
```
但這個設計又引入了另一個新的問題點,因為有好幾個執行步驟,就會產生執行順序的問題。
如果我們並沒有按照順序來調用這些函式 (例如 commit 之後 又去調用 setXRay,或是在 commit 之前調用 drawsphere),由於我們並沒有定義這樣會發生什麼事,所以編輯器和編譯器全都幫不上什麼忙。實際上一直要等到執行階段,才能偵測出這樣的錯誤
你或許可以用某種約定慣例的做法,來協助大家避免掉這種執行順序上的錯誤,不過這並不是我們所能採用的最佳做法
## 最理想就是讓執行順序不可能出錯
其中一種做法就是把兩個階段切分成兩個物件,先構建參數,再利用參數來進行繪製。
```cpp=
void markCharacterPosition(const Character * character)
{
Params params;
params.setXRay(0.5);
params.setDrawStyle(DrawStyle::Solid);
params.setPulse(true);
Draw draw(params);
draw.drawSphere(
character->getPosition() + Vector(0.0, 0.0, 2.0),
0.015,
Color(Red));
}
```
在這樣的結構下,執行的順序就被隱含進來了。你一定要先有一個 Params 物件,才能去建立Draw物件,所以當然會先建立 Params
```cpp=
void markCharacterPosition(const Character * character)
{
const Params params = Params()
.setXRay(0.5)
.setDrawStyle(DrawStyle::Solid)
.setPulse(true);
Draw draw(params);
draw.drawSphere(
character->getPosition() + Vector(0.0, 0.0, 2.0),
0.015,
Color(Red));
}
```
由於所有 Params 相關的操作,全都是在我們這一整串方法中進行的,所以我們可以利用 C++ 的 const 關鍵字,讓這個 Params物件變成一個常量。這也就表示,一旦構建完成之後,編譯器就不會再讓我們去對它進行修改了。
如果不只是變得更清楚,而是真的變成不可能這麼做,那就更好了
```cpp=
void markCharacterPosition(const Character * character)
{
Draw draw = Params()
.setXRay(0.5)
.setDrawStyle(DrawStyle::Solid)
.setPulse(true);
draw.drawSphere(
character->getPosition() + Vector(0.0, 0.0, 2.0),
0.015,
Color(Red));
}
```
它根本沒機會讓我們不小心又改動到參數。這確 實是非常緊湊的程式碼,像這樣寫的話,我們就等於是透過了設計,排除掉會出問題的情況了
## 「狀態」的協調控制
需求:讓角色進入「無敵」的狀態
- 當進入過場動畫時
- 喝下無敵藥水時
```cpp=
void playCelebrationCutScene()
{
Character * player = getPlayer();
player->setInvulnerable(true);
playCutScene("where's chewie's medal.cut");
player->setInvulnerable(false);
}
void chugInvulnerabilityPotion()
{
Character * player = getPlayer();
player->setInvulnerable(true);
sleepUntil(now() + 5.0);
player->setInvulnerable(false);
}
```
這樣的設計很容易導致我們犯下使用上的錯誤
如果在進入遊戲過場動畫時,玩家正好也打開了無敵藥水的軟木塞,那我們可就麻煩了
1. 一開始播放過場動畫,setInvulnerable 就會把玩家設定為無敵
2. 然後又因為喝下了藥水,所以 setInvulnerable 又會被再次調用
3. 實際上,此時並不會有什麼變化,因為這時候玩家已經無敵了。
4. 過了五秒鐘之後藥水的效力消失,於是程式碼又會去調用setInvulnerable(false)
5. 但這時候還在播放過場動畫
嘗試解法,讓玩家回到進入無敵前的狀態
```cpp=
void playCelebrationCutScene()
{
Character * player = getPlayer();
bool wasInvulnerable = player->isInvulnerable();
player->setInvulnerable(true);
playCutScene("where's chewie's medal.cut");
player->setInvulnerable(wasInvulnerable);
}
void chugInvulnerabilityPotion()
{
Character * player = getPlayer();
bool wasInvulnerable = player->isInvulnerable();
player->setInvulnerable(true);
sleepUntil(now() + 5.0);
player->setInvulnerable(wasInvulnerable);
}
```
這個解法同樣會出問題
舉例來說,如果玩家在過場動畫開始之前,先喝了無敵藥水,後來藥水在播放過場動畫期間失去作用 ,這時候還原到喝藥水之前的狀態就不對了。
我們可以考慮把每一段無敵相關的程式碼,各自切開來處理 。如果我們針對每一段相關的程式碼,個別去維護相應的無敵標記,這些程式碼就不會糾纏在一起了
```cpp=
void playCelebrationCutScene()
{
Character * player = getPlayer();
player->setInvulnerable(InvulnerabilityReason::CutScene, true);
playCutScene("it's anti-fur bias, that's what it is.cut");
player->setInvulnerable(InvulnerabilityReason::CutScene, false);
}
void chugInvulnerabilityPotion()
{
Character * player = getPlayer();
player->setInvulnerable(InvulnerabilityReason::Potion, true);
sleepUntil(now() + 5.0);
player->setInvulnerable(InvulnerabilityReason::Potion, false);
}
```
如果採用這樣的做法,我們就必須去檢查所有的無敵標記,而不能只檢查單 一個標記。如果其中有任何一個標記被設為無敵,玩家就是處於無敵的狀 態
這種做法是可行的,不過還是需要遵守一定的紀律。如果有人太懶惰,在不 同段的程式碼裡重複使用同一個 InvulnerabilityReason ,還是有可能會把程式搞掛掉
我們也可以考慮透過持續追蹤無敵計數值的方式,來消除掉糾纏不清的問 題。只要有任何程式碼把角色設為無敵,那就是無敵。這樣就可以得出一個非常簡單的「推入- 彈出 」(push-pop )模型
```cpp=
void playCelebrationCutScene()
{
Character * player = getPlayer();
player->pushInvulnerability();
playCutScene("I'm getting my own ship.cut");
player->popInvulnerability();
}
void chugInvulnerabilityPotion()
{
Character * player = getPlayer();
player->pushInvulnerability();
sleepUntil(now() + 5.0);
player->popInvulnerability();
}
```
像這樣的「推入-彈出」模型,確實是可行的。一旦你習慣這種慣用的做法就很容易理解了。這樣的做法也很容易進行擴展,每次要寫新的程式碼來進行無敵相關的操作,都不會影響到其他的程式碼,因為不同的程式碼都可以獨立去推入和彈出無敵的計數值,而不會破壞到任何重要的東西。
不過如果你的程式碼忘了去調用 popInvulnerability,這個角色就會永遠保持無敵狀態
像這種使用上的錯誤,最好還是要完全消除掉。最簡單的方式,就是把「推入-彈出」的操作,包裝在構建函式和解構函式內。然後編譯器就會再次成為我們的好朋友:
```cpp=
struct InvulnerableToken
{
InvulnerableToken(Character * character) :
m_character(character)
{ m_character->pushInvulnerability(); }
~InvulnerableToken()
{ m_character->popInvulnerability(); }
Character * m_character;
};
void playCelebrationCutScene()
{
Character * player = getPlayer();
InvulnerableToken invulnerable(player);
playCutScene("see you later, losers.cut");
}
void chugInvulnerabilityPotion()
{
Character * player = getPlayer();
InvulnerableToken invulnerable(player);
sleepUntil(now() + 5.0);
}
```
## 小結
雖然我們無法讓設計完全防呆,但我們所擋下的每一件蠢事,確實可以讓我們的系統更加可靠。因此,你打從一開始就要盡可能找機會,從你的設計裡消除掉那些會出問題的狀況。