Dependency Injection

Dependency Injection là gì ?

Dependency Injection (DI) dịch ra tiếng Việt có nghĩa là "chèn Dependency". DI là kỹ thuật sử dụng các Dependencies bằng cách chèn chúng vào module/ class. Kỹ thuật chèn ở đây chính là việc chúng ta passing các denpendency.

Ví dụ:

class UserListViewController: UIViewController {
    var userService: UserService!
    var dbService: DatabaseService!

    override func viewDidLoad() {
        // initializer
        userService = UserService()
    }

    // injection
    func setDatabaseService(dbService: DatabaseService) {
        self.dbService = dbService
    }
}

Ở trên có 2 dependency là userService, và dbService, trong đó userService được sử dụng bằng cách khởi tạo trực tiếp bên trong UserListViewController, còn dbService được passing qua hàm setDatabaseService. Ở đây ta nói, ta đang sử dụng Dependency Injection với dbService

Nhưng tại sao lại cần phải passing/inject dbService ? Tại sao không làm như userService? Nó có đem lại lợi ích gì hơn không ?

Phân tích kĩ thuật Dependency Injection:

So sánh 2 cách sử dụng dependency trên, rõ ràng ở trường hợp của userService, chúng ta đã vô tình kết dính userService vào UserListViewController, điều này tối kị trong việc thiết kế code, vì nó sẽ làm giảm khả nằng maintain, cũng như gây khó khăn khi sửa đổi source code. Ví dụ, UserService thay đổi constructor của nó:

class UserService {

    init(with identifier: String) {

    }

}

class UserListViewController: UIViewController {
    var userService: UserService!
    var dbService: DatabaseService!

    override func viewDidLoad() {
        // old initializer
//        userService = userService()

        // new initializer
        userService = UserService(with: "UserService")
    }

    // injection
    func setDatabaseService(dbService: DatabaseService) {
        self.dbService = dbService
    }
}

Ta phải thay đổi code trong viewDidLoad của UserListViewController tương ứng. Rõ ràng, thay đổi từ phía denpency buộc module sử dụng nó phải thay đổi theo, compile lại, test lại.

Với bài toán thay đổi DatabaseService thì sao?

class DatabaseService {
    init(with identifier: String) {

    }
    init(with property: [String: Any]) {

    }
}

class UserListViewController: UIViewController {
    var userService: UserService!
    var dbService: DatabaseService!

    override func viewDidLoad() {
        // old initializer
        //        userService = userService()

        // new initializer
        userService = UserService(with: "UserService")
    }

    // injection
    func setDatabaseService(dbService: DatabaseService) {
        self.dbService = dbService
    }
}

Chẳng có gì thay đổi ở phía UserListViewController cả, bởi vì cái mà UserListViewController cần từ DatabaseService chỉ là 1 instance của nó, để có thể dùng instance đó thực hiện các logic và xử lý nó cần. Đứng trên quan điểm của UserListViewController: này anh DatabaseService, tôi là UserListViewController, tôi cần xử lý tác vụ về Database, do đó tôi mượn anh để thực hiện, tôi và anh là 2 người riêng biệt, do đó tôi không muốn quản lý hay liên quan gì đến nội tại hoạt động của anh, cái tôi cần là tôi giao anh 1 việc, anh trả tôi kết quả.

Làm thế nào để Inject Dependency ?

Có 3 cách cơ bản để có thể thực hiện injection: constructor injection, setter injection và interface injection

Setter Injection:

Đối với các ngôn ngữ OOP nói chung, thông thường để đảm bảo tính đóng gói và bảo mật, các property thường được gán private, và truy cập trong thông qua cặp method getter/setter. Tuy nhiên, Swift không vậy, getter và setter của swift có thể được viết theo dạng closure, do đó, nếu muốn thực hiện inject thông qua setter, bạn buộc phải thực hiện theo template của các ngôn ngữ OOP khác:

class UserListViewController: UIViewController {
    var userService: UserService!
    var dbService: DatabaseService!

    // injection
    func setDatabaseService(dbService: DatabaseService) {
        self.dbService = dbService
    }

    func setUserService(userService: UserService) {
        self.userService = userService
    }
}

Constructor Injection:

Tương tự như setter Injection, phương pháp này sử dụng các hàm khởi tạo để thực hiện inject:

class UserListViewController: UIViewController {
    let userService: UserService
    let dbService: DatabaseService

    init(userService: UserService, dbService: DatabaseService) {
        self.userService = userService
        self.dbService = dbService
        super.init(nibName: "UserListViewController", bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Constructor Injection cũng khá phổ biến, tuy nhiên nhược điểm của nó là:

  • Dễ thay đổi khi constructor thay đổi, và việc thay đổi constructor trong giai đoạn thiết kế và phát triển là điều bình thường
  • Khi có quá nhiều dependency, hàm dễ trở nên dài và cồng kềnh.
  • Không tối ưu và hoạt động tốt với lập trình iOS.

Interface Injection:

Interface Injection, nghĩa là bạn inject dependency của các bạn thông qua Interface:

protocol Injectable {
    var userService: UserService! { get set }
    var dbService: DatabaseService! { get set }
}

extension Injectable {
    mutating func inject(userService: UserService, dbService: DatabaseService) {
        self.userService = userService
        self.dbService = dbService
    }
}

class UserListViewController: UIViewController, Injectable {
    var userService: UserService!
    var dbService: DatabaseService!
}

var userVC: Injectable = UserListViewController()
userVC.inject(userService: UserService(), dbService: DatabaseService())