Dependency Injection trong iOS

by Hoang Anh Tuan
1.2K views

Trong lập trình OOP hay POP, thì luôn dc recommend code 1 cách tách biệt hệ thống thành những module sao cho chúng liên kết với nhau 1 cách lỏng lẻo, để những module đó có thể hoạt động 1 cách độc lập nhiều nhất có thể.
Nếu các module liên kết quá chặt chẽ với nhau thì khi hệ thống cần nâng cấp, phát triển thì sẽ gặp nhiều vấn đề, ngoài ra việc maintain sau này hay việc viết unit test cũng gặp nhiều khó khăn.
-> Dependency Injection (DI) là 1 design pattern để ngăn chặn sự phụ thuộc chặt chẽ này.

Content

  • Ý tưởng của DI
  • Implement DI trong swift

Ý tưởng của DI:

Chữ D trong SOLID, là Dependency Inversion Principle. Nội dung của nguyên lý này được tóm gọn lại như sau:

  • Các module cấp cao không nên phụ thuộc vào các module cấp thấp.
  • Interface không nên phụ thuộc vào chi tiết, mà ngược lại.
  • Các class giao tiếp với nhau thông qua interface, chứ không phải implementation.

Note:

  • Dependency Injection là 1 design pattern để code có thể tuân thủ nguyên lý Dependency Inversion.
  • DI là 1 kỹ thuật cho phép xóa bỏ sự phụ thuộc chặt chẽ giữa các module, làm cho ứng dụng dễ dàng hơn trong việc maintain, extend, debug và test bởi khi đó các module hoạt động độc lập.

Implement DI trong swift:

Trước hết hãy đến 1 ví dụ không sử dụng DI:

Vì k sử dụng DI, ví dụ ở trên sẽ có những hạn chế như sau:
Class Car bị phụ thuộc vào NormalEngine:

  • Trong trường hợp muốn nâng cấp bằng cách thay vào FastEngine, hay nhiều loại Engine khác thì sẽ gặp nhiều vấn đề,
  • Khi có thay đổi trong class NormalEngine, thì có thể sẽ ảnh hưởng trực tiếp đến class Car.
    Ex: Thêm nhiều thuộc tính cho NormalEngine thì constructor sẽ thay đổi.
  • Khó khăn trong việc viết unit test.

-> Chúng ta sẽ áp dụng DI Design pattern để giải quyết điều này.

Constructor Injection

// 1
protocol Engine {
    func start()
}

// 2
class NormalEngine: Engine {
    func start() {
        print("Normal engine start running...")
    }
}

class FastEngine: Engine {
    func start() {
        print("Fast engine start running...")
    }
}

class Car {
    // 3
    var engine: Engine
    init(engine: Engine) {
        self.engine = engine
    }

    func run() {
        engine.start()
    }
}

// 4
let car = Car(engine: NormalEngine())
car.run()
car.engine = FastEngine()
car.run()
  1. Tạo 1 protocol(interface) để giao tiếp với class thay vì giao tiếp bằng implementation (nguyên lý Dependency Inversion).
  2. Các class NormalEngine, FastEngine implement protocol đó
  3. Khai báo 1 biến kiểu Engine trong class Car
  4. Khi khởi tạo class Car, chúng ta sẽ truyền 1 engine cụ thể vào thông qua constructor.
Ưu điểmNhược điểm
– Tính đóng gói cao
– Chắc chắn rằng đối tượng đã được khởi tạo
– Nếu giao tiếp với nhiều protocol thì sẽ làm cho constructor của class đó nhiều lên.
– Đối với các ViewController, đặc biệt là ViewController được define trong storyboard thì sẽ không có hàm khởi tạo.

Property Injection

Property Injection được dùng để tránh nhược điểm của Constructor Injection,bằng cách không khởi tạo các dependencies trong constructor mà khởi tạo bằng cách set value cho property.

class Car {
    var engine: Engine!

    func run() {
        engine.start()
    }
}

let car = Car()
car.engine = NormalEngine()

Ưu điểm:

  • Cho phép set dependencies vào thời điểm bạn muốn.

Nhược điểm:

  • Tính đóng gói thấp.
  • Vì các dependency được khai báo theo kiểu !, nên nếu quên không set dependency trước khi sử dụng thì dễ gây ra crash; còn nếu khai báo dependency theo kiểu optional thì mỗi lần sử dụng phải unwrap.

Interface Injection

  • Ở đây bạn tạo ra 1 hàm setter để set dependency.
protocol HasEngine {
    func setEngine(dependency: Engine)
}

class Car: HasEngine {
    var engine: Engine!

    func setEngine(dependency: Engine) {
        self.engine = dependency
    }

    func run() {
        engine.start()
    }
}

let car = Car()
car.setEngine(dependency: FastEngine())

Method Injection

  • Nếu bạn chỉ cần sử dụng dependency để dùng 1 lần thì bạn không cần lưuu lại như là 1 biến. Bạn chỉ đơn giản là pass dependency như là 1 method param.
class Car {
    func run(engine: Engine) {
        engine.start()
    }
}
let car = Car()
car.run(engine: NormalEngine())

Ps: Thật ra ví dụ này không phù hợp thực tế lắm bởi xe nào cũng cần động cơ :)) Nhưng để hiểu được thì thế là đủ hiểu r 😉

Ambition Context:

  • Đây là 1 cách tránh DI nhưng chỉ nên được sử dụng cho các global dependency mà được chia sẻ với nhiều đối tượng khác.
  • Tuy nhiên bạn nên hạn chế sử dụng cách này, bởi nếu không quản lí đọc ghi 1 biến dùng chung 1 cách cẩn thận thì sẽ dễ dẫn tới Race Condition.
class Car {
    static var engine: Engine = NormalEngine()

    func run() {
        Car.engine.start()
    }
}

let car = Car()
car.run()

Ưu điểm:

  • Dependency được sử dụng global.
  • Vẫn có thể thay đổi dependency trong quá trình sử dụng.

Nhược điểm:

  • Tính đóng gói thấp.
  • Yêu cầu Thread safe.

Kết luận:

  • Dependency Injection là 1 kỹ thuật mạnh mẽ để giúp viết code 1 cách clean và dễ maintain.
  • Ngoài ra có thể sử dụng các design pattern khác để tránh dependency như Factory, Service Locator hay Dependency Injection Container.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.