Skip to content

Tips & Tricks

Ilya Puchka edited this page Aug 26, 2016 · 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, 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(tag: "api") as APIClient

This will perfectly fit 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

###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:))

###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.