AliSoftware/Dip

Using tags seems to create a separate scope.

Closed this issue · 4 comments

Xiot commented

I have a bunch of plugin-type objects that all need implement the same protocol and I need to be able to resolve them by name when they are needed. When I saw the Named Components, I thought, great, this is exactly what I need.

I ended up running into an issue though then those plugins have shared dependencies.
It appears that when an object is resolved with a tag, it creates a new scope for all of the dependencies.

Heres a simplified example

import Dip

var count = 0
class Dependency {
    let id: Int
    init() {
        self.id = count++
    }
}

class Root {
    let dep: Dependency
    init(dep: Dependency) {
        self.dep = dep
    }
}

let container = DependencyContainer()
container.register(.Singleton) {Dependency()}

container.register(.Singleton) {Root(dep: $0)}
container.register(.Singleton, tag: "other") {Root(dep: $0)}

let dep = try! container.resolve() as Dependency
let noTag = try! container.resolve() as Root
let withTag = try! container.resolve(tag: "other") as Root

let rawId     = dep.id
let noTagId   = noTag.dep.id
let withTagId = withTag.dep.id

Since Dependency is marked as a .Singleton I would expect that there would only be one instance of it created in the container. What I found is that the instance that is injected into withTag is different than the one that is injected into the one resolved without the tag.

In the example above

rawId == 0
noTagId == 0
withTagId == 1

This was all ran in the Playground on the swift2.3 branch.
I'll look into the code, but I figured that I'd post it here in case anyone else has noticed this.

Xiot commented

I'm just reading through the code now, and I am trying to figure out if what I witnessed above is by design or not.
From the comments on Context

When auto-wiring or auto-injecting tag will be implicitly passed through by the container.

I understand that it will use the tag used to resolve the root for any dependencies, so If there was a separate registration of Dependency above with a tag of other then it would use that when resolving Root with the other tag.

Was the intention to create an implicit registration with the tag for all of the dependencies, or was it just to use it if it existed?

I changed my sample to not use auto-wiring for the named component and I saw the behavior that I was expecting:

container.register(.Singleton, tag: "other") {
    Root(dep: try! container.resolve() as Dependency)
}

Hi @Xiot . You understand it correctly - tags are implicitly passed to resolve dependencies in the graph when auto-wiring or auto-injecting. It is used so that if there is a registration of the dependency with such tag we use it instead of always using untagged. It's done to avoid unneeded sharing when auto-wiring or auto-injecting as there is less user control there to decide what tags to use. If that implicit behaviour is undesirable there is always an option to resolve dependencies explicitly. Exactly like you did in your last code snippet.

The side effect of that is that when we try to resolve type with different tags, but it is only registered without tag, resolved instances will be not shared, as using not tagged registration is a fallback, but resolved instances are still associated with the tag that is used to resolve. This is also intentional and to avoid unexpected sharing. That's just how named definitions work in Dip.
That applies to any scope as the tag is a part of the key used to reference resolved instances. That might be considered as a bug for singleton scope, but I would think about that more before deciding to change that behaviour or not. It may make sense to have several singletons for different tags in some setups. At the end it's not a real singleton, it's just shared in dependencies graphs.

I hope the explanation makes sense. I will inspect documentation to add that info there if it is missing. It's definitely should be explained there.

Xiot commented

My initial confusion was that it created a separate scope that I was not expecting.
I can see the benefits doing things either way. A singleton may take a dependency on a tagged service so sharing that across 'tag scopes' could give incorrect results.

ie.

container.register(.Singleton) {Service(config: $0, other: $1)}
container.register(.Singleton, tag: "dev") {Config("dev")}
container.register(.Singleton, tag: "release") {Config("release")}
container.register(.Singleton) {Other()}

let devService = container.resolve(tag: "dev") as Service
let releaseService = container.resolve(tag: "release") as Service

With way the tag scopes are configured, devService and releaseService would be separate instances would would be expected as they rely on dependencies that are actually tagged.
Their instances of Other would be different as well, which may not be expected, and is what I ran into.

One way around this would be to walk down the dependency tree to see if there are any dependencies that have the tag you are looking for and if so then promote it to the 'tag scope'. This gets extra tricky when you are dealing with resolveDependencies and the ability to directly look into the context to see and use the current tag.
More than likely, this is not worth the engineering effort to get this to work effectively.

Adding something to the documentation describing that the use of tags creates its own scope would be helpful to others trying to use this feature.

Thanks for the explanation.

Wiki pages about named definitions, auto-wirigin and auto-injection are updated. Added new page about using DependencyContainer.Context.