Coder Social home page Coder Social logo

inject's Introduction

Tweet

Documentation Release Build Status Code Coverage

This repository will be archived

After using this library in production for a year, I made some conclusions about library API and really useful features. I don't want to make breaking changes and have a v3 version. It's not that popular.

I put old and new ideas in goava/di.

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/defval/inject/v2

This library follows SemVer strictly.

Tutorial

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

The full tutorial code is available here

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.

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

The function inject.New() parse our constructors, compile dependency graph and return *inject.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 inject.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(
	inject.Provide(NewServer),        // provide http server
	inject.Provide(NewServeMux),       // provide http serve mux
	// endpoints
	inject.Provide(NewOrderController, inject.As(new(Controller))),  // provide order controller
	inject.Provide(NewUserController, inject.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 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

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
inject.Provide(NewServer, inject.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 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.

Visualization

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

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

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

And paste it to graphviz online tool:

Contributing

I will be glad if you contribute to this library. I don't know much English, so contributing to the documentation is very meaningful to me.

inject's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

inject's Issues

Resolving named interfaces as map[<name>]<interface>

Problem

In case, when I have several named implementations of an interface, I need a simple and clean way to get needed implementation by name.

For example, I have two writer types: file and s3.

inject.New(
  inject.Provide(file.NewDestination, inject.As(new(writer.Destination)), inject.WithName("file")),
  inject.Provide(s3.NewDestination, inject.As(new(writer.Destination)), inject.WithName("s3")),
)

And I need to resolve concrete writer by name.

Proposal

For named providers that will be injected as interfaces create map[<name>]<interface> group.

// NewDestinationWriter
func NewDestinationWriter(destinations map[string]writer.Destination) *DestinationWriter {
  return &DestinationWriter{
    destinations: destinations,
  }
}

// DestinationWriter writes data to destination.
type DestinationWriter {
  destinations map[string]writer.Destination
}

// WriteTo writes data to destination by its name.
func (w *DestinationWriter) WriteTo(destination string, data []byte) error  {
  writer, ok := w.destinations[destination]
  if !ok {
    return fmt.Errorf("destination %s not found", destination)
  }

  return writer.WriteBytes(data)
}

Graph visualization

The first version has a visualization feature. Since I change graph implementation, this feature is not implemented now.

  • Constructor provider visualization
  • Interface provider visualization
  • Group provider visualization
  • ParameterBag visualization (?)

Replacing already defined provider

I want to add duplicate type name within an container.providers,and then I can't found replace method of v.2.x yet. that it had in v1.x .

My intent was replace old type with new registered type. and then get new type with Container.Extract .

Container.Invoke() implementation

I have a need to call some functions via container.

container := inject.New(
  // providers
)

func SomeWork(dep1, dep2) (optionalError errror) {
   // stuff
}

container.Invoke(SomeWork)

Check that provider implements interface

Example from my code:

panic: reflect.Set: value of type *adapters.MockBankAdapter is not assignable to type processing.BankService

goroutine 1 [running]:
reflect.Value.assignTo(0x17eeb60, 0x20eb310, 0x16, 0x191d8be, 0xb, 0x181af60, 0xc0004fa1a0, 0x199, 0x20eb310, 0x1802c00)
	/usr/local/Cellar/go/1.12.5/libexec/src/reflect/value.go:2339 +0x437
reflect.Value.Set(0x181af60, 0xc0004fa1a0, 0x194, 0x17eeb60, 0x20eb310, 0x16)
	/usr/local/Cellar/go/1.12.5/libexec/src/reflect/value.go:1473 +0xa8
github.com/defval/inject/internal/graph.(*ProviderNode).Extract(0xc00037cc00, 0x181af60, 0xc0004fa1a0, 0x194, 0x181af60, 0xc00032f760)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/internal/graph/node_provider.go:73 +0x2d0
github.com/defval/inject/internal/graph.(*InterfaceNode).Extract(0xc00033e960, 0x181af60, 0xc0004fa1a0, 0x194, 0xc0004fa1a0, 0x194)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/internal/graph/node_interface.go:65 +0x1bb
github.com/defval/inject/internal/graph.(*ProviderNode).Extract(0xc00037c480, 0x1809900, 0xc000010390, 0x196, 0xc000010390, 0x196)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/internal/graph/node_provider.go:55 +0x170
github.com/defval/inject/internal/graph.(*ProviderNode).Extract(0xc00037c980, 0x1814120, 0xc0004fa180, 0x194, 0x1814120, 0xc00032f9f8)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/internal/graph/node_provider.go:55 +0x170
github.com/defval/inject/internal/graph.(*InterfaceNode).Extract(0xc00033e820, 0x1814120, 0xc0004fa180, 0x194, 0xc0004fa180, 0x194)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/internal/graph/node_interface.go:65 +0x1bb
github.com/defval/inject/internal/graph.(*ProviderNode).Extract(0xc00037c300, 0x18b2040, 0xc000294008, 0x196, 0x18b2001, 0x17b1700)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/internal/graph/node_provider.go:55 +0x170
github.com/defval/inject/internal/graph.(*Storage).Extract(0xc00000f0e0, 0x0, 0x0, 0x18b2040, 0xc000294008, 0x196, 0x20cf3a0, 0x2400d98)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/internal/graph/storage.go:92 +0xf6
github.com/defval/inject.(*Container).Extract(0xc00008cdc0, 0x17798c0, 0xc000294008, 0x0, 0x0, 0x0, 0x0, 0xc)
	/Users/defval/go/pkg/mod/github.com/defval/[email protected]/container.go:72 +0x1e6
main.main()

Cleanup ordering

Cleanup functions must be called in the same order as constructor calls.

Destructors

Add new provide option OnDestruct().

// OnDestruct is a provide option, which modify default providing behavior.
// It adds destructor callback function.
func OnDestruct(fn interface) ProvideOption

Add new container function Destruct(), which destructs all initialized instances in the container.

// Destruct is a container function, that destruct all initialized instances.
func (c *Container) Destruct() error

Example usage:

// StopServer is a callback function that will be called with a container destruction.
func StopServer(server *http.Server) error
container, err := inject.New(
  inject.Provide(NewHTTPServer, inject.OnDestruct(StopServer))
)

if err != nil {
  panic(err)
}

// do stuff

if err = container.Destruct(); err != nil {
  panic(err)
}

TBD:

  • Timeouts

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.