接口Interface—塑造健壯與可擴(kuò)展的Go應(yīng)用程序
本文擬以一個(gè)接近實(shí)際的項(xiàng)目需求例子,來(lái)幫助讀者體會(huì)接口使用的重要性,理解Go接口Interface是如何提高項(xiàng)目的魯棒性和擴(kuò)展性。
場(chǎng)景與接口定義
場(chǎng)景:假設(shè)有一個(gè)在線商城,需要在Go后臺(tái)提供存儲(chǔ)與查詢產(chǎn)品的服務(wù)。那么我們?cè)陧?xiàng)目中應(yīng)該怎么設(shè)計(jì)該服務(wù)?
ok,需求很明朗,其實(shí)就是要一個(gè)負(fù)責(zé)保存和檢索產(chǎn)品的存儲(chǔ)庫(kù)。
package?productrepo
type?ProductRepository?interface?{
?StoreProduct(name?string,?id?int)
?FindProductByID(id?int)
}為此,我們創(chuàng)建一個(gè)productrepo的包和一個(gè)api.go的文件。該API應(yīng)該暴露出存儲(chǔ)庫(kù)里所有的產(chǎn)品方法。在productrepo包下,定義了ProductRepository接口,它代表的就是存儲(chǔ)庫(kù)。該接口中我們只定義兩個(gè)簡(jiǎn)單的方法,StoreProduct()方法用于存儲(chǔ)產(chǎn)品信息,FindProductByID()方法通過(guò)產(chǎn)品ID查找產(chǎn)品信息。
接口實(shí)現(xiàn)示例
既然已經(jīng)定義了存儲(chǔ)庫(kù)接口,那么現(xiàn)在就需要有實(shí)體對(duì)象去實(shí)現(xiàn)該接口。
package?productrepo
import?"fmt"
type?mockProductRepo?struct?{
}
func?(m?mockProductRepo)?StoreProduct(name?string,?id?int)?{
?fmt.Println("mocking?the?StoreProduct?func")
}
func?(m?mockProductRepo)?FindProductByID(id?int)?{
?fmt.Println("mocking?the?FindProductByID?func")
}
如上,在productrepo包下,新建mock.go文件,定義了mockProductRepo對(duì)象。正如名字一樣,在示例代碼中我們并不會(huì)真的去做什么(僅僅做個(gè)輸出打?。?,但是會(huì)mock出ProductRepository接口所需的方法。
這時(shí),在api.go文件中增加一個(gè)方法New(),它返回的一個(gè)實(shí)現(xiàn)了ProductRepository接口的對(duì)象。
func?New()?ProductRepository?{
?return?mockProductRepo{}
}
為什么要使用接口?
對(duì)于我們已經(jīng)定義的ProductRepository接口,可以有多種對(duì)象去實(shí)現(xiàn)它。但是,在最開(kāi)始做開(kāi)發(fā)時(shí),小菜刀對(duì)于接口總是會(huì)很疑惑:為什么要搞個(gè)接口,我就一個(gè)存儲(chǔ)庫(kù)?。ɡ绫镜豈ySQL存儲(chǔ)),何必要這麻煩!
這種想法,對(duì)于小型的個(gè)人項(xiàng)目來(lái)說(shuō)可能是正確的。但是,事情往往不是這么簡(jiǎn)單。在復(fù)雜的實(shí)際應(yīng)用項(xiàng)目中,我們通常會(huì)有很多種存儲(chǔ)對(duì)象:例如,你可能選擇使用本地MySQL存儲(chǔ),也可能連接到云數(shù)據(jù)庫(kù)(例如阿里云、谷歌云和騰訊云等)存儲(chǔ)。而它們均需要實(shí)現(xiàn)ProductRepository接口定義的StoreProduct()方法和FindProductByID()方法。
以本地MySQL存儲(chǔ)庫(kù)為例,它要管理產(chǎn)品對(duì)象,需要實(shí)現(xiàn)ProductRepository接口。
package?productrepo
import?"fmt"
type?mysqlProductRepo?struct?{
}
func?(m?mysqlProductRepo)?StoreProduct(name?string,?id?int)?{
?fmt.Println("mysqlProductRepo:?mocking?the?StoreProduct?func")
?//?In?a?real?world?project?you?would?query?a?MySQL?database?here.
}
func?(m?mysqlProductRepo)?FindProductByID(id?int)?{
?fmt.Println("mysqlProductRepo:?mocking?the?FindProductByID?func")
?//?In?a?real?world?project?you?would?query?a?MySQL?database?here.
}
如上,在productrepo包下,新建mysql.go文件,定義了mysqlProductRepo對(duì)象并實(shí)現(xiàn)接口方法。
相似地,當(dāng)項(xiàng)目中同時(shí)需要把產(chǎn)品信息存儲(chǔ)到云端時(shí),以阿里云為例,在productrepo包下,新建aliyun.go文件,定義了aliCloudProductRepo對(duì)象并實(shí)現(xiàn)接口方法。
package?productrepo
import?"fmt"
type?aliCloudProductRepo?struct?{
}
func?(m?aliCloudProductRepo)?StoreProduct(name?string,?id?int)?{
?fmt.Println("aliCloudProductRepo:?mocking?the?StoreProduct?func")
?//?In?a?real?world?project?you?would?query?an?ali?Cloud?database?here.
}
func?(m?aliCloudProductRepo)?FindProductByID(id?int)?{
?fmt.Println("aliCloudProductRepo:?mocking?the?FindProductByID?func")
?//?In?a?real?world?project?you?would?query?an?ali?Cloud?database?here.
}
此時(shí),更新前面提到的api.go中定義的New()方法。
func?New(environment?string)?ProductRepository?{
?switch?environment?{
?case?"aliCloud":
??return?aliCloudProductRepo{}
?case?"local-mysql":
??return?mysqlProductRepo{}
?}
?return?mockProductRepo{}
}
通過(guò)將環(huán)境變量environment傳遞給New()函數(shù),它將基于該環(huán)境值返回ProductRepository接口的正確實(shí)現(xiàn)對(duì)象。
定義程序入口main.go文件以及main函數(shù)。
package?main
import?"workspace/example/example/productrepo"
func?main()?{
?env?:=?"aliCloud"
?repo?:=?productrepo.New(env)
?repo.StoreProduct("HuaWei?mate?40",?105)
}
這里,通過(guò)使用productrepo.New()方法基于環(huán)境值來(lái)獲取ProductRepository接口對(duì)象。如果你需要切換產(chǎn)品存儲(chǔ)庫(kù),則只需要使用對(duì)應(yīng)的env值調(diào)用productrepo.New()方法即可。
最終,本文的代碼結(jié)構(gòu)如下
.
├──?go.mod
├──?main.go
└──?productrepo
????├──?aliyun.go
????├──?api.go
????├──?mock.go
????└──?mysql.go
運(yùn)行main.go,結(jié)果如下
$?go?run?main.go
aliCloudProductRepo:?mocking?the?StoreProduct?func如果沒(méi)有接口,要實(shí)現(xiàn)上述main函數(shù)中的調(diào)用,需要增加多少代碼?
//?1. 需要為每個(gè)對(duì)象增加初始化方法
msql.go中增加NewMysqlProductRepo()方法
func?NewMysqlProductRepo()?*mysqlProductRepo?{
?return?&mysqlProductRepo{}
}
aliyun.go中增加NewAliCloudProductRepo()方法
func?NewAliCloudProductRepo()??*aliCloudProductRepo{
?return?&aliCloudProductRepo{}
}
mock.go中增加NewMockProductRepo()方法
func?NewMockProductRepo()?*mockProductRepo?{
?return?&mockProductRepo{}
}
//?2.??調(diào)用對(duì)象處產(chǎn)生大量重復(fù)代碼
package?main
import?"workspace/example/example/productrepo"
func?main()?{
?env?:=?"aliCloud"
?switch?env?{
?case?"aliCloud":
??repo?:=?productrepo.NewAliCloudProductRepo()
??repo.StoreProduct("HuaWei?mate?40",?105)
????//?the?more?function?to?do,?the?more?code?is?repeated.
?case?"local-mysql":
??repo?:=?productrepo.NewMysqlProductRepo()
??repo.StoreProduct("HuaWei?mate?40",?105)
????//?the?more?function?to?do,?the?more?code?is?repeated.
?default:
??repo?:=?productrepo.NewMockProductRepo()
??repo.StoreProduct("HuaWei?mate?40",?105)
????//?the?more?function?to?do,?the?more?code?is?repeated.
?}
}
在項(xiàng)目演進(jìn)過(guò)程中,我們不知道會(huì)迭代多少存儲(chǔ)庫(kù)對(duì)象,而通過(guò)ProductRepository接口,可以輕松地實(shí)現(xiàn)擴(kuò)展,而不必反復(fù)編寫相同邏輯的代碼。
總結(jié)
開(kāi)發(fā)中,我們常常提到要功能模塊化,本文的示例就是一個(gè)典型示例:通過(guò)接口為載體,一類服務(wù)就是一個(gè)接口,接口即服務(wù)。
最后,你感受到Go接口賦予應(yīng)用的高擴(kuò)展性了嗎?
推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語(yǔ)言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛(ài)好者值得關(guān)注
