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

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...

Swift Tool Belt, Part 1: Adding a Border, Corner Radius, and Shadow to a UIView with Interface Builder

During my iOS work, I’ve assembled a set of code that I bring with me on every iOS project. I’m not talking about large frameworks or CocoaPods here. These are smaller Swift extensions or control overrides that are applicable to many projects. I think of them as my tool belt. In this post, I’ll show you an extension that will add a border, a corner radius, and a shadow to any UIView, UIButton, or UILabel and allow you to preview what it will look like in Interface Builder. Back in 2014, I wrote a blog post on Expanding User-Defined Runtime Attributes in Xcode where I added a border, corner radius, and shadow to a UIView using Interface Builder’s user-defined runtime attributes. This solution had no type checking—you had to type the property you wanted to modify by hand and often had to look up what it was called. You also had to run your project in order to see the effect of the runtime attribute. Starting with Xcode 6 , there is a new mech...

Frame vs Bounds in iOS

This article is a repost of an answer I wrote on Stack Overflow . Short description frame = a view’s location and size using the parent view’s coordinate system ( important for placing the view in the parent) bounds = a view’s location and size using its own coordinate system (important for placing the view’s content or subviews within itself) Details To help me remember frame , I think of a picture frame on a wall . The picture frame is like the border of a view. I can hang the picture anywhere I want on the wall. In the same way, I can put a view anywhere I want inside a parent view (also called a superview). The parent view is like the wall. The origin of the coordinate system in iOS is the top left. We can put our view at the origin of the superview by setting the view frame’s x-y coordinates to (0, 0), which is like hanging our picture in the very top left corner of the wall. To move it right, increase x, to move it down increase y. To help me remember bound...