Pure DI is a way to do a dependency injection without a DI container. The term was first introduced by Mark Seemann. The core concept of Pure DI is not to use a DI container and to compose an entire object dependency graph in the Composition Root.
Composition Root
The Composion Root is where the entire object graph is resolved. In a Cocoa application, AppDelegate is the Composition Root.
AppDependency
The root dependencies are the app delegate’s dependency and the root view controller’s dependency. The best way to inject those dependencies is to create a struct named AppDependency and store both dependencies in it.
struct AppDependency {
let networking: Networking
let remoteNotificationService: RemoteNotificationService
}
extension AppDependency {
static func resolve() -> AppDependency {
let networking = Networking()
let remoteNotificationService = RemoteNotificationService()
return AppDependency(
networking: networking
remoteNotificationService: remoteNotificationService
)
}
}
It is important to separate a production environment from a testing environment. We have to use an actual object in a production environment and a mock object in a testing environment.
AppDelegate is created automatically by the system using init(). In this initializer we’re going to initialize the actaul app dependency with AppDependency.resolve(). On the other hand, we’re going to provide a init(dependency:) to inject a mock app dependency in a testing environment.
class AppDelegate: UIResponder, UIApplicationDelegate {
private let dependency: AppDependency
/// Called from the system (it's private: not accessible in the testing environment)
private override init() {
self.dependency = AppDependency.resolve()
super.init()
}
/// Called in a testing environment
init(dependency: AppDependency) {
self.dependency = dependency
super.init()
}
}
AppDelegate is one of the most important class in a Cocoa application. It resolves an app dependency and handles app events. It can be easily tested as we separated the app dependency.
This is an example test case of AppDelegate. It verifies that AppDelegate correctly injects root view controller’s dependency in application(_:didFinishLaunchingWithOptions).
class AppDelegateTests: XCTestCase {
func testInjectRootViewControllerDependencies() {
// given
let networking = MockNetworking()
let mockDependency = AppDependency(
networking: networking,
remoteNotificationService: MockRemoteNotificationService()
)
let appDelegate = AppDelegate(dependency: mockDependency)
appDelegate.window = UIWindow()
appDelegate.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
// when
_ = appDelegate.application(.shared, didFinishLaunchingWithOptions: nil)
// then
let rootViewController = appDelegate.window?.rootViewController as? RootViewController
XCTAssertTrue(rootViewController?.networking === networking)
}
}
You can write tests for verifying remote notification events, open url events and even an app termination event.
Separating AppDelegate
But there is a problem: AppDelegate is still created by the system while testing. It causes AppDependency.resolve() gets called so we have to use a fake app delegate class in a testing environment.
First of all, create a new file in the test target. Define a new class named TestAppDelegate and implement basic requirements of the delegate protocol.
// iOS
class TestAppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?
}
// macOS
class TestAppDelegate: NSObject, NSApplicationDelegate {
}
Then create another file named main.swift to your application target. This file will replace the entry point of the application. We are going to provide different app delegates in this file. Don’t forget to replace "MyAppTests.TestAppDelegate" with your project target and class name.
Finally, remove the @UIApplicationMain and @NSApplicationMain from the AppDelegate.
// iOS
- @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
// macOS
- @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate
It is also a good practice to add a test case to verify that the application is using TestAppDelegate in a testing environment.
XCTAssertTrue(UIApplication.shared.delegate is TestAppDelegate)
Lazy Dependency
Using Factory
In Cocoa applications, view controllers are created lazily. For example, DetailViewController is not created until the user taps an item on ListViewController. In this case we have to pass a factory closure of DetailViewController to ListViewController.
/// A root view controller
class ListViewController {
var detailViewControllerFactory: ((Item) -> DetailViewController)!
func presentItemDetail(_ selectedItem: Item) {
let detailViewController = self.detailViewControllerFactory(selectedItem)
self.present(detailViewController, animated: true)
}
}
static func resolve() -> AppDependency {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let networking = Networking()
let detailViewControllerFactory: (Item) -> DetailViewController = { selectedItem in
let detailViewController = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
detailViewController.networking = networking
detailViewController.item = selectedItem
return detailViewController
}
return AppDependency(
networking: networking,
detailViewControllerFactory: detailViewControllerFactory
)
}
But it has a critical problem: we cannot test the factory closure. Because the factory closure is created in the Composition Root but we should not access the Composition Root in a testing environment. What if I forget to inject the DetailViewController.networking property?
One possible approach is to create a factory closure outside of the Composition Root. Note that Storyboard and Networking is from the Composition Root, and Item is from the previous view controller so we have to separate the scope.
extension DetailViewController {
static let factory: (UIStoryboard, Networking) -> (Item) -> DetailViewController = { storyboard, networking in
return { selectedItem in
let detailViewController = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
detailViewController.networking = networking
detailViewController.item = selectedItem
return detailViewController
}
}
}
static func resolve() -> AppDependency {
let storyboard = ...
let networking = ...
return .init(
detailViewControllerFactory: DetailViewController.factory(storyboard, networking)
)
}
Now we can test the DetailViewController.factory closure. Every dependencies are resolved in the Composition Root and a selected item can be passed from ListViewController to DetailViewController in runtime.
Using Configurator
There is another lazy dependency. Cells are created lazily but we cannot use the factory closure because the cells are created by the framework. We can just configure the cells.
Imagine that an UICollectionViewCell or UITableViewCell displays an image. There is an imageDownloader which downloads an actual image in a production environment and returns a mock image in a testing environment.
class ItemCell {
var imageDownloader: ImageDownloaderType?
var imageURL: URL? {
didSet {
guard let imageDownloader = self.imageDownloader else { return }
self.imageView.setImage(with: self.imageURL, using: imageDownloader)
}
}
}
This cell is displayed in DetailViewController . DetailViewController should inject imageDownloader to the cell and sets the image property. Like we did in the factory, we can create a configurator closure for it. But this closure takes an existing instance and doens’t have a return value.
class ItemCell {
static let configurator: (ImageDownloaderType) -> (ItemCell, UIImage) -> Void = { imageDownloader
return { cell, image in
cell.imageDownloader = imageDownloader
cell.image = image
}
}
}
DetailViewController can have the configurator and use it when configurating cell.
static func resolve() -> AppDependency {
let storybard = ...
let networking = ...
let imageDownloader = ...
let listViewController = ...
listViewController.detailViewControllerFactory = DetailViewController.factory(
storyboard,
networking,
ImageCell.configurator(imageDownloader)
)
...
}
Problem
Theoretically it works. But as you can see in the DetailViewController.factory it will be very complicated when there are many dependencies. This is why I created Pure. Pure makes factories and configurators neat.
Getting Started
Dependency and Payload
First of all, take a look at the factory and the configurator we used in the example code.
Those are the functions that return another function. The outer functions are executed in the Composition Root to inject static dependencies like Networking and the inner functions are executed in the view controllers to pass a runtime information like selectedItem. The parameter of the outer function is Dependency. The parameter of the inner function is Payload.
Pure generalizes the factory and configurator using Dependency and Payload.
Module
Pure treats every class that requires a dependency and a payload as a Module. A protocol Module requires two types: Dependency and Payload.
protocol Module {
/// A dependency that is resolved in the Composition Root.
associatedtype Dependency
/// A runtime information for configuring the module.
associatedtype Payload
}
There are two types of module: FactoryModule and ConfiguratorModule.
Factory Module
FactoryModule is a generalized version of factory closure. It requires an initializer which takes both dependency and payload.
protocol FactoryModule: Module {
init(dependency: Dependency, payload: Payload)
}
class DetailViewController: FactoryModule {
struct Dependency {
let storyboard: UIStoryboard
let networking: Networking
}
struct Payload {
let selectedItem: Item
}
init(dependency: Dependency, payload: Payload) {
}
}
When a class conforms to FactoryModule, it will have a nested type Factory. Factory.init(dependency:) takes a dependency of the module and has a method create(payload:) which creates a new instance.
class Factory<Module> {
let dependency: Module.Dependency
func create(payload: Module.Payload) -> Module
}
// In AppDependency
let factory = DetailViewController.Factory(dependency: .init(
storyboard: storyboard
networking: networking
))
// In ListViewController
let viewController = factory.create(payload: .init(
selectedItem: selectedItem
))
Configurator Module
ConfiguratorModule is a generalized version of configurator closure. It requires a configure() method which takes both dependency and payload.
When a class conforms to ConfiguratorModule, it will have a nested type Configurator. Configurator.init(dependency:) takes a dependency of the module and has a method configure(_:payload:) which configures an existing instance.
class Configurator<Module> {
let dependency: Module.Dependency
func configure(_ module: Module, payload: Module.Payload)
}
// In AppDependency
let configurator = ItemCell.Configurator(dependency: .init(
imageDownloader: imageDownloader
))
// In DetailViewController
configurator.configure(cell, payload: .init(image: image))
With FactoryModule and ConfiguratorModule, the example can be refactored as below:
FactoryModule can support Storyboard-instantiated view controllers using customizing feature. The code below is an example for storyboard support of DetailViewController:
Pure
Pure makes Pure DI easy in Swift. This repository also introduces a way to do Pure DI in a Swift application.
Table of Contents
Background
Pure DI
Pure DI is a way to do a dependency injection without a DI container. The term was first introduced by Mark Seemann. The core concept of Pure DI is not to use a DI container and to compose an entire object dependency graph in the Composition Root.
Composition Root
The Composion Root is where the entire object graph is resolved. In a Cocoa application,
AppDelegate
is the Composition Root.AppDependency
The root dependencies are the app delegate’s dependency and the root view controller’s dependency. The best way to inject those dependencies is to create a struct named
AppDependency
and store both dependencies in it.It is important to separate a production environment from a testing environment. We have to use an actual object in a production environment and a mock object in a testing environment.
AppDelegate is created automatically by the system using
init()
. In this initializer we’re going to initialize the actaul app dependency withAppDependency.resolve()
. On the other hand, we’re going to provide ainit(dependency:)
to inject a mock app dependency in a testing environment.The app dependency can be used as the code below:
Testing AppDelegate
AppDelegate
is one of the most important class in a Cocoa application. It resolves an app dependency and handles app events. It can be easily tested as we separated the app dependency.This is an example test case of
AppDelegate
. It verifies thatAppDelegate
correctly injects root view controller’s dependency inapplication(_:didFinishLaunchingWithOptions)
.You can write tests for verifying remote notification events, open url events and even an app termination event.
Separating AppDelegate
But there is a problem:
AppDelegate
is still created by the system while testing. It causesAppDependency.resolve()
gets called so we have to use a fake app delegate class in a testing environment.First of all, create a new file in the test target. Define a new class named
TestAppDelegate
and implement basic requirements of the delegate protocol.Then create another file named
main.swift
to your application target. This file will replace the entry point of the application. We are going to provide different app delegates in this file. Don’t forget to replace"MyAppTests.TestAppDelegate"
with your project target and class name.Finally, remove the
@UIApplicationMain
and@NSApplicationMain
from theAppDelegate
.It is also a good practice to add a test case to verify that the application is using
TestAppDelegate
in a testing environment.Lazy Dependency
Using Factory
In Cocoa applications, view controllers are created lazily. For example,
DetailViewController
is not created until the user taps an item onListViewController
. In this case we have to pass a factory closure ofDetailViewController
toListViewController
.But it has a critical problem: we cannot test the factory closure. Because the factory closure is created in the Composition Root but we should not access the Composition Root in a testing environment. What if I forget to inject the
DetailViewController.networking
property?One possible approach is to create a factory closure outside of the Composition Root. Note that
Storyboard
andNetworking
is from the Composition Root, andItem
is from the previous view controller so we have to separate the scope.Now we can test the
DetailViewController.factory
closure. Every dependencies are resolved in the Composition Root and a selected item can be passed fromListViewController
toDetailViewController
in runtime.Using Configurator
There is another lazy dependency. Cells are created lazily but we cannot use the factory closure because the cells are created by the framework. We can just configure the cells.
Imagine that an
UICollectionViewCell
orUITableViewCell
displays an image. There is animageDownloader
which downloads an actual image in a production environment and returns a mock image in a testing environment.This cell is displayed in
DetailViewController
.DetailViewController
should injectimageDownloader
to the cell and sets theimage
property. Like we did in the factory, we can create a configurator closure for it. But this closure takes an existing instance and doens’t have a return value.DetailViewController
can have the configurator and use it when configurating cell.DetailViewController.itemCellConfigurator
is injected from a factory.And the Composition Root finally looks like:
Problem
Theoretically it works. But as you can see in the
DetailViewController.factory
it will be very complicated when there are many dependencies. This is why I created Pure. Pure makes factories and configurators neat.Getting Started
Dependency and Payload
First of all, take a look at the factory and the configurator we used in the example code.
Those are the functions that return another function. The outer functions are executed in the Composition Root to inject static dependencies like
Networking
and the inner functions are executed in the view controllers to pass a runtime information likeselectedItem
. The parameter of the outer function is Dependency. The parameter of the inner function is Payload.Pure generalizes the factory and configurator using Dependency and Payload.
Module
Pure treats every class that requires a dependency and a payload as a Module. A protocol
Module
requires two types:Dependency
andPayload
.There are two types of module:
FactoryModule
andConfiguratorModule
.Factory Module
FactoryModule is a generalized version of factory closure. It requires an initializer which takes both
dependency
andpayload
.When a class conforms to
FactoryModule
, it will have a nested typeFactory
.Factory.init(dependency:)
takes a dependency of the module and has a methodcreate(payload:)
which creates a new instance.Configurator Module
ConfiguratorModule is a generalized version of configurator closure. It requires a
configure()
method which takes bothdependency
andpayload
.When a class conforms to
ConfiguratorModule
, it will have a nested typeConfigurator
.Configurator.init(dependency:)
takes a dependency of the module and has a methodconfigure(_:payload:)
which configures an existing instance.With
FactoryModule
andConfiguratorModule
, the example can be refactored as below:Customizing
Factory
andConfigurator
are customizable. This is an example of customized factory:Storyboard Support
FactoryModule
can support Storyboard-instantiated view controllers using customizing feature. The code below is an example for storyboard support ofDetailViewController
:URLNavigator Support
URLNavigator is an elegant library for deeplink support. Pure can be also used in registering a view controller to a navigator.
Installation
Using CocoaPods:
Carthage is not yet supported.
Contribution
Any discussions and pull requests are welcomed 💖
To development:
To test:
License
Pure is under MIT license. See the LICENSE file for more info.