Dependency Injection (DI) is a $25 word for a 5¢ idea, but it's an idea that has become wholly foundation to how I write software. I want to take a look at some of the ways our team have been using DI in Swift.
DI users in Swift (and Objective-C) are generally in one of a few camps:
- Use initializer injection to provide objects with their dependencies.
- Use property injection (with laziness even!).
- Use frameworks like Swinject to build dependency graphs at run time.
If you've used storybards or nibs before, you have probably already used property injection via IBOutlets. I actually consider initializer injection and property injection to be roughly the equivalent, just with different timing.
If I had to pick a favourite, I like the initializer injection because it fits appropriately with the level of dynamism Swift offers. But Swift is still super young and there're lots of programming techniques to explore, so I've been experimenting with something new.
The idea is similar to initializer injection, where you provide an instance's dependencies, but instead of providing the dependencies directly, you provide closures that return a dependency. It sounds odd, and is best explained using an example that starts without any DI at all.
Okay, we've got a network layer that communicates with an API. We're writing the class that takes the parsed data from the
NetworkProvider class and turns it into models consumable by the rest of the app. Right now it looks like this.
1 2 3 4 5 6 7
There are some limitations to this, specifically around testing it. It would be better to have the
networkProvider passed in as an argument to
init(). That's initializer injection, and my opposition to it is that we've moved the responsibility for creating the
networkProvider up the stack.
1 2 3 4 5 6 7 8 9 10 11 12 13
The thing is, now some other object has to know how to do create the
NetworkProvider. Hrm. You can repeat this process of injecting dependencies from further up the stack until you have a general-purpose DI framework, and that's not my bag.
My approach passes a closure that returns a network provider instead of passing in a
networkProvider instance directly. The parameter can be given a default implementation, too.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
There's a lot to unpack here, so let's take it slowly. The initializer has a new
networkProviderCreator, a closure that returns a
NetworkProvider. In the initializer, we set our property to the return value of the closure. We also have a class method that gives us a default implementation that's used in production.
But in tests, we can initialize the
StateManager with a stub closure, something like:
Now you get the benefits of initializer injection, but the flexibility to only use DI when you need to.
Note: we should still test the
defaultNetworkCreator() function to make sure it works, too. Having code behave differently specifically while being tested is not generally a good idea.
Applying the advice on using
typealias from my last post, we can tidy our code up a little bit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
But wait, there's more!
The other benefits of passing in a closure instead of an instance is that it lets the initializer customize the dependency based on other data. For example, let's say the state manager uses an
enum to differentiate between staging and production API endpoints (btw, two-case enums are great at this). How might our initializer change?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
I really dig this. The closure to create the dependency is close to the code that uses it, but is insulated from any specific instance, so we get the benefits of using DI.
You could argue that picking a base URL for an API shouldn't belong here, and you could probably convince me. But my point isn't that this specific example is ideal, it's that the pattern of using closures for initializer injection is pretty neat.
The logic to create dependencies has to go somewhere. I think it makes sense to keep it close to the code that actually uses the dependency, but isolated in a
class function so no actual instance is involved in its creation. As a result, developers get the benefits of initializer injection and none of the added cognitive overhead when writing your production code.
Now that I have more free time to explore the pattern, I want to take it a step further and see where it could be used outside of unit testing. It's possible that using this approach could make all our types less tightly coupled and provide a more modular codebase.