Skip to content

Registering and resolving components

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

Creating a container

First you need to create an instance of the container and store a reference to it somewhere (most likely in your AppDelegate). When creating container you can use optional configuration block to concentrate all registrations inside it (all registrations should be performed at once easy in application lifecycle).

let container: DependencyContainer = {
  let container = DependencyContainer()
  ...
  return container
}()

Optionally you can pass configuration closure to container's constructor (see Block-based container initialization)

Registering components

To register component in the container use register method. You need to provide a factory that creates the instance, scope of that component and optionally - tag to associate this definition with (see Named definitions)

let container = DependencyContainer()
container.register(.unique) { ServiceImp() as Service }

Here we register ServiceImp as implementation of Service protocol. This component will use Unique scope which means that each instance of ServiceImp will be unique (see scopes). It is a default scope.

See also: Registering using initializers/factory methods

Constructor injection

If you use constructor injection you can use container to resolve dependencies right inside factory block:

container.register() { try ServiceImp(repository: container.resolve()) as Service}

If your component depends on some runtime value which is unknown at the moment of registration you can register factory that accepts it as a parameter (see Runtime arguments):

container.register() { (url: String) in ServiceImp(url: url) as Service }

If all the constructor dependencies are also registered in the container you can use auto-wiring.

Property injection

register method will return a "definition" which describes how the component should be created. You can not do much with that object but optionally you can provide it a block that will be called after instance is created by container. In this block you can perform property injection. You can set this block only once per definition.

container.register(.unique) { ServiceImp() as Service }
  .resolvingProperties { container, service in 
    service.repository = try container.resolve() as ServiceRepository
}

You can also do the same right within a factory block:

container.register(.unique) { () -> Service in
  let service = ServiceImp()
  service.repository = try container.resolve() as ServiceRepository
  return service
}

See also: Auto-injection

Method injection

When component is resolved you can call any method on it providing its dependency resolved by the container. You can do that either in factory block or resolvingProperties block.

container.register(.unique) { ServiceImp() as Service }
  .resolvingProperties { container, service in 
    service.repository = try container.resolve() as ServiceRepository
    service.doSomething(withSomething: container.resolve())
}

It is not the same as what "method injection" means in context of Dependency Injection.

Resolving components

To resolve a type use resolve method:

let service = try! container.resolve() as Service

Before container can serve you dependencies they all should be registered in the container. If you try to resolve a type that was not registered in the container it will throw an error DipError.DefinitionNotFound:

let container = DependencyContainer()
let service = try! container.resolve() as Service

//fatal error: 'try!' expression unexpectedly raised an error: No definition registered for type: (Service in _5289435687F974F24C6B5A402DF2A59E), factory: () throws -> (Service in _5289435687F974F24C6B5A402DF2A59E), tag: nil.
//Check the tag, type you try to resolve, number, order and types of runtime arguments passed to `resolve()` and match them with registered factories for type (Service in _5289435687F974F24C6B5A402DF2A59E).: file ...

You can use weakly typed resolve methods if a type that you want to resolve is unknown at compile time:

func resolveType(type: Any.Type) -> Any? {
  return try? container.resolve(type)
}

Resetting container

Sometimes in tests you may need to register some mock implementation of the dependency just for one test. Then in tearDown method you can reset the container and configure it again in setUp method. You can also remove or add individual definitions. Use these methods only in tests, not in production code.

Note: it makes more sense to use this in integration tests when you test several real components together but need to temporary replace one or few of them with mocks. In unit tests you will not need this as you can provide dependencies manually using constructor/property/method injection without even touching container.

Bootstrapping

You can "bootstrap" your container configuration preventing it from any changes. After you call bootstrap method you will not be able to add new or remove definitions from the container, you will only be able to completely reset it. Also when bootstrapped container will instantiate components registered with EagerSingleton scope.

Thread safety

DependencyContainer is thread safe, you can register and resolve components from different threads. Still we encourage you to register components in the main thread early in the application lifecycle to prevent race conditions when you try to resolve component from one thread while it was not yet registered in container by another thread.

Errors

The resolve operation has a potential to fail because you can use the wrong type, factory or a wrong tag. For that reason Dip throws a DipError if it fails to resolve a type. Thus when calling resolve you need to use a try operator.

There are very rare use cases when your application can recover from this kind of error. In most of the cases you can use try! to cause an exception at runtime if error was thrown or try? if a dependency is optional. This way try! serves as an additional mark for developers that resolution can fail. Dip also provides helpful descriptions for errors that can occur when you call resolve.