如何使用Go語言寫出物件導向風格的程式碼

asong發表於2021-11-14

原文連結:如何使用Go語言寫出物件導向風格的程式碼

前言

哈嘍,大家好,我是asong。在上一篇文章:小白也能看懂的context包詳解:從入門到精通 分析context的原始碼時,我們看到了一種程式設計方法,在結構體裡內嵌匿名介面,這種寫法對於大多數初學Go語言的朋友看起來是懵逼的,其實在結構體裡內嵌匿名介面、匿名結構體都是在物件導向程式設計中繼承和重寫的一種實現方式,之前寫過javapython對物件導向程式設計中的繼承和重寫應該很熟悉,但是轉Go語言後寫出的程式碼都是程式導向式的程式碼,所以本文就一起來分析一下如何在Go語言中寫出物件導向的程式碼。

物件導向程式設計是一種計算機程式設計架構,英文全稱:Object Oriented Programming,簡稱OOP。OOP的一條基本原則是計算機程式由單個能夠起到子程式作用的單元或物件組合而成,OOP達到了軟體工程的三個主要目標:重用性、靈活性和擴充套件性。OOP=物件+類+繼承+多型+訊息,其中核心概念就是類和物件。

這一段話在網上介紹什麼是物件導向程式設計時經常出現,大多數學習Go語言的朋友應該也都是從C++pythonjava轉過來的,所以對物件導向程式設計的理解應該很深了,所以本文就沒必要介紹概念了,重點來看一下如何使用Go語言來實現物件導向程式設計的程式設計風格。

Go語言本身就不是一個物件導向的程式語言,所以Go語言中沒有類的概念,但是他是支援型別的,因此我們可以使用struct型別來提供類似於java中的類的服務,可以定義屬性、方法、還能定義構造器。來看個例子:

type Hero struct {
    Name string
    Age uint64
}

func NewHero() *Hero {
    return &Hero{
        Name: "蓋倫",
        Age: 18,
    }
}

func (h *Hero) GetName() string {
    return h.Name
}

func (h *Hero) GetAge() uint64 {
    return h.Age
}


func main()  {
    h := NewHero()
    print(h.GetName())
    print(h.GetAge())
}

這就一個簡單的 "類"的使用,這個類名就是Hero,其中NameAge就是我們定義的屬性,GetNameGetAge這兩個就是我們定義的類的方法,NewHero就是定義的構造器。因為Go語言的特性問題,構造器只能夠依靠我們手動來實現。

這裡方法的實現是依賴於結構體的值接收者、指標接收者的特性來實現的。

封裝

封裝是把一個物件的屬性私有化,同時提供一些可以被外界訪問的屬性和方法,如果不想被外界訪問,我們大可不必提供方法給外界訪問。在Go語言中實現封裝我們可以採用兩種方式:

  • Go語言支援包級別的封裝,小寫字母開頭的名稱只能在該包內程式中可見,所以我們如果不想暴露一些方法,可以通過這種方式私有包中的內容,這個理解比較簡單,就不舉例子了。
  • Go語言可以通過 type 關鍵字建立新的型別,所以我們為了不暴露一些屬性和方法,可以採用建立一個新型別的方式,自己手寫構造器的方式實現封裝,舉個例子:
type IdCard string

func NewIdCard(card string) IdCard {
    return IdCard(card)
}

func (i IdCard) GetPlaceOfBirth() string {
    return string(i[:6])
}

func (i IdCard) GetBirthDay() string {
    return string(i[6:14])
}

宣告一個新型別IdCard,本質是一個string型別,NewIdCard用來構造物件,

GetPlaceOfBirthGetBirthDay就是封裝的方法。

繼承

Go並沒有原生級別的繼承支援,不過我們可以使用組合的方式來實現繼承,通過結構體內嵌型別的方式實現繼承,典型的應用是內嵌匿名結構體型別和內嵌匿名介面型別,這兩種方式還有點細微差別:

  • 內嵌匿名結構體型別:將父結構體嵌入到子結構體中,子結構體擁有父結構體的屬性和方法,但是這種方式不能支援引數多型。
  • 內嵌匿名介面型別:將介面型別嵌入到結構體中,該結構體預設實現了該介面的所有方法,該結構體也可以對這些方法進行重寫,這種方式可以支援引數多型,這裡要注意一個點是如果嵌入型別沒有實現所有介面方法,會引起編譯時未被發現的執行錯誤。

內嵌匿名結構體型別實現繼承的例子

type Base struct {
    Value string
}

func (b *Base) GetMsg() string {
    return b.Value
}


type Person struct {
    Base
    Name string
    Age uint64
}

func (p *Person) GetName() string {
    return p.Name
}

func (p *Person) GetAge() uint64 {
    return p.Age
}

func check(b *Base)  {
    b.GetMsg()
}

func main()  {
    m := Base{Value: "I Love You"}
    p := &Person{
        Base: m,
        Name: "asong",
        Age: 18,
    }
    fmt.Print(p.GetName(), "  ", p.GetAge(), " and say ",p.GetMsg())
    //check(p)
}

上面註釋掉的方法就證明了不能進行引數多型。

內嵌匿名介面型別實現繼承的例子

直接拿一個業務場景舉例子,假設現在我們現在要給使用者發一個通知,webapp端傳送的通知內容都是一樣的,但是點選後的動作是不一樣的,所以我們可以進行抽象一個介面OrderChangeNotificationHandler來宣告出三個公共方法:GenerateMessageGeneratePhotosgenerateUrl,所有類都會實現這三個方法,因為webapp端傳送的內容是一樣的,所以我們可以抽相出一個父類OrderChangeNotificationHandlerImpl來實現一個預設的方法,然後在寫兩個子類WebOrderChangeNotificationHandlerAppOrderChangeNotificationHandler去繼承父類重寫generateUrl方法即可,後面如果不同端的內容有做修改,直接重寫父類方法就可以了,來看例子:

type Photos struct {
    width uint64
    height uint64
    value string
}

type OrderChangeNotificationHandler interface {
    GenerateMessage() string
    GeneratePhotos() Photos
    generateUrl() string
}


type OrderChangeNotificationHandlerImpl struct {
    url string
}

func NewOrderChangeNotificationHandlerImpl() OrderChangeNotificationHandler {
    return OrderChangeNotificationHandlerImpl{
        url: "https://base.test.com",
    }
}

func (o OrderChangeNotificationHandlerImpl) GenerateMessage() string {
    return "OrderChangeNotificationHandlerImpl GenerateMessage"
}

func (o OrderChangeNotificationHandlerImpl) GeneratePhotos() Photos {
    return Photos{
        width: 1,
        height: 1,
        value: "https://www.baidu.com",
    }
}

func (w OrderChangeNotificationHandlerImpl) generateUrl() string {
    return w.url
}

type WebOrderChangeNotificationHandler struct {
    OrderChangeNotificationHandler
    url string
}

func (w WebOrderChangeNotificationHandler) generateUrl() string {
    return w.url
}

type AppOrderChangeNotificationHandler struct {
    OrderChangeNotificationHandler
    url string
}

func (a AppOrderChangeNotificationHandler) generateUrl() string {
    return a.url
}

func check(handler OrderChangeNotificationHandler)  {
    fmt.Println(handler.GenerateMessage())
}

func main()  {
    base := NewOrderChangeNotificationHandlerImpl()
    web := WebOrderChangeNotificationHandler{
        OrderChangeNotificationHandler: base,
        url: "http://web.test.com",
    }
    fmt.Println(web.GenerateMessage())
    fmt.Println(web.generateUrl())

    check(web)
}

因為所有組合都實現了OrderChangeNotificationHandler型別,所以可以處理任何特定型別以及是該特定型別的派生類的萬用字元。

多型

多型是物件導向程式設計的本質,多型是支程式碼可以根據型別的具體實現採取不同行為的能力,在Go語言中任何使用者定義的型別都可以實現任何介面,所以通過不同實體型別對介面值方法的呼叫就是多型,舉個例子:

type SendEmail interface {
    send()
}

func Send(s SendEmail)  {
    s.send()
}

type user struct {
    name string
    email string
}

func (u *user) send()  {
    fmt.Println(u.name + " email is " + u.email + "already send")
}

type admin struct {
    name string
    email string
}

func (a *admin) send()  {
    fmt.Println(a.name + " email is " + a.email + "already send")
}

func main()  {
    u := &user{
        name: "asong",
        email: "你猜",
    }
    a := &admin{
        name: "asong1",
        email: "就不告訴你",
    }
    Send(u)
    Send(a)
}

總結

歸根結底物件導向程式設計就是一種程式設計思想,只不過有些語言在語法特性方面更好的為這種思想提供了支援,寫出物件導向的程式碼更容易,但是寫程式碼的還是我們自己,並不是我們用了java就一定會寫出更抽象的程式碼,在工作中我看到用java寫出程式導向式的程式碼不勝其數,所以無論用什麼語言,我們都應該思考如何寫好一份程式碼,大量的抽象介面幫助我們精簡程式碼,程式碼是優雅了,但也會面臨著可讀性的問題,什麼事都是有兩面性的,寫出好程式碼的路還很長,還需要不斷探索............。

文中示例程式碼已經上傳githubhttps://github.com/asong2020/...

歡迎關注公眾號:Golang夢工廠

相關文章