You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Jun 13, 2024. It is now read-only.
Factories that combine parameterization (or a seed value) and pre-bound dependencies are commonplace in any piece of software that derives its class variables from a particular state. For example, consider a customer list view that drills into a customer detail page. The detail view controller initializer may look like:
CustomerService in this example is a pre-bound dependency that can be constructed independently from CustomerDetailViewController. Our class variable customerID on the other hand is a parameter passed in from the list view that is dependent on which cell the user selected.
Today, Cleanse supports constructing subcomponents that can inject in a seed value, but doesn't have support for constructing a dependency from a seed in the same component. If one wanted to achieve the effect described in the example above, they would have to create a new factory object that injects all of the target object's dependencies, and create a custom build(_:) function.
classCustomDetailViewControllerFactory{letcustomerService:CustomerService
// seed is just a String representing `customerID`
func build(_ seed:String)->CustomerDetailViewController{returnCustomerDetailViewController(customerID: seed, customerService: customerService)}}extensionCustomDetailViewControllerFactory{structModule:Cleanse.Module{publicstaticfunc configure(binder:UnscopedBinder){
binder
.bind(CustomDetailViewControllerFactory.self).to(factory:CustomDetailViewControllerFactory.init)}}}
This creates a large amount of unnecessary boilerplate that Cleanse can help eliminate without needing to create a whole new subcomponent.
Proposed Solution
To eliminate the unnecessary boilerplate required, Cleanse will support a new binding builder whose purpose is to support Assisted Inject.
The above binding will create a Factory<CustomerDetailViewController.AssistedSeed> instance that can be used in the dependency graph. For instance, our customer detail list view could look like:
This is a very lightweight and simple class. It's primary purpose is exposing the public API build(:) and passing the provided seed parameter into the factory closure.
Assisted<E>
The Assisted object wraps the seed value that is injected via the build(:) function from our factory.
The purpose of Assisted is primarily annotative to make it more explicit that these values
are provided via the assisted inject mechanism, similar to Guice's @Assisted annotation. Wrapping the seed inside the Assisted instead of directly using the seed type adds helpful transform functions (i.e map) and can be used to create more succinct bindings leveraging Swift's type inference. More on this later.
AssistedFactory inherits from the protocol Tag, which makes it expand to:
publicprotocolAssistedFactory:Tag{associatedtypeSeed
// From Tag
associatedtypeElement}
Utilizing a tagging system for assisted injection make the surface area of changes required smaller and easier to spot when adding or changing an assisted inject object.
This point is easier to show if we removed the tagging system. So instead of Factory having a generic over an AssistedFactory instance, let's say we turned it into Factory<E, S> where E and S are the same as Element and Seed respectively from the AssistedFactory protocol, and had our builder function with(:) take in the raw types.
If we were to change the Seed by adding another parameter (say a new String), we would have to change every injection of Factory<CoffeeMachine, ((Bean, CoffeeHandle, Int))> to Factory<CoffeeMachine, ((Bean, CoffeeHandle, Int, String))> in addition to any semantical changes required. This can make refactors and changes burdensome and annoying.
Assisted Injector Builder Objects
The entry point into our assisted injector builder is through the bindFactory(_:) function:
The default type for Tag is EmptySeed<Element> whose Seed = Void, meaning that the binding builder with(:) is actually an optional builder step. This means that if one uses assisted injection
without a seed, the resulting Factory type expands to Factory<Element, Void>.
Terminating Step and Generated Arity Code
The terminating builder step for an assisted injection will have 2 functions. For example, the 1st-arity function will look like:
Note the difference between factory parameters, where the Assisted<Tag.Seed> comes at the beginning of one, and the end of the other. The reasoning behind why we are choosing to provide two functions is explained more in the Swift Type Inference section.
Generated arity-code
Similar to property injection and constructor injection, assisted injection will also generate (1,n] functions to support a variadic number of injected dependencies. This code will also live in main.swift for the CleanseGen target.
Error Handling
Assisted injection only supports 1 unique binding per AssistedFactory tag. Any additional bindings will throw an exception.
It is possible however, to create create a constructor and assisted injection binding for the same type. For example both of the following are allowed:
structCoffee{letname:String}structCoffeeModule:Cleanse.Module{structCoffeeAssistedInject:AssistedInject{typealiasElement=CoffeetypealiasSeed=String}staticfunc configure(binder:UnscopedBinder){
// Both of these bindings are valid and will pass validation.
binder
.bind(Coffee.self).to{Coffee(name:"Hello")}
binder
.bindFactory(Coffee.self).with(CoffeeAssistedInject.self).to{(seed:Assisted<CoffeeAssistedInject.Seed>)inreturnCoffee(name: seed.get())}}}
The difference lies in the final types that are bound into the object graph. In the first case, an instance of Coffee.self is bound into the object graph, in the second an instance of Factory<CoffeeAssistedInject>.self is bound into the object graph.
As of this proposal, it is not possible to create tagged bindings for assisted injector objects for binding different instances of the same factory.
Swift Type Inference
Cleanse's API was implemented to leverage Swift's type inference via the binding arity functions we generate. For instance, we can create a succinct binding based on the init function like:
binder
.bind(Coffee.self).to(factory:Coffee.init)
When it comes to assisted injection, we can still leverage the type inference system if we include the Assisted<Seed> parameter in our initializer:
However, it's important to note that the Assisted<Seed> parameter must either come first or last in the initializer function to leverage the type inference from the generated arity functions. This is because if we allowed Assisted<Seed> to go anywhere in the initializer, then the number of functions required to generate would grow exponentially, slowing down the generator and bloating up the binary size.
Revisions
[9/4] Initial Draft.
[9/5] Rename AssistedInjector to AssistedFactory and changed the initial binding function from bindAssisted to bindFactory.
The text was updated successfully, but these errors were encountered:
Cleanse: Assisted Inject
Author: sebastianv1
Date: 9/4/2019
Related Links:
https://github.com/google/guice/wiki/AssistedInject
Background
Factories that combine parameterization (or a seed value) and pre-bound dependencies are commonplace in any piece of software that derives its class variables from a particular state. For example, consider a customer list view that drills into a customer detail page. The detail view controller initializer may look like:
CustomerService
in this example is a pre-bound dependency that can be constructed independently fromCustomerDetailViewController
. Our class variablecustomerID
on the other hand is a parameter passed in from the list view that is dependent on which cell the user selected.Today, Cleanse supports constructing subcomponents that can inject in a seed value, but doesn't have support for constructing a dependency from a seed in the same component. If one wanted to achieve the effect described in the example above, they would have to create a new factory object that injects all of the target object's dependencies, and create a custom
build(_:)
function.This creates a large amount of unnecessary boilerplate that Cleanse can help eliminate without needing to create a whole new subcomponent.
Proposed Solution
To eliminate the unnecessary boilerplate required, Cleanse will support a new binding builder whose purpose is to support Assisted Inject.
The above binding will create a
Factory<CustomerDetailViewController.AssistedSeed>
instance that can be used in the dependency graph. For instance, our customer detail list view could look like:Detailed Design
Factory<Tag: AssistedFactory>
This is a very lightweight and simple class. It's primary purpose is exposing the public API
build(:)
and passing the providedseed
parameter into the factory closure.Assisted<E>
The
Assisted
object wraps the seed value that is injected via thebuild(:)
function from our factory.The purpose of
Assisted
is primarily annotative to make it more explicit that these valuesare provided via the assisted inject mechanism, similar to Guice's
@Assisted
annotation. Wrapping the seed inside theAssisted
instead of directly using the seed type adds helpful transform functions (i.emap
) and can be used to create more succinct bindings leveraging Swift's type inference. More on this later.protocol AssistedFactory
AssistedFactory
inherits from the protocolTag
, which makes it expand to:Utilizing a tagging system for assisted injection make the surface area of changes required smaller and easier to spot when adding or changing an assisted inject object.
This point is easier to show if we removed the tagging system. So instead of
Factory
having a generic over anAssistedFactory
instance, let's say we turned it intoFactory<E, S>
whereE
andS
are the same asElement
andSeed
respectively from theAssistedFactory
protocol, and had our builder functionwith(:)
take in the raw types.Consider the following assisted injection class:
We would create our binding as such:
And our injection would be:
If we were to change the Seed by adding another parameter (say a new String), we would have to change every injection of
Factory<CoffeeMachine, ((Bean, CoffeeHandle, Int))>
toFactory<CoffeeMachine, ((Bean, CoffeeHandle, Int, String))>
in addition to any semantical changes required. This can make refactors and changes burdensome and annoying.Assisted Injector Builder Objects
The entry point into our assisted injector builder is through the
bindFactory(_:)
function:AssistedInjectionBindingBuilder
is a type that conforms toBaseAssistedInjectorBuilder
The default type for
Tag
isEmptySeed<Element>
whoseSeed = Void
, meaning that the binding builderwith(:)
is actually an optional builder step. This means that if one uses assisted injectionwithout a seed, the resulting
Factory
type expands toFactory<Element, Void>
.Terminating Step and Generated Arity Code
The terminating builder step for an assisted injection will have 2 functions. For example, the 1st-arity function will look like:
@discardableResult public func to<P_1>(file: StaticString=#file, line: Int=#line, function: StaticString=#function, factory: @escaping (Assisted<Tag.Seed>, P_1) -> Element) -> BindingReceipt<Factory<Tag>>
and
@discardableResult public func to<P_1>(file: StaticString=#file, line: Int=#line, function: StaticString=#function, factory: @escaping (P_1, Assisted<Tag.Seed>) -> Element) -> BindingReceipt<Factory<Tag>>
Note the difference between
factory
parameters, where theAssisted<Tag.Seed>
comes at the beginning of one, and the end of the other. The reasoning behind why we are choosing to provide two functions is explained more in the Swift Type Inference section.Generated arity-code
Similar to property injection and constructor injection, assisted injection will also generate
(1,n]
functions to support a variadic number of injected dependencies. This code will also live inmain.swift
for theCleanseGen
target.Error Handling
Assisted injection only supports 1 unique binding per
AssistedFactory
tag. Any additional bindings will throw an exception.It is possible however, to create create a constructor and assisted injection binding for the same type. For example both of the following are allowed:
The difference lies in the final types that are bound into the object graph. In the first case, an instance of
Coffee.self
is bound into the object graph, in the second an instance ofFactory<CoffeeAssistedInject>.self
is bound into the object graph.As of this proposal, it is not possible to create tagged bindings for assisted injector objects for binding different instances of the same factory.
Swift Type Inference
Cleanse's API was implemented to leverage Swift's type inference via the binding arity functions we generate. For instance, we can create a succinct binding based on the
init
function like:When it comes to assisted injection, we can still leverage the type inference system if we include the
Assisted<Seed>
parameter in our initializer:However, it's important to note that the
Assisted<Seed>
parameter must either come first or last in the initializer function to leverage the type inference from the generated arity functions. This is because if we allowedAssisted<Seed>
to go anywhere in the initializer, then the number of functions required to generate would grow exponentially, slowing down the generator and bloating up the binary size.Revisions
AssistedInjector
toAssistedFactory
and changed the initial binding function frombindAssisted
tobindFactory
.The text was updated successfully, but these errors were encountered: