Using closures for dependencies instead of protocols

Published on: April 2, 2024

It’s common for developers to leverage protocols as a means to model and abstract dependencies. Usually this works perfectly well and there’s really no reason to try and pretend that there’s any issue with this approach that warrants an immediate switch to something else.

However, protocols are not the only way that we can model dependencies.

Often, you’ll have a protocol that holds a handful of methods and properties that dependents might need to access. Sometimes, your protocol is injected into multiple dependents and they don’t all need access to all properties that you’ve added to your protocol.

Also, when you're testing code that depends on protocols you need to write mocks that implement all protocol methods even if your test will only require one or two out of several methods to be callable.

We can solve this through techniques used in functional programming allowing us to inject functionality into our objects instead of injecting an entire object that conforms to a protocol.

In this post, I’ll explore how we can do this, what the pros are, and most importantly we’ll take a look at downsides and pitfalls associated with this way of designing dependencies.

If you’re not familiar with the topic of dependency injection, I highly recommend that you read this post where explain what dependency injection is, and why you need it.

This post heavily assumes that you are familiar and comfortable with closures. Read this post if you could use a refresher on closures.

If you prefer learning through videos, check out the video for this post here:

Defining objects that depend on closures

When we talk about injecting functionality into objects instead of full blown protocols, we talk about injecting closures that provide the functionality we need.

For example, instead of injecting an instance of an object that conforms to a protocol called ‘Caching’ that implements two methods; read and write, we could inject closures that call the read and write functionality that we’ve defined in our Cache object.

Here’s what the protocol based code might look like:

protocol Caching {
  func read(_ key: String) -> Data
  func write(_ object: Data)
}

class NetworkingProvider {
  let cache: Caching

  // ...
}

Like I’ve said in the intro for this post, there’s nothing wrong with doing this. However, you can see that our object only calls the Cache’s read method. We never write into the cache.

Depending on an object that can both read and write means that whenever we mock our cache for this object, we’d probably end up with an empty write function and a read function that provides our mock functionality.

When we refactor this code to depend on closures instead of a protocol, the code changes like this:

class NetworkingProvider {
  let readCache: (String) -> Data

  // ...
}

With this approach, we can still define a Cache object that contains our methods, but the dependent only receives the functionality that it needs. In this case, it only asks for a closure that provides read functionality from our Cache.

There are some limitations to what we can do with objects that depend on closures though. The Caching protocol we’ve defined could be improved a little by redefining the protocol as follows:

protocol Caching {
  func read<T: Decodable>(_ key: String) -> T
  func write<T: Encodable>(_ object: T)
}

The read and write methods defined here can’t be expressed as closures because closures don’t work with generic arguments like our Caching protocol does. This is a downside of closures as dependencies that you could work around if you really wanted to, but at that point you might ask whether that even makes sense; the protocol approach would cause far less friction.

Depending on closures instead of protocols when possible can make mocking trivial, especially when you’re mocking larger objects that might have dependencies of their own.

In your unit tests, you can now completely separate mocks from functions which can be a huge productivity boost. This approach can also help you prevent accidentally depending on implementation details because instead of a full object you now only have access to a closure. You don’t know which other variables or functions the object you’re depending on might have. Even if you did know, you wouldn’t be able to access any of these methods and properties because they were never injected into your object.

If you end up with loads of injected closures, you might want to wrap them all up in a tuple. I’m personally not a huge fan of doing this but I’ve seen this done as a means to help structure code. Here’s what that looks like:

struct ProfileViewModel {
  typealias Dependencies = (
    getProfileInfo: @escaping () async throws -> ProfileInfo,
    getUserSettings: @escaping () async throws -> UserSettings,
    updateSettings: @escaping (UserSettings) async throws -> Void
  )

  let dependencies: Dependencies

  init(dependencies: Dependencies) {
    self.dependencies = dependencies
  }
}

With this approach you’re creating something that sits between an object and just plain closures which essentially gets you the best of both worlds. You have your closures as dependencies, but you don’t end up with loads of properties on your object because you wrap them all into a single tuple.

It’s really up to you to decide what makes the most sense.

Note that I haven’t provided you examples for dependencies that have properties that you want to access. For example, you might have an object that’s able to load page after page of content as long as its hasNewPage property is set to true.

The approach of dependency injection I’m outlining here can be made to work if you really wanted to (you’d inject closures to get / set the property, much like SwiftUI’s Binding) but I’ve found that in those cases it’s far more manageable to use the protocol-based dependency approach instead.

Now that you’ve seen how you can depend on closures instead of objects that implement specific protocols, let’s see how you can make instances of these objects that depend on closures.

Injecting closures instead of objects

Once you’ve defined your object, it’d be kind of nice to know how you’re supposed to use them.

Since you’re injecting closures instead of objects, your initialization code for your objects will be a bit longer than you might be used to. Here’s my favorite way of passing closures as dependencies using the ProfileViewModel that you’ve seen before:

let viewModel = ProfileViewModel(dependencies: (
  getProfileInfo: { [weak self] in
    guard let self else { throw ScopingError.deallocated }

    return try await self.networking.getProfileInfo()
  },
  getUserSettings: { [weak self] in 
    guard let self else { throw ScopingError.deallocated }  
    return try await self.networking.getUserSettings()
  },
  updateSettings: { [weak self]  newSettings in 
    guard let self else { throw ScopingError.deallocated }

    try await self.networking.updateSettings(newSettings)
  }
))

Writing this code is certainly a lot more than just writing let viewModel = ProfileViewModel(networking: AppNetworking) but it’s a tradeoff that can be worth the hassle.

Having a view model that can access your entire networking stack means that it’s very easy to make more network calls than the object should be making. Which can lead to code that creeps into being too broad, and too intertwined with functionality from other objects.

By only injecting calls to the functions you intended to make, your view model can’t accidentally grow larger than it should without having to go through several steps.

And this is immediately a downside too; you sacrifice a lot of flexibility. It’s really up to you to decide whether that’s a tradeoff worth making.

If you’re working on a smaller scale app, the tradeoff most likely isn’t worth it. You’re introducing mental overhead and complexity to solve a problem that you either don’t have or is incredibly limited in its impact.

If your project is large and has many developers and is split up into many modules, then using closures as dependencies instead of protocols might make a lot of sense.

It’s worth noting that memory leaks can become an issues in a closure-driven dependency tree if you’re not careful. Notice how I had a [weak self] on each of my closures. This is to make sure I don’t accidentally create a retain cycle.

That said, not capturing self strongly here could be considered bad practice.

The self in this example would be an object that has access to all dependencies we need for our view model. Without that object, our view model can’t exist. And our view model will most likely go away long before our view model creator goes away.

For example, if you’re following the Factory pattern then you might have a ViewModelFactory that can make instances of our ProfileViewModel and other view models too. This factory object will stay around for the entire time your app exists. It’s fine for a view model to receive a strong self capture because it won’t prevent the factory from being deallocated. The factory wasn’t going to get deallocated anyway.

With that thought in place, we can update the code from before:

let viewModel = ProfileViewModel(dependencies: (
  getProfileInfo: networking.getProfileInfo,
  getUserSettings: networking.getUserSettings,
  updateSettings: networking.updateSettings
))

This code is much, much, shorter. We pass the functions that we want to call directly instead of wrapping calls to these functions in closures.

Normally, I would consider this dangerous. When you’re passing functions like this you’re also passing strong references to self. However, because we know that the view models won’t prevent their factories from being deallocated anyway we can do this relatively safely.

I’ll leave it up to you to decide how you feel about this. I’m always a little reluctant to skip the weak self captures but logic often tells me that I can. Even then, I usually just go for the more verbose code just because it feels wrong to not have a weak self.

In Summary

Dependency Injection is something that most apps deal with in some way, shape, or form. There are different ways in which apps can model their dependencies but there’s always one clear goal; to be explicit in what you depend on.

As you’ve seen in this post, you can use protocols to declare what you depend on but that often means you’re depending on more than you actually need. Instead, we can depend on closures instead which means that you’re depending on very granular, and flexible, bodies of code that are easy to mock, test, replace, and manage.

There’s definitely a tradeoff to be made in terms of ease of use, flexibility and readability. Passing dependencies as closures comes at a cost and I’ll leave it up to you to decide whether that’s a cost you and your team are able and willing to pay.

I’ve worked on projects where we’ve used this approach with great satisfaction, and I’ve also declined this approach on small projects where we didn’t have a need for the granularity provided by closures as dependencies; we needed flexibility and ease of use instead.

All in all I think closures as dependencies are an interesting topic that’s well worth exploring even if you end up modeling your dependencies with protocols.

Categories

Swift

Subscribe to my newsletter