-
Notifications
You must be signed in to change notification settings - Fork 72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Circular dependencies #11
Conversation
4197af4
to
77b9a66
Compare
@AliSoftware hey, could you take a look when you have time? |
Missed that, sorry, will take a look during the week! BTW don't you think we should do a release (containing the runtime arguments feature you added) first before adding this? The runtime arguments stuff is pretty huge already but is still in develop waiting. |
Actually I have one more feature ready in a queue - auto-injections, that I also thought to suggest for next release. |
Mmmh then maybe we should make a release right now with runtime parameters, then merge circular deps + auto-injections next, to make incremental releases instead of big leaps? |
I would let you to decide =) Maybe auto-injection feature will take some time to approve and merge in. |
@@ -50,20 +50,62 @@ func ==(lhs: DefinitionKey, rhs: DefinitionKey) -> Bool { | |||
public enum ComponentScope { | |||
/// Indicates that a new instance of the component will be created each time it's resolved. | |||
case Prototype | |||
/// Indicates that instances will be reused during resolve but will be discurded when topmost `resolve` method returns. | |||
case ObjectGraph | |||
/// Indicates that resolved component should be retained by container and always reused. | |||
case Singleton | |||
} | |||
|
|||
///Definition of type T describes how instances of this type should be created when they are resolved by container. | |||
public final class DefinitionOf<T>: Definition { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking (didn't think it through, that's just a though, will have to check if it really makes sense): couldn't DefinitionOf
be a struct
to leverage value type?
Then inScope
would return a new DefinitionOf
instead of changing it in place (which seems less FP)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That will not work because if those methods on DefinitionOf
will produce new copies then we will need to replace them in registry. We could solve that by having register<T>(definition: DefinitionOf<T>)
and creating definition manually, but that will break initially nice syntax in separate steps without adding anything valuable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I see that now, but tbh I'm not sure I like that "nice syntax" of being able to register { … }.inScope(.ObjectGraph)
. Because having that inScope
operator being able to change an already-registered dependency means that one can then change it at any time even later after registration, which feels wrong to me:
let def = container.register { [unowned container] ClientImp(server: container.resolve() as Server) as Client }
def.inScope(.ObjectGraph)
// … use it
def.inScope(.Singleton)
// … use it again
def.inScope(.Prototype)
// ... use it again
I think I'd prefer having container.register(.ObjectGraph) { … }
instead (or use currying if you prefer or whatnot, but not a mutating function)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair. What about resolveDependencies
block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't grasped that one yet. It seems like it has the same issue (mutating postfix function), but not sure how to avoid it in this case or if that's as problematic.
I guess I'm less annoyed by this case somehow. Maybe we could add a security in that resolveDependencies
method so that it fatalError
if we try to call it while it already has a value and so has already been called before, to prevent multiple calls at least?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can try this:
- make
DefinitionOf<T, F>
where F will be type of factory and use it for factory property instead ofAny
- add container argument to
resolveDependencies
method ofDefinitionOf
and use it to reinsert updated definition - add
remove<T, F>(_: DefinitionOf<T, F>)
andregister<T, F>(_: DefinitionOf<T, F>)
to container (these methods can be handy anyway)
If that would not work (I suspect there will be ambiguity in method calls caused by generics) I would stick to using final class
, adding fatal error and removing return self
in resolveDependencies
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think in that case directly keeping final class
+ fatalError + remove return self
is probably the way to go. Adding that <T,F>
will probably make the code harder to read — and indeed there is a also a risk of ambiguity for the compiler so better keep it simple and go with your second solution right away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually it worked and I kind of satisfied with the result. You can see it in latest commit for this PR.
Using additional generic type added some complexity for sure, but that also lets get rid of force casting factory (which was Any
) in resolve
method.
Another two very positive side effects of this refactoring is ability to register/remove individual dependencies. These methods can be useful. Without generic type of factory that would be impossible cause there will be no way to compare definitions (because factory is of type 'Any', so no way to use ==
).
Drawback is that now you have to separate creating definition and call to resolve dependencies like this:
var serverDefinition = container.register(.ObjectGraph) { Server() as Server }
serverDefinition.resolveDependencies(container) { container, server in
server.client = container.resolve() as Client
}
If you are ok with that change I will proceed with updating readme and playground.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we do that?
container.register(.ObjectGraph) { Server() as Server }
.resolveDependencies(container) { container, server in
server.client = container.resolve() as Client
}
Sure it might seem odd to chain a call after a trailing closure, but that works, and some might be ok with that style. I myself as a user of the lib I would prefer writing it like you did with two lines but others can choose to chain too, so I'm ok with that ;)
One thing newcomers might be wondering is why you call resolveDependencies
on serverDefinition
and not the clientDefinition
of your ObjectGraph or both. Be sure to explain the rationale and logic of that in the playground.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If resolveDependency
is mutating method then this will not work cause methods return immutable values (the same as using let serverDefinition
instead of var serverDefinition
). But I guess we can make it not mutating and return new definition. Made another commit with that change.
I have to go, will let you make the pending changes and I'll take a deeper look directly in Xcode later |
7acb9e0
to
ddff436
Compare
@AliSoftware Updated playground and documentation. I think it's ready for merge now. |
1a793a9
to
78f15fc
Compare
Now I've tried to reimplement auto-injection feature on top of that and it looks like structs semantics of |
I've finally decided to release the 3.0.0 first, as the rest of the PRs will be big changes anyway and I liked having intermediate release. Even if we'll hopefully merge this PR and the next one soon, feel better to go incremental. I've renamed the "Sample app" folder to "SampleApp" (I like avoiding spaces in paths), so you might want to rebase your branches on top of the new develop. (I was gonna do it myself but as it will mess with your local git working copy I didn't want to surprise you) Regarding the auto-injection feature being harder to implement because of Will take a look at the playground now 😉 and keep you posted. |
XCTAssertEqual(def.scope, ComponentScope.Prototype) | ||
} | ||
|
||
func testThatCallingInScopeChangesScope() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is mis-named now that inScope
is not a thing anymore 😉
As license says: "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software." ;-)
a4c9bd5
to
37d4228
Compare
I finally did the rebase myself and merged this PR after review. I admit I didn't have time to review all your code implementation to double-check it, but I figured I trust you on this and better not wait too much before merging. Be careful as this as been rebased now: you'll probably need to rebase your new |
Thanks for very constructive review! =) |
Well maybe I merged a bit early as I forgot to address the comment about the documentation of |
This PR adds support for circular dependencies. That feature and using
resolve
inside factory block requires recursion, but lock aroundresolve
method causes deadlock. I removed locking completely and propose to managed thread safety on client side.