init
function and global variables, excessive complexity of setting up applications, etc.dig
library). struct
in Go) when they are created externally. This contrasts with the anti-pattern of components that themselves form their dependencies during initialization. Let's turn to an example.Server
structure that requires Config
to implement its behavior. As one of the options, the Server can create its own Config structure during initialization. type Server struct { config *Config } func New() *Server { return &Server{ config: buildMyConfigSomehow(), } }
Server
requires access to the Config
. Such information is hidden from the user function.Config
, then along with it we will have to change all those places that cause the corresponding code. Suppose the buildMyConfigSomehow
function now requests an argument. This means that access to this argument is now needed for any call to this function.Config
structure for its testing without dependencies. To test the creation of Config
using arbitrary data (monkey testing), we will have to somehow get into the bowels of the New
function. type Server struct { config *Config } func New(config *Config) *Server { return &Server{ config: config, } }
Server
and Config
structures are created separately from each other. We can use any suitable logic to create a Config
, and then pass the data to the New
function.Config
is an interface, then we can easily conduct mock testing for it. Any argument that allows us to implement our interface, we can pass to the New
function. This simplifies testing the Server
structure with the help of Config
mock objects.Config
structure before we can create a Server
. It is very uncomfortable. Here we have a dependency graph: first you need to create a Config
structure, because Server
depends on it. In real-world applications, such graphs can grow too much, which complicates the logic of creating all the components necessary for the application to work properly.GET
request to /people
. We will consider it in parts. To simplify this example, all our code will be in one package ( main
). In real Go applications, this should not be done. You can find the full code from this example here .Person
structure. It does not implement any behavior, only a few JSON tags are declared. type Person struct { Id int `json:"id"` Name string `json:"name"` Age int `json:"age"` }
Person
structure there are tags Id
, Name
and Age
. And that's all.Config
. Like Person
, this structure has no dependencies. However, unlike Person
, it has a constructor. type Config struct { Enabled bool DatabasePath string Port string } func NewConfig() *Config { return &Config{ Enabled: true, DatabasePath: "./example.db", Port: "8000", } }
Enabled
field determines whether our application will return real data. The DatabasePath
field indicates the path to the database (we use SQlite). The Port
field specifies the port on which our server will run.Config
and returns *sql.D
B. func ConnectDatabase(config *Config) (*sql.DB, error) { return sql.Open("sqlite3", config.DatabasePath) }
PersonRepository
structure. She will be responsible for retrieving information about people from our database and deserializing it into appropriate Person
structures. type PersonRepository struct { database *sql.DB } func (repository *PersonRepository) FindAll() []*Person { rows, _ := repository.database.Query( `SELECT id, name, age FROM people;` ) defer rows.Close() people := []*Person{} for rows.Next() { var ( id int name string age int ) rows.Scan(&id, &name, &age) people = append(people, &Person{ Id: id, Name: name, Age: age, }) } return people } func NewPersonRepository(database *sql.DB) *PersonRepository { return &PersonRepository{database: database} }
PersonRepository
structure requires a database connection. It provides only one function — FindAll
, which uses this connection to return a list of Person
structures that correspond to information in the database.PersonService
structure to create a layer between the HTTP server and PersonRepository
. type PersonService struct { config *Config repository *PersonRepository } func (service *PersonService) FindAll() []*Person { if service.config.Enabled { return service.repository.FindAll() } return []*Person{} } func NewPersonService(config *Config, repository *PersonRepository) *PersonService { return &PersonService{config: config, repository: repository} }
PersonService
depends not only on Config
, but also on PersonRepository
. It contains the FindAll
function, which conditionally calls the PersonRepository
if the application is enabled.Server
structure. It is responsible for executing the HTTP server and sending the corresponding requests to the PersonService
. type Server struct { config *Config personService *PersonService } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/people", s.people) return mux } func (s *Server) Run() { httpServer := &http.Server{ Addr: ":" + s.config.Port, Handler: s.Handler(), } httpServer.ListenAndServe() } func (s *Server) people(w http.ResponseWriter, r *http.Request) { people := s.personService.FindAll() bytes, _ := json.Marshal(people) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(bytes) } func NewServer(config *Config, service *PersonService) *Server { return &Server{ config: config, personService: service, } }
Server
depends on the PersonService
and Config
structures.main()
function in the traditional way. func main() { config := NewConfig() db, err := ConnectDatabase(config) if err != nil { panic(err) } personRepository := NewPersonRepository(db) personService := NewPersonService(config, personRepository) server := NewServer(config, personService) server.Run() }
Config
structure. Then with its help we create a connection to the database. After that, you can create a PersonRepository
structure, and on its basis, a PersonService
structure. Finally, we use all of this to create and launch a Server
.main
function will become more and more complicated. Every time we add dependencies to any of our components, we will have to add logic and re-revise the main
function.dig
library provides us with Provide
and Invoke
functions. The first one is used to add suppliers, the second one is to retrieve completely finished objects from the container.container := dig.New()
Provide
container function. It has one argument: a function that can have any number of arguments (they reflect the dependencies of the component being created), as well as one or two return values ​​(the component provided by the function and, if necessary, an error). container.Provide(func() *Config { return NewConfig() })
Config
. I don't need anything else to create it. ” Now that our container knows how to create a Config
type, we can use it to create other types. container.Provide(func(config *Config) (*sql.DB, error) { return ConnectDatabase(config) })
*sql.DB
To create it, I need Config
. In addition, if necessary, I can return the error. "NewConfig
and ConnectDatabase
functions NewConfig
, we can directly use them as providers for the container. container.Provide(NewConfig) container.Provide(ConnectDatabase)
Invoke
function. The function argument Invoke
is a function with any number of arguments. They are the types that we create our container for. container.Invoke(func(database *sql.DB) { })
*sql.DB
;ConnectDatabase
function;ConnectDatabase
function depends on the type of Config;Config
- the function NewConfig
;NewConfig
has no dependencies, so this function can be called;Config
type resulting from the NewConfig
function is passed to the ConnectDatabase
function;ConnectionDatabase
, type *sql.DB
, is returned to the *sql.DB
the Invoke
function.dig
container works, let's use it to optimize the main function. func BuildContainer() *dig.Container { container := dig.New() container.Provide(NewConfig) container.Provide(ConnectDatabase) container.Provide(NewPersonRepository) container.Provide(NewPersonService) container.Provide(NewServer) return container } func main() { container := BuildContainer() err := container.Invoke(func(server *Server) { server.Run() }) if err != nil { panic(err) } }
error
value returned by the Invoke
function. If any of the providers used by the Invoke
function returns an error, the function will be suspended and returned to the caller.main
. The more our application becomes, the more obvious these advantages will be.PersonRepository
now needs access to the Config
. All we have to do is add Config
as an argument to the NewPersonRepository
constructor. No additional code changes are required.init
mechanisms). In addition, this approach allows us to simplify testing of individual components. Imagine that during testing you create a container and request a complete object. Or that you need to create an object with fictitious implementations of all its dependencies (mock-object). All this is much easier to do with the mechanism of dependency injection.dig
library is a great tool for dependency injection. I think that the Go programmers community should pay more attention to DI and more often use this mechanism in applications.Source: https://habr.com/ru/post/372199/
All Articles