Skip to content

Commit 04c73db

Browse files
authored
Merge pull request #9 from fgm/8-convert-container-to-interface
Issue #8: convert Container to an interface.
2 parents 8abb1d9 + 296598b commit 04c73db

File tree

7 files changed

+205
-96
lines changed

7 files changed

+205
-96
lines changed

README.md

+40-31
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,23 @@
88

99
## Description
1010

11-
Package izidic defines a tiny dependency injection container for Go projects.
11+
Package [izidic](https://github.com/fgm/izidic) defines a tiny dependency injection container for Go projects.
1212

1313
That container can hold two different kinds of data:
1414

1515
- parameters, which are mutable data without any dependency;
16-
- services, which are functions providing a typed object providing a feature,
16+
- services, which are functions returning a typed object providing a feature,
1717
and may depend on other services and parameters.
1818

1919
The basic feature is that storing service definitions does not create instances,
20-
allowing users to store definitions of services requiring other services
20+
allowing users to store definitions of services requiring other services,
2121
before those are actually defined.
2222

2323
Notice that parameters do not need to be primitive types.
2424
For instance, most applications are likely to store a `stdout` object with value `os.Stdout`.
2525

2626
Unlike heavyweights like google/wire or uber/zap, it works as a single step,
27-
explicit, process, without reflection or code generation, to keep everything in sight.
27+
explicit process, without reflection or code generation, to keep everything in sight.
2828

2929
## Usage
3030

@@ -37,8 +37,8 @@ explicit, process, without reflection or code generation, to keep everything in
3737
| Store parameters in the DIC | `dic.Store("executable", os.Args[0])` |
3838
| Register services with the DIC | `dic.Register("logger", loggerService)` |
3939
| Freeze the container | `dic.Freeze()` |
40-
| Read a parameter from the DIC | `dic.Param(name)` |
41-
| Get a service instance from the DIC | `dic.Service(name)` |
40+
| Read a parameter from the DIC | `p, err := dic.Param(name)` |
41+
| Get a service instance from the DIC | `s, err := dic.Service(name)` |
4242

4343
Freezing applies once all parameters and services are stored and registered,
4444
and enables concurrent access to the container.
@@ -54,9 +54,9 @@ Parameters can be any value type. They can be stored in the container in any ord
5454
Services like `loggerService` in the previous example are instances ot the `Service` type,
5555
which is defined as:
5656

57-
`type Service func(*Container) (any, error)`
57+
`type Service func(Container) (any, error)`
5858

59-
- Services can use any other service and parameters to return the instance they
59+
- Services can reference any other service and parameters from the container, to return the instance they
6060
build. The only restriction is that cycles are not supported.
6161
- Like parameters, services can be registered in any order on the container,
6262
so feel free to order the registrations in alphabetical order for readability.
@@ -68,22 +68,20 @@ which is defined as:
6868

6969
### Accessing the container
7070

71-
- General parameter access: `s, err := dic.Param("name")`
71+
- Parameter access: `s, err := dic.Param("name")`
7272
- Check the error against `nil`
73-
- Type-assert the parameter value: `name := s.(string)`
74-
- The type assertion cannot fail if the error was `nil`
75-
- Simplified parameter access: `name := dic.MustParam("name").(string)`
76-
- General service access: `s, err := dic.Service("logger")`
73+
- Type-assert the parameter value: `name, ok := s.(string)`
74+
- Or use shortcut: `name := dic.MustParam("name").(string)`
75+
- Service access: `s, err := dic.Service("logger")`
7776
- Check the error against `nil`
78-
- Type-assert the service instance value: `logger := s.(*log.Logger)`
79-
- The type assertion cannot fail if the error was `nil`
80-
- Simplified service access: `logger := dic.MustService("logger").(*log.Logger)`
77+
- Type-assert the service instance value: `logger, ok := s.(*log.Logger)`
78+
- Or use shortcut: `logger := dic.MustService("logger").(*log.Logger)`
8179

8280

8381
## Best practices
8482
### Create a simpler developer experience
8583

86-
One limitation of having `Container.(Must)Param()` and `Container.(MustService)`
84+
One limitation of having `Container.(Must)Param()` and `Container.(Must)Service()`
8785
return untyped results as `any` is the need to type-assert results on every access.
8886

8987
To make this safer and better looking, a neat approach is to define an application
@@ -100,54 +98,63 @@ import (
10098
"github.com/fgm/izidic"
10199
)
102100

103-
type container struct {
104-
*izidic.Container
101+
type Container struct {
102+
izidic.Container
105103
}
106104

107105
// Logger is a typed service accessor.
108-
func (c *container) Logger() *log.Logger {
106+
func (c *Container) Logger() *log.Logger {
109107
return c.MustService("logger").(*log.Logger)
110108
}
111109

112110
// Name is a types parameter accessor.
113-
func (c *container) Name() string {
111+
func (c *Container) Name() string {
114112
return c.MustParam("name").(string)
115113
}
116114

117115
// loggerService is an izidic.Service also containing a one-time initialization action.
118-
func loggerService(dic *izidic.Container) (any, error) {
116+
func loggerService(dic izidic.Container) (any, error) {
119117
w := dic.MustParam("writer").(io.Writer)
120-
log.SetOutput(w) // Support dependency code not taking an injected logger.
121-
logger := log.New(w, "", log.LstdFlags)
118+
log.SetOutput(w) // Support dependency code not taking an injected logger.
119+
logger := log.New(w, "", log.LstdFlags)
122120
return logger, nil
123121
}
124122

125-
func appService(dic *izidic.Container) (any, error) {
126-
wdic := container{dic} // wrapped container with typed accessors
127-
logger := dic.Logger() // typed service instance
128-
name := dic.Name() // typed parameter value
123+
func appService(dic izidic.Container) (any, error) {
124+
wdic := Container{dic} // wrapped container with typed accessors
125+
logger := wdic.Logger() // typed service instance
126+
name := wdic.Name() // typed parameter value
129127
appFeature := makeAppFeature(name, logger)
130128
return appFeature, nil
131129
}
132130

133-
func resolve(w io.Writer, name string, args []string) izidic.Container {
131+
func Resolve(w io.Writer, name string, args []string) izidic.Container {
134132
dic := izidic.New()
133+
dic.Store("name", name)
135134
dic.Store("writer", w)
136135
dic.Register("logger", loggerService)
136+
dic.Register("app", appService)
137137
// others...
138138
dic.Freeze()
139139
return dic
140140
}
141141
```
142142

143143
These accessors will be useful when defining services, as in `appService` above,
144-
or in the boot sequence, which typically neeeds at least a `logger` and one or
144+
or in the boot sequence, which typically needs at least a `logger` and one or
145145
more application-domain service instances.
146146

147147

148+
### Create the container in a `Resolve` function
149+
150+
The cleanest way to initialize a container is to have the
151+
project contain an function, conventionally called `Resolve`, which takes all globals used in the project,
152+
and returns an instance of the custom container type defined above, as in [examples/di/di.go](examples/di/di.go).
153+
154+
148155
### Do not pass the container
149156

150-
Passing the container, although it works, defines the "service locator" anti-pattern.
157+
Passing the container to application code, although it works, defines the "service locator" anti-pattern.
151158

152159
Because the container is a complex object with variable contents,
153160
code receiving the container is hard to test.
@@ -160,3 +167,5 @@ Instead, in the service providing a given feature, use something like `appServic
160167

161168
In most cases, the value obtained thus will be a `struct` or a `func`,
162169
ready to be used without further data from the container.
170+
171+
See a complete demo in [examples/demo.go](examples/demo.go).

examples/demo.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
7+
"github.com/fgm/izidic/examples/di"
8+
)
9+
10+
func main() {
11+
dic := di.Resolve(os.Stdout, os.Args[0], os.Args[1:])
12+
app := dic.MustService("app").(di.App)
13+
log.Printf("app: %#v\n", app)
14+
if err := app(); err != nil {
15+
os.Exit(1)
16+
}
17+
}

examples/di/app.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package di
2+
3+
import "log"
4+
5+
// App represents whatever an actual application as a function would be.
6+
type App func() error
7+
8+
func makeAppFeature(name string, logger *log.Logger) App {
9+
return func() error {
10+
logger.Println(name)
11+
return nil
12+
}
13+
}

examples/di/di.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package di
2+
3+
import (
4+
"io"
5+
"log"
6+
7+
"github.com/fgm/izidic"
8+
)
9+
10+
// Container is an application-specific wrapper for a basic izidic container,
11+
// adding typed accessors for simpler use by application code, obviating the need
12+
// for type assertions.
13+
type Container struct {
14+
izidic.Container
15+
}
16+
17+
// Logger is a typed service accessor.
18+
func (c *Container) Logger() *log.Logger {
19+
return c.MustService("logger").(*log.Logger)
20+
}
21+
22+
// Name is a typed parameter accessor.
23+
func (c *Container) Name() string {
24+
return c.MustParam("name").(string)
25+
}
26+
27+
// Resolve is the location where the parameters and services in the container
28+
//
29+
// are assembled and the container readied for use.
30+
func Resolve(w io.Writer, name string, args []string) izidic.Container {
31+
dic := izidic.New()
32+
dic.Store("name", name)
33+
dic.Store("writer", w)
34+
dic.Register("app", appService)
35+
dic.Register("logger", loggerService)
36+
dic.Freeze()
37+
return dic
38+
}
39+
40+
func appService(dic izidic.Container) (any, error) {
41+
wdic := Container{dic} // wrapped Container with typed accessors
42+
logger := wdic.Logger() // typed service instance: *log.Logger
43+
name := wdic.Name() // typed parameter value: string
44+
appFeature := makeAppFeature(name, logger)
45+
return appFeature, nil
46+
}
47+
48+
// loggerService is an izidic.Service also containing a one-time initialization action.
49+
//
50+
// Keep in mind that the initialization will only be performed once the service has
51+
// actually been instantiated.
52+
func loggerService(dic izidic.Container) (any, error) {
53+
w := dic.MustParam("writer").(io.Writer)
54+
log.SetOutput(w) // Support dependency code not taking an injected logger.
55+
logger := log.New(w, "", log.LstdFlags)
56+
return logger, nil
57+
}

izidic-100x100.png

21.8 KB
Loading

0 commit comments

Comments
 (0)