Golang 物件導向深入理解

2023-10-31 18:02:02

1 封裝

Java 中封裝是基於類(Class),Golang 中封裝是基於結構體(struct)

Golang 的開發中經常直接將成員變數設定為大寫使用,當然這樣使用並不符合物件導向封裝的思想。

Golang 沒有建構函式,但有一些約定俗成的方式:

  1. 提供 NewStruct(s Struct) *Struct 這樣的函數
  2. 提供 (s *Struct) New() 這樣的方法
  3. 也可以直接用傳統的 new(struct) 或 Struct{} 來初始化,隨後用 Set 方法對成員變數賦值
type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

func main() {
	peo := new(People)
	peo.SetName("張三")
	peo.SetAge(13)
	fmt.Println(peo.GetName(), peo.GetAge())
    // 張三 13
}

2 繼承 or 組合

Golang 不支援繼承,支援組合,算是「組合優於繼承」思想的體現。

雖然不支援繼承,但 Golang 的匿名組合組合可以實現物件導向繼承的特性。

2.1 非匿名組合和匿名組合

組合分為非匿名組合匿名組合

非匿名組合不能直接使用內嵌結構體的方法,需要通過內嵌結構體的變數名,間接呼叫內嵌結構體的方法。

匿名組合可以直接使用內嵌結構體的方法,如果有多個內嵌結構體,可以直接使用所有內嵌結構體的方法。

非匿名組合範例:

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	people People
	grade  string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func main() {
	stu := Student{}
	stu.people.SetName("張三")
	stu.people.SetAge(13)
	stu.SetGrade("七年級")
	fmt.Println(stu.people.GetName(), stu.people.GetAge(), stu.GetGrade())
	// 張三 13 七年級
}

非匿名組合主要體現在 Student 結構體中對 People 的組合需要明確命名。

匿名組合範例:

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	People
	grade  string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) GetName() string {
	return fmt.Sprintf("student-%s",s.People.GetName())
}

func main() {
	stu := Student{}
	stu.SetName("張三")
	stu.SetAge(13)
	stu.SetGrade("七年級")
	fmt.Println(stu.GetName(), stu.GetAge(), stu.GetGrade())
	// student-張三 13 七年級
}

從上面範例可以看出,匿名組合中:

  1. Student 中的 People 沒有顯式命名
  2. Student 可以直接使用 People 的方法
  3. 注意 StudentGetName 方法,與 People 中的 GetName 方法重複,可以看做是面相物件程式設計中的重寫(Overriding)
  4. StudentGetName 方法中使用 s.People.GetName() 呼叫People 中的 GetName 方法,這是對匿名組合的顯式呼叫,類似 Java 中的 super 用法

可以看出,匿名組合的使用感官上類似物件導向程式設計的繼承,可以說是一種『偽繼承』的實現,但匿名組合並不是繼承!

2.2 組合的使用方式

2.2.1 結構體中內嵌結構體

上面範例所用的就是結構體中內嵌結構體,不再多說。

2.2.2 結構體中內嵌介面

內嵌介面如下面例子:

type Member interface {
	SayHello() // 問候語
	DoWork()   // 開始工作
	SitDown()  // 坐下
}

type Normal struct{}

func (n *Normal) SayHello() {
	fmt.Println("normal:", "大家好!")
}

func (n *Normal) DoWork() {
	fmt.Println("normal:", "記筆記")
}

func (n *Normal) SitDown() {
	fmt.Println("normal:", "坐下")
}

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	Member
	People
	grade string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) SayHello() {
	fmt.Println("student:", s.name, "說: 老師好!")
}

type Teacher struct {
	Member
	People
	subject string
}

func (t *Teacher) SetSubject(subject string) {
	t.subject = subject
}

func (t *Teacher) GetSubject() string {
	return t.subject
}

func (t *Teacher) SayHello() {
	fmt.Println("teacher", t.name, "說: 同學們好!")
}

func (t *Teacher) DoWork() {
	fmt.Println("teacher", t.name, "講課!")
}

func main() {
	stu := &Student{Member: &Normal{}}
	stu.SetName("張三")
	stu.SetAge(13)
	stu.SetGrade("七年級")

	tea := &Teacher{Member: &Normal{}}
	tea.SetName("李四")
	tea.SetAge(31)
	tea.SetSubject("語文")

	var member Member
	member = stu
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// student: 張三 說: 老師好!
	// normal: 坐下
	// normal: 記筆記

	member = tea
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// teacher 李四 說: 同學們好!
	// normal: 坐下
	// teacher 李四 講課!
}

從上面例子可以看出:

  1. TeacherStudent 結構體都沒有完全實現 Member 的方法
  2. Normal 結構體實現了 Member 方法
  3. TeacherStudent 初始化時注入了 Normal 結構
  4. TeacherStudent 可以作為 Member 型別的介面使用,並預設使用 Normal 的實現。除非 TeacherStudent 有自己的實現。

TeacherStudent 並非沒有實現 Member 介面。編譯器自動為型別 *Teacher*Student 實現了 Member 中定義的方法,類似:

func (t *Teacher) SitDown() {  
    t.Member.SitDown()  
}

所以即使在初始化時沒有指定 NormalTeacherStudent 也可以賦值給 Member 型別的變數。但呼叫未實現的 Member 的方法時會報 panic

官方實踐

可以參考 sort.Reverse 方法,reverse 結構體內嵌了 Interface 介面,並只實現了 Less 方法

type IntSlice []int

func (x IntSlice) Len() int           { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
func (x IntSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }

type Interface interface {
    // 長度
	Len() int
    // 對比兩個數 i,j,返回結果作為排序的依據
	Less(i, j int) bool
    // 交換兩個數 i,j
	Swap(i, j int)
}

type reverse struct {
	Interface
}

func (r reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

func Reverse(data Interface) Interface {
	return &reverse{data}
}

使用時:

lst := []int{4, 5, 2, 8, 1, 9, 3}
sort.Sort(sort.Reverse(sort.IntSlice(lst)))
fmt.Println(lst)
// 列印:[9 8 5 4 3 2 1]

可以看出,Reverse 就是用內嵌介面的方式,接收一個 Interface 介面,將 Less 方法的兩個引數反轉了。

2.2.3 介面中內嵌介面

是針對方法的組合。如 Golang 的 ReadWriter 介面就是 Reader 和 Writer 介面的組合。

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
	Reader
	Writer
}

3 多型

多型的定義比較寬鬆:指一個行為具有多種不同的表現形式。

本質上多型分兩種:編譯時多型(靜態)和執行時多型(動態)

  1. 編譯時多型在編譯期間,多型就已經確定。過載是編譯時多型的一個例子。
  2. 執行時多型在編譯時不確定呼叫哪個具體方法,一直延遲到執行時才能確定。

通常情況下,我們討論的多型都是執行時多型。Golang 介面就是基於動態繫結實現的多型。

由於 Golang 結構體是『組合』而非『繼承』,不能相互轉換,所以只有基於介面的多型。

3.1 向上轉型

向上轉型是實現多型的必要條件。即用父類別的參照指向一個子類物件,通過父類別參照呼叫方法時會呼叫子類的方法。通過父類別參照無法呼叫子類的特有方法,需要『向下轉型』。

type Member interface {
	SayHello() // 問候語
	DoWork()   // 開始工作
	SitDown()  // 坐下
}

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	People
	grade string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) SayHello() {
	fmt.Println("student:", s.name, "說: 老師好!")
}

func (s *Student) DoWork() {
	fmt.Println("student:", s.name, "記筆記")
}

func (s *Student) SitDown() {
	fmt.Println("student:", s.name, "坐下")
}

type Teacher struct {
	People
	subject string
}

func (t *Teacher) SetSubject(subject string) {
	t.subject = subject
}

func (t *Teacher) GetSubject() string {
	return t.subject
}

func (t *Teacher) SayHello() {
	fmt.Println("teacher", t.name, "說: 同學們好!")
}

func (t *Teacher) DoWork() {
	fmt.Println("teacher", t.name, "講課!")
}

func (t *Teacher) SitDown() {
	fmt.Println("teacher:", t.name, "站著講課,不能坐下!")
}

func main() {
	stu := &Student{}
	stu.SetName("張三")
	stu.SetAge(13)
	stu.SetGrade("七年級")

	tea := &Teacher{}
	tea.SetName("李四")
	tea.SetAge(31)
	tea.SetSubject("語文")

	var member Member
	member = stu
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// student: 張三 說: 老師好!
	// student: 張三 坐下
	// student: 張三 記筆記

	member = tea
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// teacher 李四 說: 同學們好!
	// teacher: 李四 站著講課,不能坐下!
	// teacher 李四 講課!
}

這裡是基於介面實現的『向下轉型』,沒有父類別、子類之分。而在 Java 中,當子類沒有重寫父類別方法時,父類別的參照會呼叫到父類別的方法裡。其實也有類似的實現,在上面已經有了,即2.2.2 結構體中內嵌介面

3.2 向下轉型

上面提到,用父類別的參照指向一個子類物件,通過父類別參照無法呼叫子類的特有方法。但在某些情況下需要呼叫子類的特有方法,例如子類有一些特殊邏輯需要處理,這時就需要『向下轉型』還原出子類的參照。

在 Java 裡,通常用 User user = (User) people; 來向下轉型。

Golang 向下轉型通過型別斷言。基本用法是t,ok := intefaceValue.(T)

一個例子:

type Member interface {
	SayHello() // 問候語
}

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	People
	grade string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) SayHello() {
	fmt.Println("student:", s.name, "說: 老師好!")
}

func main() {
	stu := &Student{}
	stu.SetName("張三")
	stu.SetAge(13)
	stu.SetGrade("七年級")

	var member Member = stu
	member.SayHello()
	// student: 張三 說: 老師好!
	if student, ok := member.(*Student); ok {
		student.SetName("王五")
		student.SayHello()
		// student: 王五 說: 老師好!
	}
}
  1. 使用 Member 型別的參照,指向一個 Student 物件
  2. 想對 Student 物件設定一個新名稱,使用型別斷言向下轉型,可以呼叫 StudentSetName 方法(嚴格的說,是 PeopleSetName 方法)

除了上面場景,還有一種場景需要型別斷言:當有一個函數,其引數是 interface{} 型別時:

func SayHello(inter interface{}) {
	if member, ok := inter.(Member); ok {
		member.SayHello()
	}
}

func main() {
	stu := &Student{}
	stu.SetName("張三")
	stu.SetAge(13)
	stu.SetGrade("七年級")

	SayHello(stu)
	// student: 張三 說: 老師好!
}

補充:除了型別斷言,Golang 還有一種 type-switch 的方式可以用於對 interface 的型別探測,從而進行不同邏輯的處理。

4 參考資料

  1. Golang 中 struct 嵌入 interface
  2. 物件導向思考與 golang cobra 庫實現原理
  3. 多型中的向上轉型與向下轉型