Architecture Swift

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

Hello, how are you doing ? I hope you’re doing fantastic 🤗 .

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:

Dependency Injection Explained: Dependency

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:

Example wireframes

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.
  3. The testability of this approach is to zero. We cannot test the logic from outside of the view controller.

I think that three main reasons are 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 ✅ All the problems above are gone now. We removed the hidden logic away from the view controller, we’re also enable testability because we can inject a test button attributes for the view controller.

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 it, 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. See you in the next post.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *