Cleaning up your dependencies with protocols

Published by donnywals on

If you’re into writing clean, testable and maintainable code you must have come across the term “Dependency Injection” at some point. If you’re not sure what dependency injection is, that’s okay. I will explain it briefly so we’re all on the same page before we get to the main point of this post.

Dependency Injection in a Nutshell

Dependency injection is the practice of making sure that no object creates or manages its own dependencies. This is best illustrated using an example. Imagine you’re building a login page for an app and you have separate service objects for registering a user and logging them in. Personally I like using an MVVM-ish approach in my apps so I would wrap these services in a LoginPageViewModel. This would leave you with roughly the following code:

// NOTE: this snippet omits parts of the code that I consider non-essential to explaining the practice of Dependency Injection. Copy and pasting this snippet won't work.

class LoginViewController: UIViewController {
  let viewModel: LoginViewModel
}

protocol LoginService {
  func login(_ email: String, password: String) -> Promise<Result<User, Error>>
}
protocol RegisterService {
  func register(_ email: String, password: String) -> Promise<Result<User, Error>>
}

struct LoginViewModel {
  let loginService: LoginService
  let registerService: RegisterService
}

Notice how none of these definitions create instances of their dependencies. This means that the object that is responsible for creating a LoginViewController is also responsible for creating (or obtaining) a LoginViewModel object. And since LoginViewModel depends on two service objects, the object that creates a LoginViewModel must also be able to create (or obtain) the service objects it depends on.

Using dependency injection makes your code more flexible, testable and future proof, and it’s considered good practice amongst practically all areas of software development. However, it has its imperfections and downside, especially in Swift where we don’t really have great support for dependency injection libraries like you might find in, for instance, Java.

Cleaning up your dependencies

As your app grows, you’ll typically find that the number of dependencies that you manage in your application grows too. It’s not uncommon for a single object to (indirectly) depend on a dozen other objects that must be injected into the object’s initializer. This leads to unwieldy initializers which is can be hard to read. On top of this, it makes writing tests more tedious too because if you want to test an object with many dependencies, you’ll have to create an instance of each dependency in your test.

Cleaning up common dependencies is sometimes done by creating one or more dependency containers that might look a bit like this:

struct Services {
  let loginService: LoginService
  let registerService: RegisterService
  let feedService: FeedService
  let shopService: ShopService
  let artistService: ArtistService
  let profileService: ProfileService
}

When using this approach you might inject the entire Services object into the initializer of a LoginViewModel and extract the required services there:

struct LoginViewModel {
  let loginService: LoginService
  let registerService: RegisterService

  init(services: Services) {
    loginService = services.loginService
    regsisterService = services.regsisterService
  }
}

While this solves the problem of having large initializers, it doesn’t solve the problem of having to create many dependencies when you’re writing tests. In fact, this problem is now worse because instead of only having to set up the two services that the LoginViewModel depends on, the entire Services container has to be created. It also breaks encapsulation in some ways because LoginViewModel now has implicit access to all services instead of just the ones that it depends on.

Luckily there is a neat way to get the best of both worlds by composing protocols together and using a typealias. It’s quite simple really, so here’s what I mean.

typealias LoginViewModelServices = LoginService & RegisterService

extension Services: LoginViewModelServices {
  func login(_ email: String, password: String) -> Promise<Result<User, Error>> {
    return loginService.login(email, password: password)
  }

  func register(_ email: String, password: String) -> Promise<Result<User, Error>> {
    return registerService.register(email, password: password)
  }
}

struct LoginViewModel {
  let services: LoginViewModelServices

  init(services: LoginViewModelServices) {
    self.services = services
  }
}

The above code defines a typealias that composes the two dependencies that LoginViewModel has into a single definition. Services is then extended to implement proxy methods for the register and login methods, forwarding them directly to the relevant services, making it conform to LoginViewModelServices and only exposing the methods that are required to make the LoginViewModel work.

This approach neatly wraps up dependencies, making it agnostic of the underlying details of how services are created and managed without exposing too much information to dependent objects. Writing tests for the LoginViewModel is also simplified right now because you can create a single object that conforms to LoginViewModelServices instead of having to create two or more separate objects that might have dependencies of their own.

Of course, this approach might not always work in every context but I’ve personally found that this approached has allowed me to write both cleaner code and cleaner tests. I hope that you’ll find a use for this neat little trick in your own projects and that it allows you to make your code a little bit better just like I have. Don’t hesitate to reach out on Twitter if you have any questions, compliments or other feedback on this post!

Receive weekly updates about my posts