Skip to content

Tips & Tricks

Ilya Puchka edited this page Mar 9, 2018 · 17 revisions

Runtime arguments and auto-wiring

When using auto-wiring we effectively create definition with a factory that accepts runtime arguments, so that there are two ways to resolve the component - with auto-wiring just calling resolve() or by calling resolve(arguments:...) and providing those runtime arguments. To make an intention clear specify names and types of dependencies that are supposed to be passed as arguments to resolve(arguments:) and not resolved by auto-wiring.

//use such registration style when you plan to use auto-wiring to resolve `APIClient`
container.register { APIClientImp(credentialsStorage: $0) as APIClient }

//use such registration style when you plan to pass dependency as runtime argument
container.register { (url: NSURL) in APIClientImp(url: url) as APIClient }

When you will read your configuration after some time the intention will be clear for you. Also you can add parameters type annotations what will also improve readability and compile speed.

Registering using initializers/factory methods (v 5.0)

In Swift methods can be used in place of closures of the same type. So you can register your components in a several ways.

The most common way will be:

container.register { APIClientImp(...) as APIClient }

Alternatively you can register using initializer/factory method:

container.register(type: APIClient.self, factory: APIClientImp.init)

type parameter is optional and by default will be inferred as a return type of a closure/initializer/factory method. So if you don't provide it when using initializer you will register concrete type instead of abstraction:

container.register(factory: APIClientImp.init)
try container.resolve() as APIClient //fails
try container.resolve() as APIClientImp //succeeds

Registering existing instances

It is possible to register existing instances in the container:

let keychain = ...
container.register { keychain as KeychainService }

Note that scope will be ignored in this case and this component will be always resolved to the registered instance, making it equivalent to .Singleton scope.

Registering values

You can easily register value types or any "primitive" values in a container:

container.register(tag: "api") { NSURL(string: ...)! }
container.register { try APIClientImp(url: container.resolve(tag: "api") as NSURL) as APIClient }

let container = try! container.resolve() as APIClient

This will perfectly fit with auto-wiring:

container.register(tag: "api") { NSURL(string: ...)! }
container.register { try APIClientImp(url: $0) as APIClient }

let container = try! container.resolve(tag: "api") as APIClient

When auto-wiring container will fallback to untagged definition for APIClient but will pass through the tag to resolve each of initializer dependencies (see also: Auto-wiring).

Resolving multiple instances

With named definitions you can register alternative implementations for the same protocol. Here is how you can resolve all of them as array:

enum Services: String, DependencyTagConvertible {
  case GMail, Yahoo, Outlook
  let allValues: [Services] = [.GMail, .Yahoo, .Outlook]
}
container.register(tag: Services.GMail) { GmailService() as ThirdPartyEmailService }
container.register(tag: Services.Yahoo) { YahooService() as ThirdPartyEmailService }
container.register(tag: Services.Outlook) { OutlookService() as ThirdPartyEmailService }

let allServices: [ThirdPartyEmailService] = try Services.allValues.map(container.resolve(tag:))

Resolving & inheritance (v 5.0)

Swift runtime does not provide any information about inheritance relationships between classes so container can not automatically invoke definition for super class when you are resolving it's sub class. To solve that you can extract resoling of inherited properties in a separate method and call it from resolvingProperties block of sub classes definitions.

Alternatively you can utilise Resolvable protocol method resolveDependencies(_:DependencyContainer). This method will be called right after container creates an instance. With it if you need to resolve dependencies from super class just call this method on super as you usually do when overriding methods. Note that it is not required to register super class in the container if you are not going to resolve it.

container.register() { ConcreteService() }

class BaseService: Resolvable {
      
  func resolveDependencies(_ container: DependencyContainer) {
    //resolve base dependencies using container
  }
}
    
class ConcreteService: BaseService {

  override func resolveDependencies(_ container: DependencyContainer) {
    super.resolveDependencies(container)
    //resolve concrete service dependencies
  }
}

Currently Swift does not allow to override in extensions so you will have to implement resolveDependencies(_: DependencyContainer) in a subclass itself. Or you can define a dummy subclass, like ResolvableConcreteService in the composition root, implement resolveDependencies(_: DependencyContainer) in it and register it in the container instead of ConcreteService. This way you can avoid coupling of your code with Dip.

You can also use auto-injection to inject properties. Inherited auto-injected properties will be automatically resolved when resolving sub class. Note that container will resolve properties iterating inheritance chain from a sub class to its super class. So first it will resolve properties of sub class and then inherited properties from its super class and so on. That sequence corresponds to the order in which in Swift instance properties with initial values are initialised when you create a new instance of a sub class and the order of initializing subclass properties when overriding constructors.

Service locator anti-pattern

When using any DI container it is very easy to end up with Service Locator anti-pattern. Service locator is some service that you query for dependencies instead of creating them manually. It may seem that it is the same as DI container. And indeed it can be implemented the same way as DI container. The difference that makes it an anti-pattern is not the implementations, but how you use it. When you access DI container directly from you classes instead of passing dependencies with constructor, property or method injection - you are making it a service locator. Then it is just a replacement of direct constructor call. Instead you should access container directly only inside composition root.

Implementing composition root

TODO

Constructor over-injection

TODO

Separating configuration from usage

All the configuration should be done in composition root and only there. But composition root should not contain any other application logic. If you need to perform some actions using dependencies when they are resolved you can use Resolvable protocol or do the same by your own means (if you don't want to reference Dip outside composition root).

//MyViewController.swift
import Dip
extension MyViewController: Resolvable {
  func didResolveDependencies() {
    //do something with dependencies
  }
}

didResolveDependencies callback will be called on all resolved instances that conform to Resolvable protocol in the reverse order. That means that the last resolved instance will receive a callback first.