Golang的面向對象機制與Smalltalk或者Java等傳統面向對象的編程語言不同。傳統面向對象編程語言的一個重要特性是繼承機制。因為繼承機制支持在關聯對象間進行代碼復用和數據共享。繼承機制曾在代碼復用和數據共享的設計模式占據主導地位,但是目前組合這一古老的技術重新煥發了活力。
本篇文章轉自Tim Henderson的 "Object Oriented Inheritance in Go", 原文地址是 http://hackthology.com/object-oriented-inheritance-in-go.html 。非常感謝李浩和駿奇對於這篇文章的翻譯。
在我們探討如何在Go中實現繼承機制之前(Golong中的繼承機制和其他語言(Java)的繼承機制有區別),我們先看一下Java中如何實現繼承機制。
繼承與組合
讓我們先看一下我最喜歡的話題之一:編譯器!編譯器由管道轉換構成,該管道讀取text文本並將其轉化為機器代碼、匯編語言、字節碼或者其他的編程語言。管道首先會使用語法分析器對目標變成語言進行語法分析。一般情況下文本會被分解為不同的組成部分,例如:關鍵詞、標識符、標點和數字等等。每個組成部分都會被相應的數據類型標記。例如下面這個Java數據類型:
public class Main {}
這些組成部分(可以稱作標記)如下所示:
<public keyword>, "public"
<class keyword>, "class"
<idenitifier>, "Main"
<left-bracket>, "{"
<right-bracket>, "}"
這些標記可以划分為兩個部分:
- 標記類型
- 語義部分
這會導致我們進行如下的Java設計方式:
public enum TokenType { KEYWORD, IDENTIFIER, LBRACKET, RBRACKET, ... } public class Token { public TokenToken type; **Here, I think TokenToken should be "TokenType"** public String lexeme; }
對於一些標記類型來說,例如數值常量,標記類型最好能夠將包含這些屬性信息。就數值常量來說,在他的標記類型里應該包括常量值這一屬性。實現這一設計的傳統方式是使用繼承機制產生Token子類。
public class IntegerConstant extends Token { public long value; }
另外一種完成該設計的方式是利用組合方式產生IntegerConstant,IntegerConstant包含token的引用:
public class IntegerConstant { public Token type; public long value; }
在這個例子中,繼承機制是一個比較恰當的選擇。理由是語法分析器需要返回一個通用類型。考慮一下語法分析器的接口設計:
public class Lexer { public Lexer(InputStream in) public boolean EOF() public Token peek() throws Error public Token next() throws Error }
在繼承機制中,IntegerConstant 屬於Token類型,所以它可以在Lexer中調用。這不是唯一可用或者最好的設計,但是這種設計方式是有效的。讓我們看一下Go是如何完成這一目的的。
Inheritance and Composition in Go
Go中實現組合是一件十分容易的事情。簡單組合兩個結構體就能夠構造一個新的數據類型。
type TokenType uint16 const ( KEYWORD TokenType = iota IDENTIFIER LBRACKET RBRACKET INT ) type Token struct { Type TokenType Lexeme string } type IntegerConstant struct { Token *Token Value uint64 }
這就是Go中實現代碼和數據共享的常用方式。然而如果你想實現繼承機制,我們該如何去做?
Why would you want to use inheritance in go
一個可選的方案是將Token設計成接口類型。這種方案在Java和Go都適用:
type Token interface { Type() TokenType Lexeme() string } type Match struct { toktype TokenType lexeme string } type IntegerConstant struct { token Token value uint64 } func (m *Match) Type() TokenType { return m.toktype } func (m *Match) Lexeme() string { return m.lexeme } func (i *IntegerConstant) Type() TokenType { return i.token.Type() } func (i *IntegerConstant) Lexeme() string { return i.token.Lexeme() } func (i *IntegerConstant) Value() uint64 { return i.value }
這樣分析器就可以返回滿足Match 和 IntegerConstant類型的Token接口。
繼承機制的簡化版
上面的實現方案的一個問題是*IntegerConstant的方法調用中,出現了重復造輪子的問題。但是我們可以使用Go內建的嵌入機制來避免此類情況的出現。嵌入機制(匿名嵌入)允許類型之前共享代碼和數據。
type IntegerConstant struct { Token value uint64 } func (i *IntegerConstant) Value() uint64 { return i.value }
IntegerConstant中匿名嵌入了Token類型,使得IntegerConstant"繼承"了Token的字段和方法。很酷的方法!我們可以這樣寫代碼:
t := IntegerConstant{&Match{KEYWORD, "wizard"}, 2} fmt.Println(t.Type(), t.Lexeme(), t.Value()) x := Token(t) fmt.Println(x.Type(), x.Lexeme())
(可以在這里試一下 :https://play.golang.org/p/PJW7VShpE0)
我們沒有編寫Type()和Value()方法的代碼,但是*IntegerConstant也實現了Token接口,非常棒。
結構體的"繼承"機制
Go中有三種方式完成”繼承“機制,您已經看到了第一種實現方式:在結構體的第一個字段匿名嵌入接口類型。你還可以利用結構體實現其他兩種”繼承“機制:
1. 匿名嵌入結構體實例
type IntegerConstant struct { Match value uint64 }
2. 匿名嵌入結構體實例指針
type IntegerConstant struct { *Match value uint64 }
在所有的例子中,與正常嵌入類型不同的是我們使用匿名嵌入。然而,這個字段還是有字段名稱的,名稱是嵌入類型名稱。在IntegerConstant 的Match字段中,字段名稱是Match,無論嵌入類型是實例還是指針。
在以上的方案中,你不能嵌入與嵌入類型相同的方法名。例如結構體Bar匿名嵌入結構體Foo后,就不能擁有名稱為Foo的方法,同樣也不能實現type Fooer interface { Foo() }接口類型。
共享代碼、共享數據或者兩者兼得
相比於Java,Go在繼承和聚合之間的界限是很模糊的。Go中沒有extends關鍵詞。在語法的層次上,繼承看上去與聚合沒有什么區別。Go中聚合跟繼承唯一的不同在於,繼承自其他結構體的struct類型可以直接訪問父類結構體的字段和方法。
type Pet struct { name string } type Dog struct { Pet Breed string } func (p *Pet) Speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func (d *Dog) Speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed) } func main() { d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"} fmt.Println(d.Name()) fmt.Println(d.Speak()) }
(可以試一下 https://play.golang.org/p/Pmkd27Nqqy)
輸出:
spot
my name is spot and I am a pointer
嵌入式繼承機制的的局限
相比於Java, Go的繼承機制的作用是非常有限的。有很多的設計方案可以在Java輕松實現,但是Go卻不可能完成同樣的工作。讓我們看一下:
Overriding Methods
上面的Pet例子中,Dog類型重載了Speak()方法。然而如果Pet有另外一個方法Play()被調用,但是Dog沒有實現Play()的時候,Dog類型的Speak()方法則不會被調用。
package main import ( "fmt" ) type Pet struct { name string } type Dog struct { Pet Breed string } func (p *Pet) Play() { fmt.Println(p.Speak()) } func (p *Pet) Speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func (d *Dog) Speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed) } func main() { d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"} fmt.Println(d.Name()) fmt.Println(d.Speak()) d.Play() }
(試一下 https://play.golang.org/p/id-aDKW8L6)
輸出:
spot
my name is spot and I am a pointer
my name is spot
但是Java中就會像我們預想的那樣工作:
public class Main { public static void main(String[] args) { Dog d = new Dog("spot", "pointer"); System.out.println(d.Name()); System.out.println(d.Speak()); d.Play(); } } class Pet { public String name; public Pet(String name) { this.name = name; } public void Play() { System.out.println(Speak()); } public String Speak() { return String.format("my name is %s", name); } public String Name() { return name; } } class Dog extends Pet { public String breed; public Dog(String name, String breed) { super(name); this.breed = breed; } public String Speak() { return String.format("my name is %s and I am a %s", name, breed); } }
輸出:
$ javac Main.java && java Main
spot
my name is spot and I am a pointer
my name is spot and I am a pointer
這個明顯的區別是因為Go從根本上阻止了抽象方法的使用。讓我們看看下面這個例子:
package main import ( "fmt" ) type Pet struct { speaker func() string name string } type Dog struct { Pet Breed string } func NewPet(name string) *Pet { p := &Pet{ name: name, } p.speaker = p.speak return p } func (p *Pet) Play() { fmt.Println(p.Speak()) } func (p *Pet) Speak() string { return p.speaker() } func (p *Pet) speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func NewDog(name, breed string) *Dog { d := &Dog{ Pet: Pet{name: name}, Breed: breed, } d.speaker = d.speak return d } func (d *Dog) speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.speak(), d.Breed) } func main() { d := NewDog("spot", "pointer") fmt.Println(d.Name()) fmt.Println(d.Speak()) d.Play() }
(試一下 https://play.golang.org/p/9iIb2px7jH)
輸出:
spot
my name is spot and I am a pointer
my name is spot and I am a pointer
現在跟我們預想的一樣了,但是跟Java相比略顯冗長和晦澀。你必須手工重載方法簽名。而且,代碼在結構體未正確初始化的情況下會崩潰,例如當調用Speak()時,speaker()卻沒有完成初始化工作的時候。
Subtyping
在Java中,Dog繼承自Pet,那么Dog類型就是Pet子類。這意味着在任何需要調用Pet類型的場景都可以使用Dog類型替換。這種關系稱作多態性,但Go的結構體類型不存在這種機制。
讓我們看下面的例子:
package main import ( "fmt" ) type Pet struct { speaker func() string name string } type Dog struct { Pet Breed string } func NewPet(name string) *Pet { p := &Pet{ name: name, } p.speaker = p.speak return p } func (p *Pet) Play() { fmt.Println(p.Speak()) } func (p *Pet) Speak() string { return p.speaker() } func (p *Pet) speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func NewDog(name, breed string) *Dog { d := &Dog{ Pet: Pet{name: name}, Breed: breed, } d.speaker = d.speak return d } func (d *Dog) speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.speak(), d.Breed) } func Play(p *Pet) { p.Play() } func main() { d := NewDog("spot", "pointer") fmt.Println(d.Name()) fmt.Println(d.Speak()) Play(d) }
(試一下 https://play.golang.org/p/e1Ujx0VhwK)
輸出:
prog.go:62: cannot use d (type *Dog) as type *Pet in argument to Play
然而,接口類型中存在子類化的多態機制!
package main import ( "fmt" ) type Pet interface { Name() string Speak() string Play() } type pet struct { speaker func() string name string } type Dog interface { Pet Breed() string } type dog struct { pet breed string } func NewPet(name string) *pet { p := &pet{ name: name, } p.speaker = p.speak return p } func (p *pet) Play() { fmt.Println(p.Speak()) } func (p *pet) Speak() string { return p.speaker() } func (p *pet) speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *pet) Name() string { return p.name } func NewDog(name, breed string) *dog { d := &dog{ pet: pet{name: name}, breed: breed, } d.speaker = d.speak return d } func (d *dog) speak() string { return fmt.Sprintf("%v and I am a %v", d.pet.speak(), d.breed) } func Play(p Pet) { p.Play() } func main() { d := NewDog("spot", "pointer") fmt.Println(d.Name()) fmt