Go語言中的代碼重用 - 繼承還是組合?


故事要從我在一個項目中,想要假裝的專業一點而遇到的一個陷阱說起。

代碼重用

在這個項目中,我們已經有了類似如下的代碼:

package main
 
import (
    "fmt"
)
 
func main() {
    user := &User{name: "Chris"}
    user.sayHi()
}
 
type User struct {
    name string
}
 
func (u *User) sayHi() {
    u.sayName()
    u.sayType()
}
 
func (u *User) sayName() {
    fmt.Printf("I am %s.", u.name)
}
 
func (u *User) sayType() {
    fmt.Println("I am a user.")
}
I am Chris.I am a user.

然后我接到的新需求是這樣的,我需要開發一種新的用戶,它和當前這種用戶有一些相同的行為。當然,最主要的是也有很多不同的行為。作為一名老司機,我當然知道,這些不同的地方才是我需要重點關注並且實現的。
為了區分這兩種用戶,我們就叫他們普通用戶和文藝用戶吧。
因為我們已經有了普通用戶的實現代碼了,作為一個資深(誤)Java工程師,我想通過繼承這個普通用戶來實現代碼的復用。然而悲傷辣么大,我發現在Go語言中是不支持繼承的。

嵌入類型

好吧,只要思想不滑坡,辦法總比困難多。我發現在Go中有一種叫做Embedding的東西。在網上的一些文章中,他們說這就是Go中實現繼承的方式。可是看起來,這更像是Java中的組合,至少語法上像,是不?

package main
 
import (
    "fmt"
)
 
func main() {
    artisticUser := &ArtisticUser{User: &User{name: "Chris"}}
    artisticUser.sayName()
    artisticUser.sayType()
}
 
type User struct {
    name string
}
 
func (u *User) sayHi() {
    u.sayName()
    u.sayType()
}
 
func (u *User) sayName() {
    fmt.Printf("I am %s.", u.name)
}
 
func (u *User) sayType() {
    fmt.Println("I am a user.")
}
 
type ArtisticUser struct {
    *User
}
 
func (u *ArtisticUser) sayType() {
    fmt.Println("I am an artistic user.")
}
I am Chris.I am an artistic user.

干得漂亮!這樣我就可以復用User的sayName方法,只要把sayType方法用我自己的邏輯實現就好了。這正是我想要的。

繼承?組合?

但是,少俠請留步!我們試一下sayHi方法看看會發生什么?

package main
 
import (
    "fmt"
)
 
func main() {
    artisticUser := &ArtisticUser{User: &User{name: "Chris"}}
    artisticUser.sayHi()
}
 
type User struct {
    name string
}
 
func (u *User) sayHi() {
    u.sayName()
    u.sayType()
}
 
func (u *User) sayName() {
    fmt.Printf("I am %s.", u.name)
}
 
func (u *User) sayType() {
    fmt.Println("I am a user.")
}
 
type ArtisticUser struct {
    *User
}
 
func (a *ArtisticUser) sayType() {
    fmt.Println("I am an artistic user.")
}
I am Chris.I am a user.

這不科學!在Java里,子類總是會調用自己的方法的(已經override了父類的方法)。除非子類沒有覆蓋父類的方法,才會使用從父類繼承來的方法。
在這個例子中,我override了(其實Go中沒有這個概念)sayType方法,但是當我們在sayHi中調用它時,卻沒有調用這個override方法,而是用了父類的原始方法。

實際上,類型嵌入不是繼承。它只是某種形式上的語法糖而已。在面向對象編程中,子類應該是可以被當做父類來使用的。在里氏替換原則中,子類應該能在任何需要的地方替換掉父類。(注意一點,我們這里一開始嘗試覆蓋父類的非抽象方法已經違背了里氏替換原則)。
但是在上邊的例子中,ArtisticUser和User是兩種不同的類型。且不能替換使用。

package main
 
import (
    "fmt"
)
 
func main() {
    user := &User{name: "Chris"}
    artisticUser := &ArtisticUser{User: user}
    fmt.Printf("user's type is: %T\n", user)
    fmt.Printf("artisticUser's type is: %T\n", artisticUser)
    acceptUser(user)
    //acceptUser(artisticUser)
}
 
type User struct {
    name string
}
 
func (u *User) sayHi() {
    u.sayName()
    u.sayType()
}
 
func (u *User) sayName() {
    fmt.Printf("I am %s.", u.name)
}
 
func (u *User) sayType() {
    fmt.Println("I am a user.")
}
 
type ArtisticUser struct {
    *User
}
 
func (a *ArtisticUser) sayType() {
    fmt.Println("I am an artistic user.")
}
 
func acceptUser(u *User) {
 
}
user's type is: *main.User
artisticUser's type is: *main.ArtisticUser

如果你嘗試去掉注釋掉的那一行,你會得到一個build錯誤:

cannot use artisticUser (type *ArtisticUser) as type *User in argument to acceptUser

要我說,嵌入類型既不是繼承,也不是組合,只是跟它們都有點像。

多態性

那么回到我的問題。事實上我一開始就不該嘗試繼承。即使Go提供了繼承機制,覆蓋一個父類的非抽象方法也將破壞里氏替換原則。我一開始想要試試繼承其實是一種偷懶的行為,因為我並不想重構已有的那么一大坨代碼。

但是我們不應該害怕重構。你看,就算我想試着逃避重構,還是掉進別的溝里了。

如果能重來,我要選李白。。。呸,如果能讓我重構已有代碼的話,也許我可以試試接口。在Go語言中,接口非常靈活,是實現多態的手段。

package main
 
import (
    "fmt"
)
 
func main() {
    user := &User{name: "Chris"}
    user.ISubUser = &NormalUser{}
    user.sayHi()
    user.ISubUser = &ArtisticUser{}
    user.sayHi()
}
 
type ISubUser interface {
    sayType()
}
 
type User struct {
    name string
    ISubUser
}
 
func (u *User) sayHi() {
    u.sayName()
    u.sayType()
}
 
func (u *User) sayName() {
    fmt.Printf("I am %s.", u.name)
}
 
type NormalUser struct {
 
}
 
func (n *NormalUser) sayType() {
    fmt.Println("I am a normal user.")
}
 
type ArtisticUser struct {
 
}
 
func (a *ArtisticUser) sayType() {
    fmt.Println("I am an artistic user.")
}
I am Chris.I am a normal user.
I am Chris.I am a artistic user.

這樣我就重用了sayName和sayHi方法,並且把sayType方法留給多態來實現。

完美。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM