DEV Community

Cover image for Dependency Injection Patterns
Abdullah Althobetey
Abdullah Althobetey

Posted on • Updated on • Originally published at abdullahth.com

 

Dependency Injection Patterns

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 lets or guards 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)

An Animated Guide to Node.js Event Loop

Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

What happens under the hood when Node.js works on tasks such as database queries? We will explore it by following this piece of code step by step.