Objects not identical when using container collaboration
Closed this issue · 8 comments
Hello,
First of all, thanks to all for making this well-documented and easy to use (most of the time) framework :) I'm having a hard time understanding why someClass1
and someClass2
in my Playground code below are not pointing to the same object as someClassFromSecondContainer
. Because SomeClass
/SomeProtocol
is registered as a singleton and rootContainer
and secondContainer
are collaborating I would expect that a resolve via secondContainer
would point to the same object as a resolve via rootContainer
.
I'm resolving with rootContainer
twice in my code below (someClass1
and someClass2
) to prove that multiple resolves of a singleton from the the same container do indeed point to the same object, however, a resolve from a collaborating container does not.
Is this a bug in Dip or is this expected behaviour? Thanks so much for your help :)
import Dip
protocol SomeProtocol { }
class SomeClass: SomeProtocol { }
let rootContainer = DependencyContainer() { c in
c.register(.Singleton) { SomeClass() as SomeProtocol }
}
let secondContainer = DependencyContainer() { c in
// ... Normally I would register some other things here
}
rootContainer.collaborate(with: secondContainer)
secondContainer.collaborate(with: rootContainer)
let someClass1 = try? rootContainer.resolve() as SomeProtocol
let someClass2 = try? rootContainer.resolve() as SomeProtocol
let someClassFromSecondContainer = try? secondContainer.resolve() as SomeProtocol
if let someClass1 = someClass1 as? SomeClass,
let someClass2 = someClass2 as? SomeClass,
let someClassFromSecondContainer = someClassFromSecondContainer as? SomeClass {
unsafeAddressOf(someClass1) // "UnsafePointer(0x7FCDB05131B0)"
unsafeAddressOf(someClass2) // "UnsafePointer(0x7FCDB05131B0)"
unsafeAddressOf(someClassFromSecondContainer) // "UnsafePointer(0x7FA9B1531710)"
someClass1 === someClass2 // true
someClass1 === someClassFromSecondContainer // false
someClass2 === someClassFromSecondContainer // false
}
Hi @basalphenaar. Can you check the same in the app instead of playground? Sometimes I got weird behaviour in playgrounds. I will take a look at this issue later. Thank you!
Indeed there is an issue in the framework, thanks for pointing it out! Fix will follow soon.
@ilyapuchka Thanks for the insanely fast fix, will try it out immediately tomorrow!
@ilyapuchka seems it's not working 100% right
I'm running tests in release 4.6.1
When resolving after collaboration, all is ok
let container1 = DependencyContainer()
container1.register(scope) { ServiceImp1() as Service }
let container2 = DependencyContainer()
container2.collaborate(with: container1)
let service1: ServiceImp1 = try! container1.resolve()
let service2: ServiceImp1 = try! container2.resolve()
XCTAssertTrue(service1 === service2) // ok
In this case services are not equal
let container1 = DependencyContainer()
container1.register(scope) { ServiceImp1() as Service }
let service1: ServiceImp1 = try! container1.resolve()
let container2 = DependencyContainer()
container2.collaborate(with: container1)
let service2: ServiceImp1 = try! container2.resolve()
XCTAssertTrue(service1 === service2) // failed
@sergthedeveloper thanks for mentioning that. If you look at the implementation of collaborate(with:)
method you will see that it overrides resolved instances pool of container in parameter with resolved instances pool of container on which this method is called so that they share those pools. So here it will override instances pool of container1
which contains resolved instance with instances pool of container2
which is empty. And on next resolve it creates new instance. That might be considered as a bug, but I would strongly discourage from using containers like that and only resolve after they are fully set up.
@ilyapuchka
Ok, I see. Here is my scenario:
I have one root container with application-wide services (like ServerApi
or Preferences
or whatever) and some scoped containers for separate screens or user stories with story-oriented services (like PurchaseModel
).
Scoped container depends on root and meant to be initialized later, when corresponding screen or user story begins and destroyed when story ends (container's reset()
method should do the trick, right?).
So my concern was to make subcontainers depending on (collaboration with) root container.
All of these subcontainers should be using the same instance of ServerApi
or Preferences
from root, whenever they are instantiated.
What I'm doing now is registering already instantiated objects, not factories, in root container.
But it kills benefit of lazy instantiation.
Any advice?
@sergthedeveloper there is no reason to create containers later at runtime. They are lightweight and basically store just two maps between definition key and definition and instances pool for singleton scoped definitions only. So you can simply set them up right away. There is no need to call reset
on container too as it will remove all registered definitions, not just resolved instances. Also it will make you reference container in your code, which is also strongly discouraged. Basically reset
might be helpful for tests.
If I understood correctly you want to re-instantiate some graph of objects specific for some user story when user goes into it. For that you should have some root object in that graph scoped as WeakSingleton
. So when user leaves user story you simply nilify all references to this object that you have in your code and then the whole graph will be deallocated (as container holds a weak reference to such instances). The rest of the setup does not change. You still register root components in a root container and user story specific components in separate containers. And make them to collaborate with root container.
You can look into VIPER-SWIFT example and play with it, there are a lot of comments to help and about using weak singletons too. I hope it will help.
@ilyapuchka thanks, will have a look!