inject

package module
v2.0.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Nov 29, 2019 License: MIT Imports: 1 Imported by: 4

README

Tweet

Documentation Release Build Status Code Coverage

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.

Disclaimer

I use v2 version in production, but it in a pre-release state. I need time to finish documentation and fix possible bugs.

You can see latest v1 here.

Contents

Installing

go get -u github.com/defval/inject/v2

Getting Started

Tutorial

Let's learn to use inject by example. We will code a simple application that processes HTTP requests.

Providing

To start, we will need to create two fundamental types: server and router. We will create a simple constructors that initialize this.

Our constructors:

// 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.

// Collect container parameters, build and compile container.
container := inject.New(
	inject.Provide(NewServer),  // provide http server
	inject.Provide(NewServeMux) // provide http serve mux
)

The function New() parse our constructors and compile dependency graph.

Container panics if it could not compile. I think that panic at the initialization of the application and not in runtime is usual.

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

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.

Interfaces and groups

Let's add some endpoints to our application.

// NewAuthEndpoint creates a auth http endpoint.
func NewAuthEndpoint() *AuthEndpoint {
	return &AuthEndpoint{}
}

// AuthEndpoint is a http endpoint for auth.
type AuthEndpoint struct {}

// Login tries authenticate a user and write result using the writer.
func (a *AuthEndpoint) Login(writer http.ResponseWriter, request *http.Request) {
	// implementation
}
// NewUserEndpoint creates a user http endpoint.
func NewUserEndpoint() *UserEndpoint {
	return &UserEndpoint{}
}

// UserEndpoint is a http endpoint for user.
type UserEndpoint struct {}

// Retrieve loads of user data and writes it using a writer.
func (e *UserEndpoint) Retrieve(writer http.ResponseWriter, request *http.Request) {
    // implementation
}

Change *http.ServeMux constructor for register endpoint routes.

// NewServeMux creates a new http serve mux and register user endpoint.
func NewServeMux(auth *AuthEndpoint, users *UserEndpoint) *http.ServeMux {
	mux := &http.ServeMux{}
	mux.HandleFunc("/user", users.Retrieve)
	mux.HandleFunc("/auth", auth.Login)
	return mux
}

Updated container initialization code:

container := inject.New(
	inject.Provide(NewServer),        // provide http server
	inject.Provide(NewServeMux)       // provide http serve mux
	// endpoints
	inject.Provide(NewUserEndpoint),  // provide user endpoint
	inject.Provide(NewAuthEndpoint),  // provide auth endpoint
)

Container knows that building mux requires AuthEndpoint and UserEndpoint. And construct it for our *http.ServeMux on demand.

Frequently, dependency injection is used to bind a concrete implementation for an interface.

Our endpoints have typical behavior. It is registering routes. Let's create an interface for it:

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

And implement it:

// RegisterRoutes is a Endpoint interface implementation.
func (a *AuthEndpoint) RegisterRoutes(mux *http.ServeMux) {
	mux.HandleFunc("/login", a.Login)
}

// RegisterRoutes is a Endpoint interface implementation.
func (e *UserEndpoint) RegisterRoutes(mux *http.ServeMux) {
	mux.HandleFunc("/user", e.Retrieve)
}

Now we can provide endpoint implementation as Endpoint interface. For a container to know that as an implementation of Endpoint is necessary to use, we use the option inject.As(). The argument of this option must be a pointer to an interface like new(Endpoint). This syntax may seem strange, but I have not found a better way to specify the interface.

container := inject.New(
	inject.Provide(NewServer),        // provide http server
	inject.Provide(NewServeMux)       // provide http serve mux
	// endpoints
	inject.Provide(NewUserEndpoint, inject.As(new(Endpoint))),  // provide user endpoint
	inject.Provide(NewAuthEndpoint, inject.As(new(Endpoint))),  // provide auth endpoint
)

Container groups all implementation of interface to []<interface> group. For example, inject.As(new(Endpoint) automatically creates a group []Endpoint.

We can use it in our mux. See updated code:

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

	for _, endpoint := range endpoints {
		endpoint.RegisterRoutes(mux)
	}

	return mux
}

If you have only one implementation of an interface, then you can use the interface instead of the implementation. It contributes to writing more testable code and not contrary to "return structs, accept interfaces" principle.

Inversion of control

TBD

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 inject.WithName() provide option:

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

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

var db *Database
container.Extract(&db, inject.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

TBD

Prototypes

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

inject.Provide(NewRequestContext, inject.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 := inject.New(
	// ...
    inject.Provide(NewFile),
)

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

Cleanup now work incorrectly with prototype providers.

Contributing

Documentation

Overview

Package inject make your dependency injection easy. Container allows you to inject dependencies into constructors or structures without the need to have specified each argument manually.

Provide

First of all, when creating a new container, you need to describe how to create each instance of a dependency. To do this, use the container option inject.Provide().

container, err := New(
	Provide(NewDependency),
	Provide(NewAnotherDependency)
)

func NewDependency(dependency *pkg.AnotherDependency) *pkg.Dependency {
	return &pkg.Dependency{
		dependency: dependency,
	}
}

func NewAnotherDependency() (*pkg.AnotherDependency, error) {
	if dependency, err = initAnotherDependency(); err != nil {
		return nil, err
	}

	return dependency, nil
}

Now, container knows how to create *pkg.Dependency and *pkg.AnotherDependency. For advanced providing see inject.Provide() and inject.ProvideOption documentation.

Extract

After building a container, it is easy to get any previously provided type. To do this, use the container's Extract() method.

var anotherDependency *pkg.AnotherDependency
if err = container.Extract(&anotherDependency); err != nil {
	// handle error
}

The container collects a dependencies of *pkg.AnotherDependency, creates its instance and places it in a target pointer. For advanced extraction see Extract() and inject.ExtractOption 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.

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 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.

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
container.Extract(&server1, &server1)

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"))

Directories

Path Synopsis
di

Jump to

Keyboard shortcuts

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