Closed erickskrauch closed 4 years ago
@erickskrauch Hi! Thanks for your interest =)
In the current implementation, the change of dependency graph after compilation is restricted for a more clear injection mechanism. This means that you can't add a provider of type based on the other providing result. The dependency graph must be formed before it is compiled and it can't change in the future.
In this case, you can create a configuration outside a container and change the list of options:
config := viper.New()
// config initialization code
// ...
var options []di.Option
switch config.GetString("preferred_service") {
case "A":
options = append(options, di.Provide(NewServiceA))
case "B":
options = append(options, di.Provide(NewServiceB))
}
options = append(options,
di.Provide(func() *viper.Viper { return config }), // provide already existing config
)
container, err := di.New(options...)
I'm just thinking about how to make this functionality easy and clear.
Now, we have some ideas:
WithCondition()
provide optionI will form some proposals for this in the near future.
Thanks for the reply!
The first idea was to create some sort of reduced container interface (without methods that would modify the dependency graph) that could be accepted as service dependency and will let to call the Invoke
, Resolve
and Has
methods. But I think disclosing the last two methods makes it somehow close to some kind of service locator :)
Updated example:
package main
import (
"github.com/goava/di"
"github.com/spf13/viper"
)
type ServiceInterface interface {
Do()
}
type DependencyA interface {}
type ServiceImplementationA struct {DependencyA}
func (s *ServiceImplementationA) Do() {}
func NewServiceA(a DependencyA) *ServiceImplementationA {
return &ServiceImplementationA{a}
}
type DependencyB interface {}
type ServiceImplementationB struct {DependencyB}
func (s *ServiceImplementationB) Do() {}
func NewServiceB(b DependencyB) *ServiceImplementationB {
return &ServiceImplementationB{b}
}
func NewService(container di.InjectedContainer, config *viper.Viper) ServiceInterface {
if viper.GetString("preferred_service") == "B" {
return container.Invoke(NewServiceB)
}
return container.Invoke(NewServiceA)
}
func main() {
container, _ := di.New(
di.Provide(viper.New),
di.Provide(NewService),
di.Provide(func() DependencyA {
return &struct{}{}
}),
di.Provide(func() DependencyB {
return &struct{}{}
}),
)
var service ServiceInterface
container.Resolve(&service)
}
The example looks very identical to my late provide idea ^^
I just thought that this is how the Invoke
method turns out to be the most useless because there are no generics in Go and it will be impossible to recognize the return type (and Invoke
returns only the err
).
Need to think of something else that will allow you to automatically determine the list of dependencies and at the same time not to lose the returned type so that you don't have to perform typecasting.
You need to merge the automatic arguments injection of Invoke
's method with Resolve
's result passing by reference (I hope you were able to understand that "English" 😅).
Let's call it InvokeWithResult
(the name is temp 😅):
func NewService(config *viper.Viper, container di.InjectedContainer) (result ServiceInterface, err error) {
if config.GetString("preferred_service") == "B" {
err = container.InvokeWithResult(NewServiceB, &result)
} else {
err = container.InvokeWithResult(NewServiceA, &result)
}
}
@erickskrauch, @fionera I lean towards to implement WithCondition()
provide option:
func NewConfig() *viper.Viper {
// initialization code omitted
return viper.New()
}
func IsServiceA(config *viper.Viper) bool {
return config.GetString("preferred_service") == "A"
}
func IsServiceB(config *viper.Viper) bool {
return config.GetString("preferred_service") == "B"
}
func main() {
c, err := di.New(
di.Provide(NewConfig),
di.Provide(NewServiceA, di.WithCondition(IsServiceA)),
di.Provide(NewServiceB, di.WithCondition(IsServiceB)),
)
}
When you implement this, you could basically implement it so that di.WithCondition gets executed like a late provide wrapper
func NewService(container di.InjectedContainer, config *viper.Viper) di.Option {
if viper.GetString("preferred_service") == "B" {
return di.Provide(NewServiceB)
}
return di.Provide(NewServiceA)
}
func main() {
c, err := di.New(
di.Provide(NewConfig),
di.Provide(NewService)
)
}
Introducing the WithCondition
option looks like an acceptable (but very verbose) solution for some simple conditions of choosing the target dependency. But if the logic becomes more complex, each case will have to be rewritten over and over again, which will be the source of hard debuggable errors.
Introducing some magic interface will be a more natural way to solve this task. Maybe you should introduce some di.LateProvide(NewService)
, which should return di.Option
as @fionera suggested. Although this raises the problem that the dependency graph is modified by the service creator result.
That's why my solution with InvokeWithResult
seems to be more suitable: the dependency graph doesn't change, but the initial task can be reached :)
@erickskrauch WithCondition()
separates condition and provide logic. Providing already tested. Condition logic will be easy to test. Even complex cases can be tested and the test will be primitive.
@fionera Provide di.Option
needs additional instruments to test on the user side. You must be sure that you have provided the appropriate option.
@erickskrauch Your solution returns interface instead of implementation in the constructor, that doesn't fit the principle "return structs accept interfaces". And does not allow you to use di.As() as it is now. Also, introducing a magic interface is not "go way". Dependency injection already contains a lot of "magic". I wouldn't want to make it more complicated.
And if we are talking about the possibility of container resolving. Then we don't need to add additional methods:
package main
import (
"errors"
"fmt"
"log"
"net"
"github.com/goava/di"
)
type ConnectionType string
// NewConnection
func NewConnection(typ ConnectionType, container *di.Container) (net.Conn, error) {
switch typ {
case "tcp":
var conn *net.TCPConn
return conn, container.Resolve(&conn)
case "udp":
var conn *net.UDPConn
return conn, container.Resolve(&conn)
}
return nil, errors.New("unknown connection type")
}
func NewTCPConn() *net.TCPConn {
return &net.TCPConn{}
}
func NewUDPConn() *net.UDPConn {
return &net.UDPConn{}
}
func NewConnectionType() ConnectionType {
return "udp" // change to "tcp" for tcp connection
}
func main() {
c, err := di.New(
di.WithCompile(),
di.Provide(NewConnectionType),
di.Provide(NewTCPConn),
di.Provide(NewUDPConn),
di.Provide(NewConnection),
)
if err != nil {
log.Fatalln(err)
}
// hack to provide container
if err := c.Provide(func() *di.Container { return c }); err != nil {
log.Fatalln(err)
}
if err := c.Compile(); err != nil {
log.Fatalln(err)
}
var conn net.Conn
if err := c.Resolve(&conn); err != nil {
log.Fatalln(err)
}
switch conn.(type) {
case *net.TCPConn:
fmt.Println("TCP")
case *net.UDPConn:
fmt.Println("UDP")
}
}
ConnectionType
can be replaced with the configuration type and this will work. But it is the hack =D
Your solution returns interface instead of implementation in the constructor, that doesn't fit the principle "return structs accept interfaces".
But the target structures, for the creation of which di is necessary, expect interfaces, not concrete implementations. Otherwise, what is the point of all this at all?
I need to define the interface implementation for the application. But its implementation is chosen based on the configuration. That's why I need some layer, which will allow me to make a decision about the final implementation based on the configuration or other dependencies.
Thus, we come to the conclusion that if we ensure the availability of the container when resolving dependencies, then, in general, the problem is solved. Maybe we should then make container available by default?
Sorry for delay, I had a hard time today.
But the target structures, for the creation of which di is necessary, expect interfaces, not concrete implementations. Otherwise, what is the point of all this at all?
di.As()
binds implementation to the interface. It is bad practice to return the interface from the constructor.
Thus, we come to the conclusion that if we ensure the availability of the container when resolving dependencies, then, in general, the problem is solved. Maybe we should then make container available by default?
This can be true. I create #9 with some improvements:
Late provide example: https://github.com/goava/di/blob/make-graph-editable/_examples/lateprovide/main.go
It still needs testing, but if it shows up well, we'll release it.
I still need to reimplement the acyclicity check and di.Prototype()
.
Glad we could come to a decision :)
I have already started implementing DI in my project. You can have a look at it: https://github.com/elyby/chrly/tree/di/di. I'll update the code to #9's solution when it'll be ready.
Fixes merged. I need some time to prepare the release.
Hello.
I'm considering introducing DI into my project. Your DI implementation container looks good enough :) But I have a question: how to create services that are created through user's configuration? Here's an example:
Depending on the configuration, I want to create a service interface implementation. But each implementation has its own dependencies list (I have not provided them manually in the example). I need a way to somehow call
container.Resolve(&concreteImplementation)
from theNewService
function.Is there any way to do this? Or maybe I just misunderstand how it should be done at all?