Skip to content

Commit

Permalink
Issue #8: convert Container to an interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
fgm committed Apr 26, 2023
1 parent 8abb1d9 commit 7ecd578
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 98 deletions.
70 changes: 37 additions & 33 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 Down Expand Up @@ -54,7 +54,7 @@ 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
build. The only restriction is that cycles are not supported.
Expand Down Expand Up @@ -94,54 +94,56 @@ as in this example:
package di

import (
"io"
"log"
"io"
"log"

"github.com/fgm/izidic"
"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 {
return c.MustService("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 {
return c.MustParam("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) {
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
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
}

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
appFeature := makeAppFeature(name, logger)
return appFeature, nil
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 {
dic := izidic.New()
dic.Store("writer", w)
dic.Register("logger", loggerService)
// others...
dic.Freeze()
return dic
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.


Expand All @@ -160,3 +162,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)
}
}
12 changes: 12 additions & 0 deletions examples/di/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package di

import "log"

type App func() error

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

import (
"io"
"log"

"github.com/fgm/izidic"
)

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 types parameter accessor.
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) {
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
}

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 {
dic := izidic.New()
dic.Store("name", name)
dic.Store("writer", w)
dic.Register("logger", loggerService)
dic.Register("app", appService)
// others...
dic.Freeze()
return dic
}
99 changes: 55 additions & 44 deletions izidic.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// allowing users to store definitions of services requiring other services
// before those are actually defined.
//
// Container writes are not concurrency-safe, so they are locked with Container.Freeze()
// container writes are not concurrency-safe, so they are locked with container.Freeze()
// after the initial setup, which is assumed to be non-concurrent
package izidic

Expand All @@ -22,10 +22,21 @@ import (
// which should then be type-asserted before use.
//
// Any access to a service from the container returns the same instance.
type Service func(dic *Container) (any, error)
type Service func(dic Container) (any, error)

type Container interface {
Freeze()
MustParam(name string) any
MustService(name string) any
Names() map[string][]string
Param(name string) (any, error)
Register(name string, fn Service)
Store(name string, param any)
Service(name string) (any, error)
}

// Container is the container, holding both parameters and services
type Container struct {
// container is the container, holding both parameters and services
type container struct {
sync.RWMutex // Lock for service instances
frozen bool
parameters map[string]any
Expand All @@ -35,12 +46,28 @@ type Container struct {

// Freeze converts the container from build mode, which does not support
// concurrency, to run mode, which does.
func (dic *Container) Freeze() {
func (dic *container) Freeze() {
dic.frozen = true
}

func (dic *container) MustParam(name string) any {
p, err := dic.Param(name)
if err != nil {
panic(err)
}
return p
}

func (dic *container) MustService(name string) any {
instance, err := dic.Service(name)
if err != nil {
panic(err)
}
return instance
}

// Names returns the names of all the parameters and instances defined on the container.
func (dic *Container) Names() map[string][]string {
func (dic *container) Names() map[string][]string {
dump := map[string][]string{
"params": make([]string, 0, len(dic.parameters)),
"services": make([]string, 0, len(dic.serviceDefs)),
Expand All @@ -58,24 +85,27 @@ func (dic *Container) Names() map[string][]string {
return dump
}

// Register registers a service with the container.
func (dic *Container) Register(name string, fn Service) {
if dic.frozen {
panic("Cannot register services on frozen container")
func (dic *container) Param(name string) (any, error) {
dic.RLock()
defer dic.RUnlock()

p, found := dic.parameters[name]
if !found {
return nil, fmt.Errorf("parameter not found: %q", name)
}
dic.serviceDefs[name] = fn
return p, nil
}

// Store stores a parameter in the container.
func (dic *Container) Store(name string, param any) {
// Register registers a service with the container.
func (dic *container) Register(name string, fn Service) {
if dic.frozen {
panic("Cannot store parameters on frozen container")
panic("Cannot register services on frozen container")
}
dic.parameters[name] = param
dic.serviceDefs[name] = fn
}

// Service returns the single instance of the requested service on success.
func (dic *Container) Service(name string) (any, error) {
func (dic *container) Service(name string) (any, error) {
// Reuse existing instance if any.
dic.RLock()
instance, found := dic.services[name]
Expand All @@ -86,7 +116,7 @@ func (dic *Container) Service(name string) (any, error) {

// Otherwise instantiate. No lock because no concurrent writes can happen:
// - during build, recursive calls may happen, but not concurrently
// - after freeze, no new services may be created: see Container.Register
// - after freeze, no new services may be created: see container.Register
service, found := dic.serviceDefs[name]
if !found {
return nil, fmt.Errorf("service not found: %q", name)
Expand All @@ -96,7 +126,7 @@ func (dic *Container) Service(name string) (any, error) {
// this step than there are services defined in the container, then resolution
// for at least one service was attempted more than once, which implies a
// dependency cycle.
const funcName = "github.com/fgm/izidic.(*Container).Service"
const funcName = "github.com/fgm/izidic.(*container).Service"
// We need a vastly oversized value to cover the case of deeply nested dic.Service() calls.
pcs := make([]uintptr, 1e6)
n := runtime.Callers(1, pcs)
Expand Down Expand Up @@ -128,36 +158,17 @@ func (dic *Container) Service(name string) (any, error) {
return instance, nil
}

func (dic *Container) MustService(name string) any {
instance, err := dic.Service(name)
if err != nil {
panic(err)
}
return instance
}

func (dic *Container) Param(name string) (any, error) {
dic.RLock()
defer dic.RUnlock()

p, found := dic.parameters[name]
if !found {
return nil, fmt.Errorf("parameter not found: %q", name)
}
return p, nil
}

func (dic *Container) MustParam(name string) any {
p, err := dic.Param(name)
if err != nil {
panic(err)
// Store stores a parameter in the container.
func (dic *container) Store(name string, param any) {
if dic.frozen {
panic("Cannot store parameters on frozen container")
}
return p
dic.parameters[name] = param
}

// New creates a container ready for use.
func New() *Container {
return &Container{
func New() Container {
return &container{
RWMutex: sync.RWMutex{},
parameters: make(map[string]any),
serviceDefs: make(map[string]Service),
Expand Down
Loading

0 comments on commit 7ecd578

Please sign in to comment.