Abdullah Althobetey
Posted on February 16, 2022
In this post, I will explain dependency injection patterns with examples written in Swift.
Dependency injection (DI for short) is a software engineering technique where an object, method, or function receives other objects or functions that it needs.
Unfortunately, many developers often overlook DI (I certainly did in the past) despite their incredible benefits, especially when your project grows.
Why dependency injection
For me, the most significant benefit of DI is control. With proper DI, you will have more control over your program. Without DI, and as your program grows, your program will probably control you more. We've all been there; think of that legacy library or framework you couldn't replace because it's used all over the place.
For more about DI benefits, see my previous post. For now, let's focus on DI patterns in Swift.
Initializer injection
Initializer injections is the act of defining the required dependencies as parameters to the type's initializer. It can be applied to a class, struct, or enum, and the dependencies can be other classes, structs, enums, or functions.
For example:
class Profile {
private let user: User
init(user: User) {
self.user = user
}
}
Initializer injection is the most important pattern in DI, and it should be your go-to pattern unless required (see other patterns below).
In the code example above, it doesn't make sense for a Profile
to exists without a User
, so if you want to use the Profile
class, you have to supply a User
when you create it.
This will save your code from optionals mayhem. If we define user
as an optional inside Profile
, we will have a lot of if let
s or guard
s in all over the place in the Profile
class.
Method/function injection
Like initializer injection, in method/function injection, you supply the required dependencies as parameters to the method or function.
The reason to use method/function injection instead of initializer injection is that the required dependencies may vary with each method/function call.
For example, let's say in Profile, we can redeem some kind of coupon for user, and the user can redeem any number of coupons. So the dependency (coupon) can vary at each call to the redeem service:
class Profile {
private let user: User
private let couponRedemption: CouponRedemption
init(user: User, couponRedemption: CouponRedemption) {
self.user = user
self.couponRedemption = couponRedemption
}
func redeem(coupon: Coupon) {
couponRedemption.redeem(coupon)
}
}
To do the actual redemption, we need to call some sort of a service object. In this case, it's CouponRedemption
, which is another excellent example of initializer injection!
Property injection
Property injection can be applied when a type exposes a writeable property so that callers can replace the property value with another one.
Property injection is often used when a type has a good default dependency. But beware that the default dependency should be what it's often called a stable dependency and not a volatile one. If the dependency is volatile, you should use the initializer injection instead. I'll save the explanation about stable dependency versus volatile dependency in another post.
Another thing to be aware of is the default dependency should originate from the same module or layer. Because if you use a dependency from another module, you will unnecessarily couple the two modules together. In that case, initializer injection is the way to go. Again I will hopefully write about this in another post. For now, let's focus on DI patterns.
As an example of a property injection, let's say in profile, we want the user to select a theme for the application. Of course, we will have a default theme for the app. It's not wise to tell the user to choose a theme the first time they open our app!
enum Theme {
case darkOrange
case liteOrange
case gray
case blackAndWhite
}
class Profile {
private let user: User
private let couponRedemption: CouponRedemption
var theme: Theme = .darkOrange
init(user: User, couponRedemption: CouponRedemption) {
self.user = user
self.couponRedemption = couponRedemption
}
func redeem(coupon: Coupon) {
couponRedemption.redeem(coupon)
}
}
Conclusion
As a good tip, always prefer initializer injection unless required. If the dependency your type requires will vary through method/function calls, then use method/function injection. If you have a good default dependency that is stable and originate in the same module or layer, use property injection.
Posted on February 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.