兩分鐘讓你明白Go中如何繼承

  • 2019 年 10 月 25 日
  • 筆記

最近在重構代碼的時候,抽象了大量的接口。也使用這些抽象的接口做了很多偽繼承的操作,極大的減少了代碼冗餘,同時也增加了代碼的可讀性。

然後隨便搜了一下關於Go繼承的文章,發現有的文章的代碼量過多,並且代碼format極其粗糙,命名極其隨意,類似於A、B這種,讓人看着看着就忘了到底是誰繼承誰,我又要回去看一遍邏輯。

雖然只是樣例代碼,我認為仍然需要做到簡潔、清晰以及明了。這也是我為什麼要寫這篇博客的原因。接下里在這裡簡單分享一下在Go中如何實現繼承。

1. 簡單的組合

說到繼承我們都知道,在Go中沒有extends關鍵字,也就意味着Go並沒有原生級別的繼承支持。這也是為什麼我在文章開頭用了偽繼承這個詞。本質上,Go使用interface實現的功能叫組合,Go是使用組合來實現的繼承,說的更精確一點,是使用組合來代替的繼承,舉個很簡單的例子。

1.1 實現父類

我們用很容易理解的動物來舉例子,廢話不多說,直接看代碼。

type Animal struct {      Name string  }    func (a *Animal) Eat() {      fmt.Printf("%v is eating", a.Name)      fmt.Println()  }    type Cat struct {      *Animal  }    cat := &Cat{      Animal: &Animal{          Name: "cat",      },  }  cat.Eat() // cat is eating

1.2 代碼分析

首先,我們實現了一個Animal的結構體,代表動物類。並聲明了Name字段,用於描述動物的名字。

然後,實現了一個以Animal為receiver的Eat方法,來描述動物進食的行為。

最後,聲明了一個Cat結構體,組合了Cat字段。再實例化一個貓,調用Eat方法,可以看到會正常的輸出。

可以看到,Cat結構體本身沒有Name字段,也沒有去實現Eat方法。唯一有的就是組合了Animal父類,至此,我們就證明了已經通過組合實現了繼承。

2. 優雅的組合

熟悉Go的人看到上面的代碼可能會發出如下感嘆

這也太粗糙了吧 — By 魯迅:我沒說過這句話

的確,上面的僅僅是為了給還沒有了解過Go組合的人看的。作為一個簡單的例子來理解Go的組合繼承,這是完全沒有問題的 。但如果要運用在真正的開發中,那還是遠遠不夠的。

舉個例子,我如果是這個抽象類的使用者,我拿到animal類不能一目了然的知道這個類幹了什麼,有哪些方法可以調用。以及,沒有統一的初始化方式,這意味着凡是涉及到初始化的地方都會有重複代碼。如果後期有初始化相關的修改,那麼只有一個一個挨着改。所以接下來,我們對上述的代碼做一些優化。

2.1 抽象接口

接口用於描述某個類的行為。例如,我們即將要抽象的動物接口就會描述作為一個動物,具有哪些行為。常識告訴我們,動物可以進食(Eat),可以發出聲音(bark),可以移動(move)等等。這裡有一個很有意思的類比。

接口就像是一個招牌,比如一家星巴克。星巴克就是一個招牌(接口)。

你看到這個招牌會想到什麼?美式?星冰樂?抹茶拿鐵?又或者是拿鐵,甚至是店內的裝修風格。

這就是一個好的接口應該達到的效果,同樣這也是為什麼我們需要抽象接口。

// 模擬動物行為的接口  type IAnimal interface {      Eat() // 描述吃的行為  }    // 動物 所有動物的父類  type Animal struct {      Name string  }    // 動物去實現IAnimal中描述的吃的接口  func (a *Animal) Eat() {      fmt.Printf("%v is eatingn", a.Name)  }    // 動物的構造函數  func newAnimal(name string) *Animal {      return &Animal{          Name: name,      }  }    // 貓的結構體 組合了animal  type Cat struct {      *Animal  }    // 實現貓的構造函數 初始化animal結構體  func newCat(name string) *Cat {      return &Cat{          Animal: newAnimal(name),      }  }    cat := newCat("cat")  cat.Eat() // cat is eating

在Go中其實沒有關於構造函數的定義。例如我們在Java中可以使用構造函數來初始化變量,舉個很簡單的例子,Integer num = new Integer(1)。而在Go中就需要使用者自己通過結構體的初始化來模擬構造函數的實現。

然後在這裡我們實現子類Cat,使用組合的方式代替繼承,來調用Animal中的方法。運行之後我們可以看到,Cat結構體中並沒有Name字段,也沒有實現Eat方法,但是仍然可以正常運行。這證明我們已經通過組合的方式了實現了繼承。

2.2 重載方法

// 貓結構體IAnimal的Eat方法  func (cat *Cat) Eat() {      fmt.Printf("children %v is eatingn", cat.Name)  }    cat.Eat()  // children cat is eating

可以看到,Cat結構體已經重載了Animal中的Eat方法,這樣就實現了重載。

2.3 參數多態

什麼意思呢?舉個例子,我們要如何在Java中解決函數的參數多態問題?熟悉Java的可能會想到一種解決方案,那就是通配符。用一句話概括,使用了通配符可以使該函數接收某個類的所有父類型或者某個類的所有子類型。但是我個人認為對於不熟悉Java的人來說,可讀性不是特別友好。

而在Go中,就十分方便了。

func check(animal IAnimal) {      animal.Eat()  }

在這個函數中就可以處理所有組合了Animal的單位類型,對應到Java中就是上界通配符,即一個可以處理任何特定類型以及是該特定類型的派生類的通配符,再換句人話,啥動物都能處理。

3. 總結

凡事都有兩面性,做優化也不例外。大量的抽象接口的確可以精簡代碼,讓代碼看起來十分優雅、舒服。但是同樣,這會給其他不熟悉的人review代碼造成理解成本。想像你看某段代碼,全是接口,點了好幾層才能看到實現。更有的,往下找着找着突然就在另一個接口處斷掉了,必須要手動的去另一個註冊的地方去找。

這就是我認為優化的時候要面臨的幾個問題:

  • 優雅
  • 可讀
  • 性能

有的時候我們很難做到三個方面都兼顧,例如這樣寫代碼看起來很難受,但是性能要比優雅的代碼好。再例如,這樣寫看起來很優雅,但是可讀性很差等等。

還是引用我之前博客中經常寫的一句話

適合自己的才是最好的

這種時候只能根據自己項目的特定情況,選擇最適合你的解決方案。沒有萬能的解決方案。

分享一句最近彈吉他看到的毒雞湯,學習也是一樣的。

練琴的路上沒有捷徑,全是彎路

往期文章:

相關:

  • 微信公眾號: SH的全棧筆記(或直接在添加公眾號界面搜索微信號LunhaoHu)