在本文中,我將介紹依賴注入的基礎知識,以及如何使用Swinject框架將依賴注入應用到iOS項目中。
什么是依賴
依賴是我們代碼中兩個模塊之間的耦合(在面向對象語言中,指的是兩個類),通常是其中一個模塊使用另外一個提供的功能。
依賴有什么不好?
從上層到底層依賴都是危險的,因為我們在某種程度上把兩個模塊進行耦合,這樣當需要修改其中一個模塊時,我們必須修改與其耦合的模塊的代碼。這對于創建一個可測試的app來說是很不利的,因為單元測試要求測試一個模塊時,要保證它和app中其他模塊是隔離的。
舉個例子
class Module1{
var module2:Module2
init (){
module2 = Module2()
}
func doSomething(){
...
module2.doSomethingElse();
...
}
}
如何在不測試doSomethingElse函數的前提下測試doSomething函數呢?如果測試失敗,是哪個函數導致的呢?我們不得而知。如果doSomethingElse函數在數據庫中保存數據或者向服務器端發起API請求,那么事情將變得更加糟糕。
每當敲下new關鍵字我們都應該意識到這可能是需要避免的強依賴。編寫更少的模塊也不是解決方案,不要忘記單一職責原則。
依賴反轉
如果不能在一個模塊內部初始化另外的模塊,那么需要以其他的形式初始化這些模塊。你能想象如何實現嗎?沒錯,通過構造函數。這基本上就是依賴反轉原則的涵義了。你不應該依賴具體的模塊對象,應該依賴抽象。
前面的代碼應該修改為:
class Module1{
var module2:Module2
init(module2: Module2){
self.module2 = module2
}
func doSomething(){
...
module2.doSomethingElse();
...
}
}
什么是依賴注入呢?
依賴注入(Dependency Injection, DI)是一種技術,在這種技術中,可以從實體自身的范圍之外設置實體的依賴項,將整個系統轉換為松散耦合的模塊。想象一下,我們可以提供一個模塊,這個模塊包含了對應用程序其他組件的引用,這樣你就可以避免UIViewController之間的通信模式。
從上面的例子我們知道,通過構造函數傳遞依賴(注入),從而把創建模塊的任務從另一個模塊內部抽離出來。對象在其他地方創建,并以構造函數參數的形式傳遞給另一個對象。
但新問題出現了。如果我們不能在模塊內部創建其他的模塊,那么必須有個地方對這些模塊進行初始化。另外,如果我們需要創建的模塊的構造函數包含大量的依賴參數,代碼將變得丑陋和難以閱讀,app中將存在大量傳遞的對象。依賴注入正是為解決這類問題而誕生的。
我們需要在app中提供另一個模塊,專門負責提供其他模塊的實例并注入他們的依賴,這個模塊就是依賴注入器,模塊的創建集中于app中的一個統一的入口。
還是舉個例子來說明問題吧。
首先,如果沒有進行依賴注入的情況
第一步,創建Cat類:
class Cat {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Miao !"
}
}
第二步,創建Person類:
class Person {
let pet = Cat(name: "nimo")
func play() -> String {
return "I'm playing with \(pet.name). \(pet.sound())"
}
}
Person類中關聯了Cat類屬性。
實際使用:
let per = Person()
print(per.play())
輸出:
// 輸出 "I'm playing with nimo. Miao!"
問題來了,如果我不想養貓,我想養狗了,那我是不是就得新建一個Person2,關聯一個Dog類呢?
所以必須要對兩個類的依賴進行解耦, 并且改變為依賴抽象,這樣之后再進行依賴替換的時候就很容易了。
其次,我們來嘗試解耦
第一步,我們先將寵物抽象成一個接口協議,使用者不用具體實現,只是依賴這個協議即可:
protocol AnimalType {
var name: String { get }
func sound() -> String
}
第二步,讓 Cat 類實現這個協議:
class Cat: AnimalType {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Miao!"
}
}
第三步,Person中關聯一個依賴于AnimalType的pet,而非Cat的具體實現,并構造一個初始化方法,將pet傳入:
class Person {
let pet: AnimalType
init(pet: AnimalType) {
self.pet = pet
}
func play() -> String {
return "I'm playing with \(pet.name). \(pet.sound())"
}
}
具體使用:
let catPerson = Person(pet: Cat(name: "nimo"))
print(catPerson.play()) // 輸出 "I'm playing with nimo. Miao!"
如果換成養狗,則創建個Dog:
class Dog: AnimalType {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "wwww!"
}
}
具體使用:
let dogPerson = Person(pet: Dog(name: "hah"))
print(dogPerson.play()) // 輸出 "I'm playing with hah. wwww!"
以上是通過抽象出一個接口協議,代替了具體實現,如果項目中依賴關系多且復雜,使用Swinject進行依賴注入就比較方便了。
最后,我們使用Swinject來進行依賴注入
Swinject是一個很棒的用于Swift項目的DI框架,而且它是開源的。它使用泛型以一種非常簡單的方式解耦你的代碼。
第一步,通過CocoaPods將其添加到您的項目中:
pod ‘Swinject’
導入:
import Swinject
第二步,包裝container,放到一個統一的類中:
import Swinject
class DIContainer {
static let container:Container = {
let con = Container()
con.register(AnimalType.self) { _ in Cat(name: "Nimo") }
con.register(Person.self) { r in
Person(pet: r.resolve(AnimalType.self)!)
}
return con
}()
}
第三步,具體使用:
let container = DIContainer.container
override func viewDidLoad() {
super.viewDidLoad()
let per = container.resolve(Person.self)!
print(per.play())
}