We have already agreed that Construction Injection is the best way to do DI, so why bother finding other methods?
Well, it is not always possible to define the constructor the way want. A notable example is doing DI with ViewControllers that are defined in storyboards. Given we have a BasketViewController that orchestrates the service and the store, we must pass them as properties:
class BasketViewController: UIViewController {
var service: BasketService?
var store: BasketStore?
// ...
}
This pattern is less elegant than the previous one:
- The ViewController isn't in the right state until all the properties are set
- Properties introduce mutability, and immutable classes are simpler and more efficient
- The properties must be defined as optional, leading to add question marks everywhere
- They are set by an external object, so they must be writeable and this could potentially permit something else to overwrite the value set at the beginning after a while
- There is no way to enforce the validity of the setup at compile-time
However, something can be done:
- The properties can be set as implicitly unwrapped optional and then required in viewDidLoad. This is as a static check, but at least they are checked at the first sensible opportunity, which is when the view controller has been loaded.
- A function setter of all the properties prevents us from partially defining the collaborator list.
The class BasketViewController must then be written as:
class BasketViewController: UIViewController {
private var service: BasketService!
private var store: BasketStore!
func set(service: BasketService, store: BasketStore) {
self.service = service
self.store = store
}
override func viewDidLoad() {
super.viewDidLoad()
precondition(service != nil, "BasketService required")
precondition(store != nil, "BasketStore required")
// ...
}
}
The Properties Injection permits us to have overridable properties with a default value. This can be useful in the case of testing.
Let's consider a dependency to a wrapper around the time:
class CheckoutViewController: UIViewController {
var time: Time = DefaultTime()
}
protocol Time {
func now() -> Date
}
struct DefaultTime: Time {
func now() -> Date {
return Date()
}
}
In the production code, we don't need to do anything, while in the testing code we can now inject a particular date instead of always return the current time. This would permit us of testing how the software will behave in the future, or in the past.
A dependency defined in the same module or framework is Local. When it comes from another module or framework, it's Foreign.
A Local dependency can be used as a default value, but a Foreign cannot, otherwise it would introduce a strong dependency between the modules.