接口Interface—塑造健壮与可扩展的Go应用程序

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接口赋予应用的高扩展性了吗?



推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注


浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报