Skip to content

Commit

Permalink
Add unshared objects (#17)
Browse files Browse the repository at this point in the history
The definitions now have an Unshared property.
The unshared objects are not singletons and are rebuilt each time.
  • Loading branch information
snovichkov authored and sarulabs committed Oct 24, 2019
1 parent bc5c1d2 commit e630b49
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 102 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ obj := ctn.Get("my-object").(*MyObject) // retrieve the object

The `Get` method returns an `interface{}`. You need to cast the interface before using the object.

The objects are stored as singletons in the Container. You will retrieve the exact same object every time you call the `Get` method on the same Container. The `Build` function will only be called once.

By default the objects are shared that means what they stored as singletons in the Container. You will retrieve the
exact same object every time you call the `Get` method on the same Container. The `Build` function will only be called
once. In case when you don't want the objects to be shared set the `Unshared` property of object definition to true.

## Definitions and dependencies

Expand Down
2 changes: 1 addition & 1 deletion builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (b *Builder) Build() Container {
definitions: defs,
parent: nil,
children: map[*containerCore]struct{}{},
objects: map[string]interface{}{},
objects: map[objectKey]interface{}{},
dependencies: newGraph(),
},
}
Expand Down
3 changes: 2 additions & 1 deletion containerCore.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ type containerCore struct {
parent *containerCore
children map[*containerCore]struct{}
unscopedChild *containerCore
objects map[string]interface{}
objects map[objectKey]interface{}
lastUniqueID int
deleteIfNoChild bool
dependencies *graph
}
Expand Down
35 changes: 21 additions & 14 deletions containerGetter.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (g *containerGetter) SafeGet(ctn *container, name string) (interface{}, err
return nil, fmt.Errorf("could not get `%s` because the definition does not exist", name)
}

if ctn.builtList.Has(def.Name) {
if ctn.builtList.HasDef(name) {
return nil, fmt.Errorf(
"could not get `%s` because there is a cycle in the object definitions (%v)",
def.Name, ctn.builtList.OrderedList(),
Expand Down Expand Up @@ -74,15 +74,21 @@ func (g *containerGetter) getInThisContainer(ctn *container, def Def) (interface
return nil, fmt.Errorf("could not get `%s` because the container has been deleted", def.Name)
}

g.addDependencyToGraph(ctn, def.Name)
objKey := objectKey{defName: def.Name}
if def.Unshared {
ctn.lastUniqueID += 1
objKey.uniqueID = ctn.lastUniqueID
}

g.addDependencyToGraph(ctn, objKey)

obj, ok := ctn.objects[def.Name]
obj, ok := ctn.objects[objKey]
if !ok {
// the object need to be created
c := make(buildingChan)
ctn.objects[def.Name] = c
ctn.objects[objKey] = c
ctn.m.Unlock()
return g.buildInThisContainer(ctn, def, c)
return g.buildInThisContainer(ctn, def, objKey, c)
}

ctn.m.Unlock()
Expand All @@ -100,23 +106,24 @@ func (g *containerGetter) getInThisContainer(ctn *container, def Def) (interface
return g.getInThisContainer(ctn, def)
}

func (g *containerGetter) addDependencyToGraph(ctn *container, defName string) {
func (g *containerGetter) addDependencyToGraph(ctn *container, objKey objectKey) {
if last, ok := ctn.builtList.LastElement(); ok {
ctn.dependencies.AddEdge(last, defName)
ctn.dependencies.AddEdge(last, objKey)
return
}
ctn.dependencies.AddVertex(defName)

ctn.dependencies.AddVertex(objKey)
}

func (g *containerGetter) buildInThisContainer(ctn *container, def Def, c buildingChan) (interface{}, error) {
obj, err := g.build(ctn, def)
func (g *containerGetter) buildInThisContainer(ctn *container, def Def, objKey objectKey, c buildingChan) (interface{}, error) {
obj, err := g.build(ctn, def, objKey)

ctn.m.Lock()

if err != nil {
// The object could not be created. Remove the channel from the object map
// and close it to allow the object to be created again.
delete(ctn.objects, def.Name)
delete(ctn.objects, objKey)
ctn.m.Unlock()
close(c)
return nil, err
Expand All @@ -134,7 +141,7 @@ func (g *containerGetter) buildInThisContainer(ctn *container, def Def, c buildi
)
}

ctn.objects[def.Name] = obj
ctn.objects[objKey] = obj
ctn.m.Unlock()
close(c)

Expand All @@ -148,7 +155,7 @@ func (g *containerGetter) formatCloseErr(err error) string {
return " (with an error: " + err.Error() + ")"
}

func (g *containerGetter) build(ctn *container, def Def) (obj interface{}, err error) {
func (g *containerGetter) build(ctn *container, def Def, objKey objectKey) (obj interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("could not build `%s` because the build function panicked: %s", def.Name, r)
Expand All @@ -157,7 +164,7 @@ func (g *containerGetter) build(ctn *container, def Def) (obj interface{}, err e

obj, err = def.Build(&container{
containerCore: ctn.containerCore,
builtList: ctn.builtList.Add(def.Name),
builtList: ctn.builtList.Add(objKey),
})

if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions containerGetter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,29 @@ func TestSafeGet(t *testing.T) {
require.True(t, obj == objBis)
}

func TestUnsharedObjects(t *testing.T) {
b, _ := NewBuilder()

b.Add(Def{
Name: "unshared",
Build: func(ctn Container) (interface{}, error) {
return &mockObject{}, nil
},
Unshared: true,
})

var app = b.Build()

obj1, err := app.SafeGet("unshared")
require.Nil(t, err)

obj2, err := app.SafeGet("unshared")
require.Nil(t, err)

// should retrieve different object every time
require.False(t, obj1 == obj2)
}

func TestBuildPanic(t *testing.T) {
b, _ := NewBuilder()

Expand Down
2 changes: 1 addition & 1 deletion containerLineage.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (l *containerLineage) createChild(ctn *container) (*container, error) {
parent: ctn.containerCore,
children: map[*containerCore]struct{}{},
unscopedChild: nil,
objects: map[string]interface{}{},
objects: map[objectKey]interface{}{},
dependencies: newGraph(),
},
}, nil
Expand Down
8 changes: 4 additions & 4 deletions containerSlayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ func (s *containerSlayer) deleteClone(ctn *containerCore, clone *containerCore)
errBuilder.Add(err)
}

names, err := clone.dependencies.TopologicalOrdering()
keys, err := clone.dependencies.TopologicalOrdering()
errBuilder.Add(err)

for _, name := range names {
obj, hasObj := clone.objects[name]
def, hasDef := clone.definitions[name]
for _, key := range keys {
obj, hasObj := clone.objects[key]
def, hasDef := clone.definitions[key.defName]
if hasObj && hasDef {
err := s.closeObject(obj, def)
errBuilder.Add(err)
Expand Down
33 changes: 29 additions & 4 deletions containerSlayer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package di

import (
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -344,7 +345,10 @@ func TestClean(t *testing.T) {
}

func TestCloseOrder(t *testing.T) {
closed := []string{}
var (
index int
closed = []string{}
)

b, _ := NewBuilder()

Expand Down Expand Up @@ -418,43 +422,64 @@ func TestCloseOrder(t *testing.T) {
Build: func(ctn Container) (interface{}, error) {
ctn.Get("req-3")
ctn.Get("app-1")
ctn.Get("req-5")
return nil, nil
},
Close: func(obj interface{}) error {
closed = append(closed, "req-4")
return nil
},
},
{
Name: "req-5",
Scope: Request,
Build: func(ctn Container) (interface{}, error) {
ctn.Get("app-1")
ctn.Get("req-1")

index += 1
return index, nil
},
Close: func(obj interface{}) error {
closed = append(closed, fmt.Sprintf("req-5#%d", obj.(int)))
return nil
},
Unshared: true,
},
}...)

app := b.Build()

index = 0
r1, _ := app.SubContainer()
r1.Get("req-1")
r1.Get("req-2")
r1.Get("req-3")
r1.Get("req-4")
r1.Get("req-5")
r1.Get("app-1")
r1.Get("app-2")

index = 0
r2, _ := app.SubContainer()
r2.Get("app-2")
r2.Get("app-1")
r2.Get("req-4")
r2.Get("req-3")
r2.Get("req-1")
r2.Get("req-5")

var err error

err = r1.Delete()
require.Nil(t, err)
require.Equal(t, []string{"req-2", "req-4", "req-3", "req-1"}, closed)
require.Equal(t, []string{"req-5#2", "req-2", "req-4", "req-5#1", "req-3", "req-1"}, closed)

err = r2.Delete()
require.Nil(t, err)
require.Equal(t, []string{"req-2", "req-4", "req-3", "req-1", "req-4", "req-3", "req-1"}, closed)
require.Equal(t, []string{"req-5#2", "req-2", "req-4", "req-5#1", "req-3", "req-1", "req-5#2", "req-4", "req-5#1", "req-3", "req-1"}, closed)

err = app.Delete()
require.Nil(t, err)
require.Equal(t, []string{"req-2", "req-4", "req-3", "req-1", "req-4", "req-3", "req-1", "app-1", "app-2"}, closed)
require.Equal(t, []string{"req-5#2", "req-2", "req-4", "req-5#1", "req-3", "req-1", "req-5#2", "req-4", "req-5#1", "req-3", "req-1", "app-1", "app-2"}, closed)
}
11 changes: 6 additions & 5 deletions definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package di

// Def contains information to build and close an object inside a Container.
type Def struct {
Name string
Scope string
Build func(ctn Container) (interface{}, error)
Close func(obj interface{}) error
Tags []Tag
Build func(ctn Container) (interface{}, error)
Close func(obj interface{}) error
Name string
Scope string
Tags []Tag
Unshared bool
}

// Tag can contain more specific information about a Definition.
Expand Down
Loading

0 comments on commit e630b49

Please sign in to comment.