dependencyinjection

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2021 License: MIT Imports: 3 Imported by: 19

README

Dependency injection

Dependency injection for Go programming language.

Dependency injection is one form of the broader technique of inversion of control. It is used to increase modularity of the program and make it extensible.

Examples

type A struct {
	Name string
}

func NewA() *A {
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	name := "A-" + strconv.Itoa(r.Int())
	return &A{Name: ls}
}

services := NewServiceCollection()
services.AddSingleton(NewA)
//serviceCollection.AddSingletonByImplementsAndName("redis-master", NewRedis, new(abstractions.IDataSource))
//serviceCollection.AddTransientByImplements(NewRedisClient, new(redis.IClient))
//serviceCollection.AddTransientByImplements(NewRedisHealthIndicator, new(health.Indicator))
serviceProvider := services.Build()

var env *A
_ = serviceProvider.GetService(&env) // used

How will dependency injection help me?

Dependency injection is one form of the broader technique of inversion of control. It is used to increase modularity of the program and make it extensible.

Contents

Installing

go get -u github.com/yoyofxteam/dependencyinjection@v1.0.0
Providing

To start, we will need to create two fundamental types: http.Server and http.ServeMux. Let's create a simple constructors that initialize it:

// NewServer creates a http server with provided mux as handler.
func NewServer(mux *http.ServeMux) *http.Server {
	return &http.Server{
		Handler: mux,
	}
}

// NewServeMux creates a new http serve mux.
func NewServeMux() *http.ServeMux {
	return &http.ServeMux{}
}

Supported constructor signature:

func([dep1, dep2, depN]) (result, [cleanup, error])

Now let's teach a container to build these types.

import (
  di "github.com/yoyofxteam/dependencyinjection"
)

container := di.New(
	// provide http server
    di.Provide(NewServer),
    // provide http serve mux
    di.Provide(NewServeMux)
)

The function di.New() parse our constructors, compile dependency graph and return *di.Container type for interaction. Container panics if it could not compile.

I think that panic at the initialization of the application and not in runtime is usual.

Extraction

We can extract the built server from the container. For this, define the variable of extracted type and pass variable pointer to Extract function.

If extracted type not found or the process of building instance cause error, Extract return error.

If no error occurred, we can use the variable as if we had built it yourself.

// declare type variable
var server *http.Server
// extracting
err := container.Extract(&server)
if err != nil {
	// check extraction error
}

server.ListenAndServe()

Note that by default, the container creates instances as a singleton. But you can change this behaviour. See Prototypes.

Invocation

As an alternative to extraction we can use Invoke() function. It resolves function dependencies and call the function. Invoke function may return optional error.

// StartServer starts the server.
func StartServer(server *http.Server) error {
    return server.ListenAndServe()
}

container.Invoke(StartServer)
Lazy-loading

Result dependencies will be lazy-loaded. If no one requires a type from the container it will not be constructed.

Interfaces

Inject make possible to provide implementation as an interface.

// NewServer creates a http server with provided mux as handler.
func NewServer(handler http.Handler) *http.Server {
	return &http.Server{
		Handler: handler,
	}
}

For a container to know that as an implementation of http.Handler is necessary to use, we use the option di.As(). The arguments of this option must be a pointer(s) to an interface like new(Endpoint).

This syntax may seem strange, but I have not found a better way to specify the interface.

Updated container initialization code:

container := inject.New(
	// provide http server
	inject.Provide(NewServer),
	// provide http serve mux as http.Handler interface
	inject.Provide(NewServeMux, inject.As(new(http.Handler)))
)

Now container uses provide *http.ServeMux as http.Handler in server constructor. Using interfaces contributes to writing more testable code.

Groups

Container automatically groups all implementations of interface to []<interface> group. For example, provide with inject.As(new(http.Handler) automatically creates a group []http.Handler.

Let's add some http controllers using this feature. Controllers have typical behavior. It is registering routes. At first, will create an interface for it.

// Controller is an interface that can register its routes.
type Controller interface {
	RegisterRoutes(mux *http.ServeMux)
}

Now we will write controllers and implement Controller interface.

OrderController
// OrderController is a http controller for orders.
type OrderController struct {}

// NewOrderController creates a auth http controller.
func NewOrderController() *OrderController {
	return &OrderController{}
}

// RegisterRoutes is a Controller interface implementation.
func (a *OrderController) RegisterRoutes(mux *http.ServeMux) {
	mux.HandleFunc("/orders", a.RetrieveOrders)
}

// Retrieve loads orders and writes it to the writer.
func (a *OrderController) RetrieveOrders(writer http.ResponseWriter, request *http.Request) {
	// implementation
}
UserController
// UserController is a http endpoint for a user.
type UserController struct {}

// NewUserController creates a user http endpoint.
func NewUserController() *UserController {
	return &UserController{}
}

// RegisterRoutes is a Controller interface implementation.
func (e *UserController) RegisterRoutes(mux *http.ServeMux) {
	mux.HandleFunc("/users", e.RetrieveUsers)
}

// Retrieve loads users and writes it using the writer.
func (e *UserController) RetrieveUsers(writer http.ResponseWriter, request *http.Request) {
    // implementation
}

Just like in the example with interfaces, we will use inject.As() provide option.

container := inject.New(
	di.Provide(NewServer),        // provide http server
	di.Provide(NewServeMux),       // provide http serve mux
	// endpoints
	di.Provide(NewOrderController, di.As(new(Controller))),  // provide order controller
	di.Provide(NewUserController, di.As(new(Controller))),  // provide user controller
)

Now, we can use []Controller group in our mux. See updated code:

// NewServeMux creates a new http serve mux.
func NewServeMux(controllers []Controller) *http.ServeMux {
	mux := &http.ServeMux{}

	for _, controller := range controllers {
		controller.RegisterRoutes(mux)
	}

	return mux
}

Advanced features

Named definitions

In some cases you have more than one instance of one type. For example two instances of database: master - for writing, slave - for reading.

First way is a wrapping types:

// MasterDatabase provide write database access.
type MasterDatabase struct {
	*Database
}

// SlaveDatabase provide read database access.
type SlaveDatabase struct {
	*Database
}

Second way is a using named definitions with di.WithName() provide option:

// provide master database
di.Provide(NewMasterDatabase, di.WithName("master"))
// provide slave database
di.Provide(NewSlaveDatabase, di.WithName("slave"))

If you need to extract it from container use di.Name() extract option.

var db *Database
container.Extract(&db, di.Name("master"))

If you need to provide named definition in other constructor use di.Parameter with embedding.

// ServiceParameters
type ServiceParameters struct {
	di.Parameter
	
	// use `di` tag for the container to know that field need to be injected.
	MasterDatabase *Database `di:"master"`
	SlaveDatabase *Database  `di:"slave"`
}

// NewService creates new service with provided parameters.
func NewService(parameters ServiceParameters) *Service {
	return &Service{
		MasterDatabase:  parameters.MasterDatabase,
		SlaveDatabase: parameters.SlaveDatabase,
	}
}
Optional parameters

Also di.Parameter provide ability to skip dependency if it not exists in container.

// ServiceParameter
type ServiceParameter struct {
	di.Parameter
	
	Logger *Logger `di:"optional"`
}

Constructors that declare dependencies as optional must handle the case of those dependencies being absent.

You can use naming and optional together.

// ServiceParameter
type ServiceParameter struct {
	di.Parameter
	
	StdOutLogger *Logger `di:"stdout"`
	FileLogger   *Logger `di:"file,optional"`
}
Parameter Bag

If you need to specify some parameters on definition level you can use inject.ParameterBag provide option. This is a map[string]interface{} that transforms to di.ParameterBag type.

// Provide server with parameter bag
di.Provide(NewServer, di.ParameterBag{
	"addr": ":8080",
})

// NewServer create a server with provided parameter bag. Note: use di.ParameterBag type.
// Not inject.ParameterBag.
func NewServer(pb di.ParameterBag) *http.Server {
	return &http.Server{
		Addr: pb.RequireString("addr"),
	}
}
Prototypes

If you want to create a new instance on each extraction use di.Prototype() provide option.

di.Provide(NewRequestContext, di.Prototype())

todo: real use case

Cleanup

If a provider creates a value that needs to be cleaned up, then it can return a closure to clean up the resource.

func NewFile(log Logger, path Path) (*os.File, func(), error) {
    f, err := os.Open(string(path))
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() {
        if err := f.Close(); err != nil {
            log.Log(err)
        }
    }
    return f, cleanup, nil
}

After container.Cleanup() call, it iterate over instances and call cleanup function if it exists.

container := di.New(
	// ...
    di.Provide(NewFile),
)

// do something
container.Cleanup() // file was closed

Cleanup now work incorrectly with prototype providers.

Visualization

Dependency graph may be presented via (Graphviz). For it, load string representation:

var graph *di.di.Graph
if err = container.Extract(&graph); err != nil {
    // handle err
}

dotGraph := graph.String() // use string representation

And paste it to graphviz online tool:

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Container

type Container struct {
	// contains filtered or unexported fields
}

Container is a dependency injection container.

func New

func New(options ...Option) *Container

New creates a new container with provided options.

func (*Container) Cleanup

func (c *Container) Cleanup()

Cleanup cleanup container.

func (*Container) Extract

func (c *Container) Extract(target interface{}, options ...ExtractOption) (err error)

Extract populates given target pointer with type instance provided in the container.

var server *http.Server
if err = container.Extract(&server); err != nil {
  // extract failed
}

If the target type does not exist in a container or instance type building failed, Extract() returns an error. Use ExtractOption for modifying the behavior of this function.

func (*Container) Invoke

func (c *Container) Invoke(fn interface{}) error

Invoke invokes custom function. Dependencies of function will be resolved via container.

type DefaultServiceProvider

type DefaultServiceProvider struct {
	// contains filtered or unexported fields
}

func (DefaultServiceProvider) GetGraph

func (d DefaultServiceProvider) GetGraph() string

func (DefaultServiceProvider) GetService

func (d DefaultServiceProvider) GetService(refObject interface{}) (err error)

func (DefaultServiceProvider) GetServiceByName

func (d DefaultServiceProvider) GetServiceByName(refObject interface{}, name string) (err error)

func (DefaultServiceProvider) InvokeService

func (d DefaultServiceProvider) InvokeService(fn interface{}) error

type ExtractOption

type ExtractOption interface {
	// contains filtered or unexported methods
}

ExtractOption modifies default extract behavior. See inject.Name().

func Name

func Name(name string) ExtractOption

Name specify definition name.

type IServiceProvider

type IServiceProvider interface {
	GetService(refObject interface{}) error
	GetServiceByName(refObject interface{}, name string) error
	GetGraph() string
	InvokeService(fn interface{}) error
}

type IServiceProviderFactory

type IServiceProviderFactory interface {
	CreateServiceProvider() IServiceProvider
}

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures container. See inject.Provide(), inject.Bundle(), inject.Replace().

func Bundle

func Bundle(options ...Option) Option

Bundle group together container options.

accountBundle := inject.Bundle(
  inject.Provide(NewAccountController),
  inject.Provide(NewAccountRepository),
)

authBundle := inject.Bundle(
  inject.Provide(NewAuthController),
  inject.Provide(NewAuthRepository),
)

container, _ := New(
  accountBundle,
  authBundle,
)

func Provide

func Provide(provider interface{}, options ...ProvideOption) Option

Provide returns container option that explains how to create an instance of a type inside a container.

The first argument is the constructor function. A constructor is a function that creates an instance of the required type. It can take an unlimited number of arguments needed to create an instance - the first returned value.

func NewServer(mux *http.ServeMux) *http.Server {
  return &http.Server{
    Handle: mux,
  }
}

Optionally, you can return a cleanup function and initializing error.

func NewServer(mux *http.ServeMux) (*http.Server, cleanup func(), err error) {
  if time.Now().Day = 1 {
    return nil, nil, errors.New("the server is down on the first day of a month")
  }

  server := &http.Server{
    Handler: mux,
  }

  cleanup := func() {
    _ = server.Close()
  }

  return server, cleanup, nil
}

Other function signatures will cause error.

type ParameterBag

type ParameterBag map[string]interface{}

ParameterBag is a provider parameter bag. It stores a construction parameters. It is a alternative way to configure type.

inject.Provide(NewServer, inject.ParameterBag{
  "addr": ":8080",
})

NewServer(pb inject.ParameterBag) *http.Server {
  return &http.Server{
    Addr: pb.RequireString("addr"),
  }
}

type ProvideOption

type ProvideOption interface {
	// contains filtered or unexported methods
}

ProvideOption modifies default provide behavior. See inject.WithName(), inject.As(), inject.Prototype().

func As

func As(ifaces ...interface{}) ProvideOption

As specifies interfaces that implement provider instance. Provide with As() automatically checks that constructor result implements interface and creates slice group with it.

Provide(&http.ServerMux{}, inject.As(new(http.Handler)))

var handler http.Handler
container.Extract(&handler) // extract as interface

var handlers []http.Handler
container.Extract(&handlers) // extract group

func Prototype

func Prototype() ProvideOption

Prototype modifies Provide() behavior. By default, each type resolves as a singleton. This option sets that each type resolving creates a new instance of the type.

Provide(&http.Server{], inject.Prototype())

var server1 *http.Server
var server2 *http.Server
container.Extract(&server1, &server2)

func WithName

func WithName(name string) ProvideOption

WithName sets string identifier for provided value.

inject.Provide(&http.Server{}, inject.WithName("first"))
inject.Provide(&http.Server{}, inject.WithName("second"))

container.Extract(&server, inject.Name("second"))

type ServiceCollection

type ServiceCollection struct {
	// contains filtered or unexported fields
}

func NewServiceCollection

func NewServiceCollection() *ServiceCollection

func (*ServiceCollection) AddServiceDescriptor

func (sc *ServiceCollection) AddServiceDescriptor(sd *ServiceDescriptor)

Singleton Scoped Transient

func (*ServiceCollection) AddSingleton

func (sc *ServiceCollection) AddSingleton(provider interface{})

func (*ServiceCollection) AddSingletonAndName

func (sc *ServiceCollection) AddSingletonAndName(name string, provider interface{})

func (*ServiceCollection) AddSingletonByImplements

func (sc *ServiceCollection) AddSingletonByImplements(provider interface{}, implements interface{})

func (*ServiceCollection) AddSingletonByImplementsAndName

func (sc *ServiceCollection) AddSingletonByImplementsAndName(name string, provider interface{}, implements interface{})

func (*ServiceCollection) AddSingletonByName

func (sc *ServiceCollection) AddSingletonByName(name string, provider interface{})

func (*ServiceCollection) AddSingletonByNameAndImplements

func (sc *ServiceCollection) AddSingletonByNameAndImplements(name string, provider interface{}, implements interface{})

func (*ServiceCollection) AddTransient

func (sc *ServiceCollection) AddTransient(provider interface{})

func (*ServiceCollection) AddTransientByImplements

func (sc *ServiceCollection) AddTransientByImplements(provider interface{}, implements interface{})

func (*ServiceCollection) AddTransientByName

func (sc *ServiceCollection) AddTransientByName(name string, provider interface{})

func (ServiceCollection) Build

type ServiceDescriptor

type ServiceDescriptor struct {
	Name       string
	Provider   interface{}
	Implements interface{}
	Lifetime   ServiceLifetime
}

func NewServiceDescriptor

func NewServiceDescriptor(name string, provider interface{}, implements interface{}, lifetime ServiceLifetime) *ServiceDescriptor

func NewServiceDescriptorByImplements

func NewServiceDescriptorByImplements(provider interface{}, implements interface{}, lifetime ServiceLifetime) *ServiceDescriptor

func NewServiceDescriptorByName

func NewServiceDescriptorByName(name string, provider interface{}, lifetime ServiceLifetime) *ServiceDescriptor

func NewServiceDescriptorByProvider

func NewServiceDescriptorByProvider(provider interface{}, lifetime ServiceLifetime) *ServiceDescriptor

type ServiceLifetime

type ServiceLifetime int32
const (
	Singleton ServiceLifetime = 0
	Scoped    ServiceLifetime = 1
	Transient ServiceLifetime = 2
)

Directories

Path Synopsis
di

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL