首页 > 解决方案 > 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
}

现在,这实际上效果很好并且更清洁。但是,我担心我现在只是将我的函数作为方法强加,这让我感到有些奇怪。我想我可以推理它,因为当你有某种状态时结构是好的,我想我可以将依赖关系算作状态?

这些是我尝试过的事情。所以我的问题是,当把函数作为方法的唯一原因是让它们成为其他地方的依赖集合时,在这种情况下进行依赖注入的正确方法是什么?

谢谢!

标签: godependency-injection

解决方案


简单的答案是,您不能干净地将依赖注入与函数一起使用,而只能与方法一起使用。从技术上讲,您可以将函数设为全局变量( 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,这是一件真正的苦差事,没有太多好处)。这使您可以随时隔离、包装或完全替换您想要的任何特定行为。


推荐阅读