這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言中沒有繼承,但是可以用結構體嵌入實現繼承,還有介面這個東西。現在問題來了:什麼情境下應該用繼承,什麼情境下應該用介面。
問題描述
這裡從一個實際的案例出發。網遊伺服器中的一個例子。假設每個實體都有一個ObjectID,packet中都有使用到這個ObjectID,用戶端與服務端之間通過這個ObjectID知道是一個什麼實體。用物件導向的觀點,就是有一個Object對象,裡面有getObjectID()方法,所有對象都是繼承自Object對象。
Creature繼承Object,表示遊戲中的生物。然後像Monster,NPC,都繼承自Creature的。玩家分為三個種族,Slayer/Vampire/Ouster三個不同的類實現,繼承自Creature。
Item也繼承自Object,表示物品類。除了像裝備這種很直觀的物品,屍體這類Corpse也是繼承自Item的。而屍體又有分MonsterCorpse和SlayerCorpse/VampireCorpse這種玩家屍體。
Effect也繼承自Object,表示效果類。比如玩家身上的狀態。還有其它很多很多,全是以Object為基類的。
總之,Object是一個最下面的基類,直接的衍生類別很多,衍生類別的衍生類別更多,這樣一顆繼承樹結構。
第一次嘗試
首先是用繼承的方式來。
type Object uint64type Creature sturct { Object // Creature繼承自Object}type Monster struct { Creature // Monster繼承自Monster}
這樣做的好處是,Monster直接可以調用到Creature裡的方法,Creature直接可以調用Object裡的方法。不用重寫代碼,就是繼承的好處啦。
但是...Go中沒有基類指標指向衍生類別對象,不可以*Object指向一個*Monster對象,調用Monster中的方法。
而我實際上在很多地方需要這種抽象類別型機制,比如儲存需要存Creature類型,使用的時候再具體用Monster類型方法。
第二次嘗試
這次用介面的方式:
type Object interface { ObjectID() uint}
遊戲中的每個實體的特徵是有一個ObjectID,所以這次把所以實現了ObjectID方法的,都是一個Object。
好啦,這樣就可以存不同的Object了:
objs []Objectswitch objs[i].(type) { case Monster: case Item:}
還是使用繼承以實現方法重用:
type Monster struct { Creature // 繼承Creature。實現Object}
但是...新的問題出現了。比如說給將Monster賦值給Object:
var obj Objectvar monster Monsterobj = monsterif _, ok := obj.(Creature); ok { // 居然不ok}
obj是一個Monster,Monster繼承Creature,但是obj卻不是一個Creature,what the fuck?
別小看這個問題,因為它滿足不了商務邏輯。比如說:
func f(obj Object) { // 如果obj是Creature,做邏輯A // 如果obj是Item,做邏輯B}// obj實際上是Monster,或者Player,或者MonsterCorpse,或Corpse,或WearItem等最具體的衍生類別// 但是沒有辦法知道obj到底是Creature或者是Item
第三次嘗試
第二次嘗試中,對Object使用了介面,但是不徹底。導致了一個Object是Monster卻不是Creature。如果全部介面化會怎麼樣?
type Object interfacetype Creature interface { Object // 嵌入Object介面,一個東西實現Creature必須是實現Object的 XXX() // Creature自身的方法XXX}type Monster interface { Creature // 一個東西實現Monster必須是實現Creature的 YYY() // 成為Monster需要實現的方法}
額,好像可以了,現在一個東西實現Object,如果它是Monster,那麼一定是Creature。
但是...沒有用繼承了。對所有最具體的衍生類別,都需要把所有這寫方法實現一遍,這肯定不靠譜。
答案
回頭想一下,總結問題所在:
- 第一次嘗試中,只用了繼承,無法滿足需求。我需要儲存的時候存放基類,使用的時候使用衍生類別。
- 在第三次嘗試中,只使用了介面,可以實現,但是要寫很多重複代碼。
- 第二次介面與繼承都有用到,但是好像不對。
總結下來就是,為了不重寫代碼,必須使用繼承。而為了儲存抽象類別型,調用具體類型的方法,必須用介面。
好好整理一番好,我找到了正確的方式,把介面和繼承分開:
type ObjectInterface interface { // 所有能夠返回物件類型的,都實現了Object介面。 // 包括OBJECT_CLASS_ITEM/OBJECT_CLASS_CREATURE/OBJECT_CLASS_EFFECT等 ObjectClass() ObjectClass }type Object uint64type Creature struct { Object // 繼承Object對象}func (c Creature) ObjectClass() { // 實現ObjectInterface介面 return OBJECT_CLASS_CREATURE}type Item struct { Object // 繼承Object對象}func (i Item) ObjectClass() { // 實現ObjectInterface介面 return OBJECT_CLASS_ITEM}
CreatureInterface介面也類似。Monster和NPC以及玩家都是實現CreatureInterface介面,並繼承Creature對象的。想寫的話,讓CreatureInterface介面必須是Object。