mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
326 lines
8.6 KiB
Go
326 lines
8.6 KiB
Go
|
|
// Copyright (c) 2023 - 2025 IBM Corp.
|
||
|
|
// All rights reserved.
|
||
|
|
//
|
||
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
// you may not use this file except in compliance with the License.
|
||
|
|
// You may obtain a copy of the License at
|
||
|
|
//
|
||
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
//
|
||
|
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
|
// See the License for the specific language governing permissions and
|
||
|
|
// limitations under the License.
|
||
|
|
|
||
|
|
/*
|
||
|
|
Package di implements functions and data types supporting dependency injection patterns.
|
||
|
|
|
||
|
|
# Overview
|
||
|
|
|
||
|
|
The dependency injection (DI) framework provides a type-safe way to manage dependencies
|
||
|
|
between components in your application. It ensures that all dependencies are resolved
|
||
|
|
correctly and that instances are created as singletons.
|
||
|
|
|
||
|
|
# Core Concepts
|
||
|
|
|
||
|
|
Dependency - An abstract concept representing a service, function, or value with a specific type.
|
||
|
|
Dependencies can be:
|
||
|
|
- Simple values (API URLs, configuration strings, numbers)
|
||
|
|
- Complex objects (HTTP clients, database connections)
|
||
|
|
- Service interfaces
|
||
|
|
- Functions
|
||
|
|
|
||
|
|
InjectionToken - A unique identifier for a dependency that includes type information.
|
||
|
|
Created using MakeToken[T](name).
|
||
|
|
|
||
|
|
Provider - The implementation of a dependency. Providers specify:
|
||
|
|
- Which dependency they provide (via an InjectionToken)
|
||
|
|
- What other dependencies they need
|
||
|
|
- How to create the instance
|
||
|
|
|
||
|
|
InjectableFactory - The container that manages all providers and resolves dependencies.
|
||
|
|
All resolved instances are singletons.
|
||
|
|
|
||
|
|
# Basic Usage
|
||
|
|
|
||
|
|
Creating and using dependencies:
|
||
|
|
|
||
|
|
import (
|
||
|
|
"github.com/IBM/fp-go/v2/di"
|
||
|
|
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Define injection tokens
|
||
|
|
var (
|
||
|
|
ConfigToken = di.MakeToken[Config]("Config")
|
||
|
|
DBToken = di.MakeToken[Database]("Database")
|
||
|
|
APIToken = di.MakeToken[APIService]("APIService")
|
||
|
|
)
|
||
|
|
|
||
|
|
// Create providers
|
||
|
|
configProvider := di.ConstProvider(ConfigToken, Config{Port: 8080})
|
||
|
|
|
||
|
|
dbProvider := di.MakeProvider1(
|
||
|
|
DBToken,
|
||
|
|
ConfigToken.Identity(),
|
||
|
|
func(cfg Config) IOE.IOEither[error, Database] {
|
||
|
|
return IOE.Of[error](NewDatabase(cfg))
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
apiProvider := di.MakeProvider2(
|
||
|
|
APIToken,
|
||
|
|
ConfigToken.Identity(),
|
||
|
|
DBToken.Identity(),
|
||
|
|
func(cfg Config, db Database) IOE.IOEither[error, APIService] {
|
||
|
|
return IOE.Of[error](NewAPIService(cfg, db))
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
// Create injector and resolve
|
||
|
|
injector := DIE.MakeInjector([]DIE.Provider{
|
||
|
|
configProvider,
|
||
|
|
dbProvider,
|
||
|
|
apiProvider,
|
||
|
|
})
|
||
|
|
|
||
|
|
// Resolve the API service
|
||
|
|
resolver := di.Resolve(APIToken)
|
||
|
|
result := resolver(injector)()
|
||
|
|
|
||
|
|
# Dependency Types
|
||
|
|
|
||
|
|
Identity (Required) - The dependency must be resolved, or the injection fails:
|
||
|
|
|
||
|
|
token.Identity() // Returns Dependency[T]
|
||
|
|
|
||
|
|
Option (Optional) - The dependency is optional, returns Option[T]:
|
||
|
|
|
||
|
|
token.Option() // Returns Dependency[Option[T]]
|
||
|
|
|
||
|
|
IOEither (Lazy Required) - Lazy evaluation, memoized singleton:
|
||
|
|
|
||
|
|
token.IOEither() // Returns Dependency[IOEither[error, T]]
|
||
|
|
|
||
|
|
IOOption (Lazy Optional) - Lazy optional evaluation:
|
||
|
|
|
||
|
|
token.IOOption() // Returns Dependency[IOOption[T]]
|
||
|
|
|
||
|
|
# Provider Creation
|
||
|
|
|
||
|
|
Providers are created using MakeProvider functions with suffixes indicating
|
||
|
|
the number of dependencies (0-15):
|
||
|
|
|
||
|
|
MakeProvider0 - No dependencies:
|
||
|
|
|
||
|
|
provider := di.MakeProvider0(
|
||
|
|
token,
|
||
|
|
IOE.Of[error](value),
|
||
|
|
)
|
||
|
|
|
||
|
|
MakeProvider1 - One dependency:
|
||
|
|
|
||
|
|
provider := di.MakeProvider1(
|
||
|
|
resultToken,
|
||
|
|
dep1Token.Identity(),
|
||
|
|
func(dep1 Dep1Type) IOE.IOEither[error, ResultType] {
|
||
|
|
return IOE.Of[error](createResult(dep1))
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
MakeProvider2 - Two dependencies:
|
||
|
|
|
||
|
|
provider := di.MakeProvider2(
|
||
|
|
resultToken,
|
||
|
|
dep1Token.Identity(),
|
||
|
|
dep2Token.Identity(),
|
||
|
|
func(dep1 Dep1Type, dep2 Dep2Type) IOE.IOEither[error, ResultType] {
|
||
|
|
return IOE.Of[error](createResult(dep1, dep2))
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Constant Providers
|
||
|
|
|
||
|
|
For simple constant values:
|
||
|
|
|
||
|
|
provider := di.ConstProvider(token, value)
|
||
|
|
|
||
|
|
# Default Implementations
|
||
|
|
|
||
|
|
Tokens can have default implementations that are used when no explicit
|
||
|
|
provider is registered:
|
||
|
|
|
||
|
|
token := di.MakeTokenWithDefault0(
|
||
|
|
"ServiceName",
|
||
|
|
IOE.Of[error](defaultImplementation),
|
||
|
|
)
|
||
|
|
|
||
|
|
// Or with dependencies
|
||
|
|
token := di.MakeTokenWithDefault2(
|
||
|
|
"ServiceName",
|
||
|
|
dep1Token.Identity(),
|
||
|
|
dep2Token.Identity(),
|
||
|
|
func(dep1 Dep1Type, dep2 Dep2Type) IOE.IOEither[error, ResultType] {
|
||
|
|
return IOE.Of[error](createDefault(dep1, dep2))
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Multi-Value Dependencies
|
||
|
|
|
||
|
|
For dependencies that can have multiple implementations:
|
||
|
|
|
||
|
|
// Create a multi-token
|
||
|
|
loggersToken := di.MakeMultiToken[Logger]("Loggers")
|
||
|
|
|
||
|
|
// Provide multiple items
|
||
|
|
consoleLogger := di.ConstProvider(loggersToken.Item(), ConsoleLogger{})
|
||
|
|
fileLogger := di.ConstProvider(loggersToken.Item(), FileLogger{})
|
||
|
|
|
||
|
|
// Resolve all items as an array
|
||
|
|
resolver := di.Resolve(loggersToken.Container())
|
||
|
|
loggers := resolver(injector)() // Returns []Logger
|
||
|
|
|
||
|
|
# Lazy vs Eager Resolution
|
||
|
|
|
||
|
|
Eager (Identity/Option) - Resolved immediately when the injector is created:
|
||
|
|
|
||
|
|
dep1Token.Identity() // Resolved eagerly
|
||
|
|
dep2Token.Option() // Resolved eagerly
|
||
|
|
|
||
|
|
Lazy (IOEither/IOOption) - Resolved only when accessed:
|
||
|
|
|
||
|
|
dep3Token.IOEither() // Resolved lazily
|
||
|
|
dep4Token.IOOption() // Resolved lazily
|
||
|
|
|
||
|
|
Lazy dependencies are memoized, so they're only created once.
|
||
|
|
|
||
|
|
# Main Application Pattern
|
||
|
|
|
||
|
|
The framework provides a convenient pattern for running applications:
|
||
|
|
|
||
|
|
import (
|
||
|
|
"github.com/IBM/fp-go/v2/di"
|
||
|
|
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Define your main application logic
|
||
|
|
mainProvider := di.MakeProvider1(
|
||
|
|
di.InjMain,
|
||
|
|
APIToken.Identity(),
|
||
|
|
func(api APIService) IOE.IOEither[error, any] {
|
||
|
|
return IOE.Of[error](api.Start())
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
// Run the application
|
||
|
|
err := di.RunMain([]DIE.Provider{
|
||
|
|
configProvider,
|
||
|
|
dbProvider,
|
||
|
|
apiProvider,
|
||
|
|
mainProvider,
|
||
|
|
})()
|
||
|
|
|
||
|
|
# Practical Examples
|
||
|
|
|
||
|
|
Example 1: Configuration-based Service
|
||
|
|
|
||
|
|
type Config struct {
|
||
|
|
APIKey string
|
||
|
|
Timeout int
|
||
|
|
}
|
||
|
|
|
||
|
|
type HTTPClient struct {
|
||
|
|
config Config
|
||
|
|
}
|
||
|
|
|
||
|
|
var (
|
||
|
|
ConfigToken = di.MakeToken[Config]("Config")
|
||
|
|
ClientToken = di.MakeToken[HTTPClient]("HTTPClient")
|
||
|
|
)
|
||
|
|
|
||
|
|
configProvider := di.ConstProvider(ConfigToken, Config{
|
||
|
|
APIKey: "secret",
|
||
|
|
Timeout: 30,
|
||
|
|
})
|
||
|
|
|
||
|
|
clientProvider := di.MakeProvider1(
|
||
|
|
ClientToken,
|
||
|
|
ConfigToken.Identity(),
|
||
|
|
func(cfg Config) IOE.IOEither[error, HTTPClient] {
|
||
|
|
return IOE.Of[error](HTTPClient{config: cfg})
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
Example 2: Optional Dependencies
|
||
|
|
|
||
|
|
var (
|
||
|
|
CacheToken = di.MakeToken[Cache]("Cache")
|
||
|
|
ServiceToken = di.MakeToken[Service]("Service")
|
||
|
|
)
|
||
|
|
|
||
|
|
// Service works with or without cache
|
||
|
|
serviceProvider := di.MakeProvider1(
|
||
|
|
ServiceToken,
|
||
|
|
CacheToken.Option(), // Optional dependency
|
||
|
|
func(cache O.Option[Cache]) IOE.IOEither[error, Service] {
|
||
|
|
return IOE.Of[error](NewService(cache))
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
Example 3: Lazy Dependencies
|
||
|
|
|
||
|
|
var (
|
||
|
|
DBToken = di.MakeToken[Database]("Database")
|
||
|
|
ReporterToken = di.MakeToken[Reporter]("Reporter")
|
||
|
|
)
|
||
|
|
|
||
|
|
// Reporter only connects to DB when needed
|
||
|
|
reporterProvider := di.MakeProvider1(
|
||
|
|
ReporterToken,
|
||
|
|
DBToken.IOEither(), // Lazy dependency
|
||
|
|
func(dbIO IOE.IOEither[error, Database]) IOE.IOEither[error, Reporter] {
|
||
|
|
return IOE.Of[error](NewReporter(dbIO))
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Function Reference
|
||
|
|
|
||
|
|
Token Creation:
|
||
|
|
- MakeToken[T](name) InjectionToken[T] - Creates a unique injection token
|
||
|
|
- MakeTokenWithDefault[T](name, factory) InjectionToken[T] - Token with default implementation
|
||
|
|
- MakeTokenWithDefault0-15 - Token with default and N dependencies
|
||
|
|
- MakeMultiToken[T](name) MultiInjectionToken[T] - Token for multiple implementations
|
||
|
|
|
||
|
|
Provider Creation:
|
||
|
|
- ConstProvider[T](token, value) Provider - Simple constant provider
|
||
|
|
- MakeProvider0[R](token, factory) Provider - Provider with no dependencies
|
||
|
|
- MakeProvider1-15 - Providers with 1-15 dependencies
|
||
|
|
- MakeProviderFactory0-15 - Lower-level factory creation
|
||
|
|
|
||
|
|
Resolution:
|
||
|
|
- Resolve[T](token) ReaderIOEither[InjectableFactory, error, T] - Resolves a dependency
|
||
|
|
|
||
|
|
Application:
|
||
|
|
- InjMain - Injection token for main application
|
||
|
|
- Main - Resolver for main application
|
||
|
|
- RunMain(providers) IO[error] - Runs the main application
|
||
|
|
|
||
|
|
Utility:
|
||
|
|
- asDependency[T](t) Dependency - Converts to dependency interface
|
||
|
|
|
||
|
|
# Related Packages
|
||
|
|
|
||
|
|
- github.com/IBM/fp-go/v2/di/erasure - Type-erased DI implementation
|
||
|
|
- github.com/IBM/fp-go/v2/ioeither - IO operations with error handling
|
||
|
|
- github.com/IBM/fp-go/v2/option - Optional values
|
||
|
|
- github.com/IBM/fp-go/v2/either - Either monad for error handling
|
||
|
|
|
||
|
|
[Provider]: [github.com/IBM/fp-go/v2/di/erasure.Provider]
|
||
|
|
[InjectableFactory]: [github.com/IBM/fp-go/v2/di/erasure.InjectableFactory]
|
||
|
|
[MakeInjector]: [github.com/IBM/fp-go/v2/di/erasure.MakeInjector]
|
||
|
|
*/
|
||
|
|
package di
|
||
|
|
|
||
|
|
//go:generate go run .. di --count 15 --filename gen.go
|