Different flavors of dependency injection in Swift

In previous posts, we've taken a look at a few different ways to use dependency injection to achieve a more decoupled and testable architecture in Swift apps. For example by combining dependency injection with the factory pattern in "Dependency injection using factories in Swift", and by replacing singletons with dependency injection in "Avoiding singletons in Swift".

So far, most of my posts and examples have used initializer-based dependency injection. However, just like with most programming techniques, there are multiple "flavors" of dependency injection - each with its own pros & cons. This week, let's take a look at three such flavors and how they can be used in Swift.

Initializer-based

Let's start with a quick recap of the most common flavor of dependency injection - initializer-based - the idea that an object should be given the dependencies it needs when being initialized. The big benefit of this flavor is that it guarantees that our objects have everything they need in order to do their work right away.

Let's say we're building a FileLoader that loads files from disk. To do that it uses two dependencies - an instance of the system-provided FileManager and a Cache. Using initializer-based dependency injection, an implementation could look like this:

class FileLoader {
    private let fileManager: FileManager
    private let cache: Cache

    init(fileManager: FileManager = .default,
         cache: Cache = .init()) {
        self.fileManager = fileManager
        self.cache = cache
    }
}

Note how default arguments are used above to avoid having to always create the dependencies when either a singleton or a new instance should be used. This enables us to simply create a file loader using FileLoader() in our production code, while still enabling testing by injecting mocks or explicit instances in our testing code.

Property-based

While initializer-based dependency injection is usually a great fit for your own custom classes, sometimes it can be a bit hard to use when you have to inherit from a system class. An example of that is when building view controllers, especially if you are using XIBs or Storyboards to define them, since then you are no longer in control of your class' initializer.

For these types of situations, property-based dependency injection can be a great alternative. Instead of injecting an object's dependencies in its initializer, they can simply be assigned afterwards. This flavor of dependency injection can also help you reduce boilerplate, especially when there is a good default that doesn't necessarily need to be injected.

Let's take a look at another example - in which we're building a PhotoEditorViewController that lets the user edit one of the photos from their library. To function, this view controller needs an instance of the system-provided PHPhotoLibrary class (which is a singleton), as well as an instance of our own PhotoEditorEngine class. To enable dependency injection without a custom initializer, we can create mutable properties that both have default values, like this:

class PhotoEditorViewController: UIViewController {
    var library: PhotoLibrary = PHPhotoLibrary.shared()
    var engine = PhotoEditorEngine()
}

Note how the technique from "Testing Swift code that uses system singletons in 3 easy steps" is used above to provide a more abstract PhotoLibrary interface to the system photo library class, by using a protocol. This will make testing & mocking a lot easier!

The good thing about the above is that we can still easily inject mocks in our tests, by simply re-assigning our view controller's properties:

class PhotoEditorViewControllerTests: XCTestCase {
    func testApplyingBlackAndWhiteFilter() {
        let viewController = PhotoEditorViewController()

        // Assign a mock photo library to gain complete control over
        // what photos are stored in it
        let library = PhotoLibraryMock()
        library.photos = [TestPhotoFactory.photoWithColor(.red)]
        viewController.library = library

        // Run our testing commands
        viewController.selectPhoto(atIndex: 0)
        viewController.apply(filter: .blackAndWhite)
        viewController.savePhoto()

        // Assert that the outcome is correct
        XCTAssertTrue(photoIsBlackAndWhite(library.photos[0]))
    }
}

Parameter-based

Finally, let's take a look at parameter-based dependency injection. This flavor is particularly useful when you want to easily make legacy code more testable, without having to change too much of its existing structure.

Many times, we only need a specific dependency once, or we only need to mock it under certain conditions. Instead of having to change an object's initializer or expose properties as mutable (which is not always a good idea), we can open up a certain API to accept a dependency as a parameter.

Let's take a look at a NoteManager class that's part of a note-taking app. It's job is to manage all notes that the user has written, and provides an API for searching for notes based on a query. Since this is an operation that could take a while (if the user has many notes, which is quite likely), we normally perform it on a background queue, like this:

class NoteManager {
    func loadNotes(matching query: String,
                   completionHandler: @escaping ([Note]) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            let database = self.loadDatabase()
            let notes = database.filter { note in
                return note.matches(query: query)
            }

            completionHandler(notes)
        }
    }
}

While the above is a great solution for our production code, in tests we normally want to avoid asynchronous code and parallelism as much as possible, in order to avoid flakiness. While it would be nice to use initializer- or property-based dependency injection to be able to specify an explicit queue that NoteManager should always use, it may require big changes to the class that we're not able/willing to make right now.

This is where parameter-based dependency injection comes in. Instead of having to refactor our entire class, let's just make it possible to inject what queue to run the loadNotes operation on:

class NoteManager {
    func loadNotes(matching query: String,
                   on queue: DispatchQueue = .global(qos: .userInitiated),
                   completionHandler: @escaping ([Note]) -> Void) {
        queue.async {
            let database = self.loadDatabase()
            let notes = database.filter { note in
                return note.matches(query: query)
            }

            completionHandler(notes)
        }
    }
}

This enables us to easily use a custom queue in our testing code, which we can wait on. This almost lets us turn the above API into a synchronous one in our tests, which makes things a lot easier and more predictable.

Another use case of parameter-based dependency injection is when you want to test static APIs. With static APIs we don't have an initializer, and we ideally shouldn't be keeping any state statically either, so parameter-based dependency injection becomes a great option. Let's take a look at a static MessageSender class that is currently relying on singletons for its dependencies:

class MessageSender {
    static func send(_ message: Message, to user: User) throws {
        Database.shared.insert(message)

        let data: Data = try wrap(message)
        let endpoint = Endpoint.sendMessage(to: user)
        NetworkManager.shared.post(data, to: endpoint.url)
    }
}

While an ideal long-term solution here would probably be to refactor MessageSender into being non-static and properly injected everywhere it's used, but in order to easily be able to test it (for example, in order to reproduce/verify a bug) we can simply inject its dependencies as parameters instead of relying on singletons:

class MessageSender {
    static func send(_ message: Message,
                     to user: User,
                     database: Database = .shared,
                     networkManager: NetworkManager = .shared) throws {
        database.insert(message)

        let data: Data = try wrap(message)
        let endpoint = Endpoint.sendMessage(to: user)
        networkManager.post(data, to: endpoint.url)
    }
}

Again we use default arguments, both as a convenience, but here more importantly to be able to add testing support to our code while still maintaining 100% backwards compatibility 👍.

Conclusion

So what flavor of dependency injection is the best one? My answer is, like in many cases, the boring one: it depends 😅. One thing that I always try to do on this blog is to present many different solutions to a given problem. The reason for this is simple - I really don't believe in silver bullets and I think having multiple tools and flavors of certain techniques at our disposal enables us to make better, more informed decisions when writing code.

I hope this post has given you some new ideas on how to apply dependency injection to your own code, and if you have another flavor that I didn't cover in this post, I'd love to hear about it. Let me know, along with any questions, comments or feedback you have - either in the comments section below or on Twitter @johnsundell. Feel free to also share this post on Twitter if you liked it, I would really appreciate it 😊.

Thanks for reading! 🚀

Separation of concerns using protocols in Swift

UI testing analytics code in Swift