inj

package module
v0.0.0-...-9250637 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2016 License: MIT Imports: 3 Imported by: 9

README

Build Status Coverage Status GoDoc Example YHP Slashie

Are you troubled by dependency configuration issues in the middle of the night? Do you experience feelings of dread at the code boundaries of your application or during functional testing? Have you or your team ever used a global variable, exported package-level object or a build constraint hack? If the answer is yes, then don't wait another minute. Pick up your terminal and go get github.com/yourheropaul/inj today.

Inject yourself before you wreck yourself.

What is this thing?

inj provides reflection-based dependency injection for Go structs and functions. Some parts of it will be familiar to anyone who's ever used facebookgo/inject; others bear a passing similarity to dependency injection in Angular.js. It's designed for medium to large applications, but it works just fine for small apps too. It's especially useful if your project is is BDD/TDD-orientated.

Dependency injection is boring and hard to maintain. I want something that works out of the box.

inj may well be for you. A good, idiomatic Go package is a simple, clearly laid out, well-tested Go package – and inj is, or aspires to be, all of those things. It works well and it works intuitively. There's only one, optional configuration option (whether or not to use a global variable inside the package) and there's very little to think about once you're set up.

How do I use it?

A simple and unrealistically trivial example is this:

package main

import (
	"fmt"

	"github.com/yourheropaul/inj"
)

type ServerConfig struct {
	Port int    `inj:""`
	Host string `inj:""`
}

func main() {
	config := ServerConfig{}
	inj.Provide(&config, 6060, "localhost")

	// The struct fields have now been set by the graph, and
	// this will print "localhost:6060"
	fmt.Printf("%s:%d", config.Host, config.Port)
}

There's a full explanation for this basic example in the Godoc. Obviously this example is trivial in the extreme, and you'd probably never use the the package in that way. The easiest way to understand inj for real-world applications is to refer to the example application in this repository. The API is small, and everything in the core API is demonstrated there.

Dependency injection is great and everything, but I really want to be able to pull data directly from external services, not just the object graph.

You mean you want to read from a JSON or TOML config file, and inject the values into Go objects directly? Maybe you'd like to pull values from a DynamoDB instance and insert them into Go struct instances with almost zero code overhead?

That's what's inj is designed for! And what's more, intrepid programmer Adrian Duke has already done the leg work for you in his fantastic configr package – see his readme for brief instructions.

I want absolutely, positively no globals in my application. None. Can I do that with this package?

Of course, and it couldn't be easier! Just compile your application with the tag noglobals, and the package-level API functions (including the one package-level variable they use) won't be included. You can create a new graph for your application by calling inj.NewGraph(), which has the same functional interface as the package API.

This whole thing sounds too useful to be true

I appreciate your skepticism, so let's gather some data. There are two things you need to be aware of when using inj.

The first is that the application graph is one dimensional and indexed by type. That means you can't call inj.Provide(someIntValue,someOtherIntValue) and expect both integers to be in the graph – the second will override the first. Other dependency injection approaches allow for named and private depdendencies, but that has been sacrified here so that inj.Inject() could exist in a consistent way. When there's a choice, inj errs on the side of simplicity, consistency and idiomatic implementation over complexity and magic.

The second consideration is execution speed. Obviously, calling inj.Inject(fn) is slower than calling fn() directly. In Go 1.4, with a medium-sized graph, it takes about 350 times longer to execute the call; in Go 1.5 rc1, it's about 240 times. If those numbers seem high, it's because they are. The impact on an application is measurable, but for most purposes negligible.

If the average execution time of a pure Go function is around 4 nanoseconds (as it is in my tests) then the execution time of inj.Inject() will be somewhere between 900 and 1,400 nanoseconds. Or in more useful terms, 0.0014 milliseconds (which is 1.4e-6 seconds). If your application is built for speed, then you will need to be judicious in your use of inj.Inect(). Even if speed isn't a concern, it's generally not a good idea to nest injection calls, or put them in loops.

Finally, inj.Provide() is fairly slow, but it's designed to executed at runtime only. There are benchmark tests in the package if you want to see how it performs on your system.

But how do I use it?

Seriously? I just explained that a minute ago. Maybe look at the example application or the Godoc.

Documentation

Overview

Package inj provides reflection-based dependency injection for structs and functions. Some parts of it will be familiar to anyone who's ever used https://github.com/facebookgo/inject; others bear a passing similarity to depdendency injection in Angular.js. It's designed for medium to large applications, but it works just fine for small apps too. It's especially useful if your project is is BDD/TDD-orientated.

The essential premise of the package is that of object graphs. An object graph is just an index of objects organised by their explicitly nominated relationships. Depending on the application, a graph can be very simple - struct A depends on struct B - or extremely complicated. Package inj aims to simplify the process of creating and maintaining a graph of any complexity using out-of-the-box features of the Go language, and to povide easy access to the objects in the graph.

A simple and unrealistically trivial example is this:

package main

import (
    "fmt"
    "github.com/yourheropaul/inj"
)

type ServerConfig struct {
    Port int    `inj:""`
    Host string `inj:""`
}

func main() {
    config := ServerConfig{}
    inj.Provide(&config, 6060, "localhost")

    // The struct fields have now been set by the graph, and
    // this will print "localhost:6060"
    fmt.Printf("%s:%d",config.Host,config.Port)
}

To understand what's happening there, you have to perform a bit of counter-intuitive reasoning. Inj has a global graph (which is optional, and can be disabled with the noglobals build tag) that is accessed through the three main API functions – inj.Provide(), inj.Assert() and inj.Inject(). The first and most fundamental of those functions - inj.Provide() - inserts anything passed to it into the graph, and then tries to wire up any dependency requirements.

Before we get on to what dependency requirements are, it's important to know how a graph works. It is, in its simplest form, a map of types to values. In the example above, the graph - after inj.Provide() is called - will have three entries:

[ServerConfig] => (a pointer to the config variable)
[int] => 6060
[string] => "localhost"

There can only be one entry for each type in a graph, but since Go allows new types to be created arbitrarly, that's not very much of a problem for inj. For example, if I wanted a second string-like type to be stored in the graph, I could simply type one:

package main

import (
    "github.com/yourheropaul/inj"
)

type MyString string

func main() {
    var basicString string = "this is a 'normal' string"
    var myString MyString = "this is a typed string"
    inj.Provide(basicString,myString)
}

In that example, the graph would now contain two separate, stringer entities:

[string] => "this is a 'normal' string"
[MyString] => "this is a typed string"

Back to depdendency requirements. The ServerConfig struct above has two, indicated by the inj:"" struct field tags (advanced usage of the package makes use of values in the tags, but we can ignore that for now). At the end of the inj.Provide() call, the graph is wired up – which essentially means finding values for all of the dependency requirements by type. The Port field of the ServerConfig struct requires and int, and the graph has one, so it's assigned; the Host field requires as string, and that can be assigned from the graph too.

Obviously these examples are trivial in the extreme, and you'd probably never use the inj package in that way. The easiest way to understand the package for real-world applications is to refer to the example application: https://github.com/yourheropaul/inj/tree/master/example.

For more general information, see the Wikipedia article for a technical breakdown of dependency injection: https://en.wikipedia.org/wiki/Dependency_injection.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddDatasource

func AddDatasource(ds ...interface{}) error

Add any number of Datasources, DatasourceReaders or DatasourceWriters to the graph. Returns an error if any of the supplied arguments aren't one of the accepted types.

Once added, the datasources will be active immediately, and the graph will automatically re-Provide itself, so that any depdendencies that can only be met by an external datasource will be wired up automatically.

func Assert

func Assert() (valid bool, errors []string)

Make sure that all provided dependencies have their requirements met, and return a list of errors if they haven't. A graph is never really finalised, so Provide() and Assert() can be called any number of times.

func Inject

func Inject(fn interface{}, args ...interface{})

Given a function, call it with arguments assigned from the graph. Additional arguments can be provided for the sake of utility.

Inject() will panic if the provided argument isn't a function, or if the provided function accepts variadic arguments (because that's not currently supported in the scope of inj).

func Provide

func Provide(inputs ...interface{}) error

Insert zero or more objected into the graph, and then attempt to wire up any unmet dependencies in the graph.

As explained in the main documentation (https://godoc.org/github.com/yourheropaul/inj), a graph consists of what is essentially a map of types to values. If the same type is provided twice with different values, the *last* value will be stored in the graph.

func SetGrapher

func SetGrapher(g Grapher)

Set a specific grapher instance, which will replace the global graph.

Types

type Datasource

type Datasource interface {
	DatasourceReader
	DatasourceWriter
}

A Datasource is any external interface that provides an interface given a string key. It's split into two fundamental components: the DatasourceReader and the DatasourceWriter. For the purposes of automatic configuration via dependency injection, a DatasourceReader is all that's required.

Datasource paths are supplied by values in structfield tags (which are empty for normal inj operation). Here's an example of a struct that will try to fulfil its dependencies from a datasource:

type MyStruct stuct {
    SomeIntVal int `inj:"some.path.in.a.datasource"`
}

When trying to connect the dependency on an instance of the struct above, inj will poll any available DatasourceReaders in the graph by calling their Read() function with the string argument "some.path.in.a.datasource". If the function doesn't return an error, then the resultant value will be used for the dependency injection.

A struct tag may contain multiple datasource paths, separated by commas. The paths will be polled in order of their appearance in the code, and the value from the first DatasourceReader that doesn't return an error on its Read() function will be used to meet the dependency.

DatasourceWriters function in a similar way: when a value is set on an instance of a struct via inj's dependency injection, any associated DatasourceWriters' Write() functions are called for each datasource path.

type DatasourceReader

type DatasourceReader interface {
	Read(string) (interface{}, error)
}

A datasource reader provides an object from a datasource, identified by a given string key. Refer to the documentation for the Datasource interface for more information.

type DatasourceWriter

type DatasourceWriter interface {
	Write(string, interface{}) error
}

A datasource reader sends an object to a datasource, identified by a given string key. Refer to the documentation for the Datasource interface for more information.

type Grapher

type Grapher interface {
	Provide(inputs ...interface{}) error
	Inject(fn interface{}, args ...interface{})
	Assert() (valid bool, errors []string)
	AddDatasource(...interface{}) error
}

A Grapher is anything that can represent an application graph

func GetGrapher

func GetGrapher() Grapher

Fetch the current grapher instance (in other words, get the global graph)

func NewGraph

func NewGraph(providers ...interface{}) Grapher

Create a new instance of a graph with allocated memory

Directories

Path Synopsis
Package inj/example is a demonstration the inj package.
Package inj/example is a demonstration the inj package.

Jump to

Keyboard shortcuts

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