Questions on usage & design
Closed this issue · 3 comments
I've been evaluating using this library, and I have a few questions about usage patterns.
I'm fine with a struct declaring certain fields as required (normally done by simply having a constructor), but I'm uncomfortable with a struct knowing the name of the specific instance it should be injected with (even if it uses an interface instead of a specific struct type). It seems that only a higher orchestration layer should know how to tie your classes together. Doesn't this put all names in the same global namespace? Wouldn't you eventually run into conflicts, especially using structs from various sources?
In order to pass struct pointers to Provide, the structs must be publicly accessible, and their injected fields must also be publicly accessible. Was there ever any thought put into accepting a local "context" struct with named fields using interfaces and then passing constructor functions to Provide? This would allow populating private structs that implement public interfaces.
Example:
func NewImplA(b InterfaceB) { return &implA{ b: b } }
func NewImplB() { return &implB{} }
type Context struct {
A InterfaceA
B InterfaceB
}
func main() {
var context Context
context.B = NewImplB()
inject.Provide(&context, context.B, NewImplB)
inject.Provide(&context, context.A, NewImplA)
}
If the structs needing injection all must be publicly accessible, and each struct also needs an interface to allow for mocking/faking for testing, what pattern do you use for naming your structs/interfaces when they only have one implementation?
... I'm uncomfortable with a struct knowing the name of the specific instance it should be injected with (even if it uses an interface instead of a specific struct type).
The struct can know either the specific instance it should be injected with, or expect an interface. Often it's easier to start with the specific instance type (typically a pointer to a struct), and switch to an interface when the injected instance needs to vary. There are likely other cases where a specific instance may be valuable.
Doesn't this put all names in the same global namespace? Wouldn't you eventually run into conflicts, especially using structs from various sources?
By "Names" do you me the types? If so the types are fully qualified and unique, so struct foo.Time
will not conflicts with struct time.Time
. As for interface types, the library requires that only 1 concrete implementation for a matching interface is provided, albeit it can only do so at runtime (when Populate
is called). We've found this works well in practice (combined with some tests that populate our graph and check for such errors).
In order to pass struct pointers to Provide, the structs must be publicly accessible, and their injected fields must also be publicly accessible. Was there ever any thought put into accepting a local "context" struct with named fields using interfaces and then passing constructor functions to Provide? This would allow populating private structs that implement public interfaces.
We thought about doing this by allowing constructor functions and using reflect to inject their arguments and use the return value as a provides. We think this is useful, and we'll accept patches to do this. We've managed to avoid this because we wanted both constructors and destructors. Our destructors are essentially our "application is shutting down, finish your background work". We implemented this using the object graph exposed by this library combined with https://github.com/facebookgo/startstop.
If the structs needing injection all must be publicly accessible, and each struct also needs an interface to allow for mocking/faking for testing, what pattern do you use for naming your structs/interfaces when they only have one implementation?
We don't prescribe any naming strategy here. Sometimes we have an interface called Store
with implementations called S3Store
or DiskStore
, others it's http.RoundTripper
and MockTransport
.
Hope that helps!
Thanks for the quick reply.
I think you misunderstood what I meant by name, tho. I was referring to the ability for a struct to name its tagged injected dependency fields using inject:"name"
. This inject name is only namespaced by the type of the field it annotates, which could be an interface or primitive, with multiple potential implementations not known to the struct that wants to be injected with one.
If naming is used to distinguish between multiple implementations, how is the developer of the struct supposed to pick a name without knowing the possible implementations that might be injected? The name could easily overlap with any other struct that wants to be injected with the same type.
example:
type ModelA struct {
Name string `inject:"model-name"`
}
type ModelB struct {
Name string `inject:"model-name"`
}
These models could be from very different libraries, both needing to be injected with named dependencies. How does a struct author choose a name that wont collide?
In other DI frameworks, like Java Spring for example, names are chosen by the creator of the context, not the author of the class. This allows you to inject the same named object into multiple classes that depend on the same interface, without the implementations needing to know about each other to coordinate naming. Requiring this kind of coordination would seem to violate the goals of inversion of control.
Ah yes, the named injects feature. The way I think of this feature is that it's a weak version of the javax.inject.Named
qualifier. It requires co-ordination since the mapping is a human provided string. The way we used it was to distinguish multiple top level instances of the same type. But for reasons you mentioned, and really for lack of a great use for them, we've stopped using them. We also don't typically inject primitives like strings, and types are sufficient for us to avoid collisions.