In previous posts, we described dependency injection, which is a technique to inject an object into another object that needs it. But, should we inject all objects that an object needs? Of course not. We should inject volatile dependencies. We can safely directly depend on stable dependencies. This post is about these two concepts.
Stable Dependencies
Stable dependencies contain deterministic behavior. That is, given input (or no input), it always returns the same result (or does the same side-effect).
For example, say we have a simple cart class that calculates the total of given items:
class Cart {
private var items = [Item]()
var total: Int {
var result = 0
items.forEach { result += $0.amount }
return result
}
func add(item: Item) {
items.append(item)
}
}
The behavior of this class is deterministic. When we add an item with an amount of 1, we will always get 1 as the total. When we add another item also of amount 1, we will always get 2 as the total.
So, if we have, for example, a CartViewController
class that wants to use this Cart
class, we can safely depend on Cart
directly.
class CartViewController: UIViewController {
let cart = Cart()
// ...
}
In the book Dependency Injection Principles, Practices, and Patterns by Steven van Deursen and Mark Seemann, they describe this rule for stable dependencies:
In general, DEPENDENCIES can be considered stable by exclusion. They’re stable if they aren’t volatile.
I find this general rule very helpful when deciding if I need to inject a dependency or create it directly. So let's look at how to spot volatile dependencies.
Volatile Dependencies
A volatile dependency, on the other hand, contains a nondeterministic behavior. You give it some input, and you get some output. But If you rerun it with the same input, you might get a different output.
A volatile dependency is volatile because:
- It depends on an external world to do its work. It’s known as out-of-process dependencies because it does their work outside of the application process.
- do a nondeterministic computation such as computations based on random numbers, date, or time
- It’s under development or doesn’t exist yet, so we don’t know the outcome of using it.
Let’s look at an example for each one of them.
Out-of-process dependencies
An out-of-process dependency, as the name suggests, does their work outside of our application process. That is, it depend’s on an external world that is outside of our world (the application world).
The obvious example of such dependencies is the network. When you send a request across the network, your request is processed on some external system, a server, or the cloud (which is another name for a server 😄).
For example, this ProductsViewController
class directly depends on a volatile dependency:
class ProductsViewController: UIViewController {
let session = URLSession.shared
func fetchProducts() {
let request = URLRequest(url: URL(string: "https://example.com/api")!)
session.dataTask(with: request) { data, response, error in
// ...
}
}
}
URLSession
is a volatile dependency because it deals with the network, an outside world to our application. You will never certainly know the outcomes of the requests it sends. It may fail from the operating system side (e.g., No connection available), it may reach the server, but the server is down, or the server may respond with an unexpected response.
To decouple our view controller from this uncertainty, we should instead inject URLSession
:
class ProductsViewController: UIViewController {
let session: URLSession
init(session: URLSession) {
self.session = session
super.init(nibName: nil, bundle: nil)
}
func fetchProducts() {
let request = URLRequest(url: URL(string: "https://example.com/api")!)
session.dataTask(with: request) { data, response, error in
// ...
}
}
}
Now you can inject a URLSession
with any configuration you want, not just URLSession.shared
. Moreover, you can mock network requests using URLProtocol
so that you can control the response coming from URLSession
either for development or testing purposes:
let config = URLSessionConfiguration.default
config.protocolClasses = [URLProtocolMock.self]
let session = URLSession(configuration: config)
let productsViewController = ProductsViewController(session: session)
For more about mocking with URLProtocol
, see this post.
Nondeterministic computation dependencies
The main problem with dependencies that has nondeterministic computations is that you cannot unit test them. Because every time you run them, they will produce a different result.
As an example, say we need to greet a user and tell them either good morning or good evening based on time:
func great(user: User) -> String {
let now = Date()
let components = Calendar.current.dateComponents([.hour], from: now)
guard let hour = components.hour else { return "Hello \(user.name)!" }
let isMorning = (0...11).contains(hour)
let greeting = isMorning ? "Good morning" : "Good evening"
return greeting + " \(user.name)!"
}
great(user: User(name: "Abdullah"))
// Output will be "Good morning Abdullah!" or "Good evening Abdullah!"
Here is an attempt to test this code:
class GreetingTests: XCTestCase {
func test_greetUser() {
let user = User(name: "Abdullah")
XCTAssertEqual(greet(user: user), "Good morning Abdullah!")
}
}
The problem with this test is that it will succeed in the morning but fail in the evening. This is because our greet function directly depends on a volatile dependency, which is Date()
.
So, the solution is to use DI to inject a Date
:
func greet(user: User, at date: Date) -> String {
let components = Calendar.current.dateComponents([.hour], from: date)
guard let hour = components.hour else { return "Hello \(user.name)!" }
let isMorning = (0...11).contains(hour)
let greeting = isMorning ? "Good morning" : "Good evening"
return greeting + " \(user.name)!"
}
greet(user: User(name: "Abdullah"), at: Date())
// Output will be "Good morning Abdullah!" or "Good evening Abdullah!" based on current time
And here is how we can easily test this new implementation:
class GreetingTests: XCTestCase {
func test_greetUserAtMorning() {
let user = User(name: "Abdullah")
let date = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: Date())!
XCTAssertEqual(greet(user: user, at: date), "Good morning Abdullah!")
}
func test_greetUserAtEvening() {
let user = User(name: "Abdullah")
let date = Calendar.current.date(bySettingHour: 16, minute: 0, second: 0, of: Date())!
XCTAssertEqual(greet(user: user, at: date), "Good evening Abdullah!")
}
}
Under development dependencies
Let’s say you want to create the user interface of a news application. You didn’t yet implement how to fetch that news, or maybe someone else is working on that. To start implementing the UI, you need at least a stub to fetch some news and drive your UI.
struct News {
let title: String
let content: String
}
protocol NewsLoader {
func fetch(completion: (Result<[News], Error>) -> Void)
}
class StubNewsLoader: NewsLoader {
func fetch(completion: (Result<[News], Error>) -> Void) {
completion(.success([
News(
title: "Title 1",
content: "Content 1 ..."
),
News(
title: "Title 2",
content: "Content 2 ..."
),
News(
title: "Title 3",
content: "Content 3 ..."
)
]))
}
}
class NewsViewController: UIViewController {
let newsLoader: NewsLoader
init(newsLoader: NewsLoader) {
self.newsLoader = newsLoader
super.init(nibName: nil, bundle: nil)
}
}
Once you or someone else is done implementing the real news fetching, you can inject the actual implementation instead of the stub.
class APINewsLoader: NewsLoader {
let session: URLSession
init(session: URLSession) {
self.session = session
}
func fetch(completion: (Result<[News], Error>) -> Void) {
session.dataTask(with: request) { data, response, error in
// ...
}
}
}
// Somewhere in your application
let apiNewsLoader = APINewsLoader(session: .shared)
let newsViewController = NewsViewController(newsLoader: apiNewsLoader)
Stable Dependency That Depends on Volatile Dependency
So we described that volatile dependencies should be injected and not directly created. On the other hand, we can safely depend on stable dependencies.
But can we depend on a stable dependency that has a volatile dependency? Let’s see if we do.
Let’s say we want to create a view model for our news example. The view model will handle the fetching of news, so we will inject a NewsLoader
in the view model instead of the view controller.
class NewsViewModel {
@Published var allNews = [News]()
@Published var loadingError: Error? = nil
let newsLoader: NewsLoader
init(newsLoader: NewsLoader) {
self.newsLoader = newsLoader
}
func load() {
newsLoader.fetch { [weak self] result in
switch result {
case .success(let loadedNews):
self?.allNews = loadedNews
case .failure(let error):
self?.loadingError = error
}
}
}
}
The NewsViewModel
depends on NewsLoader
, which is a volatile dependency, so we inject it. In NewsViewController
, we cannot create NewsViewModel
directly because it requires a NewsLoader
.
One solution is to inject a NewsLoader
into NewsViewController
, and then the NewsViewController
passes the NewsLoader
to the NewsViewModel
.
class NewsViewController: UIViewController {
let newsViewModel: NewsViewModel
init(newsLoader: NewsLoader) {
self.newsViewModel = NewsViewModel(newsLoader: newsLoader)
super.init(nibName: nil, bundle: nil)
}
// ...
}
As you can see, we only pass NewsLoader
to NewsViewController
so that we can create a NewsViewModel
. This is similar to the middle-man anti-pattern.
Indeed the NewsViewController
needs a NewsViewModel
, but it doesn’t have to create it.
A better solution is to inject the NewsViewModel
into the NewsViewController
directly.
class NewsViewController: UIViewController {
let newsViewModel: NewsViewModel
init(newsViewModel: NewsViewModel) {
self.newsViewModel = newsViewModel
super.init(nibName: nil, bundle: nil)
}
// ...
}
Somewhere in your application, you create the NewsViewController
like the following:
let apiNewsLoader = APINewsLoader(session: .shared)
let newsViewModel = NewsViewModel(newsLoader: apiNewsLoader)
let newsViewController = NewsViewController(newsViewModel: newsViewModel)
By the way, the NewsViewModel
should be considered as a volatile dependency, not a stable dependency that depends on a volatile one.
Once a type depends on a volatile dependency, it itself also becomes volatile. I included this section because it’s a common mistake.
Injecting stable dependencies to pass data
The last thing I want to touch upon is that you might use DI to inject stable dependencies just to pass data.
For example, let’s say we want to display each news on it’s own screen.
class NewsDetailViewController: UIViewController {
let news: News
init(news: News) {
self.news = news
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
display(news)
}
private func display(_ news: News) {
// ...
}
}
The news struct is pretty stable, it’s just two fields of type String
, but we don’t know at compile time which News
to pass.
At run time, when, for example, we tap on a news in NewsViewController
, the news will be passed to NewsDetailViewController
.
class NewsViewController: UIViewController {
let newsViewModel: NewsViewModel
init(newsViewModel: NewsViewModel) {
self.newsViewModel = newsViewModel
super.init(nibName: nil, bundle: nil)
}
func tappedOn(news: News) {
let newsDetailViewController = NewsDetailViewController(news: news)
show(newsDetailViewController, sender: nil)
}
}
Conclusion
We looked at the difference between stable and volatile dependencies and that we should always inject the volatile ones. Because volatile dependencies are hard to control, you will lose control of your application if you always create your volatile dependencies directly. If you inject them instead, you will have absolute control over these difficult dependencies.
If each type keeps asking the layer before it for its dependencies, you will find yourself creating all of your dependencies at the root (entry point) of your application, and that is a good thing.
The next post will be about the composition root pattern, a way to create and control all your app dependencies and extend or intercept them.
Top comments (0)