Skip to content

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
}

// ...

  1. A constructor returns a struct as a pointer:
    func NewProductRepository() *ProductRepository {
        return &ProductRepository{}
    }
    
  2. A constructor returns a struct as an interface:
    func NewProductRepository() ProductRepository {
        return &productRepository{}
    }
    
  3. A constructor can return an error as the latest value:
    func NewProductRepository() (ProductRepository, error) {
        return &productRepository{}, nil
    }
    
  4. A constructor can accept an unlimited number of dependencies:
    func NewProductRepository(db * database.Provider) ProductRepository {
        return &productRepository{
            db: db,
        }
    }
    
  5. You can even do things like this:
    func NewProductRepository(di componego.DependencyInvoker) (ProductRepository, error) {
        repo := &productRepository{}
        return repo, di.PopulateFields(repo)
    }
    

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) {
    // ...
})
The function may also return an error as the last return value:
_, err := env.DependencyInvoker().Invoke(func(service SomeService) error {
    // ...
    return service.Action()
})
The invoked function can also return a value:
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)
})
However, in this case, you could use closures.

Populate

This function populates a variable that is a pointer.

var service *Service
err := env.DependencyInvoker().Populate(&service)

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)
This method fills only those fields that have the special tag shown in the example. All other fields are ignored. Fields can be private or public. The field type can be any one.

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
}

// ...
In this case, the second service will be used because it is defined after the constructor of the first service.

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.