Protocol composition for dependency injection

Original author: Krzysztof Zabłocki
  • Transfer

I like to use composition and dependency injection, but when each entity begins to be injected with several dependencies, a certain heap is obtained.


The project is growing and you have to inject more and more dependencies into objects, refactor methods many times, Xcode doesn’t really help with this, as we know.


But there is a more manageable way.



This article will be useful to those who have just started using DI or do not use it at all, and are not yet particularly familiar with IoC. The approach applies to other languages, but because the author writes in Swift, all the examples will be on it (approx. Per.)


Problem


Suppose there is an object that suddenly needs a provider ImageProvider, write something like this:


class FooCoordinator {
  let imageProvider: ImageProvider
  init(..., imageProvider: ImageProvider)
  ///...
}

Quite simple and convenient + this will allow you to replace the provider in the tests.


As the code base grows, more and more dependencies appear, each of which forces:


  • refactor the place where it is used
  • add new variable
  • write some boilerplate

for example, after several months the object may appear 3 dependencies:


class FooCoordinator {
  let imageProvider: ImageProvider
  let articleProvider: ArticleProvider
  let persistanceProvider: PersistanceProvider
  init(..., imageProvider: ImageProvider, articleProvider: ArticleProvider, persistanceProvider: PersistanceProvider) {
      self.imageProvider = imageProvider
      self.articleProvider = articleProvider
      self.persistanceProvider = persistanceProvider
      ///...
  }
  ///...
}

Since there is usually more than one class in a project, the same pattern is repeated many times.


And we must not forget that you need somewhere to store links to all these dependencies, for example, in AppControlleror Flow Coordinator.


This approach will inevitably lead to pain. Pain can motivate the use of not quite the right decisions, for example Singleton.


But we need simple and easy code support, with all the benefits of code injection.


Alternative


We can use the composition of protocols (interfaces) to increase the readability and quality of service of the code.


Let's describe the base protocol container for any dependency we have:


protocol Has{Dependency} {
    var {dependency}: {Dependency} { get }
}

Change {Dependency} to the name of the object


For example, ImageProviderit would look like this:


protocol HasImageProvider {
    var imageProvider: ImageProvider { get }
}

Swift allows you to compose protocols using the operator &, which means that our entities can now contain just one dependency store:


class FooCoordinator {
    typealias Dependencies = HasImageProvider & HasArticleProvider
    let dependencies: Dependencies
    init(..., dependencies: Dependencies)
}

Now in AppControlleror Flow Coordinator(Or what is used there to create entities) you can hide all the dependencies under one container in the form of a structure:


struct AppDependency: HasImageProvider, HasArticleProvider, HasPersistanceProvider {
  let imageProvider: ImageProvider
  let articleProvider: ArticlesProvider
  let persistanceProvider: PersistenceProvider
}

And all the application dependencies will now be stored in a container that does not have any logic or anything magical, just a structure.


This approach improves readability, all dependencies are stored together, but more importantly, the configuration code is always the same, regardless of which dependencies your object needs:


class FlowCoordinator {
    let dependencies: AppDependency
    func configureViewController(vc: ViewController) {
        vc.dependencies = dependencies
    }
}

Each object describes only those dependencies that it really needs, and he will receive only them.


For example, if ours is FooCoordinatorneeded ImageProvider, then it will roll through the AppDependencystructure, and type checking in Swift will provide access only toImageProvider


If in the future you need more dependencies, for example PersistanceProvider, then you just need to add it to ours typealias:


class FooCoordinator {
    typealias Dependencies = HasImageProvider & HasArticleProvider & HasPersistanceProvider
}

That's all.


This approach has several advantages:


  • Dependencies are clearly defined and always consistent, in any object, throughout the project
  • When the dependencies of an object change, you only need to change typealias
  • Neither the initializer nor the configuration function needs to be touched.
  • Any of the objects, thanks to the type checking system in Swift, gets only those dependencies that it needs.

Also popular now: