Factory 2.0! Input on Factory's future is needed! #60
Replies: 30 comments 12 replies
-
Sure, I don't see why not.
Yes, I think breaking changes such as the initializer changes in the discussion above are more than reasonable for a 2.0 release.
My preference would be to provide all of them. In addition to the points noted above for 2.0 -- my main feature request:
|
Beta Was this translation helpful? Give feedback.
-
@rydamckinney Resolver has a .container scope that's basically a cache for the current Resolver instance. That scope didn't make a lot of sense for Factory since it didn't really have containers, but I was in fact planning on adding that capability back to Factory 2.0. Only in this case it applies to any scope. Basically that means if a container goes out of scope then any cached instances associated with it are also released. I mean, if the container goes away you're not going to be able to ask it for a new singleton, are you? From a user's perspective, that means that each instance of container "X" appears to have its own complete set of scopes. Or in other words, I think you're going to get your wish. ;) |
Beta Was this translation helpful? Give feedback.
-
It makes sense but I have a suggestion, instead of passing the container scope, let's use a typed container for example, factory for an instance, shared for a singleton, and graph for graph, etc...
|
Beta Was this translation helpful? Give feedback.
-
I mentioned that above, "The fifth variant adds a few more helpers that do the same thing as factory but specifically indicates the scope (unique, singleton, shared, etc.) at the same time."
Property wrapper key paths depend on variables.
Factory 2.0 is multi-modal. You can use "pure" containers for DI, or you can use .shared and basically do a variant of what's done today. The shared variable ensures that you know what instance of what container is responsible for caching and registrations. It's also required if you decide you want to use the annotation property wrappers keypaths as they need to be able to find an instance of a container to use. |
Beta Was this translation helpful? Give feedback.
-
Just seconding your point 😄
You are right, I usually rely on constructor injection so I missed the injection using the property wrapper feature. |
Beta Was this translation helpful? Give feedback.
-
So in defining a 2.0-version Factory I could do... var sample1: Factory<MyServiceType> { Factory(self, scope: .shared) { MyService() } } But that's clunky. I'd prefer to do... var sample2: Factory<MyServiceType> { Shared { MyService() } } Which is a lot cleaner. The problem, however, is that the capitalized I could write it as follows... var sample3: Factory<MyServiceType> { shared { MyService() } } But some stubborn part of me prefers the capitalized version. Anyone for or against? |
Beta Was this translation helpful? Give feedback.
-
It's great to read up on the new plans, looking forward to it! And cool that you gather feedback here :) I prefer
That seems to me the swifty way. An uppercased |
Beta Was this translation helpful? Give feedback.
-
@bensLine The shared function works because it's an extension on the container and as such knows about the container. var sample9: Factory<MyServiceType> {
shared { MyService() }
} A dot-shared would be a static on the Factory itself and as such it won't work. Or, rather, it would, but it would then need to be var sample9: Factory<MyServiceType> {
.shared(self) { MyService() }
} |
Beta Was this translation helpful? Give feedback.
-
Closing in on final syntax for Factory registrations, adding "builder" helpers for scopes and decorators. // see below Think this provides a good compromise between clarity and expressiveness. Thoughts? |
Beta Was this translation helpful? Give feedback.
-
A working version of 2.0 I in the develop branch... |
Beta Was this translation helpful? Give feedback.
-
I'm not a fan of defining a factory with a scope this way:
I guess passing it as a type would be more elegant.
|
Beta Was this translation helpful? Give feedback.
-
I prefer the non-capitalized version, makes more sense to me. |
Beta Was this translation helpful? Give feedback.
-
// Example of parameterized functional registration in a Factory 2.0 container
So you reverted from this ?
I would prefer the second to be honest, it is cleaner than the first. |
Beta Was this translation helpful? Give feedback.
-
Would something speak against providing functions in the Container extension to modify the scope? Also, to me it feels weird to use a noun for a function. Could it be named as an action, as is common practice? This way you get natural readability, as in "this is a Factory making MyService" Using your example:
|
Beta Was this translation helpful? Give feedback.
-
So did I, except it doesn't work. You need access to the factory to resolve it and pass in the parameter (service(5)). And written as a function you can't get to the Factory to register a new closure w/o calling the function with a parameter. Turns out I originally wrote it that way for a reason. |
Beta Was this translation helpful? Give feedback.
-
One could, but I was really trying to avoid polluting the container namespace with a lot of factory-specific helper functions for each and every scope. The modifier syntax helped eliminate that, came in handy for adding decorator, and will be useful for a few other things I have in mind. Will consider the verb thing, but haven't found one I really like... |
Beta Was this translation helpful? Give feedback.
-
Consider... extension Container {
// Formally constructing Factory
var service1: Factory<SimpleService> {
Factory(self) { SimpleService() }
}
// Using .init shortcut
var service2: Factory<SimpleService> {
.init(self) { SimpleService() }
}
// Just container to do it...
var service3: Factory<MyServiceType> {
self { MyService() }
}
} Playing with options and thought the later Although it's actually starting to grow on me... |
Beta Was this translation helpful? Give feedback.
-
What about my other comments ? 😄
What about this point ? 😄 |
Beta Was this translation helpful? Give feedback.
-
@ahmadmssm As mentioned, the modifier syntax helped eliminate a lot of the container namespace pollution, came in handy for adding decorator, and will be useful for a few other things I have in mind. I really don't want to start providing 15 different ways of doing the same thing. |
Beta Was this translation helpful? Give feedback.
-
So no hope to change it ?, because even from a convention perspective, it seems misleading a little bit and for me, the function param makes much more sense, but this is just my opinion. |
Beta Was this translation helpful? Give feedback.
-
Once again closing in on final syntax for Factory registrations, using SwiftUI-style modifiers for scopes and decorators. // Example of formal registration in a Factory 2.0 container.
extension Container {
var service: Factory<MyServiceType> {
Factory(self) { MyService() }
}
var service2: Factory< MyServiceType > {
.init(self) { MyService() }
}
}
// Same as above, but asks current container to do the binding and make our Factory for us.
extension Container {
var service: Factory<MyServiceType> {
makes { MyService() }
}
}
// Examples of scoped services in a Factory 2.0 container using scope modifiers
extension Container {
var cachedService: Factory<MyServiceType> {
makes { MyService() }.cached
}
var singletonService: Factory<SimpleService> {
makes { SimpleService() }.singleton
}
var sharedService: Factory<MyServiceType> {
makes { MyService() }
.decorator { print("DECORATING \($0.id)") }
.shared
}
}
// Example of service with constructor injection that requires another service
extension Container {
var constructedService: Factory<MyConstructedService> {
makes { MyConstructedService(service: self.cachedService()) }
}
}
// Example of parameterized functional registration in a Factory 2.0 container
extension Container {
var parameterService: ParameterFactory<Int, ParameterService> {
makes { ParameterService(value: $0) }
}
} Note that any factory modifiers added (.shared, .decorator, .etc) work on both Factory and ParameterFactory. This simplifies implementation internally and helps prevent the multiplication of helper functions on the Container. Settled on |
Beta Was this translation helpful? Give feedback.
-
One more suggestion, could we change |
Beta Was this translation helpful? Give feedback.
-
With Factory 2.0 can be possible a dynamic resolver like koin library does? Ex: |
Beta Was this translation helpful? Give feedback.
-
Well, you could do something like...
But why?
Bottom line is that that sort of type-inference defeats the compile-time-safe rationale behind Factory. If you want blind registrations and resolutions look at Resolver. |
Beta Was this translation helpful? Give feedback.
-
With 2.0, it seems like we'll have to write an initializer to pass on the container first. One of the benefits we were reaping of the 1.0 design was conciseness. We didn't had to write down an Also, as far as the following syntax goes, it looks ambiguous compared to 1.0.
So, the way someone will read the above is - create a member variable that is a Factory returning type Then, the following is a different way of creating a Factory where
Would it make sense to create scope based Factory sub-classes rather than using |
Beta Was this translation helpful? Give feedback.
-
Liking the new design so far. The only thing I found a bit confusing at first was the order of the parameters on |
Beta Was this translation helpful? Give feedback.
-
Personally I do not get why there is a need for a container and would have to see the code of 2.0 to reason about it. I'm a big fan of a simple injection system that is type safe. Only I do not see the point of Container. So what would I like to do
This I do now inside a library that has its own namespace. So I figured as global variables are lazy and thread safe by default that I configure the facturies like globals and not as an extension on public private(set) var typographyFactory: Factory<TypographyManager> = Factory(scope: .cached) { TypographyManager() }
// and reset them
func bootstrap( typography: @autoclosure @escaping () -> TypographyManager ) {
typographyFactory.reset()
typographyFactory.register { typography() }
} In code I can use it then import Factory
struct FooView: View {
@Injected(typographyFactory) private var typography
/// ...
} I do not see why this has to change and become more complex and I'm also more worried about thread safety and lazy instantiation. Factories in my humble opinion can slow down startup time of they are all injected at once. So a system that garanties them to be lazy is my preference. I'm now, without more knowledge of the code, strongly against adding the need for a Container. I find it refreshing to not use it. But I stand to be convinced otherwise? Thanks for this awesome lib and the fact that we can discuss about its future! Just a tip could we not use githubs discussion thing for that instate of an issue? |
Beta Was this translation helpful? Give feedback.
-
Think I'm falling back to my original "syntactic sugar" in regard to factory definitions. extension Container {
var cachedService: Factory<MyServiceType> {
self { MyService() }.cached
}
var parameterService: ParameterFactory<Int, ParameterService> {
self { ParameterService(value: $0) }
}
} |
Beta Was this translation helpful? Give feedback.
-
Why is that ? :( |
Beta Was this translation helpful? Give feedback.
-
This one will also be included ?
IMHO, it is much more intuitive than the |
Beta Was this translation helpful? Give feedback.
-
Factory is evolving and I'm currently working on Factory 2.0 which adds true container-based dependency injection! This allows Factory to provide a variety of traditional dependency injection and service locator strategies.
Supporting containers has been one of my goals for awhile now, as it dramatically increases the environments and applications in which Factory can be used and considered.
But, as with most things, there's a catch: In order to do this the dependency definition has to change, making this a breaking change between 1.0 and 2.0.
Here's the existing mechanism.
And here's a version of the same thing in Factory 2.0.
The Factory is now defined as a Container Member, and not a Container Class Member as is done today.
Note that three things must occur to make container Factory definitions work as expected.
Factory<MyServiceType>
).This adds a bit of extra code, but makes Factory much, much more powerful.
Note the basic definition could be written in several ways:
The first is fully explicit. The second uses the .init shorthand. The third uses a helper function on the container that returns the correctly bound Factory; and the fourth version demonstrates the same with the scope syntax added. The fifth variant adds a few more helpers that do the same thing as
factory
but specifically indicates the scope (unique, singleton, shared, etc.) at the same time.At this point in time I'm considering going with the later option exclusively, as it ensures the correct binding and also requires the developer to explicitly consider and define the desired scope for a given factory.
ParameterFactory would be going away as well, as it's easy to simply do...
But updating and reseting services would work much as it does today.
Or it can be done as follows if you have access to the specific container. This version changes the behavior on that instance of that container and nowhere else.
This leaves me a few questions.
It's possible to make Factory 1.0 and 2.0 work together, but I'm inclined to deprecate the existing methodology as the two approaches differ somewhat in behavior, in how dependencies are managed, and, of course, supporting both the old and new code styles increases the library size.
So...
One drawback to the service4 style is that containers start to get "busy" with predefined helper functions (unique, shared, singleton, cached) in addition to the factory definitions.
I really want Factory to be the best DI system for Swift, and to do that I really need some help here. So leave a comment if you approve, disapprove, or have questions.
Beta Was this translation helpful? Give feedback.
All reactions