Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $19.99/month. Cancel anytime
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.
Method Injection
This pattern just passes a collaborator in the method:
class BasketClient {
func add(product: Product, to store: BasketStore) {
store.add(product: product)
calculateAppliedDiscount()
/...
}
/ ...
private func calculateAppliedDiscount() {
/ ...
}
}
This is useful when the object has several collaborators, but most of them are just temporary and it isn't worth having the relationship set up for the whole life cycle of the object.
Ambient Context
The final pattern, Ambient Context, is similar to the Singleton.
We still have a single instance as a static variable, but the class has multiple subclasses with different behaviors, and each static variable is writeable with a static function:
class Analytics {
static private(set) var instance: Analytics = NoAnalytics()
static func setAnaylics(analitics: Analytics) {
self.instance = analitics
}
func track(event: Event) {
fatalError("Implement in a subclass")
}
}
class NoAnalytics: Analytics {
override func track(event: Event) {}
}
class GoogleAnalytics: Analytics {
override func track(event: Event) {
/...
}
}
class AdobeAnalytics: Analytics {
override func track(event: Event) {
/...
}
}
struct Event {
/...
}
This pattern should be used only for universal dependencies, representing some cross-cutting concerns, such as analytics, logging, and times and dates.
This pattern has some advantages. The dependencies are always accessible and don't need to change the API.
It works well for cross-cutting concerns, but it doesn't fit in other cases when the object isn't unique.
Also, it makes the dependency implicit and it represents a global mutable state that sometimes can lead to issues that are difficult to debug.
DI anti-patterns
When we try to implement a new technique, it is quite easy to lose control and implement it in the wrong way. Let's see then the most common anti-patterns in Dependency Injection.
Control Freak
The first one is pretty easy to spot: we are not using the Injection at all. Instead of being Injected, the dependency is instantiated inside the object that depends on it:
class FeaturedProductsController {
private let restProductsService: ProductsService
init() {
self.restProductsService = RestProductsService(configuration: Configuration.loadFromBundleId())
}
}
In this example, ProductsService could have been injected in the constructor but it is instantiated there instead.
Mark Seeman, in his book Dependency Injection in .NET, Chapter 5.1 - DI anti-patterns, calls it Control Freak because it describes a class that will not relinquish its dependencies.
The Control Freak is the dominant DI anti-pattern and it happens every time a class directly instantiates its dependencies, instead of relying on the Inversion of Control for that. In the case of the example, even though the rest of the class is programmed against an interface, there is no way of changing the actual implementation of ProductsService and the type of concrete class that it is, it will always be RestProductsService. The only way to change it is to modify the code and compile it again, but with DI it should be possible to change the behavior at runtime.
Sometimes, someone tries to fix the Control Freak anti-pattern using the factory pattern, but the reality is that the only way to fix it is to apply the Inversion of Control for the dependency and inject it in the constructor:
class FeaturedProductsController {
private let productsService: ProductsService
init(service: ProductsService) {
self.productsService = service
}
}
As already mentioned, Control Freak is the most common DI anti-pattern; pay particular attention so you don't slip into its trap.
Bastard Injection
Constructor overloads are fairly common in Swift codebases, but these could lead to the Bastard Injection anti-pattern. A common scenario is when we have a constructor that lets us inject a Test Double, but it also has a default parameter in the constructor:
class TodosService {
let repository: TodosRepository
init(repository: TodosRepository = SqlLiteTodosRepository()) {
self.repository = repository
}
}
The biggest problem here is when the default implementation is a Foreign dependency, which is a class defined using another module; this creates a strong relationship between the two modules, making it impossible to reuse the class without including the dependent module too.
The reason someone is tempted to write a default implementation it is pretty obvious since it is an easy way to instantiate the class just with TodoService() without the need of Composition Root or something similar. However, this nullifies the benefits of DI and it should be avoided removing the default implementation and injecting the dependency.
Service Locator
The final anti-pattern that we will explore is the most dangerous one: the Service Locator.
It's funny because this is often considered a good pattern and is widely used, even in the famous Spring framework.
Originally, the Service Locator pattern was defined in Microsoft patterns & practices' Enterprise Library, as Mark Seeman writes in his book Dependency Injection in .NET, Chapter 5.4 - Service Locator, but now he is advocating strongly against it. Service Locator is a common name for a service that we can query for different objects that were previously registered in it. As mentioned, it is a tricky one because it makes everything seem OK, but in fact, it nullifies all the advantage of the Dependency Injection:
let locator = ServiceLocator.instance
locator.register( SqlLiteTodosRepository(),
forType: TodosRepository.self)
class TodosService {
private let repository: TodosRepository
init() {
let locator = ServiceLocator.instance
self.repository = locator.resolve(TodosRepository.self)
}
}
Here we have a service locator as a singleton, to whom we register the classes we want to resolve. Instead of injecting the class into the constructor, we just query from the service. It looks like the Service Locator has all the advantages of Dependency Injection, it provides testability and extensibility since we can use different implementations without changing the client. It also enables parallel development and separated configuration from the usage.
But it has some major disadvantages. With DI, the dependencies are explicit; it's enough to look at the signature of the constructor or the exposed properties to understand what the dependencies for a class are. With a Service Locator, these dependencies are implicit, and the only way to find them is to inspect the implementation, which breaks the encapsulation.
Also, all the classes are depending on the Service Locator and this makes the code tightly coupled with it. If we want to reuse a class, other then that class, we also need to add the Service Locator in our project, which could be in a different module and then adding the whole module as dependency where we wanted just to use one class. Service Locator could also give us the impression that we are not using DI at all because all the dependencies are hidden inside the classes.
In this article, we covered the different flavors of dependency injection and examines how each can solve a particular set of problems in real-world scenarios.
If you found this post useful, do check out the book, Hands-On Design Patterns with Swift. From learning about the most sought-after design patterns to comprehensive coverage of architectural patterns and code testing, this book is all you need to write clean, reusable code in Swift.
Implementing Dependency Injection in Google Guice [Tutorial]
Implementing Dependency Injection in Spring [Tutorial]
Dagger 2.17, a dependency injection framework for Java and Android, is now out!