Skip to main content

Dependency Injection in Swift


Bài viết gốc: https://medium.com/makingtuenti/dependency-injection-in-swift-part-1-236fddad144a

Khi bạn đang phát triển ứng dụng sử dụng mô hình (paradigm) OOP, bạn sẽ nhận ra (realize) rằng, nếu bạn muốn khả năng kiểm thử phần mềm, bạn cần sử dụng nguyên tắc(principle) IoC
IoC - Inversion of Control: là nguyên lý phát triển phần mềm rất trừu tượng khó hiểu :((
Thông thường, kiến trúc này, chúng ta sử dụng mẫu dependency ịnection, loại sẽ cung cấp mỗi class dependencies nó cần.
Trong bài viết này, tôi sẽ chỉ ra một con đường sử dụng pattern này trong Swift.

Nếu như bạn không quan tâm về việc testing ứng dụng của bạn, tôi sẽ giới thiệu bạn ứng dụng nguyên tắc này trong code của bạn, bạn sẽ thấy nó hữu ích trong những dự án lớn. 

Using default values in constructors
Trong Swift, chúng ta có thể implement một ịnjection ngu ngốc bằng cách sử dụng giá trị default trong constructor.


class ContactsRepository {
    let contactsDataSource: ContactsDataSource

    init(contactsDataSource: ContactsDataSource = NetworkContactsDataSource()) {
 self.contactsDataSource = contactsDataSource
    }
}
 
Easy, right? Ban đầu nó giống như một ý tưởng tốt bởi vì nó cung cấp khả năng kiểm thử và nó dễ dàng để sử dụng, nhưng nó thực sự có một vài vấn đề. Vấn đề chính là phương pháp này là nó liên kết chặt chẽ (tightly coupled) với default constructor của dependencies của bạn. Hãy tưởng tượng (Imagine) rằng bạn muốn việt một UI test, cái mà
bạn phải sử dụng một double test để mô phỏng một HTTP response. Trong trường hợp này, bạn sẽ cần cung cấp tất cả dependencies của UIViewController này, bởi vì bạn có thể không sử dụng default constructors lần nào nữa. Nó thật lãng phí (waste) thời gian, phải không ?

Dip & Swinject
Do vấn đề đã thảo luận ở trên, tôi quyết định dùng thử một vài lựa chọn thay thế như Dip hoặc Swinject. Hai frameworks này thực ra là giống nhau. Chúng là nơi chứa các dependency, cái mà bạn có thể đăng ký để sử dụng với một lambda. Tại hight-level, việc mà bạn cần làm là lưu trữ những lambdas đó trong một dictionary và sử dụng nó khi bạn cần khởi tạo một service. Vì thế để giải quyết vấn đề, bạn chỉ cần ghi đè bất cứ thứ gì bạn muốn để thay thế với kiểm thử kép (replace with the test double)

Tuy nhiên, những frameworks này có một số nhược điểm như là :
. Đồ hình dependency được chạy trên runtime, vì thế nếu bạn quên đăng ký cách giải quyết một dependency, app sẽ crash.
 . Khi mà bạn cần đăng ký dependencies trước khi sử dụng chúng, việc mở app sẽ chậm hơn.
. Bạn cần sử dụng unwrap (Swinject) hoặc try (Dip) khi giải quyết dependencies.
. Và điều tồi tệ nhất là theo ý của tôi, nếu bạn cần truyền đối số trong runtime, nó sẽ không tự động hoàn tất. Sẽ rất dễ dàng để xảy ra lỗi, một thay đổi đơn giản thứ tự dependencies sẽ làm cho app crash.

Bởi vì điều này, tôi quyết định không sử dụng chúng và tìm một giải pháp phù hợp cho tôi mà không làm ảnh hưởng hiệu suất và không làm mất tính bảo mật.
Để cấu trúc nó, giải pháp là sử dụng một vaì tính năng của Swift như là inference và protocol extension. Chúng ta sẽ xem một ví dụ đơn giản.

Tưởng tượng có struct đại diện cho superheroes:


struct SuperHero {
 let name: String
}
 
Chúng ta có thể định nghĩa một protocol với một function cung cấp cho bạn một hàm khởi tạo của một superhero và kế thừa nó như là một sự thực hiện mặc định. Chúng ta sẽ đăt tên nó là SuperHeroAssembler:

protocol SuperHeroAssembler {
     func resolve() -> SuperHero
}

extension SuperHeroAssembler {
    func resolve() -> SuperHero {
         return SuperHero(name: "Iron Man")
    }
}
Bây giờ chúng ta có thể sử dụng nó để khởi tạo SuperHero

class AppAssembler: SuperHeroAssembler {}

let assembler: SuperHeroAssembler = AppAssembler()
let superHero: SuperHero = assembler.resolve()
print(superHero.name) // Iron Man

Và làm cách nào chúng ta có thể mock nó trong tests của chúng ta. Well, nó thực sự rất đơn giản, chúng ta chỉ cần tạo một class mới phù hợp với SuperHeroAssembler nhưng với một hàm resolve khác. Chúng ta sẽ thấy cấu trúc của nó như sau:

class TestAssembler: SuperHeroAssembler {}
extension SuperHeroAssembler where Self: TestAssembler {
     func resolve() -> SuperHero {
         return SuperHero(name: "Test SuperHero!")
     }
}
let testAssembler: SuperHeroAssembler = TestAssembler()
let testSuperHero: SuperHero = testAssembler.resolve()
print(testSuperHero.name) //Test SuperHero!

Có thể bạn thấy ví dụ đơn giản này không có nhiều ích lợi (advantages), nhưng ý tưởng này có thể sẽ rất hữu dụng nếu được sử dụng tốt. Chúng ta hãy xem ví dụ một cách kỹ lưỡng hơn để thấy cách sử dụng của nó.

Nó làm việc như thế nào trong tình huống cụ thể ? (How does it work with a use case ?)
Hãy tưởng tượng (Imagine) một màn hình nơi bạn cần phải hiển thị tất cả địa chỉ liên lạc của người dùng. Địa chỉ liên lạc lấy từ network và chúng ta muốn có khả năng sử dụng một kiểm thử kép (test double)

Chúng ta sẽ sử dụng cấu trúc như sau:

class ContactsListViewController.swift 

class ContactsListViewController: UIViewController {
      private let presenter: ContactsListPresenter
      init(presenter: ContactsListPresenter) {
          self.presenter = presenter
          super.init(nibName: nil, bundle: nil)
          self.presenter.ui = self
      }
      // View controller logic
}

extension ContactsListViewController: ContactsListUI {
       func show(contacts: [Contact]) {
           // Logic to show contacts list
       }
}

class ContactsListPresenter.swift 

protocol ContactsListUI {
 func show(contacts: [Contact])
}

class ContactsListPresenter {

 weak var ui: ContactsListUI?
 private let getAllContacts: GetAllContacts

 init(getAllContacts: GetAllContacts) {
  self.getAllContacts = getAllContacts
 }

 // Presenter logic
}

class GetAllContacts.swift 

class GetAllContacts {

 private let contactsDataSource: ContactsDataSource

 init(contactsDataSource: ContactsDataSource) {
  self.contactsDataSource = contactsDataSource
 }

 // Use case functions

}

class ContactsDataSource.swift 

protocol ContactsDataSource {
 func getAll() -> [Contact]
}

class NetworkContactsDataSource: ContactsDataSource {
 func getAll() -> [Contact] {
  return [Contact(name: "Network Contact")]
 }
}
 
Chúng ta sẽ xem cách để dependencies trong class ContactsSceneAssember.swift 

class Assembler: ContactsSceneAssembler { }

protocol ContactsSceneAssembler {
 func resolve(user: User) -> ContactsListViewController

 func resolve(user: User) -> ContactsListPresenter

 func resolve() -> GetAllContacts

 func resolve() -> ContactsDataSource
}

extension ContactsSceneAssembler {
 func resolve(user: User) -> ContactsListViewController {
  // Nice! We can and must use the assembler itself to resolve the dependencies
  return ContactsListViewController(presenter: resolve(user: user))
 }

 func resolve(user: User) -> ContactsListPresenter {
  return ContactsListPresenter(user: user, getAllContacts: resolve())
 }

 func resolve() -> GetAllContacts {
  return GetAllContacts(contactsDataSource: resolve())
 }

 func resolve() -> ContactsDataSource {
  return NetworkContactsDataSource()
 }
}

Như bạn có thể thấy, chúng ta có thể (hoặc phải) tái sử dụng chính nó để giải quyết dependencies cuả chúng ta. Một điều tuyệt vời là nếu bạn biết tham số đầu vào và kiểu dữ liệu khi chạy, mọi công việc có thể tự động hoàn thành

Bây giờ, để tạo một ContactsListViewController chúng ta chỉ cần sử dụng assembler để giải quyết nó sử dụng một user 

let contactsListViewController: ContactsListViewController = assembler.resolve(user: user)

Như bạn thấy trong ví dụ đầu tiên, nó sẽ có thể sử dụng một test double thay thế NetworkContactsDataSource một cách dễ dàng. Bạn chỉ cần tạo một assembler, cái bạn resolve ContactsDataSource để test double object của bạn: 

class TestAssembler: ContactsSceneAssembler {
 func resolve() -> ContactsDataSource {
  return StubContactsDataSource()
 }
}















Comments

Popular posts from this blog

MVVM và VIPER: Con đường trở thành Senior

Trong bài viết trước chúng ta đã tìm hiểu về MVC và MVP để ứng dụng cho một iOS App đơn giản. Bài này chúng ta sẽ tiếp tục ứng dụng 2 mô hình MVVM và VIPER . Nhắc lại là ứng dụng của chúng ta cụ thể khi chạy sẽ như sau: Source code đầy đủ cho tất cả mô hình MVC, MVP, MVVM và VIPER các bạn có thể download tại đây . MVVM MVVM có thể nói là mô hình kiến trúc được rất nhiều các cư dân trong cộng đồng ưa chuộng. Điểm tinh hoa của kiến trúc này là ở ViewModel , mặc dù rất giống với Presenter trong MVP tuy nhiên có 2 điều làm nên tên tuổi của kiến trúc này đó là: ViewModel không hề biết gì về View , một ViewModel có thể được sử dụng cho nhiều View (one-to-many). ViewModel sử dụng Observer design pattern để liên lạc với View (thường được gọi là binding data , có thể là 1 chiều hoặc 2 chiều tùy nhu cầu ứng dụng). Chính đặc điểm này MVVM thường được phối hợp với các thư viện hỗ trợ Reactive Programming hay Event/Data Stream , đây là triết lý lập trình hiện đại và hiệu

Alamofire vs URLSession

Alamofire vs URLSession: a comparison for networking in Swift Alamofire and URLSession both help you to make network requests in Swift. The URLSession API is part of the foundation framework, whereas Alamofire needs to be added as an external dependency. Many  developers  doubt  whether it’s needed to include an extra dependency on something basic like networking in Swift. In the end, it’s perfectly doable to implement a networking layer with the great URLSession API’s which are available nowadays. This blog post is here to compare both frameworks and to find out when to add Alamofire as an external dependency. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! This shows the real power of Alamofire as the framework makes a lot of things easier. What is Alamofire? Where URLSession can be found within the s

Fileprivate vs private: Giải thích sự khác biệt

Fileprivate vs private in Swift: The differences explained Fileprivate and private are part of the access control modifiers in Swift. These keywords, together with internal, public, and open, make it possible to restrict access to parts of your code from code in other source files and modules. The private access level is the lowest and most restrictive level whereas open access is the highest and least restrictive. The documentation of Swift will explain all access levels in detail to you, but in this blog post, I’m going to explain the differences between two close friends: fileprivate and private. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! Open access is the highest (least restrictive) access level and private access is the lowest (most restrictive) access level. This will improve readability and mak