golang组合垂直思维 type embedding
什么是 Golang 的正交组合 - 垂直组合思维:Tony Bai 的博客 - Coding in GO way - Orthogonal Composition
Go 语言通过 type embedding 实现垂直组合。组合方式莫过于以下这么几种:
a):construct interface
by embedding interface
b):construct struct
by embedding interface
c):construct struct
by embedding struct
Go 语言中没有继承,但是可以用结构体嵌入实现继承,还有接口这个东西。现在问题来了:什么场景下应该用继承,什么场景下应该用接口。这里从一个实际的案例出发。
问题描述:
网游服务器中的一个例子。假设每个实体都有一个 ObjectID,每一个实例都有一个独一无二的 ObjectID。用面向对象的观点,就是有一个 Object 对象,里面有 getID() 方法,所有对象都是继承自 Object 对象。
Creature 继承 Object,表示游戏中的生物。然后像 Monster,Human,都继承自 Creature 的。Item 也继承自 Object,表示物品类。除了像装备这种很直观的物品,尸体这类 Corpse 也是继承自 Item 的。而尸体又有分 MonsterCorpse 和 HumanCorpse 等。
Effect 也继承自 Object,表示效果类。比如玩家身上的状态。还有其它很多很多,全是以 Object 为基类的。
总之,Object 是一个最下面的基类,直接的派生类很多,派生类的派生类更多,这样一颗继承树结构。
实现方法:
construct struct by embedding struct
这是最简单的继承的方式:
1
2
3
4
5
6
7
8
9
10
|
//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 实例。
1
2
3
4
5
6
7
|
// 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.
}
|
construct interface by embedding interface
我们新建一个工程并定义 Object 接口:
1
2
3
4
5
6
|
//object/object.go
package object
type Object interface {
GetID() uint
//每一个Object的实现类型都有一个ID值,通过GetID()获取其ID
}
|
Creature 也定义为一个接口,他继承于 Object,且拥有自己的方法 Create()。为了体现继承的关系,我把它放在了子目录下:
1
2
3
4
5
6
7
8
9
10
11
|
//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():
1
2
3
4
5
6
7
8
9
10
11
12
|
//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:
1
2
3
4
|
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 方法。如果这些方法实现基本相同,那么将会出现大量冗余代码。
construct struct by embedding interface
为了复用父类的方法,我们必须把方法定义在父类中。于是我们就想到了在父类中创建 struct 供子类继承,在父类的 struct 中实现方法 func:
1
2
3
4
5
6
7
8
9
10
11
|
//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 的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//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)的前提是库中使用 Obj 的代码都与 Obj 在同一个 package 中 (不符合业务逻辑,但作者看过的几个第三方包中的确有将所有代码写在一个 package 甚至一个文件中的情况)。
之所以在此绑定了 Objstruct
而非 Objectinterface
,是因为我们只创建了一个 Objectinterface
的实例,省去了赋值(给interface
赋struct
)的麻烦。具体而言,标准库中的 package context 中 timerCtx 绑定的是 cancelCtx 这一个 struct。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//context.go
package context
type Context interface {
//......
}
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
type timerCtx struct {
cancelCtx //construct struct by embedding struct
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err) //具体调用
//......
|
需要注意的是,具体调用是、timerCtx 调用 (cancelCtx.cancel) 的方法,而非 (timerCtx.cancelCtx) 调用 cancel 方法。(很基础但很重要)
而如果我们在父类实现多个 GetID 的方法,并希望在子类中加以选择,那么我们就需要创建两个struct
并分别实现不同的方法,使用construct struct by embedding interface
来决定绑定哪一个struct
。另外,如果使用construct struct by embedding interface
,则不可以越过父类的方法 (如果存在的话) 去执行爷类 (???) 定义的方法。
为什么说在不同 package 下不公开 struct(即 struct obj)不可行,因为不是在同一个 package 中进行赋值。也就是说必须公开对外可见后,外部才得以使用他来赋值。而使用 NewObj(…) 作包裹从本质而言也是一个道理。
最后我们给出 Monster 的代码,可以发现,他只需要实现自己独有的方法即可。当然它也可以有选择性的 override 父类的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
//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
:
1
2
3
4
5
6
7
8
9
10
|
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 并提出修改意见。