Dependency Injection¶
Basic Information¶
Dependency injection is a design pattern used in software engineering to achieve inversion of control between classes and their dependencies. In simpler terms, it’s a technique where a class's dependencies are provided from the outside rather than created within the class itself. This approach helps decouple components, promoting easier testing, maintainability, and flexibility in your code.
Dependencies can be provided by components and applications. Special methods within these entities are responsible for this. For example:
package application
import (
"github.com/componego/componego"
)
type Application struct {
}
// ...
func (a *Application) ApplicationDependencies() ([]componego.Dependency, error) {
return []componego.Dependency{
func() SomeService {
return &someService{}
},
// ...
}, nil
}
var (
_ componego.Application = (*Application)(nil)
_ componego.ApplicationDependencies = (*Application)(nil)
)
package component
import (
"github.com/componego/componego"
)
type Component struct {
}
// ...
func (c *Component) ComponentDependencies() ([]componego.Dependency, error) {
return []componego.Dependency{
func() SomeService {
return &someService{}
},
// ...
}, nil
}
var (
_ componego.Component = (*Component)(nil)
_ componego.ComponentDependencies = (*Component)(nil)
)
Now you can use the provided object in your application.
It is recommended to use constructors to create dependencies.
Dependency Constructors¶
Pay attention to the following code example and the possible variations of what the constructor might look like:
func (a *Application) ApplicationDependencies() ([]componego.Dependency, error) {
return []componego.Dependency{
NewProductRepository,
// ...
}, nil
}
// ...
- A constructor returns a struct as a pointer:
- A constructor returns a struct as an interface:
- A constructor can return an error as the latest value:
- A constructor can accept an unlimited number of dependencies:
- You can even do things like this:
Note
Constructors can accept and return an unlimited number of dependencies. However, they must be presented as pointers.
It is also recommended to use interfaces, as it can be convenient in some cases.
Like any entity in the framework, the constructor is thread-safe.
Another way is to represent the dependency directly as an object:
func (a *Application) ApplicationDependencies() ([]componego.Dependency, error) {
return []componego.Dependency{
&ProductRepository{},
// ...
}, nil
}
// ...
Note
Loops between dependencies are not allowed. If a loop occurs, you will receive an error message when starting the application.
Note
If the provided object implements the io.Closer interface, the Close() function will be called when the application stops.
Access to Dependencies¶
Dependencies can be obtained in several ways. The easiest way is to use the environment.
Invoke¶
This method accepts a function as an argument, which can utilize any dependencies provided in any components or application.
_, err := env.DependencyInvoker().Invoke(func(service SomeService, repository SomeRepository) {
// ...
})
_, err := env.DependencyInvoker().Invoke(func(service SomeService) error {
// ...
return service.Action()
})
returnValue, err := env.DependencyInvoker().Invoke(func(service SomeService) int {
// ...
return service.Action()
})
// or
returnValue, err := env.DependencyInvoker().Invoke(func(service SomeService) (int, error) {
// ...
return service.Action()
})
Note
Since the return type is any, you can use a helper to obtain the correct type:
package example
import (
"github.com/componego/componego"
"github.com/componego/componego/impl/environment/managers/dependency"
)
func GetValue(env componego.Environment) (int, error) {
intValue, err := dependency.Invoke[int](SomeFunction, env)
// intValue := dependency.InvokeOrPanic[int](SomeFunction, env)
return intValue, err
}
You can also obtain an object for dependency injection within any function:
_, err := di.Invoke(func(di componego.DependencyInvoker, service SomeService) (any, error) {
// ...
return di.Invoke(service.Action)
})
Populate¶
This function populates a variable that is a pointer.
Note
The type of the variable must exactly match the requested type.
Also, note the pointer and pointer dereferences in the example above. It is expected that *Service type has been provided for dependencies.
The difference between functions Populate and Invoke is that the first function can only accept a struct because only a struct can be a pointer. At the same time, the second function can accept arguments of any type included in the list of allowed types for dependencies.
PopulateFields¶
Populate fills only a variable, but more often, you need to fill fields in a struct. For example:
type Service struct {
dbProvider database.Provider `componego:"inject"`
}
// ...
service := &Service{}
err := env.DependencyInvoker().PopulateFields(service)
If an error occurs, the method will return it.
Default Dependencies¶
Each application has a set of standard dependencies through which you can access various functions of the application. The table below shows these dependencies:
Variable | Description |
---|---|
env componego.Environment | access to the environment |
app componego.Application | returns the current application |
appIO componego.ApplicationIO | access to the application IO |
di componego.DependencyInvoker | returns the dependency invoker |
config componego.ConfigProvider | provides access to configuration |
These are objects returned by the environment through its methods.
Note
Although you can get context through the environment, you cannot get context through dependencies. Use the environment directly to obtain the application context.
Note
Standard dependencies cannot be rewritten. You must use driver options if you want to modify them.
Rewriting Dependencies¶
Rewriting is one of the main features of the framework. Here’s an example of how you can rewrite dependencies:
func (a *Application) ApplicationDependencies() ([]componego.Dependency, error) {
return []componego.Dependency{
func() SomeService {
return &someService1{}
},
func() SomeService {
return &someService2{}
},
// ...
}, nil
}
// ...
The return types must match for rewriting rules to apply. This is the only condition. Constructors can accept any dependency, but the return types must match for rewriting to work.
If you try to return a type that was not returned in the constructors above, you will receive an error.
Note
The only exception is the last type returned, if that type is an error.
Rewriting dependencies is one of the key elements in creating mocks using this framework.
Remember that according to the documentation about the order of initialization of elements in the framework, method ApplicationDependencies is called after the same function for components (ComponentDependencies). This means that you can rewrite dependencies in your application that were declared in components. You can also rewrite dependencies in components that were added in parent components.