Golang中的依赖注入实践与探讨

Golang中的依赖注入实践与探讨 大家好,

我正在寻找一种合适的方法来注入依赖项。

假设我有以下代码,其中 FancyWriteFancyRead 函数依赖于 WriteToFileReadFromFile 函数。由于这些函数会产生副作用,我希望能够注入它们,以便在测试中替换它们。

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
}

现在,这种方法实际上效果很好,也更清晰。然而,我担心我只是在生硬地将函数作为方法使用,感觉有点奇怪。我想我可以这样理解:当你有某种状态时,结构体是很好的选择,或许我可以把依赖项看作是一种状态?

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

谢谢!


更多关于Golang中的依赖注入实践与探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

你好 @talhaguy

使用接口确实是实现依赖注入的一种简单直接的方法,详情可参考 https://appliedgo.net/di

如果你觉得涉及的 struct 太多,可以去掉 FancyFileModule 这个,像这样:

package main

func main() {
	storage := FileStorage{}

	FancyWrite(storage)
	FancyRead(storage)
}

////////////////

func FancyWrite(s Storage) {
	s.WriteToFile([]byte("content..."))
}

func FancyRead(s Storage) {
	s.ReadFromFile("/path/to/file")
}

////////////////

type Storage interface {
	WriteToFile([]byte) (bool, error)
	ReadFromFile(string) ([]byte, error)
}

type FileStorage struct{}

func (s FileStorage) WriteToFile(content []byte) (bool, error) {
	return true, nil
}

func (s FileStorage) ReadFromFile(file string) ([]byte, error) {
	return []byte{}, nil
}

更多关于Golang中的依赖注入实践与探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中,依赖注入的实践通常采用接口和结构体的方式,你最后展示的方法正是Go社区推荐的标准做法。将依赖定义为接口,通过结构体字段注入,这既便于测试也保持了代码的清晰性。

你的FancyFileModule结构体通过嵌入FileUtil接口来持有依赖,这允许你在测试时轻松替换为模拟实现。以下是更完整的示例,展示如何在实际应用和测试中使用:

package main

import "fmt"

// 定义接口
type FileUtil interface {
    WriteToFile(content []byte) (bool, error)
    ReadFromFile(file string) ([]byte, error)
}

// 业务模块
type FancyFileModule struct {
    FileUtil
}

func (fm *FancyFileModule) FancyWrite() error {
    _, err := fm.WriteToFile([]byte("content..."))
    return err
}

func (fm *FancyFileModule) FancyRead() ([]byte, error) {
    return fm.ReadFromFile("/path/to/file")
}

// 默认实现
type DefaultFileUtil struct{}

func (fu *DefaultFileUtil) WriteToFile(content []byte) (bool, error) {
    fmt.Println("Writing to file:", string(content))
    return true, nil
}

func (fu *DefaultFileUtil) ReadFromFile(file string) ([]byte, error) {
    fmt.Println("Reading from file:", file)
    return []byte("file content"), nil
}

// 测试用的模拟实现
type MockFileUtil struct {
    WriteCalls [][]byte
    ReadCalls  []string
}

func (m *MockFileUtil) WriteToFile(content []byte) (bool, error) {
    m.WriteCalls = append(m.WriteCalls, content)
    return true, nil
}

func (m *MockFileUtil) ReadFromFile(file string) ([]byte, error) {
    m.ReadCalls = append(m.ReadCalls, file)
    return []byte("mock content"), nil
}

func main() {
    // 生产环境使用默认实现
    dfu := &DefaultFileUtil{}
    ffm := &FancyFileModule{FileUtil: dfu}
    
    ffm.FancyWrite()
    ffm.FancyRead()
}

// 测试示例
func TestFancyWrite() {
    mockUtil := &MockFileUtil{}
    ffm := &FancyFileModule{FileUtil: mockUtil}
    
    ffm.FancyWrite()
    
    if len(mockUtil.WriteCalls) != 1 {
        panic("Expected WriteToFile to be called once")
    }
}

对于更复杂的依赖管理,可以考虑使用wire这样的依赖注入工具:

// 使用wire进行依赖注入
var Set = wire.NewSet(
    wire.Struct(new(FancyFileModule), "*"),
    wire.Bind(new(FileUtil), new(*DefaultFileUtil)),
    wire.Struct(new(DefaultFileUtil), "*"),
)

func InitializeModule() *FancyFileModule {
    wire.Build(Set)
    return &FancyFileModule{}
}

这种模式的优势在于:

  1. 依赖关系明确,通过接口声明
  2. 易于单元测试,可以注入模拟对象
  3. 代码结构清晰,遵循单一职责原则
  4. 便于扩展,新增依赖只需实现接口

你担心的"生硬地将函数作为方法使用"在Go中是常见且推荐的模式。依赖确实可以视为一种状态,结构体作为依赖的容器,方法则操作这些依赖。这种方式比函数参数注入更易于管理多个依赖,特别是在大型项目中。

回到顶部