go - Go 中的依赖注入
问题描述
我正在寻找一种适当的方式来注入依赖项。
假设我有这段代码,其中 FancyWrite 和 FancyRead 函数依赖于 WriteToFile 和 ReadFromFile 函数。由于这些有副作用,我希望能够注入它们,以便我可以在测试中替换它们。
package main
func main() {
FancyWrite()
FancyRead()
}
////////////////
func FancyWrite() {
WriteToFile([]byte("content..."))
}
func FancyRead() {
ReadFromFile("/path/to/file")
}
////////////////
func WriteToFile(content []byte) (bool, error) {
return true, nil
}
func ReadFromFile(file string) ([]byte, error) {
return []byte{}, nil
}
我尝试过的一件事就是将它们作为参数放入函数中:
package main
func main() {
FancyWrite(WriteToFile)
FancyRead(ReadFromFile)
}
////////////////
func FancyWrite(writeToFile func(content []byte) (bool, error)) {
writeToFile([]byte("content..."))
}
func FancyRead(readFromFile func(file string) ([]byte, error)) {
readFromFile("/path/to/file")
}
////////////////
func WriteToFile(content []byte) (bool, error) {
return true, nil
}
func ReadFromFile(file string) ([]byte, error) {
return []byte{}, nil
}
所以,这实际上效果很好,但我可以看到这变得更难维护更多的依赖关系。我还尝试了如下的工厂模式,这样主函数就不必关心构建 FancyWrite 函数。但是,语法变得有点难以阅读,并且更多的功能将难以维护。
func FancyWriteFactory(writeToFile func(content []byte) (bool, error)) func() {
return func() {
FancyWrite(writeToFile)
}
}
所以接下来我尝试将函数作为方法容纳在结构中:
package main
func main() {
dfu := DefaultFileUtil{}
ffm := FancyFileModule{
FileUtil: &dfu,
}
ffm.FancyWrite()
ffm.FancyRead()
}
////////////////
type FileUtil interface {
WriteToFile(content []byte) (bool, error)
ReadFromFile(file string) ([]byte, error)
}
type FancyFileModule struct {
FileUtil
}
func (fm *FancyFileModule) FancyWrite() {
fm.FileUtil.WriteToFile([]byte("content..."))
}
func (fm *FancyFileModule) FancyRead() {
fm.FileUtil.ReadFromFile("/path/to/file")
}
////////////////
type DefaultFileUtil struct{}
func (fu *DefaultFileUtil) WriteToFile(content []byte) (bool, error) {
return true, nil
}
func (fu *DefaultFileUtil) ReadFromFile(file string) ([]byte, error) {
return []byte{}, nil
}
现在,这实际上效果很好并且更清洁。但是,我担心我现在只是将我的函数作为方法强加,这让我感到有些奇怪。我想我可以推理它,因为当你有某种状态时结构是好的,我想我可以将依赖关系算作状态?
这些是我尝试过的事情。所以我的问题是,当把函数作为方法的唯一原因是让它们成为其他地方的依赖集合时,在这种情况下进行依赖注入的正确方法是什么?
谢谢!
解决方案
简单的答案是,您不能干净地将依赖注入与函数一起使用,而只能与方法一起使用。从技术上讲,您可以将函数设为全局变量( ex. var WriteToFile = func(content []byte) (bool, error) { [...] }
),但这是相当脆弱的代码。
从惯用的角度来看,更合适的解决方案是将您想要替换、注入或包装的任何行为放入一个方法中,然后包装在一个接口中。
例如:
type (
FancyReadWriter interface {
FancyWrite()
FancyRead()
}
fancyReadWriter struct {
w Writer
r Reader
}
Writer interface {
Write([]byte) (bool, error)
}
Reader interface {
Read() ([]byte, error)
}
fileWriter struct {
path string
// or f *os.File
}
fileReader struct {
path string
// or f *os.File
}
)
func (w fileWriter) Write([]byte) (bool, error) {
// Write to the file
return true, nil
}
func (r fileReader) Read() ([]byte, error) {
// Read from the file
return nil, nil
}
func (f fancyReadWriter) FancyWrite() {
// I like to be explicit when I'm ignoring return values,
// hence the underscores.
_, _ = f.w.Write([]byte("some content..."))
}
func (f fancyReadWriter) FancyRead() {
_, _ = f.r.Read()
}
func NewFancyReadWriter(w Writer, r Reader) FancyReadWriter {
// NOTE: Returning a pointer to the struct type, but it is actually
// returned as an interface instead, abstracting the underlying
// implementation.
return &fancyReadWriter{
w: w,
r: r,
}
}
func NewFileReader(path string) Reader {
// Same here, returning a pointer to the struct as the interface
return &fileReader {
path: path
}
}
func NewFileWriter(path string) Writer {
// Same here, returning a pointer to the struct as the interface
return &fileWriter {
path: path
}
}
func Main() {
w := NewFileWriter("/var/some/path")
r := NewFileReader("/var/some/other/path")
f := NewFancyReadWriter(w, r)
f.FancyWrite()
f.FancyRead()
}
然后在测试文件中(或任何你想进行依赖注入的地方):
type MockReader struct {}
func (m MockReader) Read() ([]byte, error) {
return nil, fmt.Errorf("test error 1")
}
type MockWriter struct {}
func (m MockWriter) Write([]byte) (bool, error) {
return false, fmt.Errorf("test error 2")
}
func TestFancyReadWriter(t *testing.T) {
var w MockWriter
var r MockReader
f := NewFancyReadWriter(w, r)
// Now the methods on f will call the mock methods instead
f.FancyWrite()
f.FancyRead()
}
然后,您可以更进一步,使模拟或注入框架具有功能性并因此变得灵活。实际上,这是我首选的测试模拟风格,因为它让我可以使用该行为定义测试中模拟依赖项的行为。例子:
type MockReader struct {
Readfunc func() ([]byte, error)
ReadCalled int
}
func (m *MockReader) Read() (ret1 []byte, ret2 error) {
m.ReadCalled++
if m.Readfunc != nil {
// Be *very* careful that you don't just call m.Read() here.
// That would result in an infinite recursion.
ret1, ret2 = m.Readfunc()
}
// if Readfunc == nil, this just returns the zero values
return
}
type MockWriter struct {
Writefunc func([]byte) (bool, error)
WriteCalled int
}
func (m MockWriter) Write(arg1 []byte) (ret1 bool, ret2 error) {
m.WriteCalled++
if m.Writefunc != nil {
ret1, ret2 = m.Writefunc(arg1)
}
// Same here, zero values if the func is nil
return
}
func TestFancyReadWriter(t *testing.T) {
var w MockWriter
var r MockReader
// Note that these definitions are optional. If you don't provide a
// definition, the mock will just return the zero values for the
// return types, so you only need to define these functions if you want
// custom behavior, like different returns or test assertions.
w.Writefunc = func(d []byte) (bool, error) {
// Whatever tests you want, like assertions on the input or w/e
// Then whatever returns you want to test how the caller handles it.
return false, nil
}
r.Readfunc = func() ([]byte, error) {
return nil, nil
}
// Since the mocks now define the methods as *pointer* receiver methods,
// so the mock can keep track of the number of calls, we have to pass in
// the address of the mocks rather than the mocks as struct values.
f := NewFancyReadWriter(&w, &r)
// Now the methods on f will call the mock methods instead
f.FancyWrite()
f.FancyRead()
// Now you have a simple way to assert that the calls happened:
if w.WriteCalled < 1 {
t.Fail("Missing expected call to Writer.Write().")
}
if r.ReadCalled < 1 {
t.Fail("Missing expected call to Reader.Read().")
}
}
由于这里涉及的所有类型(Reader、Writer 和 FancyReadWriter)都是作为接口而不是具体类型传递的,因此用中间件或类似的东西(例如日志记录、度量/跟踪、超时中止)包装它们也变得微不足道, ETC)。
这无疑是 Go 接口系统最强大的力量。开始将类型视为行为包,将您的行为附加到可以容纳它们的类型,并将所有行为类型作为接口而不是具体结构传递(仅用于组织特定数据位的数据结构在没有接口的情况下非常好,否则,您必须为所有内容定义 Getter 和 Setter,这是一件真正的苦差事,没有太多好处)。这使您可以随时隔离、包装或完全替换您想要的任何特定行为。
推荐阅读
- html - 如何在表格中进行扩展
- r - 绘制具有多列的 R 表的结果
- assembly - 装配数据部分
- python - 使用python中的请求模块POST / PUT方法将大文件拆分为小文件并将其发送到github的最佳方法是什么?
- python - 在熊猫中将一列日期时间和字符串转换为句点
- java - 在列表中排列数据
- php - php curl:Rest API Post XML
- swift - 为什么响应总是 {"detail":"Unsupported media type \"text/plain\" in request."} 在 swift 中?
- android - 在 Android Studio 中启动新项目
- php - get_bloginfo() 不在页脚上回显 - Wordpress