Dependency Injection: It’s more powerful than you think.

How are you using dependency injection ?

Support my work by reading this article from my personal blog here:

Today we’re going to learn about dependency injection (DI) through a case study I recently faced at work and I think you guys will find familiar with it.

In case you aren’t familiar with dependency injection concept. You can read another post of mine here:

Case study

I have an application that allows logged in user to scan an identity card using NFC technology and then navigate to the detail screen to show the card’s information. Behind the sense after user scan their card, I’ll call an API to check if the card is registered in the database or not. Base on that result and the user’s role the detail screen will show differently as shown bellow:

The card’s information at the top of the screen is exactly the same in all cases but the button at the bottom of the screen is different. As you can see we have three cases I can assume as follow:

  • If you are a regular user you can simply back to the app’s main screen regardless the card is registered or not.
  • If you logged in as an admin and the card is not registered. You can register the card.
  • If you are an admin and the card is already registered. You can report it for some reasons in order to delete the card from database.

OK so it’s our problem. If you were me, how would you solve this ? Stop here and try to find a solution before continue. In the next section, I’m going to show you a bad solution (and the most common solution) I’ve seen in many codebases.

Bad solution

The bad solution is using if-else statements inside the detail screen to switch between cases. You’ll see something like this:

class CardDetailViewController: UIViewController {

@IBOutlet var footerButton: UIButton!
var cardModel: CardModel!
override func viewDidLoad() {
super.viewDidLoad()

if UserManager.shared.isAdmin {
if cardModel.isRegistered {
footerButton.setTitle("Report", for: .normal)
footerButton.backgroundColor = UIColor.systemRed
footerButton.setTitleColor(.white, for: .normal)
footerButton.addTarget(self, action: #selector(reportCard), for: .touchUpInside)
} else {
footerButton.setTitle("Register card", for: .normal)
footerButton.setTitleColor(.white, for: .normal)
footerButton.backgroundColor = UIColor.systemGreen
footerButton.addTarget(self, action: #selector(registerCard), for: .touchUpInside)
}
} else {
footerButton.setTitle("Back to main", for: .normal)
footerButton.backgroundColor = UIColor.clear
footerButton.addTarget(self, action: #selector(backToMain), for: .touchUpInside)
}
}

@objc func reportCard() {
// navigate to report card screen
}

@objc func registerCard() {
// navigate to register card screen
}

@objc func backToMain() {
// back to main screen
}
}

Why is that solution bad ?

  1. When view controller responsible for displaying views. It’s not responsible for checking user’s role so by doing as above you’re making the view controller doing a thing it’s not responsible for which violates single responsibility principle.
  2. What if I my application getting more complicated with more roles ? Then the more role added the more if-else case I need to add to the CardDetailViewController and then it’ll quickly become overwhelmed. Also it violates another principle that is open-closed principle.

I think that two main reason is enough for us to find a better solution we need a way to move that logic away from CardDetailViewController and I think the best way and easiest way to do it is using dependency injection. So let’s try to apply it.

Dependency Injection

If you’re familiar with DI concept. You’ve known that it’s a concept that we manage/inject dependencies from outside its client. But sometimes we mistakenly think dependency is something should be big, it should be a class that we created or it should be a module. But it is not. Dependency is everything your client needs from its outside in order to do some operations so you you have a function that need an url of type URL:

func functionA(url: URL) {}

or another function needs an id of type Integer:

func functionA(id: Int) {}

Many many more. URL, Int,… all of those are considered as dependencies.

So apply to our case study. Instead of let the view controller decide how the button look like and what action when tap on the button. We’ll inject it.

First, I want to encapsulate variants of the button into a module could simply a struct like this:

struct ButtonAttributes {
let title: String
let font: UIFont
let backgroundColor: UIColor
let titleColor: UIColor
let action: () -> Void
}

Second, I inject dependencies to CardDetailViewController:

class CardDetailViewController: UIViewController {

@IBOutlet var footerButton: UIButton!
var cardModel: CardModel!
var footerButtonAttribute: ButtonAttributes!
convenience init(model: CardModel, footerButtonAttribute: ButtonAttributes) {
self.init()
self.cardModel = model
self.footerButtonAttribute = footerButtonAttribute
}
}

And finally, remove the if-else logic from CardDetailViewController and now the view controller just display the way it is given to:

class CardDetailViewController: UIViewController {
....
override func viewDidLoad() {
super.viewDidLoad()

footerButton.setTitle(footerButtonAttribute.title, for: .normal)
footerButton.backgroundColor = footerButtonAttribute.backgroundColor
footerButton.setTitleColor(footerButtonAttribute.titleColor, for: .normal)
footerButton.titleLabel?.font = footerButtonAttribute.font
footerButton.addTarget(self, action: #selector(footerButtonTapped), for: .touchUpInside)
}
@objc func footerButtonTapped() {
footerButtonAttribute.action()
}
}

And then we can initiate CardDetailViewController by compose its dependency from outside like this ✅

var detailViewController: CardDetailViewController!
if UserManager.shared.isAdmin {
if cardModel.isRegistered {
let attributes = ButtonAttributes(title: "Back to main",
font: .systemFont(ofSize: 17),
backgroundColor: .clear,
titleColor: .systemBlue,
action: {
navigationController.popToRoot(animated: true)
})
detailViewController = CardDetailViewController(model: model, footerButtonAttribute: attributes)
} else {
....
}
} else {
...
}
navigationController.pushViewController(detailViewController, animated: true)

Conclusion

So that’s it. Dependency injection is one of the the most powerful tool I’ve learn so far. So if you aren’t know about it then learn it. If you’ve learn it, practice it more.
Finally I hope you’ve got something for yourself from this post. If you like it don’t forget to share and claps to promote my work, if you have any questions relate to this topic feel free to comment down bellow.

Thank you, don’t forget to practice on your own. You can visit my personal blog to see more posts from me:

https://learnwithtung.co/

See you in the next post.

Passionate about writing good software. Contact me: 📮tungvuduc2805@gmail.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store