
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