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
Post a Comment