我經常遇到以下的情境
1. 系統測試不如預期
2. 老闆不爽,質疑沒有監督好品質
3. 檢討
4. 改版
5. goto 1
無窮的迴圈,老闆失去耐心,員工失去鬥志,我在一邊看,只有一個想法,大多數人都應該有這樣的經驗,
去餐廳點菜的時候沒意見,等到菜端上桌,東挑西挑,說要甜一點、鹹一點、淡一點,可是沒人肯承認,最初的那道菜根本不是你想要的,同一道菜,廚師怎麼改,永遠不能滿足你。
這點就像1~5的迴圈,大家都只在事後檢討這裏打轉,沒有人想去事前分析設計一下如何進行,問到系統想要給玩家的核心概念是什麼? 畫面風格概念是什麼? 某個地圖的特色是什麼? 介面的操作的特色是什麼? 答不出來,這難怪介面一改再改沒人愛,場景東加西加越加越顯雜亂。
也許你會說,因為東西做完了,在測試,這種情況難免,但是我要說的是,為什麼事前不先把最後的成品可能長相先溝通討論,不要在最後的時候,才一句"你怎麼做成這樣?",把責任怪到別人頭上?
以自我本位而言,沒人知道你內心想什麼,知道你想什麼,沒人知道你的標準何在,因此每次交付工作給別人,你總是感嘆天兵到處都是,所以,我覺得,以後做事之前,是不是先應該找方法,確認彼此之間,對工作的內容認知是一致的,這樣絕對可以省卻很多1-5迴圈發生。
2009年1月16日 星期五
類別的設計
類別是C++實踐物件導向程式的基礎,一般人對類別設計應該都有了些基本的概念,這篇想要以系統開發的角度,來強調類別設計與使用的注意事項。
1. 變數封裝
基本上除了特別特別的需求,一般都建議你,把資料放在private區段,為什麼呢,這樣我才可以設計函式,管控如何存取這些變數值,避免變數被莫名奇妙的變更,造成錯誤時,難以追蹤的困擾,所以一個基本的變數,如果需要被外界存取,那需要一個設定函式,一個讀取函式,如果不需要,內部使用,那一個都不需要提供。
可以放在protected嘛?基本上,類別設計,建議你忘記有這個區間的存在,為什麼?protected區間可以被後續繼承類別存取,除非你有特殊用途,否則你埋下了一個炸彈,你如何知道後續繼成出去的類別,不會胡亂更動其值?不會又寫了一個函式,把資料public出去?
可是全域變數一定會用到的,這個地方建議去參考"Design Patterns"的Singleton樣式,提供了一個將變數全域化,但卻被類別管控的方法。
2. 成員函式設計
成員函式設計,視目的而定,沒有一定的規範(除非專案本身的規定),這裡建議,請在函式放置位置的時候,多思考一下,要放在哪個區段,一般的建議是這樣,如果函式需要被外界存取,放public(廢話...),不需要的時候,請放private(廢話兩枚)。
是廢話嗎?回去隨便拿一個別人寫的類別看看,是不是有很多函式被外界呼叫後,如果數值亂填,可能造成系統錯誤之類的?你也許會反駁說,說明中有寫,這是內部使用的,錯誤是因為外界亂用啊?可是我要說,設計類別的基本原則,就是讓人容易使用,且不容易出錯,與其你放任函式public,裡頭寫一大堆exception,為什麼不把這些函式private起來,錯誤發生率也小多了不是嗎?
常在程式之間流傳的對答:啊,我知道bug發生原因了,是因為某某某呼叫了我一個函式,而這個函式是內部使用的,不可以這樣呼叫...,謹慎考慮多少可以減少潛藏可能發生問題的機會。
3. 注意別人類別的規範
接下來是使用別人類別的注意事項,不要企圖去改變回傳值的屬性,原因是來自C++轉型的方便,許多人喜歡把參數或是回傳值,亂轉型,變成其他的類型來操作,這本是無可厚非的事情,最明顯的就是const回傳值,但是,系統開發的時候,標明const,就是不希望你去改變這個數值,嚴重的說來,改變這個值,搞不好會發生嚴重的錯誤,而經常有人就愛去碰:
(a) 當你看見函式回傳值有const時,不要嘗試去改變回傳值內容
(b) 當函式宣告有throw時,請處理exception
因為throw表示,函式內可能發生無法預期的錯誤,catch它,可以明瞭錯誤的原因,方便除錯
(c) 當類別裡面沒有虛擬函式時,不要嘗試去繼承它
如果你寫個類別,會給後續類別繼承,你會怎麼寫?一定是在裡面寫一些虛擬函式,暗示後來的程式師,這幾個函式可能以後會有其他的寫法,如果你寫個類別,沒有任何虛擬函式,那表示什麼?不想被別人繼承?功能已經完備?不論哪個答案,都不該再有類別繼承它了。
除此之外,還有一個原因,虛擬解構式,在繼承情況下,解構的記憶體才會正確的釋放,你的類別什麼虛擬函式都沒有(含解構式),繼承它,未來背負著一個memory leak的風險。
(d) 正確的繼承
假設,CHuman是個基底類別,CHuman人物會動、可裝備、會說話....,假設遊戲中出現了一種東西叫"假人",人類經常習慣用名稱將之歸類為人類的一種,所以他該繼承CHuman,然後把所有不能用的功能重寫。你做了什麼事?把一個功能齊全的人類別,降級成什麼都不會的假人?只因為它長得像人?或者應該由一個物品,開始去繼承,增加功能比較適合?
這裡提一個繼承基本上的判斷標準: 假人是不是一種人? 意思是,人類會什麼,應該假人也要會什麼,少把類別寫成,假人是人的一種,但是他太多不會這不會那時...,大概就是錯誤的繼承關係了。
1. 變數封裝
基本上除了特別特別的需求,一般都建議你,把資料放在private區段,為什麼呢,這樣我才可以設計函式,管控如何存取這些變數值,避免變數被莫名奇妙的變更,造成錯誤時,難以追蹤的困擾,所以一個基本的變數,如果需要被外界存取,那需要一個設定函式,一個讀取函式,如果不需要,內部使用,那一個都不需要提供。
可以放在protected嘛?基本上,類別設計,建議你忘記有這個區間的存在,為什麼?protected區間可以被後續繼承類別存取,除非你有特殊用途,否則你埋下了一個炸彈,你如何知道後續繼成出去的類別,不會胡亂更動其值?不會又寫了一個函式,把資料public出去?
class NPC
{
private:
int m_iHp; // 生命力,除了初值化外,不應該由外界來操作這個變數
int m_iAcc; // 物理攻擊準確度,由企劃公式產生,更不應該由外界來操作
};
這樣的寫法,也斷絕了,系統開發時,有人妄想修改資料的念頭,減少將來系統長大時,潛藏錯誤發生的機會,真的需要處裡內部數值時,再以下列方法處理:
class AStrangeClass
{
private:
NPC *m_pNpc;
public:
void SetNpc( NPC *npc ){ m_pNpc=npc; }
NPC *GetNpc(){ return m_pNpc; }
};
關於全域變數呢,基本上建議,寫程式應該要把他視為罪惡,這意味著有一個完全不能改的機制在那邊,無法控制其變數名稱,只要跟這個變數有瓜葛的類別,形成了一個盤根糾結的系統,如同先前我提到的,少用繼承一樣的結果,將來改其中任何一個地方,潛在需要修改的程式是全部。可是全域變數一定會用到的,這個地方建議去參考"Design Patterns"的Singleton樣式,提供了一個將變數全域化,但卻被類別管控的方法。
2. 成員函式設計
成員函式設計,視目的而定,沒有一定的規範(除非專案本身的規定),這裡建議,請在函式放置位置的時候,多思考一下,要放在哪個區段,一般的建議是這樣,如果函式需要被外界存取,放public(廢話...),不需要的時候,請放private(廢話兩枚)。
是廢話嗎?回去隨便拿一個別人寫的類別看看,是不是有很多函式被外界呼叫後,如果數值亂填,可能造成系統錯誤之類的?你也許會反駁說,說明中有寫,這是內部使用的,錯誤是因為外界亂用啊?可是我要說,設計類別的基本原則,就是讓人容易使用,且不容易出錯,與其你放任函式public,裡頭寫一大堆exception,為什麼不把這些函式private起來,錯誤發生率也小多了不是嗎?
常在程式之間流傳的對答:啊,我知道bug發生原因了,是因為某某某呼叫了我一個函式,而這個函式是內部使用的,不可以這樣呼叫...,謹慎考慮多少可以減少潛藏可能發生問題的機會。
3. 注意別人類別的規範
接下來是使用別人類別的注意事項,不要企圖去改變回傳值的屬性,原因是來自C++轉型的方便,許多人喜歡把參數或是回傳值,亂轉型,變成其他的類型來操作,這本是無可厚非的事情,最明顯的就是const回傳值,但是,系統開發的時候,標明const,就是不希望你去改變這個數值,嚴重的說來,改變這個值,搞不好會發生嚴重的錯誤,而經常有人就愛去碰:
class A
{
public:
const char *GetTypeName(){ return "this is a type"; }
};
void main()
{
A a;
const char *ptr=a.GetTypeName();
strcpy( (char *)ptr, "this is not a type" );
}
編譯都對,執行的時候,就當給你看,所以(a) 當你看見函式回傳值有const時,不要嘗試去改變回傳值內容
(b) 當函式宣告有throw時,請處理exception
因為throw表示,函式內可能發生無法預期的錯誤,catch它,可以明瞭錯誤的原因,方便除錯
(c) 當類別裡面沒有虛擬函式時,不要嘗試去繼承它
如果你寫個類別,會給後續類別繼承,你會怎麼寫?一定是在裡面寫一些虛擬函式,暗示後來的程式師,這幾個函式可能以後會有其他的寫法,如果你寫個類別,沒有任何虛擬函式,那表示什麼?不想被別人繼承?功能已經完備?不論哪個答案,都不該再有類別繼承它了。
除此之外,還有一個原因,虛擬解構式,在繼承情況下,解構的記憶體才會正確的釋放,你的類別什麼虛擬函式都沒有(含解構式),繼承它,未來背負著一個memory leak的風險。
(d) 正確的繼承
假設,CHuman是個基底類別,CHuman人物會動、可裝備、會說話....,假設遊戲中出現了一種東西叫"假人",人類經常習慣用名稱將之歸類為人類的一種,所以他該繼承CHuman,然後把所有不能用的功能重寫。你做了什麼事?把一個功能齊全的人類別,降級成什麼都不會的假人?只因為它長得像人?或者應該由一個物品,開始去繼承,增加功能比較適合?
這裡提一個繼承基本上的判斷標準: 假人是不是一種人? 意思是,人類會什麼,應該假人也要會什麼,少把類別寫成,假人是人的一種,但是他太多不會這不會那時...,大概就是錯誤的繼承關係了。
2009年1月7日 星期三
少用繼承
物件導向程式設計中,為了提升程式碼的再利用性,提供了繼承這樣的架構給我們使用,只要看不順眼的類別,就可以用繼承改寫舊功能,達到擴展的目的,範例如下:
class AI
{
private:
virtual void FSM(); // AI類別實做FSM
};
class Monster : public AI
{
private:
virtual void FSM(); // Monster類別改寫原來的函式
};
但就"程式碼再利用的"目的而言,事實上,我們常用的方法還有一種,"組成",利用物件彼此間的協力,完成特定工作,譬如,老闆要求去做一件A、B君都只會做部分功能的工作,依照繼承的觀念,是否要生成第三個類別C,繼承A、B的能力,然後改寫部分關聯而增加的程式碼? 然而我們是不是可以換個方向思考,我可以設計個類別C,工作交代C,C會先請A君做完他會做的,然後B繼續接手,一樣可以完成工作,與繼承兩者,事實上複雜度一樣,都需要3個類別,但是程式維護的小心程度卻是不可相提並論,程式如下
class AI
{
public:
void FSM();
};
class TimeControl
{
public:
void DoSomething();
};
用繼承:
class Npc:public AI, public TimeControl
{
void DoSomething(); // 這裡遭遇第一個難關
// 多重繼承,要小心AB的成員變數、成員函式設計關係
};
用組成:
class Npc
{
private:
AI ai;
TimeControl tc;
public:
void DoSomething(); // 我可以在這裡,靈活運用ai, tc物件的函式達成老闆目標
};
你可以看見,往後ABC三個類別的修改,相依性是否會降低很多很多,說白話,只要AB類別確定功能正常,發生BUG一定就是C,A類別新增怎樣的功能,不會影響C或B的運作,同理C增減怎樣的函式,AB都不會影響,而使用繼承呢? 就算你確定ABC三類別功能都正確,卻有可能因為繼承的關係,造成邏輯錯誤,像是C發生BUG的原因,是因為C修改了一個變數值,然後是A或B非常重要的變數,不允許由外部變更的(這裡有個小伏筆,避免修改到別人家的變數,變數請用private封裝...)。
加上有許多程式濫用繼承,一個遊戲專案裡頭隨便都成千上百個類別,加上彼此繼承的關係,專案像是N坨大肉粽樹,盤根錯節,某Z類別有BUG,原因卻是在千里之外八竿子打不到關係的A類別造成,改了A,卻連鎖反應要把某個肉粽樹的函式都要改過,想到就累人啊,所以,有得選的情形下,請少用繼承多用組成。
再舉個生活上的例子,老闆要設計個簡報在客戶面前推銷商品,為了達到目標,他需要繼承文書小姐的能力+產品部門對商品規格特色了解的能力+簡報能力+....,或者,他可以把公司內文書小姐、產品部經理、行銷推廣部經理叫來,說出需求,接下來大家就自動會把事情搞定,而老闆則可專心挑毛病去,愛用繼承的,通常工作累得要死,愛用組成的,通常是老闆,你選哪個?
class AI
{
private:
virtual void FSM(); // AI類別實做FSM
};
class Monster : public AI
{
private:
virtual void FSM(); // Monster類別改寫原來的函式
};
但就"程式碼再利用的"目的而言,事實上,我們常用的方法還有一種,"組成",利用物件彼此間的協力,完成特定工作,譬如,老闆要求去做一件A、B君都只會做部分功能的工作,依照繼承的觀念,是否要生成第三個類別C,繼承A、B的能力,然後改寫部分關聯而增加的程式碼? 然而我們是不是可以換個方向思考,我可以設計個類別C,工作交代C,C會先請A君做完他會做的,然後B繼續接手,一樣可以完成工作,與繼承兩者,事實上複雜度一樣,都需要3個類別,但是程式維護的小心程度卻是不可相提並論,程式如下
class AI
{
public:
void FSM();
};
class TimeControl
{
public:
void DoSomething();
};
用繼承:
class Npc:public AI, public TimeControl
{
void DoSomething(); // 這裡遭遇第一個難關
// 多重繼承,要小心AB的成員變數、成員函式設計關係
};
用組成:
class Npc
{
private:
AI ai;
TimeControl tc;
public:
void DoSomething(); // 我可以在這裡,靈活運用ai, tc物件的函式達成老闆目標
};
你可以看見,往後ABC三個類別的修改,相依性是否會降低很多很多,說白話,只要AB類別確定功能正常,發生BUG一定就是C,A類別新增怎樣的功能,不會影響C或B的運作,同理C增減怎樣的函式,AB都不會影響,而使用繼承呢? 就算你確定ABC三類別功能都正確,卻有可能因為繼承的關係,造成邏輯錯誤,像是C發生BUG的原因,是因為C修改了一個變數值,然後是A或B非常重要的變數,不允許由外部變更的(這裡有個小伏筆,避免修改到別人家的變數,變數請用private封裝...)。
加上有許多程式濫用繼承,一個遊戲專案裡頭隨便都成千上百個類別,加上彼此繼承的關係,專案像是N坨大肉粽樹,盤根錯節,某Z類別有BUG,原因卻是在千里之外八竿子打不到關係的A類別造成,改了A,卻連鎖反應要把某個肉粽樹的函式都要改過,想到就累人啊,所以,有得選的情形下,請少用繼承多用組成。
再舉個生活上的例子,老闆要設計個簡報在客戶面前推銷商品,為了達到目標,他需要繼承文書小姐的能力+產品部門對商品規格特色了解的能力+簡報能力+....,或者,他可以把公司內文書小姐、產品部經理、行銷推廣部經理叫來,說出需求,接下來大家就自動會把事情搞定,而老闆則可專心挑毛病去,愛用繼承的,通常工作累得要死,愛用組成的,通常是老闆,你選哪個?
2009年1月1日 星期四
訂閱:
文章 (Atom)
