From 6b5ffa5d383f4dc51360fa69b9799043a8d52d7f Mon Sep 17 00:00:00 2001 From: Cililing <36789066+Cililing@users.noreply.github.com> Date: Mon, 21 Sep 2020 19:39:57 +0200 Subject: [PATCH] Add standalone container (#18) * Delete container var. Use type Container instead. * Sort methods in container. * Fix tests. * Move container definition to the pkg directory. Create default container instance. * Update readme. Co-authored-by: Przemyslaw Materna --- README.md | 12 ++ container.go | 118 ++-------------- pkg/container/container.go | 131 ++++++++++++++++++ .../container/container_test.go | 66 ++++----- 4 files changed, 191 insertions(+), 136 deletions(-) create mode 100644 pkg/container/container.go rename container_test.go => pkg/container/container_test.go (74%) diff --git a/README.md b/README.md index 2ed1a30..d64bc3c 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,18 @@ container.Singleton(func(c Config) Database { Notice: You can only resolve the dependencies in a binding resolver function that has already bound. +### Standalone instance + +Container works without any initialization keeping your bindings in the default instance. Sometimes you may want to create a standalone instance for a part of application. If so, create a new instance: + +```go +c := container.NewContainer() // returns container.Container +c.Singleton(binding) +c.Make(&resolver) +``` + +The rest stays the same. The default container is still available. + ### Usage Tips #### Performance diff --git a/container.go b/container.go index d315876..009f5e0 100644 --- a/container.go +++ b/container.go @@ -3,122 +3,32 @@ package container import ( - "reflect" + internal "github.com/golobby/container/pkg/container" ) -// invoke will call the given function and return its returned value. -// It only works for functions that return a single value. -func invoke(function interface{}) interface{} { - return reflect.ValueOf(function).Call(arguments(function))[0].Interface() +func NewContainer() internal.Container { + return make(internal.Container) } -// binding keeps a binding resolver and instance (for singleton bindings). -type binding struct { - resolver interface{} // resolver function - instance interface{} // instance stored for singleton bindings -} - -// resolve will return the concrete of related abstraction. -func (b binding) resolve() interface{} { - if b.instance != nil { - return b.instance - } - - return invoke(b.resolver) -} - -// container is the IoC container that will keep all of the bindings. -var container = map[reflect.Type]binding{} - -// bind will map an abstraction to a concrete and set instance if it's a singleton binding. -func bind(resolver interface{}, singleton bool) { - resolverTypeOf := reflect.TypeOf(resolver) - if resolverTypeOf.Kind() != reflect.Func { - panic("the resolver must be a function") - } - - for i := 0; i < resolverTypeOf.NumOut(); i++ { - var instance interface{} - if singleton { - instance = invoke(resolver) - } - - container[resolverTypeOf.Out(i)] = binding{ - resolver: resolver, - instance: instance, - } - } -} - -// arguments will return resolved arguments of the given function. -func arguments(function interface{}) []reflect.Value { - functionTypeOf := reflect.TypeOf(function) - argumentsCount := functionTypeOf.NumIn() - arguments := make([]reflect.Value, argumentsCount) - - for i := 0; i < argumentsCount; i++ { - abstraction := functionTypeOf.In(i) +// A default instance for container +var container internal.Container = internal.NewContainer() - var instance interface{} - - if concrete, ok := container[abstraction]; ok { - instance = concrete.resolve() - } else { - panic("no concrete found for the abstraction: " + abstraction.String()) - } - - arguments[i] = reflect.ValueOf(instance) - } - - return arguments -} - -// Singleton will bind an abstraction to a concrete for further singleton resolves. -// It takes a resolver function which returns the concrete and its return type matches the abstraction (interface). -// The resolver function can have arguments of abstraction that have bound already in Container. +// Singleton creates a singleton for the default instance. func Singleton(resolver interface{}) { - bind(resolver, true) + container.Singleton(resolver) } -// Transient will bind an abstraction to a concrete for further transient resolves. -// It takes a resolver function which returns the concrete and its return type matches the abstraction (interface). -// The resolver function can have arguments of abstraction that have bound already in Container. +// Transient creates a transient binding for the default instance. func Transient(resolver interface{}) { - bind(resolver, false) + container.Transient(resolver) } -// Reset will reset the container and remove all the bindings. +// Reset removes all bindings in the default instance. func Reset() { - container = map[reflect.Type]binding{} + container.Reset() } -// Make will resolve the dependency and return a appropriate concrete of the given abstraction. -// It can take an abstraction (interface reference) and fill it with the related implementation. -// It also can takes a function (receiver) with one or more arguments of the abstractions (interfaces) that need to be -// resolved, Container will invoke the receiver function and pass the related implementations. +// Make binds receiver to the default instance. func Make(receiver interface{}) { - receiverTypeOf := reflect.TypeOf(receiver) - if receiverTypeOf == nil { - panic("cannot detect type of the receiver, make sure your are passing reference of the object") - } - - if receiverTypeOf.Kind() == reflect.Ptr { - abstraction := receiverTypeOf.Elem() - - if concrete, ok := container[abstraction]; ok { - instance := concrete.resolve() - reflect.ValueOf(receiver).Elem().Set(reflect.ValueOf(instance)) - return - } - - panic("no concrete found for the abstraction " + abstraction.String()) - } - - if receiverTypeOf.Kind() == reflect.Func { - arguments := arguments(receiver) - reflect.ValueOf(receiver).Call(arguments) - return - } - - panic("the receiver must be either a reference or a callback") -} + container.Make(receiver) +} \ No newline at end of file diff --git a/pkg/container/container.go b/pkg/container/container.go new file mode 100644 index 0000000..f3e444f --- /dev/null +++ b/pkg/container/container.go @@ -0,0 +1,131 @@ +// Package container provides an IoC container for Go projects. +// It provides simple, fluent and easy-to-use interface to make dependency injection in GoLang easier. +package container + +import ( + "reflect" +) + +// binding keeps a binding resolver and instance (for singleton bindings). +type binding struct { + resolver interface{} // resolver function + instance interface{} // instance stored for singleton bindings +} + +// resolve will return the concrete of related abstraction. +func (b binding) resolve(c Container) interface{} { + if b.instance != nil { + return b.instance + } + + return c.invoke(b.resolver) +} + +// Container is a map of reflect.Type to binding +type Container map[reflect.Type]binding + +// NewContainer returns a new instance of Container +func NewContainer() Container { + return make(Container) +} + +// bind will map an abstraction to a concrete and set instance if it's a singleton binding. +func (c Container) bind(resolver interface{}, singleton bool) { + resolverTypeOf := reflect.TypeOf(resolver) + if resolverTypeOf.Kind() != reflect.Func { + panic("the resolver must be a function") + } + + for i := 0; i < resolverTypeOf.NumOut(); i++ { + var instance interface{} + if singleton { + instance = c.invoke(resolver) + } + + c[resolverTypeOf.Out(i)] = binding{ + resolver: resolver, + instance: instance, + } + } +} + +// invoke will call the given function and return its returned value. +// It only works for functions that return a single value. +func (c Container) invoke(function interface{}) interface{} { + return reflect.ValueOf(function).Call(c.arguments(function))[0].Interface() +} + +// arguments will return resolved arguments of the given function. +func (c Container) arguments(function interface{}) []reflect.Value { + functionTypeOf := reflect.TypeOf(function) + argumentsCount := functionTypeOf.NumIn() + arguments := make([]reflect.Value, argumentsCount) + + for i := 0; i < argumentsCount; i++ { + abstraction := functionTypeOf.In(i) + + var instance interface{} + + if concrete, ok := c[abstraction]; ok { + instance = concrete.resolve(c) + } else { + panic("no concrete found for the abstraction: " + abstraction.String()) + } + + arguments[i] = reflect.ValueOf(instance) + } + + return arguments +} + +// Singleton will bind an abstraction to a concrete for further singleton resolves. +// It takes a resolver function which returns the concrete and its return type matches the abstraction (interface). +// The resolver function can have arguments of abstraction that have bound already in Container. +func (c Container) Singleton(resolver interface{}) { + c.bind(resolver, true) +} + +// Transient will bind an abstraction to a concrete for further transient resolves. +// It takes a resolver function which returns the concrete and its return type matches the abstraction (interface). +// The resolver function can have arguments of abstraction that have bound already in Container. +func (c Container) Transient(resolver interface{}) { + c.bind(resolver, false) +} + +// Reset will reset the container and remove all the bindings. +func (c Container) Reset() { + for k := range c { + delete(c, k) + } +} + +// Make will resolve the dependency and return a appropriate concrete of the given abstraction. +// It can take an abstraction (interface reference) and fill it with the related implementation. +// It also can takes a function (receiver) with one or more arguments of the abstractions (interfaces) that need to be +// resolved, Container will invoke the receiver function and pass the related implementations. +func (c Container) Make(receiver interface{}) { + receiverTypeOf := reflect.TypeOf(receiver) + if receiverTypeOf == nil { + panic("cannot detect type of the receiver, make sure your are passing reference of the object") + } + + if receiverTypeOf.Kind() == reflect.Ptr { + abstraction := receiverTypeOf.Elem() + + if concrete, ok := c[abstraction]; ok { + instance := concrete.resolve(c) + reflect.ValueOf(receiver).Elem().Set(reflect.ValueOf(instance)) + return + } + + panic("no concrete found for the abstraction " + abstraction.String()) + } + + if receiverTypeOf.Kind() == reflect.Func { + arguments := c.arguments(receiver) + reflect.ValueOf(receiver).Call(arguments) + return + } + + panic("the receiver must be either a reference or a callback") +} diff --git a/container_test.go b/pkg/container/container_test.go similarity index 74% rename from container_test.go rename to pkg/container/container_test.go index 6976be5..5caefa0 100644 --- a/container_test.go +++ b/pkg/container/container_test.go @@ -1,7 +1,7 @@ package container_test import ( - "github.com/golobby/container" + "github.com/golobby/container/pkg/container" "github.com/stretchr/testify/assert" "testing" ) @@ -33,31 +33,33 @@ func (m MySQL) Connect() bool { return true } +var instance = container.NewContainer() + func TestSingletonItShouldMakeAnInstanceOfTheAbstraction(t *testing.T) { area := 5 - container.Singleton(func() Shape { + instance.Singleton(func() Shape { return &Circle{a: area} }) - container.Make(func(s Shape) { + instance.Make(func(s Shape) { a := s.GetArea() assert.Equal(t, area, a) }) } func TestSingletonItShouldMakeSameObjectEachMake(t *testing.T) { - container.Singleton(func() Shape { + instance.Singleton(func() Shape { return &Circle{a: 5} }) area := 6 - container.Make(func(s1 Shape) { + instance.Make(func(s1 Shape) { s1.SetArea(area) }) - container.Make(func(s2 Shape) { + instance.Make(func(s2 Shape) { a := s2.GetArea() assert.Equal(t, a, area) }) @@ -66,17 +68,17 @@ func TestSingletonItShouldMakeSameObjectEachMake(t *testing.T) { func TestSingletonWithNonFunctionResolverItShouldPanic(t *testing.T) { value := "the resolver must be a function" assert.PanicsWithValue(t, value, func() { - container.Singleton("STRING!") + instance.Singleton("STRING!") }, "Expected panic") } func TestSingletonItShouldResolveResolverArguments(t *testing.T) { area := 5 - container.Singleton(func() Shape { + instance.Singleton(func() Shape { return &Circle{a: area} }) - container.Singleton(func(s Shape) Database { + instance.Singleton(func(s Shape) Database { assert.Equal(t, s.GetArea(), area) return &MySQL{} }) @@ -85,15 +87,15 @@ func TestSingletonItShouldResolveResolverArguments(t *testing.T) { func TestTransientItShouldMakeDifferentObjectsOnMake(t *testing.T) { area := 5 - container.Transient(func() Shape { + instance.Transient(func() Shape { return &Circle{a: area} }) - container.Make(func(s1 Shape) { + instance.Make(func(s1 Shape) { s1.SetArea(6) }) - container.Make(func(s2 Shape) { + instance.Make(func(s2 Shape) { a := s2.GetArea() assert.Equal(t, a, area) }) @@ -102,22 +104,22 @@ func TestTransientItShouldMakeDifferentObjectsOnMake(t *testing.T) { func TestTransientItShouldMakeAnInstanceOfTheAbstraction(t *testing.T) { area := 5 - container.Transient(func() Shape { + instance.Transient(func() Shape { return &Circle{a: area} }) - container.Make(func(s Shape) { + instance.Make(func(s Shape) { a := s.GetArea() assert.Equal(t, a, area) }) } func TestMakeWithSingleInputAndCallback(t *testing.T) { - container.Singleton(func() Shape { + instance.Singleton(func() Shape { return &Circle{a: 5} }) - container.Make(func(s Shape) { + instance.Make(func(s Shape) { if _, ok := s.(*Circle); !ok { t.Error("Expected Circle") } @@ -125,15 +127,15 @@ func TestMakeWithSingleInputAndCallback(t *testing.T) { } func TestMakeWithMultipleInputsAndCallback(t *testing.T) { - container.Singleton(func() Shape { + instance.Singleton(func() Shape { return &Circle{a: 5} }) - container.Singleton(func() Database { + instance.Singleton(func() Database { return &MySQL{} }) - container.Make(func(s Shape, m Database) { + instance.Make(func(s Shape, m Database) { if _, ok := s.(*Circle); !ok { t.Error("Expected Circle") } @@ -145,13 +147,13 @@ func TestMakeWithMultipleInputsAndCallback(t *testing.T) { } func TestMakeWithSingleInputAndReference(t *testing.T) { - container.Singleton(func() Shape { + instance.Singleton(func() Shape { return &Circle{a: 5} }) var s Shape - container.Make(&s) + instance.Make(&s) if _, ok := s.(*Circle); !ok { t.Error("Expected Circle") @@ -159,11 +161,11 @@ func TestMakeWithSingleInputAndReference(t *testing.T) { } func TestMakeWithMultipleInputsAndReference(t *testing.T) { - container.Singleton(func() Shape { + instance.Singleton(func() Shape { return &Circle{a: 5} }) - container.Singleton(func() Database { + instance.Singleton(func() Database { return &MySQL{} }) @@ -172,8 +174,8 @@ func TestMakeWithMultipleInputsAndReference(t *testing.T) { d Database ) - container.Make(&s) - container.Make(&d) + instance.Make(&s) + instance.Make(&d) if _, ok := s.(*Circle); !ok { t.Error("Expected Circle") @@ -187,7 +189,7 @@ func TestMakeWithMultipleInputsAndReference(t *testing.T) { func TestMakeWithUnsupportedReceiver(t *testing.T) { value := "the receiver must be either a reference or a callback" assert.PanicsWithValue(t, value, func() { - container.Make("STRING!") + instance.Make("STRING!") }, "Expected panic") } @@ -195,7 +197,7 @@ func TestMakeWithNonReference(t *testing.T) { value := "cannot detect type of the receiver, make sure your are passing reference of the object" assert.PanicsWithValue(t, value, func() { var s Shape - container.Make(s) + instance.Make(s) }, "Expected panic") } @@ -203,18 +205,18 @@ func TestMakeWithUnboundedAbstraction(t *testing.T) { value := "no concrete found for the abstraction container_test.Shape" assert.PanicsWithValue(t, value, func() { var s Shape - container.Reset() - container.Make(&s) + instance.Reset() + instance.Make(&s) }, "Expected panic") } func TestMakeWithCallbackThatHasAUnboundedAbstraction(t *testing.T) { value := "no concrete found for the abstraction: container_test.Database" assert.PanicsWithValue(t, value, func() { - container.Reset() - container.Singleton(func() Shape { + instance.Reset() + instance.Singleton(func() Shape { return &Circle{} }) - container.Make(func(s Shape, d Database) {}) + instance.Make(func(s Shape, d Database) {}) }, "Expected panic") }