探討 Golang 實作類似繼承的不同做法

Golang 本身不支援繼承,坊間很多是透過 embedded struct 實作繼承的部分效果但不全然符合,本篇用不同的實作方式去更貼近繼承的效果

Golang 本身不支援繼承,原因在官方的 QnA 有回答,與其專注在 type 本身的關係,還不如注重在 interface 能否滿足特定的行為

Types can satisfy many interfaces at once, without the complexities of traditional multiple inheritance.

而坊間大多的 Golang 繼承實作都是透過 embedded struct 透過 composition 模擬 inheritance,如這篇 秒懂 go 语言的继承 或這一篇 Inheritance in GoLang

 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
33
34
35
36
37
38
39
40
41
// 动物类
class Animal {
    public   String name;
    public String subject;

    void eat(String food) {
        System.out.println(name + "喜欢吃:" + food + ",它属于:" + subject);
    }
}

// 猫类。 猫类继承动物类
class Cat extends Animal{
    // 猫自己的属性和方法
    public int age;
    void sleep() {
        System.out.println(name + "今年" + age + "岁了,特别喜欢睡觉");
    }
}

public class Test{
    public static void main(String[] args){
        // 创建一个动物实例
        Animal a = new Animal();
        a.name = "动物";
        a.subject = "动物科";
        a.eat("肉");

        // 创建一个猫实例
        Cat cat = new Cat();
        cat.name = "咪咪";
        cat.subject = "猫科";
        cat.age = 1;
        cat.eat("鱼");
        cat.sleep();
    }
}

————————————————
原文作者pureyb
转自链接https://learnku.com/articles/32295
版权声明著作权归作者所有商业转载请联系作者获得授权非商业转载请保留以上作者信息和原文链接

實作上 subclass (cat) 將 superclass (animal) embedded,當 subclass 呼叫沒有定義的 method 時,會去調用 superclass 的 method,例如上面案例中 cat.eat 是調用 animal.eat

但這樣就真的滿足繼承的條件了嗎?

為什麼我們會需要繼承

回歸原點,我們會在什麼時候偏好繼承大過於組合這樣的實作方式 ?
就我自己的習慣是

  1. 型別之間有很明確 is-a 的關聯
  2. 實作上大多的行為相同,只有部分的行為不同,會需要套用 樣板方法 Template Method

例如汽車、機車都是交通工具,他們都可以提供從 A 移動到 B 的服務,但他們騎乘的方式不同

另外在實作繼承時,別忘了 SOLID 原則中的 Liskov 替換原則,行為上 subclass 應該要能夠替換 superclass 所出現的地方

更具體描述繼承在實作上的需求

  1. Liskov 替換原則:subclass 替換 suplerclass 不應該有型別上的錯誤
  2. 樣板方法:superclass 定義呼叫流程,而 subclass 可以複寫部分方法
  3. method propagate:當呼叫 subclass 不存在的方法,會往 superclass 去查找

我們來檢視上面的作法以上需求

假設 Animal 有一個 method 是 wakeUp,wakeUp 固定會 yell & eat,但不同的動物有不同的 yell 方式與 eat 內容

go playground

 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
33
34
35
36
37
38
39
40
41
42
43
44
// You can edit this code!
// Click here and start typing.
package main

import "fmt"

type Animal struct {
	Name string
}

func (a *Animal) Wakeup() {
	a.yell()
	a.eat()
}
func (a *Animal) yell() {
	fmt.Println("default yell")
}
func (a *Animal) eat() {
	fmt.Println("default eat")
}

type Cat struct {
	Animal
}

func (c *Cat) yell() {
	fmt.Println("meow meow")
}
func (c *Cat) eat() {
	fmt.Println("cat eat meat")
}

func main() {
	cat := &Cat{
		Animal{
			Name: "BigCat",
		},
	}
	cat.Wakeup()
}

// output 
// default yell
// default eat

咦?! 輸出竟然不是呼叫 cat.yell() 和 cat.eat(),反而是呼叫到 animal.yell() / animal.eat(),對比 Ruby 的執行結果

 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
class Animal
  def wakeup
    yell
    eat
  end
  
  def yell
    puts "default yell"
  end
  
  def eat 
    puts "default eat"
  end
end

class Cat < Animal
  def yell
    puts "meow meow"
  end
  def eat
    puts "cat eat meat"
  end
end

cat = Cat.new
cat.wakeup

# meow meow
# cat eat meat

究竟是哪裡出錯了 ?!

注意 Golang embedded struct 執行的角色

更具體的描述可以參考此篇 Type embedding: Golang’s fake inheritance,當我們利用 method propagate 找到 superclass 定義的方法時,要注意 此時執行的角色是 superclass 而不是 subclass!

例如上面的案例,wakeup 是定義在 Animal 當中,Cat 呼叫 wakeup 最後是用 Animal 去執行,而當 Animal 執行 wakeup 時就是呼叫 Animal.yell / Animal.eat

透過 Interface 與重新調整 Embedded 方向

先上結論,將 subclass 要個別實作的方法抽成 interface,並改將 subclass embedded 到 superclass 中,參考 go playground

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// You can edit this code!
// Click here and start typing.
package main

import "fmt"

type ISubClass interface {
	Temp1()
	Temp2()
}

type SuperClass struct {
	sub ISubClass
}

func (s *SuperClass) Method() {
	s.sub.Temp1()
	s.sub.Temp2()
}

type SubClass1 struct{}

func (s *SubClass1) Temp1() {
	s.temp3()
	fmt.Println("temp1 from SubClass1")
}
func (s *SubClass1) Temp2() {
	fmt.Println("temp2 from SubClass1")
}

type SubClass2 struct{}

func (s *SubClass2) Temp1() {
	fmt.Println("temp1 from SubClass2")
}
func (s *SubClass2) Temp2() {
	fmt.Println("temp2 from SubClass2")
}

func main() {
	cls1 := &SuperClass{
		sub: &SubClass1{},
	}

	cls2 := &SuperClass{
		sub: &SubClass2{},
	}

	cls1.Method()
	cls2.Method()
}

這樣做就滿足了

  1. Liskov 替換原則:不同的 subclass 都依附在 SuperClass,所以直接替換沒問題
  2. Template method:Superclass 透過 interface 呼叫 subclass method
  3. method propagate:這個比較 tricky,因為是直接呼叫 Superclass,所以也沒有 method propagate 的問題

以上滿足我們前面一開始對於繼承的定義,但有幾個侷限

  1. 因為我們反轉 embedded struct 的位置,所以 subclass 不能呼叫 superclass 任何的方法或參數,只能從 superclass 呼叫 subclass,這點我覺得比較還好,如果呼叫方向交錯反而很亂
  2. subclass 的型別不明確,因為都掛在 superclass 下,如果要判斷型別需要額外處理
  3. subclass 不能有客製化的 Public Method,會被 interface 侷限,例如我只有 SubClass1 想要有 Temp3 的 public method,變成 interface 也要增加否則無法呼叫
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type ISubClass interface {
	....
	Temp3()
}
func (s *SubClass1) Temp3() {
	fmt.Println("temp3 from SubClass1")
}

func (s *SubClass2) Temp3() {
	// SubClass2 被迫也要增加,反則無法滿足 interface
}

如果 subclass 想要呼叫 superclass 方法

侷限第一點提到因為 embedded 目前是掛在 superclass 下,反過來就是 subclass 無法呼叫 superclass,如果希望 subclass 呼叫 superclass 的方法,就需要像 Template Method 由 superclass 主動呼叫,例如

 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
type SuperClass struct {
	SubClass
	Name string
}

type SubClass struct {}
func (s *SubClass) ShowName() {
  // 想要取得 SuperClass Name 屬性
  // 無法這樣存取!!
  "prefix" + s.Name
}
////// 調整做法 /////////
type SuperClass struct {
	SubClass
	Name string
}

// 改從 superclass 處理,把 name pass 給 subclass
func (s *SuperClass) ShowName() {
	pfxName := s.ParseName(s.Name)
}

type SubClass struct {}
func (s *SubClass) ParseName(name string) {
  // 想要取得 SuperClass Name 屬性
  // 無法這樣存取!!
  name
}

結語

以上提供不同的思路,透過改變 embedded struct 的不同層級,會有不同的效果

  1. subclass embedded superclass:subclass 可以任意定義 public method,但無法實作 Template method
  2. superclass embedded subclass:可以實作 Template method,但 subclass 無法有額外的 public method,除非擴充 interface

再仔細想想方法二,在其他 OOP language 中,套用 Template method 時 subclass 定義的 method 可以直接存取 superclass 定義的參數,但是在 Golang 中是無法做到的

所以在 Golang 中要仿造 Template method 反而會比較像 Strategy pattern,superclass 需要抽換某些行為 (策略),那就透過不同的策略設計並在初始化時帶入

整體實作還是用 組合來代替繼承,在使用 Golang 上建議還是不要用繼承的概念去思考會讓實作比較順

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus