Skip to content

Commit 042fb72

Browse files
committed
add module feature
- reduce the global surface - possibility to create 1 container per test case, then run them in parallel
1 parent 5185de5 commit 042fb72

24 files changed

+435
-347
lines changed

README.md

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ the management of object lifetimes and the inversion of control in your applicat
1818

1919
<br />
2020

21-
# Features
21+
## Features
2222

2323
- **Singletons**: Register components as singletons, ensuring that there's only one instance throughout the entire
2424
application.
@@ -46,15 +46,15 @@ the management of object lifetimes and the inversion of control in your applicat
4646

4747
<br />
4848

49-
# Installation
49+
## Installation
5050

5151
```bash
5252
go get -u github.com/firasdarwish/ore
5353
```
5454

5555
<br />
5656

57-
# Usage
57+
## Usage
5858

5959
### Import
6060

@@ -433,12 +433,52 @@ cancel() //cancel the ctx
433433
The `ore.GetResolvedScopedInstances[TInterface](context)` function returns a list of implementations of the `[TInterface]` which are Scoped in the input context:
434434

435435
- It returns only the instances which had been invoked (a.k.a resolved) during the context lifetime.
436-
- All the implementations including "keyed" one will be returned.
436+
- All the implementations (of all modules) including "keyed" one will be returned.
437437
- The returned instances are sorted by invocation order, the first one being the latest invoked one.
438438
- if "A" depends on "B", "C", Ore will make sure to return "B" and "C" first in the list so that they would be Disposed before "A".
439439

440440
<br />
441441

442+
### Multiple Containers (a.k.a Modules)
443+
444+
| DefaultContainer | Custom container |
445+
|------------------|------------------|
446+
| Get | GetFromContainer |
447+
| GetList | GetListFromContainer |
448+
| GetResolvedSingletons | GetResolvedSingletonsFromContainer |
449+
| RegisterAlias | RegisterAliasToContainer |
450+
| RegisterEagerSingleton | RegisterEagerSingletonToContainer |
451+
| RegisterLazyCreator | RegisterLazyCreatorToContainer |
452+
| RegisterLazyFunc | RegisterLazyFuncToContainer |
453+
454+
Most of time you only need the Default Container. In rare use case such as the Modular Monolith Architecture, you might want to use multiple containers, one per module. Ore provides minimum support for "module" in this case:
455+
456+
```go
457+
//broker module
458+
brokerContainer := ore.NewContainer()
459+
ore.RegisterLazyFuncToContainer(brokerContainer, ore.Singleton, func(ctx context.Context) (*Broker, context.Context) {
460+
brs, ctx = ore.GetFromContainer[*BrokerageSystem](brokerContainer, ctx)
461+
return &Broker{brs}, ctx
462+
})
463+
// brokerContainer.Build() //prevent further registration
464+
// brokerContainer.Validate() //check the dependency graph
465+
// brokerContainer.DisableValidation = true //disable check when resolve new object
466+
broker, _ := ore.GetFromContainer[*Broker](brokerContainer, context.Background())
467+
468+
//trader module
469+
traderContainer := ore.NewContainer()
470+
ore.RegisterLazyFuncToContainer(traderContainer, ore.Singleton, func(ctx context.Context) (*Trader, context.Context) {
471+
mkp, ctx = ore.GetFromContainer[*MarketPlace](traderContainer, ctx)
472+
return &Trader{mkp}, ctx
473+
})
474+
trader, _ := ore.GetFromContainer[*Trader](traderContainer, context.Background())
475+
```
476+
477+
Important: You will have to prevent cross modules access to the containers by yourself. For eg, don't let your "Broker
478+
module" to have access to the `tranderContainer` of the "Trader module".
479+
480+
<br />
481+
442482
## More Complex Example
443483

444484
```go
@@ -488,7 +528,7 @@ func main() {
488528

489529
<br />
490530

491-
# Benchmarks
531+
## Benchmarks
492532

493533
```bash
494534
goos: windows
@@ -510,16 +550,16 @@ Checkout also [examples/benchperf/README.md](examples/benchperf/README.md)
510550

511551
<br />
512552

513-
# 👤 Contributors
553+
## 👤 Contributors
514554

515555
![Contributors](https://contrib.rocks/image?repo=firasdarwish/ore)
516556

517557

518-
# Contributing
558+
## Contributing
519559

520560
Feel free to contribute by opening issues, suggesting features, or submitting pull requests. We welcome your feedback
521561
and contributions.
522562

523-
# License
563+
## License
524564

525565
This project is licensed under the MIT License - see the LICENSE file for details.

alias_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,14 @@ func TestInvalidAlias(t *testing.T) {
135135

136136
func TestGetGenericAlias(t *testing.T) {
137137
for _, registrationType := range types {
138-
clearAll()
138+
container := NewContainer()
139139

140-
RegisterLazyFunc(registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
140+
RegisterLazyFuncToContainer(container, registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
141141
return &simpleCounterUint{}, ctx
142142
})
143-
RegisterAlias[someCounterGeneric[uint], *simpleCounterUint]()
143+
RegisterAliasToContainer[someCounterGeneric[uint], *simpleCounterUint](container)
144144

145-
c, _ := Get[someCounterGeneric[uint]](context.Background())
145+
c, _ := GetFromContainer[someCounterGeneric[uint]](container, context.Background())
146146

147147
c.Add(1)
148148
c.Add(1)
@@ -153,17 +153,17 @@ func TestGetGenericAlias(t *testing.T) {
153153

154154
func TestGetListGenericAlias(t *testing.T) {
155155
for _, registrationType := range types {
156-
clearAll()
156+
container := NewContainer()
157157

158158
for i := 0; i < 3; i++ {
159-
RegisterLazyFunc(registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
159+
RegisterLazyFuncToContainer(container, registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
160160
return &simpleCounterUint{}, ctx
161161
})
162162
}
163163

164-
RegisterAlias[someCounterGeneric[uint], *simpleCounterUint]()
164+
RegisterAliasToContainer[someCounterGeneric[uint], *simpleCounterUint](container)
165165

166-
counters, _ := GetList[someCounterGeneric[uint]](context.Background())
166+
counters, _ := GetListFromContainer[someCounterGeneric[uint]](container, context.Background())
167167
assert.Equal(t, len(counters), 3)
168168

169169
c := counters[1]

container.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package ore
2+
3+
import (
4+
"context"
5+
"sync"
6+
"sync/atomic"
7+
)
8+
9+
type Container struct {
10+
//DisableValidation is false by default, Set to true to skip validation.
11+
// Use case: you called the [Validate] function (either in the test pipeline or on application startup).
12+
// So you are confident that your registrations are good:
13+
//
14+
// - no missing dependencies
15+
// - no circular dependencies
16+
// - no lifetime misalignment (a longer lifetime service depends on a shorter one).
17+
//
18+
// You don't need Ore to validate over and over again each time it creates a new concrete.
19+
// It's a waste of resource especially when you will need Ore to create milion of transient concretes
20+
// and any "pico" seconds or memory allocation matter for you.
21+
//
22+
// In this case, you can set DisableValidation = true.
23+
//
24+
// This config would impact also the the [GetResolvedSingletons] and the [GetResolvedScopedInstances] functions,
25+
// the returning order would be no longer guaranteed.
26+
DisableValidation bool
27+
containerID int32
28+
lock *sync.RWMutex
29+
isBuilt bool
30+
resolvers map[typeID][]serviceResolver
31+
32+
//isSealed will be set to `true` when `Validate()` is called AFTER `Build()` is called
33+
//it prevents any further validations thus enhancing performance
34+
isSealed bool
35+
36+
//map interface type to the implementations type
37+
aliases map[pointerTypeName][]pointerTypeName
38+
}
39+
40+
var lastContainerID atomic.Int32
41+
42+
func NewContainer() *Container {
43+
return &Container{
44+
containerID: lastContainerID.Add(1),
45+
lock: &sync.RWMutex{},
46+
isBuilt: false,
47+
resolvers: map[typeID][]serviceResolver{},
48+
aliases: map[pointerTypeName][]pointerTypeName{},
49+
}
50+
}
51+
52+
// Validate invokes all registered resolvers. It panics if any of them fails.
53+
// It is recommended to call this function on application start, or in the CI/CD test pipeline
54+
// The objectif is to panic early when the container is bad configured. For eg:
55+
//
56+
// - (1) Missing depedency (forget to register certain resolvers)
57+
// - (2) cyclic dependency
58+
// - (3) lifetime misalignment (a longer lifetime service depends on a shorter one).
59+
func (this *Container) Validate() {
60+
if this.DisableValidation {
61+
panic("Validation is disabled")
62+
}
63+
ctx := context.Background()
64+
for _, resolvers := range this.resolvers {
65+
for _, resolver := range resolvers {
66+
_, ctx = resolver.resolveService(this, ctx)
67+
}
68+
}
69+
70+
this.lock.Lock()
71+
defer this.lock.Unlock()
72+
if this.isBuilt && this.isSealed == false {
73+
this.isSealed = true
74+
}
75+
}
76+
77+
func (this *Container) Build() {
78+
this.lock.Lock()
79+
defer this.lock.Unlock()
80+
if this.isBuilt {
81+
panic(alreadyBuilt)
82+
}
83+
84+
this.isBuilt = true
85+
}
86+
87+
func (this *Container) IsBuilt() bool {
88+
return this.isBuilt
89+
}

creator_test.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package ore
33
import (
44
"context"
55
"testing"
6+
7+
"github.com/stretchr/testify/assert"
68
)
79

810
func TestRegisterLazyCreator(t *testing.T) {
@@ -24,20 +26,23 @@ func TestRegisterLazyCreator(t *testing.T) {
2426

2527
func TestRegisterLazyCreatorNilFuncTransient(t *testing.T) {
2628
clearAll()
27-
defer mustHavePanicked(t)
28-
RegisterLazyCreator[someCounter](Transient, nil)
29+
assert.Panics(t, func() {
30+
RegisterLazyCreator[someCounter](Transient, nil)
31+
})
2932
}
3033

3134
func TestRegisterLazyCreatorNilFuncScoped(t *testing.T) {
3235
clearAll()
33-
defer mustHavePanicked(t)
34-
RegisterLazyCreator[someCounter](Scoped, nil)
36+
assert.Panics(t, func() {
37+
RegisterLazyCreator[someCounter](Scoped, nil)
38+
})
3539
}
3640

3741
func TestRegisterLazyCreatorNilFuncSingleton(t *testing.T) {
3842
clearAll()
39-
defer mustHavePanicked(t)
40-
RegisterLazyCreator[someCounter](Singleton, nil)
43+
assert.Panics(t, func() {
44+
RegisterLazyCreator[someCounter](Singleton, nil)
45+
})
4146
}
4247

4348
func TestRegisterLazyCreatorMultipleImplementations(t *testing.T) {
@@ -154,16 +159,17 @@ func TestRegisterLazyCreatorTransientState(t *testing.T) {
154159

155160
func TestRegisterLazyCreatorNilKeyOnRegistering(t *testing.T) {
156161
clearAll()
157-
defer mustHavePanicked(t)
158-
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, nil)
162+
assert.Panics(t, func() {
163+
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, "", nil)
164+
})
159165
}
160166

161167
func TestRegisterLazyCreatorNilKeyOnGetting(t *testing.T) {
162168
clearAll()
163-
defer mustHavePanicked(t)
164169
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, "firas")
165-
166-
Get[someCounter](context.Background(), nil)
170+
assert.Panics(t, func() {
171+
Get[someCounter](context.Background(), nil)
172+
})
167173
}
168174

169175
func TestRegisterLazyCreatorGeneric(t *testing.T) {

eager_singleton_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package ore
33
import (
44
"context"
55
"testing"
6+
7+
"github.com/stretchr/testify/assert"
68
)
79

810
func TestRegisterEagerSingleton(t *testing.T) {
@@ -22,8 +24,9 @@ func TestRegisterEagerSingleton(t *testing.T) {
2224

2325
func TestRegisterEagerSingletonNilImplementation(t *testing.T) {
2426
clearAll()
25-
defer mustHavePanicked(t)
26-
RegisterEagerSingleton[someCounter](nil)
27+
assert.Panics(t, func() {
28+
RegisterEagerSingleton[someCounter](nil)
29+
})
2730
}
2831

2932
func TestRegisterEagerSingletonMultipleImplementations(t *testing.T) {
@@ -79,16 +82,17 @@ func TestRegisterEagerSingletonSingletonState(t *testing.T) {
7982

8083
func TestRegisterEagerSingletonNilKeyOnRegistering(t *testing.T) {
8184
clearAll()
82-
defer mustHavePanicked(t)
83-
RegisterEagerSingleton[someCounter](&simpleCounter{}, nil)
85+
assert.Panics(t, func() {
86+
RegisterEagerSingleton[someCounter](&simpleCounter{}, nil, "")
87+
})
8488
}
8589

8690
func TestRegisterEagerSingletonNilKeyOnGetting(t *testing.T) {
8791
clearAll()
88-
defer mustHavePanicked(t)
8992
RegisterEagerSingleton[someCounter](&simpleCounter{}, "firas")
90-
91-
Get[someCounter](context.Background(), nil)
93+
assert.Panics(t, func() {
94+
Get[someCounter](context.Background(), nil, "")
95+
})
9296
}
9397

9498
func TestRegisterEagerSingletonGeneric(t *testing.T) {

0 commit comments

Comments
 (0)