Friday, December 22, 2023

How to Control the World

Brandon Williams and Stephen Celis (2018, via Christian Tietze):

While unconventional, we hope that it’s obvious that this solution of controlling dependencies is superior to the traditional solutions in use today. It also gives us an opportunity to reevaluate deep-seated beliefs we may have. We should continuously question our assumptions. In this case, we found that:

  • Singletons can be good (as long as we have a means to control them) and global mutation can be good (when it’s limited to development and testing). Blanket statements against singletons and global mutation are fun to make, but we were able to find real value in using them.

  • Protocols aren’t necessarily a good choice to control dependencies. Protocol-oriented programming is all too easy to reach for when a simple value type requires less work.

The global mutable struct approach certainly reduces boilerplate code, but I’ve never understood how it can work with threads. Most basically, how do you deal with modifying the dependencies for multiple unit tests that are running concurrently?

Their newer Dependencies library handles this more directly:

A dependency management library inspired by SwiftUI’s “environment.”

[…]

For example, you can easily control these dependencies in tests. If you want to test the logic inside the addButtonTapped method, you can use the withDependencies function to override any dependencies for the scope of one single test.

The dependencies are stored in a TaskLocal. But it still feels like a partial solution because not all code uses Swift Concurrency, and you still need to worry about propagating dependencies:

It is important to note that task locals are not inherited in all escaping contexts. It does work for Task.init and TaskGroup.addTask, which make use of escaping closures, but only because the standard library special cases those tools to inherit task locals (see copyTaskLocals in this code).

But generally speaking, task local overrides are lost when crossing escaping boundaries.

[…]

In order to access dependencies across escaping closures, e.g. in a callback or Combine operator, you must do additional work to “escape” the dependencies so that they can be passed into the closure.

It’s more ergonomic not to have to propagate dependencies explicitly, but relying on implicit behavior can be harder to understand and error-prone.

Previously:

3 Comments RSS · Twitter · Mastodon


Óscar Morales Vivó

I'm not an eminence on this but when I built up https://github.com/Gabardone/GlobalDependencies I figured it was ok to inject (with a default) since it makes most of those concerns moot.

If you end up having a nightmare dealing with the injections then the problem is usually not with whatever dependency injection logic you're using but rather with your dependencies themselves having gotten out of hand.


> The global mutable struct approach certainly reduces boilerplate code, but I’ve never understood how it can work with threads. Most basically, how do you deal with modifying the dependencies for multiple unit tests that are running concurrently?

Concurrent unit tests are spun up on their own processes, so they all have their own, independent, global stores. The main issue with the global mutable approach is if you decide to mutate your "world" outside of tests, _e.g._ in the lifetime of your production application, but we definitely don't recommend that.

> It’s more ergonomic not to have to propagate dependencies explicitly, but relying on implicit behavior can be harder to understand and error-prone.

This is for sure fair, but we think it's only generally a concern when you override dependencies in certain branches of your application. If you override dependencies for a certain flow (_e.g._ an onboarding flow, a demo, etc.), then propagation needs to be explicit if you also want to unit test that flow. But for simpler setups, where you simply need to override dependencies in your unit tests, taking extra steps for propagation are not necessary.


@Stephen Ah, you are right about the separate processes. I must have been misremembering some other issue that I once had. Thanks!

Leave a Comment