A Swift-y Approach to Dependency Injection
While I would guess that fewer iOS developers are concerned with Dependency Injection than say, Java developers, the concept has gained more steam on the platform as the practice of unit and integration testing has increased.
In short, Dependency Injection (or DI) consists of:
Making your structs or classes depend on abstract types or protocols, instead of other specific structs or classes, e.g.
class MyViewModel { var networking:NetworkManagerProtocol func getData() { networking.request(someNSURLRequest) { /* some completion handler */ } } }
instead of
class MyViewModel { var networking:Alamofire.Manager func getData() { networking.request(someNSURLRequest) {/* some completion handler */ } } }
As you can see in the above example, the version that uses a protocol will work with any networking library. The second example will only work with Alamofire, and that dependency is hard coded into the class, making it less flexible and reusable.
A way of passing in the dependencies from outside the class. Both of these are common ways:
let viewModel = MyViewModel.init(networking:someNetworkingManager)
or
let viewModel = MyViewModel.init() viewModel.networking = someNetworkingManager
Either way, the overall application is able to pass an appropriate implementation of NetworkingManagerProtocol, depending on the context and requirements of the app. A common use for this pattern is in unit tests where you would pass in a mock networking manager that returns mock data instead of making real network requests. Ultimately, this lets you use MyViewModel more flexibly in more contexts without ever having to modify its internal code or change its single responsibility.
The best DI implementations allow you to control all the injections throughout an application from a single location. For example, during an integration test, it would be almost impossible to intercept every place a type is instantiated that uses the network and individually replace its normal network manager with the mock network manager. Instead, you want to be able to specify in on place that all instances of NetworkManagerProtocol should be injected with the mock implementation.
So integration tests (which test whole processes or several parts of the app working together as opposed to unit tests) are almost impossible to write well without a good DI framework. I challenge you to write some of those nifty Xcode UI tests to validate values set in labels after they are loaded in from mock data without having to jump through all sorts of hoops to change your app's behavior when running those tests vs. when running in production.
What Can Go Wrong
There are a couple main ways that DI can become more trouble than it's worth.
If you have to change how you write all your code in order to enable the DI. This usually means that your classes have to wrap every instance variable in a 'container' or get them from a special factory which decides what version of the dependency gets loaded in that container or location. Swinject is a good example of this approach. My complaint is mostly that it makes code look unfamiliar, harder to immediately understand, and it creates a specific dependency on the framework for all your classes to function at all. Great dependency injection happens outside of your class's code and is invisible to the class itself.
If you are injecting all your dependencies through constructors, it can make your code a lot less developer-friendly as you can no longer just instantiate instances quickly like this:
let instance = MyClass()
Instead, every instantiation throughout the app starts to look like this:
let dependencyOne = TypeOne(dependency: instanceOne) let dependencyTwo = TypeTwo(dependency: instanceOne, another: instanceTwo) let finally = FinallyType(dependency: dependencyOne, another: dependencyTwo)
And at some point, the time and code you save writing unit and integration tests is outweighed by extra code and overhead incorporated throughout the production code.
A More Swift-y Approach
So here's a cool way to do Dependency Injection that doesn't require classes to directly reference containers or factories, and which gives us a couple other benefits (covered further down) as well. For this approach, we'll use Swift's Protocol Extension feature.
struct InjectionMap {
static var networking:NetworkingProtocol = Alamofire.Manager()
}
protocol NetworkingInjected { }
extension NetworkingInjected {
var networking:NetworkingProtocol { get { return InjectionMap.networking } }
}
class MyViewModel : NetworkingInjected {
func getData() {
networking.request(someNSURLRequest) { /* some completion handler */ }
}
}
Notice how our class very elegantly declares its dependency ("NetworkingInjected") in its type definition. A class with several dependencies could be declared similarly, like this for example:
class OrdersViewController : UIViewController, OrdersViewModelInjected, CurrentUserInjected, AppConfigurationInjected {
...
}
Even better, all the dependencies are available during initialization, just like constructor injected dependencies. And, the class doesn't have to redeclare or set the values for the injected instance variables because that's all done externally by the protocol extension.
Now, when it comes time to write a unit test, or an integration test, changing the injection is as easy as:
func testIntegration() {
InjectionMap.networking = MockNetworkingClass()
doAWholeBunchOfStuff() // <-- Now, every instance of a class that is NetworkingInjected will use the MockNetworkingClass instead of Alamofire
}
Neat Applications
There are a lot of neat applications for this approach, particularly for testing. For example, consider this usage:
struct InjectionMap {
static var currentDate:()->NSDate = { return NSDate() }
}
protocol CurrentDateInjected { }
extension CurrentDateInjected {
var currentDate:NSDate { get { return InjectionMap.currentDate() } }
}
class OrdersViewModel : CurrentDateInjected {
func ordersPlacedToday() -> [Order] {
return allOrders.filter {
order in
return order.date.compare(self.currentDate) == .NSOrderedSame
}
}
}
In the normal context, any class that is CurrentDateInjected will get the current date as of the moment it accesses its own currentDate
property. However, for testing purposes, you can literally freeze things at a specified moment in time like this:
func testAtMomentInTime() {
let viewController = OrdersViewController()
InjectionMap.currentDate = { return someSpecificDate } //every CurrentDateInjected class that runs will think the current date is someSpecificDate
XCTAssert(viewController.tableView.numberOfRowsInSection(0) == 5) //We know there should be 5 orders on someSpecificDate */
}
Another cool benefit of this approach is that changing the InjectionMap for a specific dependency will change the value for the dependency in all instances, including instances that have already been instantiated. That's pretty uncommon for DI approaches and is even more flexible for testing purposes.
Lastly, this approach to Dependency Injection can help you complete eliminate the use of singletons in your own code. I won't get into the debate or arguments against singletons in this article, but I will point out that something like this uses a single instance throughout the app, without using actual singletons:
struct InjectionMap {
static var cache = NSCache()
}
protocol CacheInjected { }
extension CacheInjected {
var cache:NSCache { get { return InjectionMap.cache } }
}
class SomeType : CacheInjected {
func getOrderFromCache(withID:String) -> Order? {
return cache.objectForKey(withID) as? Order
}
}
Now you can use the same instance of the cache throughout the app without using a singleton, and while clearly declaring the dependency on the cache in the type definition. It's also as easy as pie to inject a different instance of the cache (for example, a pre-populated instance or a wiped-clean instance) during testing.
Update:
Following some discussion with soolwan in the comments, I think this approach would be improved by breaking the concept of "InjectionMap"into smaller, single responsibility "Injectors". This prevents one massive single InjectionMap type, and allows the injector logic to be more easily included in the same file as the protocol or type that it applies to.
Example:
protocol NetworkingInjected { }
struct NetworkingInjector {
static var networking:NetworkingProtocol = Alamofire.Manager()
}
extension NetworkingInjected {
var networking:NetworkingProtocol { get { return NetworkingInjector.networking }}
}
class MyViewModel : NetworkingInjected {
func getData() {
networking.request(someNSURLRequest) { /* some completion handler */ }
}
}
We also took a look at some conditional injection scenarios. In the below example, an "Endpoint" enum is injected with a URL to the endpoint, which would allow you to inject different endpoint URLs throughout the app from a single location, based on for example, whether the app is a QA build or an App Store release:
protocol URLInjected { }
enum Endpoint : String {
case Orders
case Users
}
struct URLInjector {
static var urlForEndpoint:(Endpoint)->NSURL = {
endpoint in
switch endpoint {
case .Orders :
return NSURL(string:"https://myapi.com/orders")!
case .Users :
return NSURL(string:"https://myapi.com/users")!
}
}
}
extension URLInjected where Self:Endpoint {
var url:NSURL = { get { return URLInjector.urlForEndpoint(self) }}
}
let endpoint = Endpoint.Orders()
print(endpoint.url) // prints "https://myapi.com/orders"
Having played with this approach for several weeks now, I'm really excited about Dependency Injection in Swift again! Would love to hear your thoughts in the comments.
Another Update
I've written a second post on this pattern, specifically to delve into solutions for injected instance properties. It's a question I've been asked about several times and there are indeed ways to use this protocol-oriented approach to get injected properties that are unique per instance, persistent, and even modifiable after initial injection. Find out more in the new post: Swift-y Dependency Injection Part Two
You might also be interested in these articles...