Dependency injection
I’ve been trying to apply dependency injection to my Swift projects. As Martin Fowler said in his 2004 article Inversion of Control Containers and the Dependency Injection pattern:
The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation for the finder interface, […]
However, I found that every attempt to build an assembler resulted in far more complicated than it reduced, resulting in systems that were either ugly to read, ugly to use, or just plain opaque.
Swift native
Swift is a static language. Only when I took a step back and chose to respect the language, instead of trying to subvert it, that I started to see a way forward.
Enter typealias
Turns out I can use Swift itself as the assember via use of type alias declarations.
struct Default {
typealias Animal = Cow
}
struct Cow { let name: String }
struct Sheep { let name: String }
let animal = Default.Animal(name: "Daisy")
print(animal) // Cow(name: "Daisy")
We can then use Swift’s #if
statements or (if we place Default
in a separate file) use Xcode targets to re-wire the entire application.
This method has some interesting effects that result in clear net benefits.
Readable
In context, it’s pretty easy to see what Default.Animal
means. Default
tells us we’re injecting something and Animal
tells us what it is.
It’s an improvement on using the original type directly as we can use the alias to describe precisely what we’re trying to achieve.
No need for a unifying protocol
A traditional solution would require a unifying protocol for Cow
and Sheep
. However, because the type alias is now the unification point we can use the compiler to check the two types are interchangeable within our application.
In fact, because it’s just an identifier, you can add even more contextual information:
struct Default {
typealias AnimalForPresenting = ...
}
...
let animal = Default.AnimalForPresenting(name: "Daisy")
present(animal)
Also, as a result, there’s no need to “share” names to fit the process, e.g. Java’s SomeInterface
and SomeInterfaceImpl
which inevitably adds clutter to the whole process.
Testing
The only way I’ve found to change how an application is built for testing is to use Xcode’s conditional compilation with a seperate build configuration. Dave Delong has an excelent set of posts entitled Conditional Compilation in Swift.
However, this is how I implemented testing in my demo project:
- Create a new Configuration in the project’s “Info” section
- Add
$(CONFIGURATION:upper)
to the “Active Compilation Conditions” property in the project’s “Build Settings” section. - Use Swift’s Conditional Compilation Blocks to define which types get used during testing.
- Edit the application’s scheme to set Test > Build Configuration to your new Configuration.