DEV Community

Cover image for Stable vs. Volatile Dependencies
Abdullah Althobetey
Abdullah Althobetey

Posted on • Edited on • Originally published at abdullahth.com

Stable vs. Volatile Dependencies

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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()

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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
            // ...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            // ...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!" 
Enter fullscreen mode Exit fullscreen mode

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!")
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!")
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)  
Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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)
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)