接口Interface—塑造健壮与可扩展的Go应用程序
共 4499字,需浏览 9分钟
·
2020-08-05 00:44
本文拟以一个接近实际的项目需求例子,来帮助读者体会接口使用的重要性,理解Go接口Interface是如何提高项目的鲁棒性和扩展性。
场景与接口定义
场景:假设有一个在线商城,需要在Go后台提供存储与查询产品的服务。那么我们在项目中应该怎么设计该服务?
ok,需求很明朗,其实就是要一个负责保存和检索产品的存储库。
package productrepo
type ProductRepository interface {
StoreProduct(name string, id int)
FindProductByID(id int)
}
为此,我们创建一个productrepo的包和一个api.go的文件。该API应该暴露出存储库里所有的产品方法。在productrepo包下,定义了ProductRepository接口,它代表的就是存储库。该接口中我们只定义两个简单的方法,StoreProduct()方法用于存储产品信息,FindProductByID()方法通过产品ID查找产品信息。
接口实现示例
既然已经定义了存储库接口,那么现在就需要有实体对象去实现该接口。
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对象。正如名字一样,在示例代码中我们并不会真的去做什么(仅仅做个输出打印),但是会mock出ProductRepository接口所需的方法。
这时,在api.go文件中增加一个方法New(),它返回的一个实现了ProductRepository接口的对象。
func New() ProductRepository {
return mockProductRepo{}
}
为什么要使用接口?
对于我们已经定义的ProductRepository接口,可以有多种对象去实现它。但是,在最开始做开发时,小菜刀对于接口总是会很疑惑:为什么要搞个接口,我就一个存储库啊(例如本地MySQL存储),何必要这麻烦!
这种想法,对于小型的个人项目来说可能是正确的。但是,事情往往不是这么简单。在复杂的实际应用项目中,我们通常会有很多种存储对象:例如,你可能选择使用本地MySQL存储,也可能连接到云数据库(例如阿里云、谷歌云和腾讯云等)存储。而它们均需要实现ProductRepository接口定义的StoreProduct()方法和FindProductByID()方法。
以本地MySQL存储库为例,它要管理产品对象,需要实现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对象并实现接口方法。
相似地,当项目中同时需要把产品信息存储到云端时,以阿里云为例,在productrepo包下,新建aliyun.go文件,定义了aliCloudProductRepo对象并实现接口方法。
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.
}
此时,更新前面提到的api.go中定义的New()方法。
func New(environment string) ProductRepository {
switch environment {
case "aliCloud":
return aliCloudProductRepo{}
case "local-mysql":
return mysqlProductRepo{}
}
return mockProductRepo{}
}
通过将环境变量environment传递给New()函数,它将基于该环境值返回ProductRepository接口的正确实现对象。
定义程序入口main.go文件以及main函数。
package main
import "workspace/example/example/productrepo"
func main() {
env := "aliCloud"
repo := productrepo.New(env)
repo.StoreProduct("HuaWei mate 40", 105)
}
这里,通过使用productrepo.New()方法基于环境值来获取ProductRepository接口对象。如果你需要切换产品存储库,则只需要使用对应的env值调用productrepo.New()方法即可。
最终,本文的代码结构如下
.
├── go.mod
├── main.go
└── productrepo
├── aliyun.go
├── api.go
├── mock.go
└── mysql.go
运行main.go,结果如下
$ go run main.go
aliCloudProductRepo: mocking the StoreProduct func
如果没有接口,要实现上述main函数中的调用,需要增加多少代码?
// 1. 需要为每个对象增加初始化方法
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. 调用对象处产生大量重复代码
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.
}
}
在项目演进过程中,我们不知道会迭代多少存储库对象,而通过ProductRepository接口,可以轻松地实现扩展,而不必反复编写相同逻辑的代码。
总结
开发中,我们常常提到要功能模块化,本文的示例就是一个典型示例:通过接口为载体,一类服务就是一个接口,接口即服务。
最后,你感受到Go接口赋予应用的高扩展性了吗?
推荐阅读
站长 polarisxu
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注