Golang的垂直組合思維——type embedding
什麼是Golang的正交組合-垂直組合思維:Tony Bai的部落格 - Coding in GO way - Orthogonal Composition
Go語言通過type embedding實現垂直組合。組合方式莫過於以下這麼幾種:
a):constructinterface
by embeddinginterface
b):construct
struct
by embeddinginterface
c):construct
struct
by embeddingstruct
Go語言中沒有繼承,但是可以用結構體嵌入實現繼承,還有介面這個東西。現在問題來了:什麼場景下應該用繼承,什麼場景下應該用介面。這裡從一個實際的案例出發。
問題描述:
網遊伺服器中的一個例子。假設每個實體都有一個ObjectID,每一個例項都有一個獨一無二的ObjectID。用面向物件的觀點,就是有一個Object物件,裡面有getID()方法,所有物件都是繼承自Object物件。
Creature繼承Object,表示遊戲中的生物。然後像Monster,Human,都繼承自Creature的。Item也繼承自Object,表示物品類。除了像裝備這種很直觀的物品,屍體這類Corpse也是繼承自Item的。而屍體又有分MonsterCorpse和HumanCorpse等。
Effect也繼承自Object,表示效果類。比如玩家身上的狀態。還有其它很多很多,全是以Object為基類的。
總之,Object是一個最下面的基類,直接的派生類很多,派生類的派生類更多,這樣一顆繼承樹結構。
實現方法:
constructstructby embeddingstruct
這是最簡單的繼承的方式:
//construct struct by embedding struct type Object struct{ ID uint } type Creature sturct { Object // Creature繼承自Object } type Monster struct { Creature // Monster繼承自Monster }
這樣做的好處就是,Monster直接可以呼叫到Creature裡的方法,Creature直接可以呼叫Object裡的方法。不用重寫程式碼。
但是,Go中沒有基類指標指向派生類物件,不可以Object
指向一個Monster
物件,呼叫Monster中的方法。
而我們實際上在很多地方需要這種抽象型別機制,比如儲存需要存Creature型別,使用的時候再具體用Monster型別方法。
struct中嵌入struct,被嵌入的struct的method會被提升到外面的型別中。比如stl中的poolLocal struct,對於外部來說它擁有了Lock和Unlock方法,但是實際呼叫時,method呼叫實際被傳給poolLocal中的Mutex例項。
// sync/pool.go type poolLocal struct { private interface{}// Can be used only by the respective P. shared[]interface{} // Can be used by any P. Mutex// Protects shared. pad[128]byte// Prevents false sharing. }
constructinterfaceby embeddinginterface
我們新建一個工程並定義Object介面:
//object/object.go package object type Object interface { GetID() uint //每一個Object的實現型別都有一個ID值,通過GetID()獲取其ID }
Creature也定義為一個介面,他繼承於Object,且擁有自己的方法Create()。為了體現繼承的關係,我把它放在了子目錄下:
//object/creature/creature.go package creature import ( "fmt" "github.com/ovenvan/multi-inheritance/object" ) type Creature interface { object.Object Create() }
同樣Human和Monster都繼承於Creature,且擁有各自獨一無二的方法Human.Born()[略]和Monster.Hatch():
//object/creature/monster/monster.go package monster import ( "fmt" "github.com/ovenvan/multi-inheritance/object" "github.com/ovenvan/multi-inheritance/object/creature" ) type Monster interface { creature.Creature Hatch() } type Mstr struct{/*some properties*/}
為了使Mstr能夠實現介面Monster,我們需要為他實現func:
func (this *Mstr) GetID() uint{/*your code*/} func (this *Mstr) Create() {/*your code*/} func (this *Mstr) Hatch(){/*your code*/} func NewMonster () Monster{return &Monster_s{}}
這樣就不會出現construct struct by embedding struct
時出現的基類指標無法指向子類的問題。現在一個東西實現Object,如果它是Monster,那麼一定是Creature。但屬於父類的GetID和Create方法是沒法複用的,也就是說對於Hum struct,我們仍需要重寫GetID和Create方法。如果這些方法實現基本相同,那麼將會出現大量冗餘程式碼。
constructstructby embeddinginterface
為了複用父類的方法,我們必須把方法定義在父類中。於是我們就想到了在父類中建立struct供子類繼承,在父類的struct中實現方法func:
//object/object.go package object type Object interface { GetID() uint } type Obj struct {//needs to be public id uint } func (this *Obj) GetID() uint{ return this.id }
為Creature介面建立的struct Crea 通過construct struct by embedding struct
繼承Obj,如此便繼承了GetID的方法:
//object/creature/creature.go package creature import ( "fmt" "github.com/ovenvan/multi-inheritance/object" ) type Creature interface { object.Object Create() } type Crea struct { object.Obj//struct 中繫結interface和struct的區別? // Object只實現了一個Obj例項,這個例項的作用是被繼承,提供父類的程式碼,因此應該繼承Obj,而非Object } func (t *Crea) Create(){ fmt.Println("This is a Base Create Method") } func (t *Crea)GetID() uint{//override fmt.Println("Override GetID from Creature") return t.Obj.GetID() //t.GetID()it is a recursive call }
為什麼是construct struct by embedding struct
而不是construct struct by embedding interface
?如果可以實現繫結介面而非例項的話,我們是否可以不對外公開struct Obj呢。作者至現在思考的結果是,繫結介面是可行的,但不對外公開struct Obj(換言之,讓使用者無法自如的建立struct Obj)是不可行的。
之所以在此綁定了Objstruct
而非Objectinterface
,是因為我們只建立了一個Objectinterface
的例項,省去了賦值(給interface
賦struct
)的麻煩。而如果我們希望在父類實現多個GetID的方法,並在子類中加以選擇,那麼我們就需要建立兩個struct
並分別實現不同的方法,使用construct struct by embedding interface
來決定繫結哪一個struct
。另外,如果使用construct struct by embedding interface
,則不可以越過父類的方法(如果存在的話)去執行爺類(???)定義的方法。
為什麼說不公開struct(即struct obj)不可行,因為不是在同一個package中進行賦值。也就是說必須公開對外可見後,外部才得以使用他來賦值。而使用NewObj(...)作包裹從本質而言也是一個道理。
最後我們給出Monster的程式碼,可以發現,他只需要實現自己獨有的方法即可。當然它也可以有選擇性的override父類的方法:
//object/creature/monster/monster.go package monster import ( "fmt" "github.com/ovenvan/multi-inheritance/object" "github.com/ovenvan/multi-inheritance/object/creature" ) type Monster interface { creature.Creature Hatch() } type Monster_s struct { creature.Crea alive bool } func (t *Monster_s) Hatch(){ t.Create() fmt.Println("After created, i was hatched") } func (t *Monster_s) Create(){ t.Crea.Create() fmt.Println("This is an Override Create Method from monster") } func (t *Monster_s)GetID() uint{ fmt.Println("Override GetID from Monster") //return t.Crea.GetID() return t.Obj.GetID()//直接呼叫父類的父類(Obj)的方法, //跳過了Creature重寫的方法。 //t.GetID()recursive call } func NewMonster (m object.ObjectManager) Monster{...}
最後看一下main.go
:
package main import ( "github.com/ovenvan/multi-inheritance/object/creature/monster" ) func main() { mstr:=monster.NewMonster() mstr.Hatch() }
我在Github-multi-inheritance 上傳了本次實驗的Demo,包括完善了各函式的程式碼,大家可以通過
go get github.com/ovenvan/multi-inheritance
下載該Demo並提出修改意見。