1. 程式人生 > >Go學習(12):面向物件

Go學習(12):面向物件

面向物件(OOP)

go並不是一個純面向物件的程式語言。在go中的面向物件,結構體替換了類。

Go並沒有提供類class,但是它提供了結構體struct,方法method,可以在結構體上新增。提供了捆綁資料和方法的行為,這些資料和方法與類類似。

1.1 定義結構體和方法

通過以下程式碼來更好的理解,首先在src目錄下建立一個package命名為oop,在oop目錄下,再建立一個子目錄命名為employee,在該目錄下建立一個go檔案命名為employee.go。

目錄結構:oop -> employee -> employee.go

在employee.go檔案中儲存以下程式碼:

package employee

import (  
    "fmt"
)

type Employee struct {  
    FirstName   string
    LastName    string
    TotalLeaves int
    LeavesTaken int
}

func (e Employee) LeavesRemaining() {  
    fmt.Printf("%s %s has %d leaves remaining", e.FirstName, e.LastName, (e.TotalLeaves - e.LeavesTaken)
) }

然後在oop目錄下,建立檔案並命名為main.go,並儲存以下內容

package main

import "oop/employee"

func main() {  
    e := employee.Employee {
        FirstName: "Sam",
        LastName: "Adolf",
        TotalLeaves: 30,
        LeavesTaken: 20,
    }
    e.LeavesRemaining()
}

執行結果:

Sam Adolf has 10 leaves remaining 

或者如下圖目錄:
在這裡插入圖片描述

1.2 New()函式替代了建構函式

我們上面寫的程式看起來不錯,但是裡面有一個微妙的問題。讓我們看看當我們用0值定義employee struct時會發生什麼。更改main的內容。轉到下面的程式碼,

package main

import "oop/employee"

func main() {  
    var e employee.Employee
    e.LeavesRemaining()
}

執行結果:

has 0 leaves remaining

通過執行結果可以知道,使用Employee的零值建立的變數是不可用的。它沒有有效的名、姓,也沒有有效的保留細節。在其他的OOP語言中,比如java,這個問題可以通過使用建構函式來解決。使用引數化建構函式可以建立一個有效的物件。

go不支援建構函式。如果某個型別的零值不可用,則程式設計師的任務是不匯出該型別以防止其他包的訪問,並提供一個名為NewT(parameters)的函式,該函式初始化型別T和所需的值。在go中,它是一個命名一個函式的約定,它建立了一個T型別的值給NewT(parameters)。這就像一個建構函式。如果包只定義了一個型別,那麼它的一個約定就是將這個函式命名為New(parameters)而不是NewT(parameters)。

更改employee.go的程式碼:

首先修改employee結構體為非匯出(可以理解為java意義上的不公開),並建立一個函式New(),它將建立一個新Employee。程式碼如下:

package employee

import (  
    "fmt"
)

type employee struct {  
    firstName   string
    lastName    string
    totalLeaves int
    leavesTaken int
}

func New(firstName string, lastName string, totalLeave int, leavesTaken int) employee {  
    e := employee {firstName, lastName, totalLeave, leavesTaken}
    return e
    //return employee {firstName, lastName, totalLeave, leavesTaken}
}

func (e employee) LeavesRemaining() {  
    fmt.Printf("%s %s has %d leaves remaining", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}

我們在這裡做了一些重要的改變。我們已經將Employee struct的起始字母e設定為小寫,即我們已經將型別Employee struct更改為type Employee struct。通過這樣做,我們成功地匯出了employee結構並阻止了其他包的訪問。將未匯出的結構的所有欄位都匯出為未匯出的方法是很好的做法,除非有特定的需要匯出它們。由於我們不需要在包之外的任何地方使用employee struct的欄位,所以我們也沒有匯出所有欄位。

由於employee是未匯出的,所以不可能從其他包中建立型別employee的值。因此,我們提供了一個輸出的新函式。將所需的引數作為輸入並返回新建立的employee。

這個程式還需要做一些修改,讓它能夠工作,但是讓我們執行這個程式來了解到目前為止變化的效果。如果這個程式執行,它將會失敗,有以下編譯錯誤,

go/src/constructor/main.go:6: undefined: employee.Employee  

這是因為我們有未匯出的Employee,因此編譯器丟擲錯誤,該型別在main中沒有定義。而這正是我們想要的。現在沒有其他的包能夠建立一個零值的員工。我們成功地防止了一個無法使用的員工被建立。現在建立員工的唯一方法是使用新函式employee.New(..)

修改main.go程式碼

package main  

import "oop/employee"

func main() {  
    e := employee.New("Sam", "Adolf", 30, 20)
    e.LeavesRemaining()
}

執行結果:

Sam Adolf has 10 leaves remaining 

因此,我們可以明白,雖然Go不支援類,但是結構體可以有效地使用,在使用建構函式的位置,使用New(parameters)的方法即可。

1.3組成(Composition )替代了繼承(Inheritance)

Go不支援繼承,但它支援組合。組合的一般定義是“放在一起”。構圖的一個例子就是汽車。汽車是由輪子、發動機和其他各種部件組成的。

部落格文章就是一個完美的組合例子。每個部落格都有標題、內容和作者資訊。這可以用組合完美地表示出來。

1.3.1 通過嵌入結構體實現組成

可以通過將一個struct型別嵌入到另一個結構中實現。

示例程式碼:

package main

import (  
    "fmt"
)

/*
我們建立了一個author struct,它包含欄位名、lastName和bio。我們還添加了一個方法fullName(),將作者作為接收者型別,這將返回作者的全名。
*/
type author struct {  
    firstName string
    lastName  string
    bio       string
}

func (a author) fullName() string {  
    return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
/*
post struct有欄位標題、內容。它還有一個嵌入式匿名欄位作者。這個欄位表示post struct是由author組成的。現在post struct可以訪問作者結構的所有欄位和方法。我們還在post struct中添加了details()方法,它打印出作者的標題、內容、全名和bio。
*/
type post struct {  
    title     string
    content   string
    author
}

func (p post) details() {  
    fmt.Println("Title: ", p.title)
    fmt.Println("Content: ", p.content)
    fmt.Println("Author: ", p.author.fullName())
    fmt.Println("Bio: ", p.author.bio)
}

func main() {  
    author1 := author{
        "Naveen",
        "Ramanathan",
        "Golang Enthusiast",
    }
    post1 := post{
        "Inheritance in Go",
        "Go supports composition instead of inheritance",
        author1,
    }
    post1.details()
}

執行結果:

Title:  Inheritance in Go  
Content:  Go supports composition instead of inheritance  
Author:  Naveen Ramanathan  
Bio:  Golang Enthusiast  

嵌入結構體的切片

在以上程式的main函式下增加以下程式碼,並執行

type website struct {  
        []post
}
func (w website) contents() {  
    fmt.Println("Contents of Website\n")
    for _, v := range w.posts {
        v.details()
        fmt.Println()
    }
}

執行報錯:

main.go:31:9: syntax error: unexpected [, expecting field name or embedded type 

這個錯誤指向structs []post的嵌入部分。原因是不可能匿名嵌入一片。需要一個欄位名。我們來修正這個錯誤,讓編譯器通過。

type website struct {  
        posts []post
}

現在讓我們修改的main函式,為我們的新的website建立幾個posts。修改完完整程式碼如下:

package main

import (  
    "fmt"
)

type author struct {  
    firstName string
    lastName  string
    bio       string
}

func (a author) fullName() string {  
    return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}

type post struct {  
    title   string
    content string
    author
}
func (p post) details() {  
    fmt.Println("Title: ", p.title)
    fmt.Println("Content: ", p.content)
    fmt.Println("Author: ", p.fullName())
    fmt.Println("Bio: ", p.bio)
}

type website struct {  
 posts []post
}
func (w website) contents() {  
    fmt.Println("Contents of Website\n")
    for _, v := range w.posts {
        v.details()
        fmt.Println()
    }
}
func main() {  
    author1 := author{
        "Naveen",
        "Ramanathan",
        "Golang Enthusiast",
    }
    post1 := post{
        "Inheritance in Go",
        "Go supports composition instead of inheritance",
        author1,
    }
    post2 := post{
        "Struct instead of Classes in Go",
        "Go does not support classes but methods can be added to structs",
        author1,
    }
    post3 := post{
        "Concurrency",
        "Go is a concurrent language and not a parallel one",
        author1,
    }
    w := website{
        posts: []post{post1, post2, post3},
    }
    w.contents()
}   

執行結果:

Contents of Website

Title:  Inheritance in Go  
Content:  Go supports composition instead of inheritance  
Author:  Naveen Ramanathan  
Bio:  Golang Enthusiast

Title:  Struct instead of Classes in Go  
Content:  Go does not support classes but methods can be added to structs  
Author:  Naveen Ramanathan  
Bio:  Golang Enthusiast

Title:  Concurrency  
Content:  Go is a concurrent language and not a parallel one  
Author:  Naveen Ramanathan  
Bio:  Golang Enthusiast  

1.4 多型性(Polymorphism)

Go中的多型性是在介面的幫助下實現的。正如我們已經討論過的,介面可以在Go中隱式地實現。如果型別為介面中宣告的所有方法提供了定義,則實現一個介面。讓我們看看在介面的幫助下如何實現多型。

任何定義介面所有方法的型別都被稱為隱式地實現該介面。

型別介面的變數可以儲存實現介面的任何值。介面的這個屬性用於實現Go中的多型性。

舉個例子,一個虛構的組織有兩種專案的收入:固定的賬單和時間和材料。組織的淨收入是由這些專案的收入之和計算出來的。為了保持本教程的簡單,我們假設貨幣是美元,我們不會處理美分。它將使用整數來表示。

首先我們定義一個介面:Income

type Income interface {  
    calculate() int
    source() string
}

接下來,定義兩個結構體:FixedBilling和TimeAndMaterial

type FixedBilling struct {  
    projectName string
    biddedAmount int
}
type TimeAndMaterial struct {  
    projectName string
    noOfHours  int
    hourlyRate int
}

下一步是定義這些結構體型別的方法,計算並返回實際收入和收入來源。

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

接下來,我們來宣告一下計算和列印總收入的calculateNetIncome函式。

func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

上面的calculateNetIncome函式接受一部分Income介面作為引數。它通過遍歷切片和呼叫calculate()方法來計算總收入。它還通過呼叫source()方法來顯示收入來源。根據收入介面的具體型別,將呼叫不同的calculate()和source()方法。因此,我們在calculateNetIncome函式中實現了多型。

在未來,如果組織增加了一種新的收入來源,這個函式仍然可以正確地計算總收入,而沒有一行程式碼更改。

最後我們寫以下主函式:

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    incomeStreams := []Income{project1, project2, project3}
    calculateNetIncome(incomeStreams)
}

執行結果:

Income From Project 1 = $5000  
Income From Project 2 = $10000  
Income From Project 3 = $4000  
Net income of organisation = $19000  

假設該組織通過廣告找到了新的收入來源。讓我們看看如何簡單地新增新的收入方式和計算總收入,而不用對calculateNetIncome函式做任何更改。由於多型性,這樣是可行的。

首先讓我們定義Advertisement型別和calculate()和source()方法。

type Advertisement struct {  
    adName     string
    CPC        int
    noOfClicks int
}

func (a Advertisement) calculate() int {  
    return a.CPC * a.noOfClicks
}

func (a Advertisement) source() string {  
    return a.adName
}

廣告型別有三個欄位adName, CPC(cost per click)和noof點選數(cost per click)。廣告的總收入是CPC和noOfClicks的產品。

修改主函式:

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
    popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
    incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
    calculateNetIncome(incomeStreams)
}

執行結果:

Income From Project 1 = $5000  
Income From Project 2 = $10000  
Income From Project 3 = $4000  
Income From Banner Ad = $1000  
Income From Popup Ad = $3750  
Net income of organisation = $23750 

綜上,我們沒有對calculateNetIncome函式做任何更改,儘管我們添加了新的收入方式。它只是因為多型性而起作用。由於新的Advertisement型別也實現了Income介面(習慣了java,怎麼理解Go中實現介面這個意思?),我們可以將它新增到incomeStreams切片中。calculateNetIncome函式也在沒有任何更改的情況下工作,因為它可以呼叫Advertisement型別的calculate()和source()方法。