diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 533ab68..0642c34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: go run ./explain go run ./optional go run ./observe + go run ./factory - name: test run: go test -race -count=1 -coverprofile=coverage.out ./... - name: what only the hand-written tests reach diff --git a/README.md b/README.md index dce8a68..3cbba2d 100644 --- a/README.md +++ b/README.md @@ -584,6 +584,81 @@ not in the picture. A complete service with a worker, a health endpoint, request scopes and graceful shutdown is in [`examples/app`](examples/app/main.go). +#### Factories + +There is no transient lifetime. A value made fresh on every use is a +function, registered like any other service and asked for like one: + +[embedmd]:# (examples/factory/main.go go) +```go +// Factories: a value made fresh on every use is a function, and one that +// also needs a lifecycle is a scope. +package main + +import ( + "context" + "fmt" + + "github.com/floatdrop/di" +) + +type Config struct{ Prefix string } + +// The function's type is the key, so it is named: two factories with the +// same signature would otherwise collide, and the name is what Explain shows +// and what a wired constructor asks for. +type NewID func(n int) string + +type Job struct{ id string } + +func NewIDs(cfg Config) NewID { + return func(n int) string { return fmt.Sprintf("%s-%d", cfg.Prefix, n) } +} +func NewJob(newID NewID) *Job { return &Job{id: newID(3)} } +func release(_ context.Context, j *Job) error { fmt.Println("released", j.id); return nil } + +func main() { + app := di.New() + app.Value(Config{Prefix: "job"}) + app.Wire[NewID](NewIDs) + // A value with a lifecycle of its own is Scoped, resolved from a child + // scope opened for one unit of work and stopped with it. + app.Wire[*Job](NewJob).Scoped().OnStop(release) + + // The container built the factory once and never sees what it makes; + // the values are the caller's. + newID := app.Get[NewID]() + fmt.Println(newID(1), newID(2)) + + work := app.Child("work") + fmt.Println("running", work.Get[*Job]().id) + _ = work.Stop(context.Background()) +} +``` + +``` +job-1 job-2 +running job-3 +released job-3 +``` + +The function's type is the key, so it is named. Two factories with the same +signature would otherwise collide, and `NewID` is what `Explain` shows and +what `NewJob` asks for. The container builds the factory once, with +dependencies and hooks of its own, and never sees what it makes: those values +are the caller's to release, as a `*sql.Conn` from `sql.DB.Conn` is, or the +factory keeps what it made and its `OnStop` releases it, which is what a pool +is. + +A value that needs a lifecycle of its own is a scope's, not a factory's. +Mark it `Scoped()`, resolve it from a child opened for one unit of work, and +stop the child when the work ends. The container then drains it, stops it in +order and shows it in `Explain`. The cost is a scope per unit of work rather +than a call, so it is for values worth tracking. samber/do has +`ProvideTransient`, whose instances the container does not track either, so a +function here is the same thing said plainly; uber/fx has no transient at all +and uses the same function. + #### Groups `Group()` makes a registration one member of the group for its type instead diff --git a/examples/factory/main.go b/examples/factory/main.go new file mode 100644 index 0000000..5f00266 --- /dev/null +++ b/examples/factory/main.go @@ -0,0 +1,43 @@ +// Factories: a value made fresh on every use is a function, and one that +// also needs a lifecycle is a scope. +package main + +import ( + "context" + "fmt" + + "github.com/floatdrop/di" +) + +type Config struct{ Prefix string } + +// The function's type is the key, so it is named: two factories with the +// same signature would otherwise collide, and the name is what Explain shows +// and what a wired constructor asks for. +type NewID func(n int) string + +type Job struct{ id string } + +func NewIDs(cfg Config) NewID { + return func(n int) string { return fmt.Sprintf("%s-%d", cfg.Prefix, n) } +} +func NewJob(newID NewID) *Job { return &Job{id: newID(3)} } +func release(_ context.Context, j *Job) error { fmt.Println("released", j.id); return nil } + +func main() { + app := di.New() + app.Value(Config{Prefix: "job"}) + app.Wire[NewID](NewIDs) + // A value with a lifecycle of its own is Scoped, resolved from a child + // scope opened for one unit of work and stopped with it. + app.Wire[*Job](NewJob).Scoped().OnStop(release) + + // The container built the factory once and never sees what it makes; + // the values are the caller's. + newID := app.Get[NewID]() + fmt.Println(newID(1), newID(2)) + + work := app.Child("work") + fmt.Println("running", work.Get[*Job]().id) + _ = work.Stop(context.Background()) +}