Allow for creating instances in the container right after it is initialized
Closed this issue · 15 comments
This is mostly about the Singleton scope. In many cases, the "create and preserve after first resolve was made" approach can cause various issues, especially if some of these singletons would use events/signals for communication. To provide more flexibility, it would be great to provide a way for singletons to be created and preserved right away and not waiting for first resolve reference. This could be configured per singleton or as a general Dip property.
I think what you are asking is something that Typhoon achieves with Singleton/LazySingleton scope (https://github.com/appsquickly/Typhoon/wiki/Scopes). Right?
But I'm not sure container should handle such cases with separate scope. Scopes can be already confusing, not sure we need to add another one (in case of Dip where components are lazy by default it will be something like EagerSingleton...).
If I understand correctly you can use something like that:
let container = DependencyContainer { container in
let singleton = ... //create and setup singleton
container.register { singleton as SingletonType }
}
First create your singleton by yourself, then registration captures this instance. Then it will always return this instance when you resolve SingletonType.
Will it work for the case that you mean? Do you have any example of possible issues with signals that you mentioned?
I'm not sure that's exactly what I wanted but maybe? It would look to me that Dip's Singleton is Typhoon's TyphoonScopeLazySingleton, but there's no Dip counterpart for Typhoon's TyphoonScopeSingleton?
What I've asked for is merely, for the container to instantiate all the objects registered within as .Singleton scope, right after initializing itself, rather than waiting for these singletones to be referenced somewhere, and resolved. At that point, during resolving, all the singletons would be already created, and they would just be returned for the purpose of resolving, but they would already sit in memory and wouldn't have to be created.
So, in essence, I really don't think any syntax changes are necessary. If I have this:
let container = DependencyContainer { container in
container.register(.Singleton) { ChatService() as ChatServiceProtocol }
container.register(.Singleton) { ClientService() as ClientServiceProtocol }
}
in the container initialization code, I'd like it to instantiate these right away for me, rather than waiting for resolving somewhere. I could either tell it to do this by using some other scope per register (.InstantSingleton, something like that), or I could set a generic property on the container itself, like container.forceSingletonInstantiation = YES.
Currently, there is a workaround, but it basically requires me to force instantiate all the singletons defined in container (usually in AppDelegate), like so:
try! container.resolve() as? ChatServiceProtocol
try! container.resolve() as? ClientServiceProtocol
which isn't very elegant. The example you've provided is just a variation of that solution. Yes, it works, but it feels out of place and rather unnecessary.
The main example is that when some of your objects are using main signal/event bus for decoupled communication. In this case, I'm using NSNotification to pass certain data around, without tightly coupling things. For instance, when I successfully log in, I'm receving a special auth token, that I need to pass to all my Service objects that work with the server API and I'm doing this using events. Service are self-contained objects and they should never have any reference to any other part of the application, thus the reason for passing the auth token to them by event. But LoginService that does that is the first service instantiated and referenced by container. When it broadcasts its event with auth token, not all services are instantiated at this point (because you're just looking at login screen), and so they will not receive this event. Later in the application cycle, once they're first referenced and created, it's already too late, and they operate without the auth token. Since they have no connection to the rest of the application, they have no mean to obtain that token anymore (well, sure, there are ways, but they're just poor design)
If they were instantiated all at the same time, right away, by the container, this wouldn't be an issue.
Instantiating elements in the IoC container right away is quite a common feature in other languages (Java's Spring, Flex's Parsley etc.), so it would be nice to have it here too.
Then I think for now instead of resolving in app delegate you can use code snippet that I provided. It will do exactly what you need, I suppose.
let container = DependencyContainer { container in
let chatService = ChatService()
container.register { chatService as ChatServiceProtocol }
let clientService = ClientService()
container.register { clientService as ClientServiceProtocol }
}
Regarding adding this to Dip I think another scope will be the proper way.
Thanks for considering it.
Currently I see some issues with implementing this kind of scope. We don't really have a point when container is considered setup, because we don't restrict calls to register
only to be done inside init(configBlock:)
(and I don't think we should). So we will need to create instance eagerly in register
method, the same way as you do, calling resolve
. But that will no let us to use resolveDependencies
block on definition (it can be set only after register
returns). Also if this instance has dependencies on other components (either in constructor or in properties), container may not be able to resolve them at this point because they were not yet registered. Requiring specific order of registration does not look right to me. So this scope will be aways kind of special and will have limitations in how it can be used (like only in init(configBlock:)
, or only everything else is registered, or only for instances without dependencies, or something else) and will be in fact the same workaround, but inside the library.
It looks for me (at least now) like adding such scope does not bring any win comparing with current workaround.
I understand it and those are valid points. But somehow, other IoC containers have it, so there was a way to do it. My initial thinking is that I don't see a point in registering objects later and thus having the initialization flow scattered. All IoC I know have just one config file that defines everything once and are initialized once, as early as possible during the application cycle - and that's it. So I believe that it would be actually beneficial to have a strict container initialization process (like just init block), and once it's all defined, you can initialize what you want, because, with all defined, you can resolve dependencies. Obviously, I'm not going to tell you how to write your own framework, but that's just how I would approach it, taking the years of using IoC in different platforms :)
I'd disagree that IoC containers should always initialise as early as possible. Maybe because I'm working in mobile where system resource use is a concern but here often it makes sense to defer initialisation of a service until its actually required. Quicker app startup and lower resource use being the benefit. For cases where immediate initialisation needed the work around discussed would seem to address the requirement although not a formally defined behaviour of the framework. Anyway that's my take on it :)
In MVVM architectures, initializaing IoC early is a must.
Also, I cannot possibly imagine, how initializing even 100 of non-ui objects would cause any visible memory/cpu footprint on modern platforms.
Anyway, it's just a food for thought. You have a chance to become THE IoC framework for Swift (other solutions aren't feasible enough). I'd say, use it well :)
My main concern about restricting registrations to init method is that it will make it harder to use in unit tests. Of course in production code container should be setup with just one call. But in tests it is convenient to have a base setup and then replace some components in individual tests. How other platforms approach that?
I'm not sure I understand. The sole purpose of IoC containers is to provide testing THROUGH them. You just provide a different config file, where you inject different objects (mocks, etc.), using the same protocol (assuming you know why and what for you use IoC and you actually use protocols/interfaces). I think the confusion might stem from the fact that you think of this in terms of "code". It's not really code, it's just a configuration provided through code, but it could be passed as well by other means. In my project, I have an AppContainer class, that encapsulate my Dip initialization and configuration - and I treat it as config file, it is just initialized in AppDelegate. For Tests, I have AppContainerTest class, that contain different config.
Yes, sure. The thing is that for different tests you may need different configs. For example in one test you need to mock one service, in another - another service. So you will need to register mock for one service in one config, and for another in another config. But the rest of config will be the same, right?
P.S. Probably it would be better to discuss that in a separate issue =)
I would think that the case where you need more than one config for tests is very very rare. In fact, I cannot remember anything like that myself. But, even if, there's nothing stopping you from architecturing your config files in such a way, that there is a common part and some overrides. Think of this - all the problems we've discussed here could be actually solved by one small addition.
container.initialize() // :)
There. You know when all the configuration has ended, you can resolve dependencies AND you have control in code over where would you exactly want this to happen. If, after that line, I could expect that singletons I've deliberately chosen, would be instantiated all at once, I would be a happy man!
That's actually a good idea! That will not break any previous behaviour, meaning you can still register components in or out of init, but will let us add an assert that after you call initialize
nothing new can be registered. And that will be the point where eager singletons can be created. Thanks for a valuable input! 🙇 Maybe calling the method bootstrap
will be better?
Like this suggestion too although not sure initialize() is the correct method name as it may suggest its required to be called to have a useable container rather than being an optional method to provide a particular behaviour.
"bootstrap" is even better, I agree. @mwoollard - I would consider this method to be mandatory, not optional. Otherwise it would be very weird and incoherent. Yes, this is of course something to consider, because it require migration information, but then again, it's just adding one line - I don't think anyone would be terribly angry about it :)