There are five primary ways of performing dependency injection using Resolver:
- Interface Injection
- Property Injection
- Constructor Injection
- Method Injection
- Service Locator
- Annotation (NEW)
The names and numbers come from the Inversion of Control design pattern. For a more thorough discussion, see the classic arcticle by Martin Fowler.
Here I'll simply provide a brief description and an example of implementing each using Resolver.
The first injection technique is to define a interface for the injection, and injecting that interface into the class or object using Swift extensions.
class XYZViewModel {
lazy var fetcher: XYZFetching = getFetcher()
lazy var service: XYZService = getService()
func load() -> Data {
return fetcher.getData(service)
}
}
extension XYZViewModel: Resolving {
func getFetcher() -> XYZFetching { return resolver.resolve() }
func getService() -> XYZService { return resolver.resolve() }
}
func setupMyRegistrations {
register { XYZFetcher() as XYZFetching }
register { XYZService() }
}
Note that you still want to call resolve()
within getFetcher()
and getService()
, otherwise you're back to tightly-coupling the dependent classes and bypassing the resolution registration system.
- Lightweight.
- Hides dependency injection system from class.
- Useful for classes like UIViewController where you don't have access during the initialization process.
- Writing an accessor function for every service that needs to be injected.
Property Injection exposes its dependencies as properties, and it's up to the Dependency Injection system to make sure everything is setup prior to any methods being called.
class XYZViewModel {
var fetcher: XYZFetching!
var service: XYZService!
func load() -> Data {
return fetcher.getData(service)
}
}
func setupMyRegistrations {
register { XYZViewModel() }
.resolveProperties { (resolver, model) in
model.fetcher = resolver.optional() // Note property is an ImplicitlyUnwrappedOptional
model.service = resolver.optional() // Ditto
}
}
func setupMyRegistrations {
register { XYZFetcher() as XYZFetching }
register { XYZService() }
}
- Clean.
- Also fairly lightweight.
- Exposes internals as public variables.
- Harder to ensure that an object has been given everything it needs to do its job.
- More work on the registration side of the fence.
A Constructor is the Java term for a Swift Initializer, but the idea is the same: Pass all of the dependencies an object needs through its initialization function.
class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}
func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher() as XYZFetching }
register { XYZService() }
}
- Ensures that the object has everything it needs to do its job, as the object can't be constructed otherwise.
- Hides dependencies as private or internal.
- Less code needed for the registration factory.
- Requires object to have initializer with all parameters needed.
- More boilerplace code needed in the object initializer to transfer parameters to object properties.
This is listed for completeness, even though it's not a pattern that uses Resolver directly.
Method Injection is pretty much what it says, injecting the object needed into a given method.
class XYZViewModel {
func load(fetcher: XYZFetching, service: XYZService) -> Data {
return fetcher.getData(service)
}
}
You've already seen it. In the load function, the service object is passed into the fetcher's getData method.
- Allows callers to configure the behavior of a method on the fly.
- Allows callers to construct their own behaviors and pass them into the method.
- Exposes those behaviors to all of the classes that use it.
In Swift, passing a closure into a method could also be considered a form of Method Injection.
A Service Locator is basically a service that locates the resources and dependencies an object needs.
Technically, Service Locator is its own Design Pattern, distinct from Dependency Injection, but Resolver supports both and the Service Locator pattern is particularly useful when supporting view controllers and other classes where the initialization process is outside of your control. (See Storyboards.)
class XYZViewModel {
var fetcher: XYZFetching = Resolver.resolve()
var service: XYZService = Resolver.resolve()
func load() -> Data {
return fetcher.getData(service)
}
}
func setupMyRegistrations {
register { XYZFetcher() as XYZFetching }
register { XYZService() }
}
- Less code.
- Useful for classes like UIViewController where you don't have access during the initialization process.
- Exposes the dependency injection system to all of the classes that use it.
Annotation uses comments or other metadata to indication that dependency injection is required. As of Swift 5.1, we can now perform annotation using Property Wrappers. (See Annotation.)
class XYZViewModel {
@Injected var fetcher: XYZFetching
@Injected var service: XYZService
func load() -> Data {
return fetcher.getData(service)
}
}
func setupMyRegistrations {
register { XYZFetcher() as XYZFetching }
register { XYZService() }
}
- Less code.
- Hides the specifics of the injection system. One could easily make an Injected property wrapper to support any DI system.
- Useful for classes like UIViewController where you don't have access during the initialization process.
- Exposes the fact that a dependency injection system is used.
This just skims the surface. For a more in-depth look at the pros and cons, see: Inversion of Control Containers and the Dependency Injection pattern ~ Martin Fowler.