使用接口来使用不同包中的方法
Using interfaces for using methods in a different package
我正在尝试了解接口的使用方式。
下面是一个人为的例子来证明我的问题。
我有主包,它实例化了一个测试数据库,然后将其传递给测试服务器,然后在该服务器上初始化服务器。
然后调用服务器,执行虚拟数据库插入(使用虚拟数据库依赖项,在服务器初始化时传递)。
main.go
package main
import (
"interfaces/database"
"interfaces/server"
)
func main() {
db := database.Start()
s := server.Start(db)
s.HandleInsert()
}
database.go
package database
import "fmt"
type Database struct {
pool string
}
func Start() *Database{
database := &Database{}
database.pool = "examplepool"
return database
}
func(db *Database) Select() {
fmt.Println("Running a Select")
}
func(db *Database) Insert() {
fmt.Println("Running an Insert")
}
func (db *Database) Delete() {
fmt.Println("Running a Delete")
}
server.go
package server
import "fmt"
type Database interface {
Select()
Insert()
Delete()
}
type Server struct {
server string
db Database
}
func Start(db Database) *Server {
fmt.Println("Created Server")
s := &Server{"exampleserver", db}
return s
}
func(s *Server) HandleInsert() {
s.db.Insert()
}
问题是,在服务器包中,要使用数据库包,我必须写出数据库对象具有的所有方法。我只有三种方法,但我的数据库对象可以很容易地拥有更多。这违背了 Go 的小接口哲学。我在这里错过了什么?我不想将Database包导入到Server包中,因为我想尽可能地封装每个包。
另一个问题是,假设我有其他包想要使用这个数据库包。它们还应该包含类似的数据库接口吗?我是否应该有一个名为“interfaces”的包,其中包含数据库接口,然后可以导入?
此代码布局的想法来自此视频:https://youtu.be/rWBSMsLG8po
乍一看,我会说你在用惯用的 golang 方式做事:
- Return 类型(数据库
Start
returns *Database
,因为它应该)
- 该包为其依赖项定义了接口(正如您所做的那样)
接口的大小不是由给定类型导出的内容(即您的 database.Database
类型实现的方法)决定的,而是由您需要的功能决定的。如果您的 server
包只需要使用 Select
、Insert
和 Delete
,那么接口应该是这样的。您传递给服务器包的类型可以实现 SelectNASASecretLizardFiles
,但如果您不使用它,server
包就不必知道该方法的存在。 server.Database
界面仍然和现在一样简单。
这就是小界面的本质含义。 Golang 接口是隐式实现的(有时人们称之为 ducktype 接口)。任何实现您在 server.Database
中定义的 3 种方法的类型都可以用作依赖项。这使您的包非常容易进行单元测试(模拟是微不足道的)。
“缺点” 可能是,如果您有多个依赖于 Database
类型的包,您最终可能会得到相同接口的重复定义。但是,如果其中一个包需要访问附加功能(或不需要使用 Insert
方法),则更改该包的接口不会影响任何其他包。这符合 golang 包自包含的整个概念。
不过,在你的具体情况下,我认为有判断的余地。如果您正在与各种数据库交互,我认为做出大多数(如果不是全部)包都需要能够 select 数据是一个合理的假设。在公共包中定义一个小的基本接口是很常见的:
|
|-- server
| |
| |--> dependencies.go (defines Databse interface for server pkg)
|
|-- foo
| |
| |--> dependencies.go (defines Database interface for this package)
|
|-- common (bad package name, but self explanatory)
| |
| |--> database.go (defines common subset of database interfaces)
接口看起来像这样:
package common
type DB interface {
// don't return a slice of maps, this is just an example
Select(query string, args ...interface{}) (rows []map[string]interface{}, err error)
Close() error
}
package server
import "your.project/common"
type Database interface {
common.DB // embedded common interface
Insert(query string, vals ...interface{}) error
Delete(query, id string) error
}
这是构建代码的常用方法,同时确保轻松模拟和测试。
说到 mocking/testing,只是一个提示,但请看一下名为 mockgen 的工具。您可以通过添加像这样的单个注释来为每个包的接口生成单元测试模拟:
package server
import "your.project/common"
//go:generate go run github.com/golang/mock/mockgen -destination mocks/db_mock.go -package mocks your.project/server Database
type Database interface {
common.DB // embedded common interface
Insert(query string, vals ...interface{}) error
Delete(query, id string) error
}
运行 go generate
会吐出你可以导入单元测试的模拟。
其他评论
我不禁注意到,您的 database
包声明了一个名为 Database
的类型。为什么要导出类型?为什么它与包同名?使用名为 database.Database
的类型只是代码味道。应避免口吃的名字。也许调用句柄 Handle
或 Conn
更有意义:db.Handle
或 db.Conn
更能描述您实际处理的内容,并且输入起来更短。
获取数据库连接的函数也有奇怪的名字(Start
)。这是一个构造函数,所以我认为将其称为 New
更有意义,结果代码为:
db := database.New()
我正在尝试了解接口的使用方式。 下面是一个人为的例子来证明我的问题。 我有主包,它实例化了一个测试数据库,然后将其传递给测试服务器,然后在该服务器上初始化服务器。
然后调用服务器,执行虚拟数据库插入(使用虚拟数据库依赖项,在服务器初始化时传递)。
main.go
package main
import (
"interfaces/database"
"interfaces/server"
)
func main() {
db := database.Start()
s := server.Start(db)
s.HandleInsert()
}
database.go
package database
import "fmt"
type Database struct {
pool string
}
func Start() *Database{
database := &Database{}
database.pool = "examplepool"
return database
}
func(db *Database) Select() {
fmt.Println("Running a Select")
}
func(db *Database) Insert() {
fmt.Println("Running an Insert")
}
func (db *Database) Delete() {
fmt.Println("Running a Delete")
}
server.go
package server
import "fmt"
type Database interface {
Select()
Insert()
Delete()
}
type Server struct {
server string
db Database
}
func Start(db Database) *Server {
fmt.Println("Created Server")
s := &Server{"exampleserver", db}
return s
}
func(s *Server) HandleInsert() {
s.db.Insert()
}
问题是,在服务器包中,要使用数据库包,我必须写出数据库对象具有的所有方法。我只有三种方法,但我的数据库对象可以很容易地拥有更多。这违背了 Go 的小接口哲学。我在这里错过了什么?我不想将Database包导入到Server包中,因为我想尽可能地封装每个包。
另一个问题是,假设我有其他包想要使用这个数据库包。它们还应该包含类似的数据库接口吗?我是否应该有一个名为“interfaces”的包,其中包含数据库接口,然后可以导入?
此代码布局的想法来自此视频:https://youtu.be/rWBSMsLG8po
乍一看,我会说你在用惯用的 golang 方式做事:
- Return 类型(数据库
Start
returns*Database
,因为它应该) - 该包为其依赖项定义了接口(正如您所做的那样)
接口的大小不是由给定类型导出的内容(即您的 database.Database
类型实现的方法)决定的,而是由您需要的功能决定的。如果您的 server
包只需要使用 Select
、Insert
和 Delete
,那么接口应该是这样的。您传递给服务器包的类型可以实现 SelectNASASecretLizardFiles
,但如果您不使用它,server
包就不必知道该方法的存在。 server.Database
界面仍然和现在一样简单。
这就是小界面的本质含义。 Golang 接口是隐式实现的(有时人们称之为 ducktype 接口)。任何实现您在 server.Database
中定义的 3 种方法的类型都可以用作依赖项。这使您的包非常容易进行单元测试(模拟是微不足道的)。
“缺点” 可能是,如果您有多个依赖于 Database
类型的包,您最终可能会得到相同接口的重复定义。但是,如果其中一个包需要访问附加功能(或不需要使用 Insert
方法),则更改该包的接口不会影响任何其他包。这符合 golang 包自包含的整个概念。
不过,在你的具体情况下,我认为有判断的余地。如果您正在与各种数据库交互,我认为做出大多数(如果不是全部)包都需要能够 select 数据是一个合理的假设。在公共包中定义一个小的基本接口是很常见的:
|
|-- server
| |
| |--> dependencies.go (defines Databse interface for server pkg)
|
|-- foo
| |
| |--> dependencies.go (defines Database interface for this package)
|
|-- common (bad package name, but self explanatory)
| |
| |--> database.go (defines common subset of database interfaces)
接口看起来像这样:
package common
type DB interface {
// don't return a slice of maps, this is just an example
Select(query string, args ...interface{}) (rows []map[string]interface{}, err error)
Close() error
}
package server
import "your.project/common"
type Database interface {
common.DB // embedded common interface
Insert(query string, vals ...interface{}) error
Delete(query, id string) error
}
这是构建代码的常用方法,同时确保轻松模拟和测试。
说到 mocking/testing,只是一个提示,但请看一下名为 mockgen 的工具。您可以通过添加像这样的单个注释来为每个包的接口生成单元测试模拟:
package server
import "your.project/common"
//go:generate go run github.com/golang/mock/mockgen -destination mocks/db_mock.go -package mocks your.project/server Database
type Database interface {
common.DB // embedded common interface
Insert(query string, vals ...interface{}) error
Delete(query, id string) error
}
运行 go generate
会吐出你可以导入单元测试的模拟。
其他评论
我不禁注意到,您的 database
包声明了一个名为 Database
的类型。为什么要导出类型?为什么它与包同名?使用名为 database.Database
的类型只是代码味道。应避免口吃的名字。也许调用句柄 Handle
或 Conn
更有意义:db.Handle
或 db.Conn
更能描述您实际处理的内容,并且输入起来更短。
获取数据库连接的函数也有奇怪的名字(Start
)。这是一个构造函数,所以我认为将其称为 New
更有意义,结果代码为:
db := database.New()