Advanced Dependency Injection in Swift

Leveraging Container Hierarchies

Navdeep Singh

--

Photo by Terry Vlisidis on Unsplash

In a previous discussion on implementing dependency injection (DI) in Swift, we explored the benefits and challenges of using a containerised approach. While such an approach offers centralised management and reduces boilerplate, it can sometimes introduce complexity, especially when your application scales. This follow-up post delves deeper into a more structured solution: employing container hierarchies.

This strategy enriches the DI container approach by offering scalability, better organization, and fine-grained control over dependency lifecycles and scopes.

Understanding Container Hierarchies

Container hierarchies involve nesting DI containers, where each container might serve a distinct scope or module within your application. This hierarchical structure allows for more granified control over dependency lifecycles, sharing dependencies across different parts of the application, and better encapsulating module-specific dependencies.

The Core Concept

The essence of container hierarchies lies in the ability to define parent-child relationships between containers.

A child container can override registrations of its parent container, providing specific implementations for its context, while still inheriting other dependencies from the parent. This setup emulates a cascading configuration that is both powerful and flexible.

Implementing Container Hierarchies in Swift

Here’s a simple conceptualisation of how you can implement DI container hierarchies in Swift:


final class DIContainer {
private var registry = [String: Any]()
private var parent: DIContainer?

init(parent: DIContainer? = nil) {
self.parent = parent
}

func register<T>(_ dependency: T, for key: String) {
registry[key] = dependency
}

func resolve<T>(for key: String) -> T? {
if let dependency = registry[key] as? T {
return dependency
} else {
return parent?.resolve(for: key)
}
}
}

Usage

  • Creating Containers: You can create a root container for app-wide dependencies and child containers for module-specific dependencies.

let appContainer = DIContainer() // Root container
appContainer.register(NetworkService() as NetworkProtocol, for: "NetworkService")

let featureContainer = DIContainer(parent: appContainer) // Child container
// Potentially override some app-wide registrations or add feature-specific ones
  • Resolving Dependencies: When resolving dependencies, the container will first attempt to resolve using its own registry. If it fails, it will delegate the resolution to its parent container.

Benefits of Using Hierarchical Containers

  • Modularity: Dependencies related to specific features or modules can be encapsulated within their own containers, promoting cleaner separation and modularity.
  • Scoped Lifecycles: Hierarchical containers can manage dependencies with lifecycles scoped to specific parts of the app, enabling more precise control over when instances are created and destroyed.
  • Override Flexibility: Child containers can override dependencies registered in parent containers, allowing for flexible replacements of implementations in different parts of the app.

Practical Considerations

When applying container hierarchies in a Swift application, consider the following:

  1. Performance: Be mindful of the potential impact on performance when cascading through containers to resolve dependencies. Proper key management and minimizing deep hierarchies can mitigate related issues.
  2. Encapsulation: Use hierarchies to encapsulate dependencies within the appropriate layers or modules of your app. This strategy promotes better encapsulation and reduces the risk of unwanted dependencies leaking into unrelated parts of your application.
  3. Testing: DI container hierarchies simplify testing by allowing you to configure test-specific containers that inherit from your app’s primary container but override certain dependencies with mock implementations.

Conclusion

Leveraging DI container hierarchies in Swift applications embodies a powerful approach to managing dependencies across different scopes and modules. By understanding and applying this strategy effectively, developers can build scalable, modular applications that are easier to maintain and test. As with any architectural pattern, the key is to apply container hierarchies judiciously, ensuring that they align with your project’s needs and contribute positively to the app’s overall architecture and maintainability.

--

--

Navdeep Singh

Author of Reactive programming with Swift, Engineering Manager — Exploring possibilities with new Tech.