Go 經(jīng)典入門系列 28:多態(tài)
歡迎來(lái)到 Golang 系列教程[1]的第 28 篇。
Go 通過接口[2]來(lái)實(shí)現(xiàn)多態(tài)。我們已經(jīng)討論過,在 Go 語(yǔ)言中,我們是隱式地實(shí)現(xiàn)接口。一個(gè)類型如果定義了接口所聲明的全部方法[3],那它就實(shí)現(xiàn)了該接口。現(xiàn)在我們來(lái)看看,利用接口,Go 是如何實(shí)現(xiàn)多態(tài)的。
使用接口實(shí)現(xiàn)多態(tài)
一個(gè)類型如果定義了接口的所有方法,那它就隱式地實(shí)現(xiàn)了該接口。
所有實(shí)現(xiàn)了接口的類型,都可以把它的值保存在一個(gè)接口類型的變量中。在 Go 中,我們使用接口的這種特性來(lái)實(shí)現(xiàn)多態(tài)。
通過一個(gè)程序我們來(lái)理解 Go 語(yǔ)言的多態(tài),它會(huì)計(jì)算一個(gè)組織機(jī)構(gòu)的凈收益。為了簡(jiǎn)單起見,我們假設(shè)這個(gè)虛構(gòu)的組織所獲得的收入來(lái)源于兩個(gè)項(xiàng)目:fixed billing 和 time and material。該組織的凈收益等于這兩個(gè)項(xiàng)目的收入總和。同樣為了簡(jiǎn)單起見,我們假設(shè)貨幣單位是美元,而無(wú)需處理美分。因此貨幣只需簡(jiǎn)單地用 int 來(lái)表示。(我建議閱讀 https://forum.golangbridge.org/t/what-is-the-proper-golang-equivalent-to-decimal-when-dealing-with-money/413 上的文章,學(xué)習(xí)如何表示美分。感謝 Andreas Matuschek 在評(píng)論區(qū)指出這一點(diǎn)。)
我們首先定義一個(gè)接口 Income。
type?Income?interface?{
????calculate()?int
????source()?string
}
上面定義了接口 Interface,它包含了兩個(gè)方法:calculate() 計(jì)算并返回項(xiàng)目的收入,而 source() 返回項(xiàng)目名稱。
下面我們定義一個(gè)表示 FixedBilling 項(xiàng)目的結(jié)構(gòu)體類型。
type?FixedBilling?struct?{
????projectName?string
????biddedAmount?int
}
項(xiàng)目 FixedBillin 有兩個(gè)字段:projectName 表示項(xiàng)目名稱,而 biddedAmount 表示組織向該項(xiàng)目投標(biāo)的金額。
TimeAndMaterial 結(jié)構(gòu)體用于表示項(xiàng)目 Time and Material。
type?TimeAndMaterial?struct?{
????projectName?string
????noOfHours??int
????hourlyRate?int
}
結(jié)構(gòu)體 TimeAndMaterial 擁有三個(gè)字段名:projectName、noOfHours 和 hourlyRate。
下一步我們給這些結(jié)構(gòu)體類型定義方法,計(jì)算并返回實(shí)際收入和項(xiàng)目名稱。
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
}
在項(xiàng)目 FixedBilling 里面,收入就是項(xiàng)目的投標(biāo)金額。因此我們返回 FixedBilling 類型的 calculate() 方法。
而在項(xiàng)目 TimeAndMaterial 里面,收入等于 noOfHours 和 hourlyRate 的乘積,作為 TimeAndMaterial 類型的 calculate() 方法的返回值。
我們還通過 source() 方法返回了表示收入來(lái)源的項(xiàng)目名稱。
由于 FixedBilling 和 TimeAndMaterial 兩個(gè)結(jié)構(gòu)體都定義了 Income 接口的兩個(gè)方法:calculate() 和 source(),因此這兩個(gè)結(jié)構(gòu)體都實(shí)現(xiàn)了 Income 接口。
我們來(lái)聲明一個(gè) calculateNetIncome 函數(shù),用來(lái)計(jì)算并打印總收入。
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)
}
上面的函數(shù)[4]接收一個(gè) Income 接口類型的切片[5]作為參數(shù)。該函數(shù)會(huì)遍歷這個(gè)接口切片,并依個(gè)調(diào)用 calculate() 方法,計(jì)算出總收入。該函數(shù)同樣也會(huì)通過調(diào)用 source() 顯示收入來(lái)源。根據(jù) Income 接口的具體類型,程序會(huì)調(diào)用不同的 calculate() 和 source() 方法。于是,我們?cè)?calculateNetIncome 函數(shù)中就實(shí)現(xiàn)了多態(tài)。
如果在該組織以后增加了新的收入來(lái)源,calculateNetIncome 無(wú)需修改一行代碼,就可以正確地計(jì)算總收入了。:)
最后就剩下這個(gè)程序的 main 函數(shù)了。
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)
}
在上面的 main 函數(shù)中,我們創(chuàng)建了三個(gè)項(xiàng)目,有兩個(gè)是 FixedBilling 類型,一個(gè)是 TimeAndMaterial 類型。接著我們創(chuàng)建了一個(gè) Income 類型的切片,存放了這三個(gè)項(xiàng)目。由于這三個(gè)項(xiàng)目都實(shí)現(xiàn)了 Interface 接口,因此可以把這三個(gè)項(xiàng)目放入 Income 切片。最后我們將該切片作為參數(shù),調(diào)用了 calculateNetIncome 函數(shù),顯示了項(xiàng)目不同的收益和收入來(lái)源。
以下完整的代碼供你參考。
package?main
import?(
????"fmt"
)
type?Income?interface?{
????calculate()?int
????source()?string
}
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
}
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)
}
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)
}
在 playground 上運(yùn)行[6]
該程序會(huì)輸出:
Income?From?Project?1?=?$5000
Income?From?Project?2?=?$10000
Income?From?Project?3?=?$4000
Net?income?of?organisation?=?$19000
新增收益流
假設(shè)前面的組織通過廣告業(yè)務(wù),建立了一個(gè)新的收益流(Income Stream)。我們可以看到添加它非常簡(jiǎn)單,并且計(jì)算總收益也很容易,我們無(wú)需對(duì) calculateNetIncome 函數(shù)進(jìn)行任何修改。這就是多態(tài)的好處。
我們首先定義 Advertisement 類型,并在 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
}
Advertisement 類型有三個(gè)字段,分別是 adName、CPC(每次點(diǎn)擊成本)和 noOfClicks(點(diǎn)擊次數(shù))。廣告的總收益等于 CPC 和 noOfClicks 的乘積。
現(xiàn)在我們稍微修改一下 main 函數(shù),把新的收益流添加進(jìn)來(lái)。
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)
}
我們創(chuàng)建了兩個(gè)廣告項(xiàng)目,即 bannerAd 和 popupAd。incomeStream 切片包含了這兩個(gè)創(chuàng)建的廣告項(xiàng)目。
package?main
import?(
????"fmt"
)
type?Income?interface?{
????calculate()?int
????source()?string
}
type?FixedBilling?struct?{
????projectName??string
????biddedAmount?int
}
type?TimeAndMaterial?struct?{
????projectName?string
????noOfHours???int
????hourlyRate??int
}
type?Advertisement?struct?{
????adName?????string
????CPC????????int
????noOfClicks?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
}
func?(a?Advertisement)?calculate()?int?{
????return?a.CPC?*?a.noOfClicks
}
func?(a?Advertisement)?source()?string?{
????return?a.adName
}
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)
}
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)
}
在 playground 中運(yùn)行[7]
上面程序會(huì)輸出:
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
你會(huì)發(fā)現(xiàn),盡管我們新增了收益流,但卻完全沒有修改 calculateNetIncome 函數(shù)。這就是多態(tài)帶來(lái)的好處。由于新的 Advertisement 同樣實(shí)現(xiàn)了 Income 接口,所以我們能夠向 incomeStreams 切片添加 Advertisement。calculateNetIncome 無(wú)需修改,因?yàn)樗軌蛘{(diào)用 Advertisement 類型的 calculate() 和 source() 方法。
本教程到此結(jié)束。祝你愉快。
下一教程 - Defer[8]
via: https://golangbot.com/polymorphism/
作者:Nick Coghlan[9]譯者:Noluye[10]校對(duì):polaris1119[11]
本文由 GCTT[12] 原創(chuàng)編譯,Go 中文網(wǎng)[13] 榮譽(yù)推出
參考資料
Golang 系列教程: https://studygolang.com/subject/2
[2]接口: https://studygolang.com/articles/12266
[3]方法: https://studygolang.com/articles/12264
[4]函數(shù): https://studygolang.com/articles/11892
[5]切片: https://studygolang.com/articles/12121
[6]在 playground 上運(yùn)行: https://play.golang.org/p/UClAagvLFT
[7]在 playground 中運(yùn)行: https://play.golang.org/p/BYRYGjSxFN
[8]Defer: https://studygolang.com/articles/12719
[9]Nick Coghlan: https://golangbot.com/about/
[10]Noluye: https://github.com/Noluye
[11]polaris1119: https://github.com/polaris1119
[12]GCTT: https://github.com/studygolang/GCTT
[13]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
