Skip to content

Commit

Permalink
Merge pull request #9 from fgm/8-convert-container-to-interface
Browse files Browse the repository at this point in the history
Issue #8: convert Container to an interface.
  • Loading branch information
fgm authored Apr 28, 2023
2 parents 8abb1d9 + 296598b commit 04c73db
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 96 deletions.
71 changes: 40 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@

## Description

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

That container can hold two different kinds of data:

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

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

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

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

## Usage

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

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

`type Service func(*Container) (any, error)`
`type Service func(Container) (any, error)`

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

### Accessing the container

- General parameter access: `s, err := dic.Param("name")`
- Parameter access: `s, err := dic.Param("name")`
- Check the error against `nil`
- Type-assert the parameter value: `name := s.(string)`
- The type assertion cannot fail if the error was `nil`
- Simplified parameter access: `name := dic.MustParam("name").(string)`
- General service access: `s, err := dic.Service("logger")`
- Type-assert the parameter value: `name, ok := s.(string)`
- Or use shortcut: `name := dic.MustParam("name").(string)`
- Service access: `s, err := dic.Service("logger")`
- Check the error against `nil`
- Type-assert the service instance value: `logger := s.(*log.Logger)`
- The type assertion cannot fail if the error was `nil`
- Simplified service access: `logger := dic.MustService("logger").(*log.Logger)`
- Type-assert the service instance value: `logger, ok := s.(*log.Logger)`
- Or use shortcut: `logger := dic.MustService("logger").(*log.Logger)`


## Best practices
### Create a simpler developer experience

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

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

type container struct {
*izidic.Container
type Container struct {
izidic.Container
}

// Logger is a typed service accessor.
func (c *container) Logger() *log.Logger {
func (c *Container) Logger() *log.Logger {
return c.MustService("logger").(*log.Logger)
}

// Name is a types parameter accessor.
func (c *container) Name() string {
func (c *Container) Name() string {
return c.MustParam("name").(string)
}

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

func appService(dic *izidic.Container) (any, error) {
wdic := container{dic} // wrapped container with typed accessors
logger := dic.Logger() // typed service instance
name := dic.Name() // typed parameter value
func appService(dic izidic.Container) (any, error) {
wdic := Container{dic} // wrapped container with typed accessors
logger := wdic.Logger() // typed service instance
name := wdic.Name() // typed parameter value
appFeature := makeAppFeature(name, logger)
return appFeature, nil
}

func resolve(w io.Writer, name string, args []string) izidic.Container {
func Resolve(w io.Writer, name string, args []string) izidic.Container {
dic := izidic.New()
dic.Store("name", name)
dic.Store("writer", w)
dic.Register("logger", loggerService)
dic.Register("app", appService)
// others...
dic.Freeze()
return dic
}
```

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


### Create the container in a `Resolve` function

The cleanest way to initialize a container is to have the
project contain an function, conventionally called `Resolve`, which takes all globals used in the project,
and returns an instance of the custom container type defined above, as in [examples/di/di.go](examples/di/di.go).


### Do not pass the container

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

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

In most cases, the value obtained thus will be a `struct` or a `func`,
ready to be used without further data from the container.

See a complete demo in [examples/demo.go](examples/demo.go).
17 changes: 17 additions & 0 deletions examples/demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"log"
"os"

"github.com/fgm/izidic/examples/di"
)

func main() {
dic := di.Resolve(os.Stdout, os.Args[0], os.Args[1:])
app := dic.MustService("app").(di.App)
log.Printf("app: %#v\n", app)
if err := app(); err != nil {
os.Exit(1)
}
}
13 changes: 13 additions & 0 deletions examples/di/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package di

import "log"

// App represents whatever an actual application as a function would be.
type App func() error

func makeAppFeature(name string, logger *log.Logger) App {
return func() error {
logger.Println(name)
return nil
}
}
57 changes: 57 additions & 0 deletions examples/di/di.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package di

import (
"io"
"log"

"github.com/fgm/izidic"
)

// Container is an application-specific wrapper for a basic izidic container,
// adding typed accessors for simpler use by application code, obviating the need
// for type assertions.
type Container struct {
izidic.Container
}

// Logger is a typed service accessor.
func (c *Container) Logger() *log.Logger {
return c.MustService("logger").(*log.Logger)
}

// Name is a typed parameter accessor.
func (c *Container) Name() string {
return c.MustParam("name").(string)
}

// Resolve is the location where the parameters and services in the container
//
// are assembled and the container readied for use.
func Resolve(w io.Writer, name string, args []string) izidic.Container {
dic := izidic.New()
dic.Store("name", name)
dic.Store("writer", w)
dic.Register("app", appService)
dic.Register("logger", loggerService)
dic.Freeze()
return dic
}

func appService(dic izidic.Container) (any, error) {
wdic := Container{dic} // wrapped Container with typed accessors
logger := wdic.Logger() // typed service instance: *log.Logger
name := wdic.Name() // typed parameter value: string
appFeature := makeAppFeature(name, logger)
return appFeature, nil
}

// loggerService is an izidic.Service also containing a one-time initialization action.
//
// Keep in mind that the initialization will only be performed once the service has
// actually been instantiated.
func loggerService(dic izidic.Container) (any, error) {
w := dic.MustParam("writer").(io.Writer)
log.SetOutput(w) // Support dependency code not taking an injected logger.
logger := log.New(w, "", log.LstdFlags)
return logger, nil
}
Binary file added izidic-100x100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 04c73db

Please sign in to comment.