+1

Dependency Injection (DI) trong IOS

1. Dạo đầu :3

Konnichiwa! Lại là mình đây 👋👋 Không biết mọi người có còn nhớ mình không nhỉ? Nếu quên rồi thì nhớ lại liền đi nha, vì hôm nay mình quay trở lại với một bài viết siêu hay ho đây!

Sau một thời gian "thai nghén" dài đằng đẵng (mãi mà chưa đẻ bài được 🤣), cuối cùng cũng có chút thời gian để ngồi xuống chia sẻ với mọi người về Dependency Injection (DI) trong iOS. Đây là một chủ đề mà mình đã muốn viết từ lâu lắm lắm rồi, nhưng cứ lần nữa mãi. Nay quyết tâm cầm bàn phím lên, không viết thì phí quá!

Bài viết này sẽ giúp anh em hiểu tổng quan về DI là gì, nó giúp ích thế nào trong iOS, và những thành phần quan trọng của nó. Ở các bài sau, mình sẽ đào sâu hơn từng phần một, đảm bảo anh em có thể áp dụng ngon lành.

Anh em đã sẵn sàng chưa? Bắt đầu thôi!

2. Giới thiệu về Dependency Injection (DI)

Dependency Injection (DI) là một kỹ thuật trong lập trình giúp quản lý dependencies của một đối tượng bằng cách cung cấp (inject) các dependencies từ bên ngoài thay vì để đối tượng tự khởi tạo. Điều này giúp code dễ bảo trì, dễ kiểm thử và giảm sự phụ thuộc chặt chẽ giữa các thành phần.

Nói dễ hiểu là cung cấp các đối tượng cần thiết cho một class thay vì để class tự tạo chúng

3. Inversion of Control (IoC) là gì?

Inversion of Control (IoC) là một nguyên tắc thiết kế giúp đảo ngược quyền kiểm soát việc khởi tạo và quản lý dependency của một đối tượng. Thay vì để đối tượng tự quyết định cách tạo ra và quản lý dependency của nó, IoC chuyển trách nhiệm này sang một thực thể khác (thường là container hoặc framework DI).

IoC giúp code linh hoạt và dễ mở rộng hơn bằng cách giảm sự phụ thuộc cứng nhắc giữa các thành phần.

4. Mối quan hệ giữa Dependency Injection và Inversion of Control

Dependency Injection là một trong những cách phổ biến để thực hiện IoC. IoC là một khái niệm rộng, còn DI là một kỹ thuật cụ thể giúp hiện thực hóa IoC.

Tiêu chí Dependency Injection (DI) Inversion of Control (IoC)
Định nghĩa Cung cấp dependency từ bên ngoài vào class Đảo ngược quyền kiểm soát khởi tạo dependency
Cách thực hiện Thông qua Constructor Injection, Property Injection, Method Injection, Thường thông qua DI, Service Locator, Framework DI (ví dụ: Swinject, Dagger)
Mục đích Giúp code dễ bảo trì, dễ kiểm thử hơn Tăng tính linh hoạt, giảm sự phụ thuộc chặt chẽ
Phạm vi Là một kỹ thuật cụ thể trong IoC Là nguyên tắc thiết kế tổng quát

5. Tại sao cần dùng Dependency Injection?

  • Dễ dàng mở rộng: Cho phép thay thế dependencies mà không cần thay đổi code bên trong đối tượng.
  • Dễ kiểm thử (Unit Testing): Có thể inject mock dependencies để kiểm thử mà không cần thay đổi logic chính.
  • Giảm sự phụ thuộc chặt chẽ (Loose Coupling): Giúp code dễ đọc và bảo trì hơn.
  • Tăng tính linh hoạt: Có thể thay đổi hoặc mở rộng chức năng mà không cần sửa đổi code gốc.

Để hiểu rõ hơn vì sao DI lại mang đến những lợi ích trên các bạn có thể tham khảo Loose Coupling và Tight Coupling trong iOS Development

6. Các loại Dependency Injection phổ biến trong iOS

  • Constructor Injection: Dependencies được truyền vào qua initializer khi đối tượng được khởi tạo.
  • Method Injection: Dependencies được gán thông qua setter methods sau khi đối tượng đã được khởi tạo.
  • Property Injection: Dependencies được gán trực tiếp vào properties của đối tượng.

6.1 Constructor Injection

Dependency được cung cấp thông qua constructor của đối tượng. Đây là phương pháp phổ biến nhất để thực hiện DI. Các dependencies cần thiết cho đối tượng được truyền vào khi đối tượng đó được khởi tạo.

Ưu điểm:

  • Đảm bảo dependencies luôn tồn tại
  • Rõ ràng và minh bạch nhất
  • Dễ dàng kiểm tra và mock các dependencies trong unit tests.

Nhược điểm:

  • Không thể thay đổi dependencies sau khi đối tượng đã được tạo.
  • Constructor có thể trở nên phức tạp nếu có nhiều dependencies
  • Không dùng được với Storyboard.
class DatabaseService {
    func fetchData() {
        print("Fetching data from database")
    }
}

class UserProfileViewController: UIViewController {
    private let databaseService: DatabaseService

    // Constructor Injection: Truyền dependency vào khi khởi tạo
    init(databaseService: DatabaseService) {
        self.databaseService = databaseService
        super.init(nibName: nil, bundle: nil)
    }

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

    func loadUserData() {
        databaseService.fetchData()
    }
}

// Sử dụng:
let databaseService = DatabaseService()
let viewController = UserProfileViewController(databaseService: databaseService)
viewController.loadUserData()

6.2 Method Injection (Truyền dependencies sau khi đối tượng đã được tạo)

Dependencies được truyền vào đối tượng sau khi nó đã được tạo, thông qua một setter method hoặc phương thức riêng.

Ưu điểm:

  • Linh hoạt hơn so với constructor injection, vì bạn có thể thay đổi dependencies sau khi đối tượng đã được tạo.

Nhược điểm:

  • Không đảm bảo rằng dependency sẽ có sẵn khi cần.
  • Thiếu an toàn (properties có thể là nil). Điều này có thể dẫn đến lỗi runtime nếu bạn quên cung cấp dependency trước khi sử dụng nó.
class UserProfileViewController: UIViewController {
    private var networkService: NetworkService?

    func setNetworkService(_ networkService: NetworkService) {
        self.networkService = networkService
    }

    func loadUserProfile() {
        networkService?.fetchData()
    }
}

6.3 Property Injection

Tương tự như Setter Injection nhưng với các thuộc tính (properties) thay vì setter methods. Đây là cách để inject dependencies vào các thuộc tính của đối tượng

Ưu điểm:

  • Linh hoạt hơn so với constructor injection, vì bạn có thể thay đổi dependencies sau khi đối tượng đã được tạo.

Nhược điểm:

  • Không đảm bảo rằng dependency sẽ có sẵn khi cần.
  • Thiếu an toàn (properties có thể là nil). Điều này có thể dẫn đến lỗi runtime nếu bạn quên cung cấp dependency trước khi sử dụng nó.
class UserProfileViewController: UIViewController {
    var databaseService: DatabaseService!

    func loadUserData() {
        databaseService.fetchData()
    }
}

// Sử dụng:
let databaseService = DatabaseService()
let viewController = UserProfileViewController()
viewController.databaseService = databaseService
viewController.loadUserData()

7. DI Container

DI Container không phải là một loại của Dependency Injection (DI), mà là một công cụ hỗ trợ việc thực hiện Dependency Injection.

Một số thư viện như Swinject hoặc Resolver có thể giúp bạn thực hiện DI container trong iOS.

Bản chất của DI Container:

  • Là một framework/công cụ quản lý dependencies
  • Hỗ trợ việc khởi tạo, quản lý và inject dependencies
  • Giúp giảm thiểu code boilerplate trong việc quản lý dependencies

Vai trò của DI Container:

  • Không phải là một loại DI mà là một cơ chế hỗ trợ DI
  • Giúp centralize việc quản lý dependencies
  • Cung cấp cơ chế đăng ký và resolve dependencies

Ưu điểm:

  • Tất cả dependencies được quản lý tập trung, giúp code dễ mở rộng và bảo trì hơn.
  • Bạn có thể tạo một cấu trúc DI phức tạp hơn mà không làm phức tạp code.

Nhược điểm:

  • Đôi khi container có thể làm mã trở nên khó đọc và khó hiểu nếu không được sử dụng đúng cách.
import Swinject

class AppContainer {
    let container = Container()

    init() {
        container.register(NetworkService.self) { _ in NetworkService() }
        container.register(UserProfileViewController.self) { r in
            let controller = UserProfileViewController()
            controller.networkService = r.resolve(NetworkService.self)
            return controller
        }
    }
}

// Sử dụng:
let container = AppContainer().container
let viewController = container.resolve(UserProfileViewController.self)
viewController?.loadUserData()

8. Tổng kết

Dependency Injection là một kỹ thuật quan trọng giúp code dễ bảo trì, kiểm thử và mở rộng hơn. DI là một phần của Inversion of Control (IoC), giúp giảm sự phụ thuộc chặt chẽ giữa các thành phần trong codebase. Tuỳ vào từng trường hợp, bạn có thể chọn phương pháp DI phù hợp nhất để áp dụng vào dự án iOS của mình!

Trong bài viết tiếp theo, chúng ta sẽ triển khai Dependency Injection bằng DI Container với Swinject. Nghe có vẻ nguy hiểm như một loại vắc-xin công nghệ, nhưng thực chất nó chỉ giúp chúng ta tiêm dependency một cách khoa học, tránh lây lan “hardcode” và nâng cao hệ miễn dịch cho codebase. Hãy chuẩn bị tinh thần vì chúng ta sắp bước vào một thế giới nơi new bị cấm cửa, còn resolve() trở thành chân lý!

Nếu có sai sót gì, mong anh em góp ý nhiệt tình ở phần comment nhé! Mọi ý kiến của anh em đều là buff kinh nghiệm cho mình, nên đừng ngại suggest để bài viết ngày càng chất lượng hơn nha!

Bài viết có tham khảo từ: https://fxstudio.dev/dependency-injection-trong-10-phut/#Dependency_injection_Container


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.