What is DI(= dependency injection) Container?

Ryuichi / Rick
3 min readJan 29, 2023

--

Photo by Dhoomil Sheta on Unsplash

Hi there, recently I traveled to Singapore and Gardens by the Bay was sooo incredible to see and feel the nature’s energy.

In this article, I’d like to explain what is DI Container from a perspective of an iOS developer.

※ I don’t explain about DI(= dependency injection) in this article.

What is DI Container?

A container which enable us to manage dependency injections in one place with a couple of advantages.

final class DIContainer {

static let shared = DIContainer()

private var instances: [String: Any] = [:]

private init() {}

func register<T>(_ type: T.Type, instance: Any) {
instances["\(type)"] = instance
}

func resolve<T>(_ type: T.Type) -> T? {
instances["\(type)"] as? T
}
}
// MARK: How to use DI Container

let downloadUseCase = DownloadUseCase(
downloadApiService: DIContainer.shared.resolve(DownloadApiService.self)!,
downloadRepository: DIContainer.shared.resolve(DownloadRepository.self)!
)
DIContainer.shared.register(DownloadUseCase.self, instance: downloadUseCase)

・・・

let downloadUseCase = DIContainer.shared.resolve(DownloadUseCase.self)!

Why use DI Container?

Easy management of calling constructers/initializers

Without DI Container, places we call a construction tend to be scattered here and there. Whereas, we can only focus on one place when the dependencies got changed.

Brief writing for instantiating/extracting a instance

Normally without DI Container, we instantiate/extract a instance by taking the steps below.

  1. Instantiate/Extract each dependency.
  2. Instantiate/Extract an instance we want to use injected dependencies.

And we can get to Singapore from Japan while we are managing to get these steps done 🛫.

But if we use DI Container, we can skip those annoying processes like below 🎉 (example of instantiating DownloadUseCase class). Awesome!!


// MARK: Without DI Container

let apiClient = ApiClient()
let downloadApiService = DownloadApiService(apiClient: apiClient)

let cache = Cache()
let downloadRepository = DownLoadRepository(cache: cache)

let downloadUseCase = DownloadUseCase(
downloadApiService: downloadApiService
downloadRepository: downloadRepository
)

// MARK: With DI Container

let downloadUseCase = DIContainer.shared.resolve(DownloadUseCase.self)!

Easy management of each instance’s lifecycle

Some DI Container offer a feature by which we can manage each instance’s lifecycle easily.

For example…

  • Alive while it’s been referenced somewhere. In this situation, as long as the instance is referenced somewhere, it can be used and shared by others. And if no one has a reference to this instance, it’s gonna be released.
  • Alive while a session of the app has been alive.
  • etc…

Easy specification of picking up a instance

Some DI Container offer a feature by which we can manage a way of picking up instance easily.

For example…

  • To instantiate a new object.
  • To extract an instance which already exists. Otherwise, instantiate a new object.
  • To extract an instance which already exists if specified identity is same. Otherwise, instantiate a new object.
  • To extract an instance which already exists which is shared within a custom scope. Otherwise, instantiate a new object.
  • etc…

Automatic code generation for injecting dependencies

Some DI Container offer a feature of generating codes around DI Container like injections.

For example, Hilt(which provides a standard way to incorporate Dagger dependency injection into an Android application) generates DI Container instead of us by adding some anotations.

And under this assumption, it is recommended to use Hilt — Dagger as an App Size gets larger.

An opinionated guide to Dependency Injection on Android (Android Dev Summit ’19) 6:03

Are there some anti-patterns when using DI Container?

ServiceLocator pattern is known as an anti-pattern of a way of injecting dependencies.

This patter is like below. Each constructor/initializer accepts DI Container to extract the dependencies.

public final class DownloadUseCase: DownloadUseCaseInterface {

private let downLoadRepository: DownLoadRepositoryInterface
private let downloadApiService: DownloadApiServiceInterface

public init(container: DIContainer) {
self.downLoadRepository = container.resolve(DownLoadRepository.self)
self.downloadApiService = container.resolve(DownloadApiService.self)
}

・・・
}

But there is some cons like below if we use this pattern although there are some pros.

  • We are not sure what is injected just by looking at a constructor/initializer. We have to get back to the definition of constructor/initializer.
  • Test double for DI Container is needed.
  • Dependency(= DI Container) for each class will be added. It doesn't add up considering goals of introducing DI Container.
  • More modules/targets are gonna have to have DI Container (library) as a dependency.

Thanks for reading. See you soon 🫶

--

--