Golang的面向對象

如你所知,封裝、繼承、多態和抽象是面向對象編程的4個基本特徵,本文描述Golang語言是如何實現這些特徵的。

1 Golang的面向對象類型

Golang實現面向對象的兩個關鍵類型是struct和interface,其中struct類似C++的普通類類型,interface則對應抽象類類型。與C++採用public/protected/private指示成員和方法的可見性不同,Golang採用大小寫標識可見性,即大寫字母開頭的成員/方法對外可見,小寫開頭的則屬於類的私有成員,外部不可以直接訪問。此外,Golang與C++在類類型的定義上還有一個重要區別,那就是Golang在struct內只需要聲明類的成員變量,而不需要在類定義體內聲明或定義所有的方法,方法定義都在struct之外完成。好了,我們開始正文。

2 Golang的面向對象實現

2.1 封裝

學生有姓名、年齡和專業等屬性,於是我們定義一個Student類型如下:

type Student struct {
	name  string
	age   int
	major string
}

學生可以跟大家打招呼:

func (s Student) SayHi() {
	fmt.Printf("Hi, I am %s aged %d, and my major is %s\n", s.name, s.age, s.major)
}

在函數定義的func關鍵字後面加上我們定義的Student類型變量定義,這個函數就成爲了Student的方法。類圖表示如下:

encapsulate

值得注意的是,在Golang內,除slice、map、channel和顯示的指針類型屬於引用類型外,其它類型都屬於值類型,前者作爲函數入參傳遞時,函數對參數的修改會影響調用對象,而後者作爲入參時,函數體內會生成調用對象的拷貝,函數對入參的修改不會影響調用對象。因此,如果我們要給Student類定義一個“構造函數”,我們希望的是這個函數的入參可以被賦值到Student的成員內,則該“構造函數”應該使用指針類型對象定義:

func (s *Student) Init(name string, age int, major string) {
	s.name = name
	s.age = age
	s.major = major
}

我們來測試一下:

s := Student{}
s.Init("pirlo", 21, "cs")
s.SayHi()

輸出結果:

$ go run test_encapsulate.go 
Hi, I am pirlo aged 21, and my major is cs.

我們定義的學生類型,屬性都是私有的,方法都是公有的,還記得麼,私有或公有都是通過屬性或方法的首字母大小寫決定的。那我們現在來試一下公有屬性和私有方法吧。
比如,我想讓專業(major)這個屬性成爲公有屬性:

type Student struct {
	name  string
	age   int
	Major string
}

func main() {
	s := Student{}
	s.Init("pirlo", 21, "cs")
	s.SayHi()

	s.Major = "finance"
	s.SayHi()
}

先調用構造函數設置專業爲cs,再通過顯示賦值的方式修改專業爲finance:

$ go run test_encapsulate.go 
Hi, I am pirlo aged 21, and my major is cs.
Hi, I am pirlo aged 21, and my major is finance.

但是如果我們試圖修改私有屬性:

s.age = 22

編譯器會告訴你:

$ go run test_encapsulate.go 
# command-line-arguments
./test_encapsulate.go:15: s.age undefined (cannot refer to unexported field or method age)

括號的註釋說明了不能引用未導出/未公開的屬性或方法。

小結一下,Golang通過struct定義類的屬性,通過在func定義時傳入類對象的方式定義類的方法,其中屬性和方法的公有/私有屬性是通過首字母的大小寫決定的。

2.2 繼承

與C++、Java等完整支持面向對象的語言不同,Golang沒有顯式的繼承,而是通過組合實現繼承。
我們先定義一個基類Person,提供姓名和年齡兩個屬性,以及SayHi一個方法(Init類似於構造函數):

type Person struct {
	name string
	age  int
}

func (p *Person) Init(name string, age int) {
	p.name = name
	p.age = age
}

func (p Person) SayHi() {
	fmt.Printf("Hi, I am %s, %d years old.\n", p.name, p.age)
}

然後,我們通過組合的方式繼承這個基類,實現Employee子類:

type Employee struct {
	Person
	company string
}

func (e *Employee) Init(name string, age int, company string) {
	e.Person.Init(name, age)
	e.company = company
}

func (e Employee) Work() {
	fmt.Printf("I'm working %s.\n", e.company)
}

Employee組合了Person這個成員,除此之外它還擁有自己的成員company,即所屬公司,僱員除了是一個Person之外,還需要工作,因此我們定義了Work這個方法。好了,我們再測試一下:

func main() {
	p := oo.Person{}
	p.Init("pirlo", 21)
	p.SayHi()

	e := oo.Employee{}
	e.Init("kaka", 22, "milan")
	e.SayHi()
	e.Work()
}
$ go run test_inherit.go 
Hi, I am pirlo, 21 years old.
Hi, I am kaka, 22 years old.
I'm working in milan.

僱員kaka可以像pirlo一樣說話,與此同時,他還可以在milan工作,類圖表示如下:

inherit

小結一下,Golang沒有完整實現繼承,而是通過組合的方式實現。組合類(子類)可以直接調用被組合類(基類)的公有方法,訪問基類的公有屬性,子類也可以定義自己的屬性,以及實現自己特有的方法。Golang的設計哲學之一就是簡潔,通過大小寫區分成員/方法的公有/私有屬性,通過組合的方式實現繼承,都是簡潔哲學的體現。

2.3 抽象

抽象的反義詞是具體,在面向對象編程中,抽象的意思是將共同的屬性和方法抽象出來形成一個不可以被實例化的類型,在Java裏面,這是通過abstract和interface實現的,其中前者可以包含屬性,後者則是純粹的方法集合;C++通過在類內定義純虛函數使得該類成爲一個抽象類。
Golang的interface類型定義的也是一個抽象的基類,它是一組方法的集合,任何完整實現這些方法的類型都被稱爲該接口的實現。由於抽象與多態是相輔相成的,或者說抽象的目的就是爲了實現多態,我們將在下一節給出實例說明Golang的抽象和多態的實現。

2.4 多態

基類指針可以指向任意派生類的對象,並在運行時綁定最終調用的方法的過程被稱爲多態。多態是運行時特性,而繼承則是編譯時特徵,也就是說,繼承關係在編譯時就已經確定了,而多態則可以實現運行時的動態綁定。
小狗和小鳥都是動物,它們都會移動,也都會叫喚。我們把它們共同的方法提煉出來定義一個抽象的接口:

type Animal interface {
	Move()
	Shout()
}

雖然小狗和小鳥都會移動,但小狗是用四條腿爬行,小鳥是用翅膀飛行,雖然它們都會叫喚,但是叫喚的方式也不一樣:

type Dog struct {
}

func (dog Dog) Move() {
	fmt.Println("A dog moves with its legs.")
}

func (dog Dog) Shout() {
	fmt.Println("wang wang wang.")
}

type Bird struct {
}

func (bird Bird) Move() {
	fmt.Println("A bird flys with its wings.")
}

func (bird Bird) Shout() {
	fmt.Println("A bird shouts.")
}

類圖表示如下:

polymorphism

那麼,運行時的多態是怎麼實現的呢?

func main() {
	var animal oo.Animal

	animal = oo.Dog{}
	animal.Move()
	animal.Shout()

	animal = oo.Bird{}
	animal.Move()
	animal.Shout()
}

如前文所述,基類指針可以指向任意派生類的對象,並在運行時動態綁定最終使用的方法。這裏指針是廣義上的概念,在C++中是真實的指針,在Java和Golang裏面,則可以是一個接口類型的對象。在上面的代碼中,我們定義了一個Animal類型的對象,並分別指向Dog和Bird類型的具體對象,並調用Move和Shout方法,它們的運行效果如下:

$ go run test_polymorphism.go 
A dog moves with its legs.
wang wang wang.
A bird flys with its wings.
A bird shouts.

可以看到,通過定義抽象的接口,以及實現接口方法的具體類型的方式,Golang實現了運行時的動態綁定,這就是所謂的抽象與多態。

完整代碼:Golang的面向對象

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章