Dependency injection

Hi all,

I’m looking for an appropriate way to inject dependencies.

Say I have this code where the FancyWrite and FancyRead functions have a dependency on the WriteToFile and ReadFromFile functions. Since these have side effects I’d like to be able to inject them so I can replace them in tests.

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
}

One thing I tried is just put them as parameters into the functions:

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
}

So, this actually works great, but I could see this becoming harder to maintain for more dependencies. I also tried a factory pattern like the following so that the main function doesn’t have to concern itself with building the FancyWrite function. But, the syntax is getting a little hard to read and with even more functions would be hard to maintain.

func FancyWriteFactory(writeToFile func(content []byte) (bool, error)) func() {
	return func() {
		FancyWrite(writeToFile)
	}
}

So next I tried housing the functions as methods in a struct:

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
}

Now, this actually works well and is cleaner. However, I’m worried I am just shoehorning my functions as methods now and something just felt odd about that. I guess I can reason about it because structs are good when you have some state, and I guess I can count the dependencies as state?

Those are the things I tried. So my question is, what is the proper way to do dependency injection in this case when the only reason to put functions as methods is to make them be a collection of dependencies elsewhere?

Thanks!

Hi @talhaguy,

Using interfaces is indeed an easy and straightforward mechanism for dependency injection, see https://appliedgo.net/di.

If you feel there are too many structs involved, you can get rid of the FancyFileModule one, like so:

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
}
2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.