PrefaceIn previous articles, we looked at some different ways to use dependency injection to achieve a more decoupled and testable architecture in your Swift applications. For example, we combined dependency injection and the factory pattern in Dependency Injection with Factories in Swift[1] and used dependency injection instead of singletons in Avoiding Singletons in Swift[2]. Most of my articles and examples so far have used initializer-based dependency injection. However, like most programming techniques, dependency injection comes in multiple flavors, each with its own advantages and disadvantages. This week, let's look at three different styles of dependency injection and how they can be used in Swift. Initializer-basedLet's quickly review the most common way of dependency injection - initializer-based dependency injection, that is, the object should be given the dependencies it needs when it is initialized. The biggest benefit of this approach is that it ensures that our objects have everything they need to start working immediately. Let's say we're building a FileLoader that loads files from disk. To do this, it uses two dependencies - an instance of the system-provided FileManager and a Cache. Using initializer-based dependency injection, this can be implemented like this: class FileLoader { Note how default arguments are used above to avoid always creating dependencies when using a singleton or a new instance. This allows us to simply create a file loader using FileLoader() in production code, while still being able to test by injecting mock data or explicit instances in test code. Attribute-basedWhile initializer-based dependency injection usually works well for your own custom classes, it can sometimes be a little difficult to use when you have to inherit from a system class. One example is when building view controllers, especially when you use XIBs or Storyboards to define them, because then you no longer have control over your class's initializers. For these types of situations, property-based dependency injection can be a great option. Rather than injecting an object's dependencies in its initializer, you can simply assign them afterwards. This style of dependency injection can also help you reduce boilerplate, especially when there's a good default value that doesn't necessarily need to be injected. Let's look at another example - in this case, we're going to set up a PhotoEditorViewController that lets the user edit a photo in their library. To function, this view controller requires an instance of the system-provided PHPhotoLibrary class (which is a singleton), as well as an instance of our own PhotoEditorEngine class. To implement dependency injection without a custom initializer, we can create two mutable properties that both have default values, like this: class PhotoEditorViewController : UIViewController { Notice how the technique in "Test Swift code that uses system singletons in 3 easy steps" gives the system photo library class a more abstract PhotoLibrary interface by using a protocol. This will make testing and data mocking much easier! The benefit of doing this is that we can still easily inject mock data in our tests by just reassigning the view controller’s properties: class PhotoEditorViewControllerTests : XCTestCase { Parameter-basedFinally, let's look at parameter-based dependency injection. This type is particularly useful when you want to easily make legacy code more testable without having to change its existing structure too much. Many times, we only need a specific dependency once, or we only need to mock it under certain conditions. Instead of changing the object's initializer or exposing properties as mutable (which is not always a good idea), we can expose an API that accepts a dependency as a parameter. Let's take a look at a NoteManager class, which is part of a note-taking application. Its job is to manage all the notes written by the user and provide an API for searching notes based on a query. Since this is an operation that can take a while (which is likely if the user has a lot of notes), we usually do it in a background queue, like this: class NoteManager { While the above approach is a good solution for our production code, in tests we generally want to avoid asynchronous code and parallelism as much as possible to avoid flakiness. While it would be nice to use an initializer or property-based dependency injection to specify an explicit queue that the NoteManager should always use, this would likely require a large modification to the class, which we are unable/unwilling to do right now. This is where parameter-based dependency injection comes in. Rather than refactoring our entire class, we can just inject which queue we want to run the loadNotes action on: class NoteManager { This allows us to easily use a custom queue in our test code that we can wait on. This almost allows us to turn the above API into a synchronous API in our tests, which makes things easier and more predictable. Another use case for parameter-based dependency injection is when you want to test a static API. For a static API, we don't have initializers, and we'd better not keep any state statically, so parameter-based dependency injection becomes a good choice. Let's take a static MessageSender class that currently relies on a singleton: class MessageSender { While the ideal long-term solution would probably be to refactor MessageSender to be non-static and properly injected wherever it's used, for ease of testing (e.g. to reproduce/verify a bug) we can simply inject its dependencies as parameters instead of relying on a singleton: class MessageSender { We use default parameters again, again for reasons of convenience, but more importantly here to be able to add testing support in our code while still maintaining 100% backwards compatibility. References[1] Dependency Injection using Factories in Swift: https://www.swiftbysundell.com/articles/dependency-injection-using-factories-in-swift. [2]Avoiding Singletons in Swift: https://www.swiftbysundell.com/articles/avoiding-singletons-in-swift. |
<<: How is it different from Alipay and WeChat? A detailed experience of the Digital RMB App
>>: Google adopts new development strategy to improve Android security
Milos training video resources introduction: Cour...
I have been independently operating my company...
This time I will share with you some live broadca...
Science Fiction Network, December 1 (Xu Mingyang)...
Audit expert: Zhang Yuhong Chief Physician of Der...
To be honest, what Apple fans are most looking for...
I believe that many cat owners would think that c...
The Internet is a sea of pictures and texts, an...
Tencent products, a high-quality performance mark...
I have been particularly fond of Haier recently, ...
Space rock could rewrite early solar system histo...
With the popularization of health knowledge, many...
Rumor: "Long-term drinking of purified water...
Recently, I was fortunate enough to become one of...
1. Background and Purpose background Recently, I ...