Skip to content

Cloning injectors with registered services #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ I love **short name** for such utility library. This name is the sum of `DI` and
- Eagerly or lazily loaded services
- Dependency graph resolution
- Default injector
- Container cloning
- Injector cloning
- Service override

🚀 Services are loaded in invocation order.
Expand Down Expand Up @@ -356,6 +356,20 @@ config := do.MustInvokeNamed[Config](injector, "configuration")
do.MustShutdownNamed(injector, "configuration")
```

### Service override

By default, providing a service twice will panic. Service can be replaced at runtime using `do.Replace****` helpers.

```go
do.Provide[Vehicle](injector, func (i *do.Injector) (Vehicle, error) {
return &CarImplem{}, nil
})

do.Override[Vehicle](injector, func (i *do.Injector) (Vehicle, error) {
return &BusImplem{}, nil
})
```

### Hooks

3 lifecycle hooks are available in Injectors:
Expand All @@ -373,6 +387,38 @@ injector := do.NewWithOpts(&do.InjectorOpts{
})
```

### Cloning injector

Cloned injector have same service registrations as it's parent, but it doesn't share invoked service state.

Clones are useful for unit testing by replacing some services to mocks.

```go
var injector *do.Injector;

func init() {
do.Provide[Service](injector, func (i *do.Injector) (Service, error) {
return &RealService{}, nil
})
do.Provide[*App](injector, func (i *do.Injector) (*App, error) {
return &App{i.MustInvoke[Service](i)}, nil
})
}

func TestService(t *testing.T) {
i := injector.Clone()
defer i.Shutdown()

// replace Service to MockService
do.Override[Service](i, func (i *do.Injector) (Service, error) {
return &MockService{}, nil
}))

app := do.Invoke[*App](i)
// do unit testing with mocked service
}
```

## 🛩 Benchmark

// @TODO
Expand Down
24 changes: 24 additions & 0 deletions injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,27 @@ func (i *Injector) onServiceShutdown(name string) {
i.hookAfterShutdown(i, name)
}
}

// Clone clones injector with provided services but not with invoked instances.
func (i *Injector) Clone() *Injector {
return i.CloneWithOpts(&InjectorOpts{})
}

// CloneWithOpts clones injector with provided services but not with invoked instances, with options.
func (i *Injector) CloneWithOpts(opts *InjectorOpts) *Injector {
clone := NewWithOpts(opts)

i.mu.RLock()
defer i.mu.RUnlock()

for name, serviceAny := range i.services {
if service, ok := serviceAny.(cloneableService); ok {
clone.services[name] = service.clone()
} else {
clone.services[name] = service
}
defer clone.onServiceRegistration(name)
}

return clone
}
36 changes: 36 additions & 0 deletions injector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,39 @@ func TestInjectorOnServiceInvoke(t *testing.T) {
is.Equal(1, i.orderedInvocation["bar"])
is.Equal(2, i.orderedInvocationIndex)
}

func TestInjectorClone(t *testing.T) {
is := assert.New(t)

count := 0

// setup original container
i1 := New()
ProvideNamed(i1, "foobar", func(_ *Injector) (int, error) {
count++
return 42, nil
})
is.NotPanics(func() {
value := MustInvokeNamed[int](i1, "foobar")
is.Equal(42, value)
})
is.Equal(1, count)

// clone container
i2 := i1.Clone()
// invoked instance is not reused
s1, err := InvokeNamed[int](i2, "foobar")
is.NoError(err)
is.Equal(42, s1)
is.Equal(2, count)

// service can be overriden
OverrideNamed(i2, "foobar", func(_ *Injector) (int, error) {
count++
return 6 * 9, nil
})
s2, err := InvokeNamed[int](i2, "foobar")
is.NoError(err)
is.Equal(54, s2)
is.Equal(3, count)
}
5 changes: 5 additions & 0 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Service[T any] interface {
getInstance(*Injector) (T, error)
healthcheck() error
shutdown() error
clone() any
}

type healthcheckableService interface {
Expand Down Expand Up @@ -39,3 +40,7 @@ type Healthcheckable interface {
type Shutdownable interface {
Shutdown() error
}

type cloneableService interface {
clone() any
}
4 changes: 4 additions & 0 deletions service_eager.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ func (s *ServiceEager[T]) shutdown() error {

return nil
}

func (s *ServiceEager[T]) clone() any {
return s
}
10 changes: 10 additions & 0 deletions service_lazy.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,13 @@ func (s *ServiceLazy[T]) shutdown() error {

return nil
}

func (s *ServiceLazy[T]) clone() any {
// reset `build` flag and instance
return &ServiceLazy[T]{
name: s.name,

built: false,
provider: s.provider,
}
}