使用接口来使用不同包中的方法

tes*_*495 2 interface go

我试图了解接口的使用方式。下面是一个人为的例子来证明我的问题。我有主包,它实例化一个测试数据库,然后将其传递到测试服务器,然后在其中初始化服务器。

然后调用服务器,执行虚拟数据库插入(使用虚拟数据库依赖项,在服务器初始化时传递)。

主程序

package main

import (
    "interfaces/database"
    "interfaces/server"
)


func main() {
    db := database.Start()
    s := server.Start(db)
    s.HandleInsert()
}
Run Code Online (Sandbox Code Playgroud)

数据库.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")
}
Run Code Online (Sandbox Code Playgroud)

服务器.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()
}
Run Code Online (Sandbox Code Playgroud)

问题是,在服务器包中,为了使用数据库包,我必须写出数据库对象具有的所有方法。我只有三个方法,但我的数据库对象可以轻松拥有更多方法。这违背了 Go 拥有小接口的理念。我在这里缺少什么?我不想将数据库包导入到服务器包中,因为我想尽可能地封装每个包。

另一个问题是,假设我有其他包想要使用这个数据库包。它们还应该包含类似的数据库接口吗?我是否应该有一个名为“interfaces”的包,其中包含数据库接口,然后可以将其导入?

此代码布局的想法来自此视频:https://youtu.be/rWBSMsLG8po

Eli*_*gem 5

乍一看,我会说你正在以惯用的 golang 方式做事:

  • 返回类型(数据库Start返回*Database,因为它应该)
  • 该包为其依赖项定义了接口(正如您所做的那样)

接口的大小不是由给定类型导出的内容(即您的database.Database类型实现的方法)决定的,而是由您需要的功能决定的。如果您的server包只需要使用SelectInsertDelete,那么接口就应该是这样的。您传递给服务器包的类型可以实现SelectNASASecretLizardFiles,但如果您不使用它,则该server包不必知道该方法存在。界面server.Database仍然像现在一样简单。

这本质上就是小界面的含义。Golang接口是隐式实现的(有时人们称其为ducktype接口)。任何实现您定义的 3 个方法的类型都server.Database可以用作依赖项。这使得你的包非常容易进行单元测试(模拟很简单)。

“缺点可能是,如果您有多个取决于Database类型的包,则最终可能会得到同一接口的重复定义。但是,如果其中一个包需要访问附加函数(或不需要使用该Insert方法),则更改该包的接口不会影响任何其他包。这符合 golang 包独立的整体概念。

不过,就您的具体情况而言,我认为还有判断的余地。如果您正在与某种数据库进行交互,我认为这是一个合理的假设,即大多数(如果不是全部)包都需要能够选择数据。在公共包中定义一个小的基本接口是很常见的:

|
|-- 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)
Run Code Online (Sandbox Code Playgroud)

接口如下所示:

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
}
Run Code Online (Sandbox Code Playgroud)

这是构建代码的常用方法,同时确保轻松模拟和测试。

说到模拟/测试,这只是一个技巧,但请看一下名为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
}
Run Code Online (Sandbox Code Playgroud)

运行go generate会吐出模拟,然后您可以在单元测试中导入。


其他的建议

我忍不住注意到你的database包声明了一个名为 的类型Database。为什么要导出类型?为什么它与包同名?使用名为的类型database.Database只是代码味道。应避免口吃的名字。也许调用句柄HandleorConn更有意义:db.Handleordb.Conn更能描述您实际正在处理的内容,并且键入的时间更短。

获取数据库连接的函数的名字也很奇怪(Start)。它是一个构造函数,所以我认为调用它更有意义New,从而产生代码:

db := database.New()
Run Code Online (Sandbox Code Playgroud)