From 296598b58e65a87067a63accc1dcc1a7ed9c5d69 Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Wed, 26 Apr 2023 15:22:48 +0200 Subject: [PATCH] Issue #8: convert Container to an interface. --- README.md | 71 ++++++++++++++++++-------------- examples/demo.go | 17 ++++++++ examples/di/app.go | 13 ++++++ examples/di/di.go | 57 ++++++++++++++++++++++++++ izidic-100x100.png | Bin 0 -> 22352 bytes izidic.go | 100 +++++++++++++++++++++++++-------------------- izidic_test.go | 43 +++++++++---------- 7 files changed, 205 insertions(+), 96 deletions(-) create mode 100644 examples/demo.go create mode 100644 examples/di/app.go create mode 100644 examples/di/di.go create mode 100644 izidic-100x100.png diff --git a/README.md b/README.md index af5879c..5aa61f5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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. @@ -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 @@ -100,40 +98,42 @@ 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 @@ -141,13 +141,20 @@ func resolve(w io.Writer, name string, args []string) izidic.Container { ``` 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. @@ -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). diff --git a/examples/demo.go b/examples/demo.go new file mode 100644 index 0000000..e0678d4 --- /dev/null +++ b/examples/demo.go @@ -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) + } +} diff --git a/examples/di/app.go b/examples/di/app.go new file mode 100644 index 0000000..c3baad8 --- /dev/null +++ b/examples/di/app.go @@ -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 + } +} diff --git a/examples/di/di.go b/examples/di/di.go new file mode 100644 index 0000000..64bb548 --- /dev/null +++ b/examples/di/di.go @@ -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 +} diff --git a/izidic-100x100.png b/izidic-100x100.png new file mode 100644 index 0000000000000000000000000000000000000000..9f55f909fec96d321d0cb0bedd91270681cf11f6 GIT binary patch literal 22352 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

Y5V1^b>l%J~ir> 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