寫更好的 Swift 程式碼:DI(依賴注入)
依賴注入是一個很重要的設計模式,它使用得非常廣泛。
本文將圍繞幾個問題來學習這種模式:
- 什麼是依賴?
- 什麼是依賴倒置原則?
- 什麼是依賴注入?
- 什麼時候用到依賴注入?
- 依賴注入的幾種常見方式?
- 依賴注入的作用
什麼是依賴?
依靠別人或事物而不能自立或自給稱為依賴
依賴是程式中常見的一種關係,比如類Vehicle
中用到了CarEngine
類的例項engine
,通常做法就是在Vehicle
類中顯示地建立CarEngine
類的例項,並賦值給engine
。如下面程式碼:
```swift // 賽車引擎 class RaceCarEngine { func move() { print("CarEngine 開動") } }
// 車 class Vehicle { var engine: RaceCarEngine
init() {
engine = RaceCarEngine()
}
func forward() {
engine.move()
}
}
let car = Vehicle() car.forward() ```
我們將CarEngine
作為Vehicle
的屬性,當car
呼叫forward
方法的時候,我們就呼叫engine
的move
方法。
存在問題:
engine
不應該是具體類,如果我們想切換成其他引擎,那麼就必須修改Vehicle
,engine
替換其他類,不符合依賴倒轉原則——依賴於抽象,不能依賴於具體實現。- 類
Vehicle
承擔了多餘的責任,負責engine
物件建立,這必然存在耦合性。 - 可擴充套件性,假設我們想修改
engine
為火箭引擎,那麼我們必然要修改Vehicle
這個類,明顯不符合開閉原則。 - 不方便單元測試。如果想測試不同
engine
對Vehicle
的影響很困難,因為engine
的初始化被寫死在了Vehicle
的建構函式中
什麼是依賴倒置原則(DIP)?
依賴倒置原則,英文縮寫 DIP,全稱Dependence Inversion Principle
。
High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。
高層模組不應該依賴低層模組,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。
所以 Vehicle
不能直接依賴 RaceCarEngine
,我們需要給引擎定義一個規則,抽象成一個協議:
```swift protocol Propulsion { func move() }
class RaceCarEngine: Propulsion { func move() { print("CarEngine 開動") } }
// 車 class Vehicle { var engine: Propulsion
init() {
engine = RaceCarEngine()
}
func forward() {
engine.move()
}
} ```
但是這就符合 DIP 了麼?答案是沒有,為什麼?
因為在 init()
方法中,用 RaceCarEngine
具體類去初始化 engine
,這也是一種依賴。這就造成,很難在沒有 RaceCarEngine
類的情況下使用 Vehicle
類。
那麼怎樣才能解決這個問題?依賴注入閃亮登場。
什麼是依賴注入?
如果模組A呼叫了模組B的方法,那麼就認為模組A依賴於模組B,模組A與模組B發生了耦合。在軟體工程中,設計的核心思想:儘可能減少程式碼耦合,採取解耦技術把關聯依賴降到最低,而不至於牽一髮而動全身。
在 Vehice
類中如何通過依賴注入來改進程式碼?程式碼如下:
```swift class Vehicle { var engine: Propulsion
init(engine: Propulsion) {
self.engine = engine
}
func forward() {
engine.move()
}
} ```
我們現在沒有直接在 Vechicle 的 init()
函式中用 RaceCarEngine
去初始化 engine
,而是通過給init
新增一個Propulsion型別的engine形參,用這個形參去初始化 engine。
雖然這改動非常小,但是效果是非常顯著的,因為Vehicle
再也不需要和RaceCarEngine
類直接產生關係。
然後我們的呼叫程式碼:
swift
let raceCarEngine = RaceCarEngine()
var car = Vehicle(engine: raceCarEngine)
car.forward()
raceCarEngine 物件是從外部注入到 Vehicle
物件中。這就是依賴注入。這兩個類仍然相互依賴,但它們不在緊密耦合——可以使用其中一個而不需要另一個。
Dependency injection means giving an object its instance variables.(依賴注入就是將例項變數傳入到一個物件中去)
通過依賴注入,顯然提高了程式碼的可擴充套件性。我們可以輕鬆地將RaceCarEngine
引擎換成RocketEngine
引擎:
```swift class RocketEngine: Propulsion { func move() { print("3-2-1... RocketEngine 發動") } }
let rocket = RocketEngine() var car = Vehicle(engine: rocket) car.forward() ```
什麼時候用到依賴注入?
依賴注入在以下場景中很有用:
- 更改您無權訪問的程式碼的實現
- 在開發過程中“模擬”或偽造程式碼中的行為
- 對程式碼進行單元測試
依賴注入的方法
-
建構函式注入:通過初始化
init()
提供依賴swift let rocket = RocketEngine() var car = Vehicle(engine: rocket)
-
屬性注入:通過屬性(或 setter)提供依賴,iOS 框架中有很多屬性注入模式,
Delegate
模式通常是這樣實現的。swift let rocket = RocketEngine() var car = Vehicle() car.engine = rocket
-
方法注入,將依賴項作為方法引數傳遞
swift let rocket = RocketEngine() car.setEngine(rocket)
實戰
讓我們看一個使用Repository
物件獲取資料的Service
類的示例:
```swift
struct Article: Equatable { let title: String }
class Basket { var articles = Article }
protocol Repository { func getAll() -> [Article] }
class Service { private let repository: Repository
init(repository: Repository) {
self.repository = repository
}
func addArticles(to basket: Basket) {
let allArticles = repository.getAll()
basket.articles.append(contentsOf: allArticles)
}
} ```
我們通過給 Service
注入注入了一個 repository,這樣 service 就不需要知道所使用的文章是如何提供的。這些文章可能來自從本地JSON檔案讀取,或從本地資料庫檢索,又或者是從伺服器通過請求獲取。我們可以注入mocked
的repository,通過使用mocked
的資料使得測試更具可預測性。
```swift
class MockRepository: Repository { var articles: [Article]
init(articles: [Article]) {
self.articles = articles
}
override func getAll() -> [Article] {
return articles
}
}
class ServiceTests: XCTestCase {
func testAddArticles() {
let expectedArticle = Article(title: "測試文章")
let mockRepository = MockRepository(articles: [expectedArticle])
let service = Service(repository: mockRepository)
let basket = Basket()
service.addArticles(to: basket)
XCTAssertEqual(basket.articles.count, 1)
XCTAssertEqual(basket.articles[0], expectedArticle)
}
}
``
我們首先建立了一個模擬的
expectedArticle物件,然後注入到的 MockRepository物件中,通過2個
XCTAssertEqual以檢查我們的
Sercice`是否按預期工作。
建構函式依賴注入確實是一個不錯的注入方式,但是也有些不便問題:
```swift class BasketViewController: UIViewController { private let service: Service
init(service: Service) {
self.service = service
}
} ```
寫了新的建構函式,我們需要額外的做些處理。
但是如何在不重寫預設建構函式的情況下使用依賴注入呢?
我們可以通過屬性注入的方式:
```swift class BasketViewController: UIViewController { var service: Service! }
class DataBaseRepository: Repository { override func getAll() -> [Article] { // TODO:從資料庫中查詢資料 return [Article(title: "測試資料")] } }
let basketViewController = BasketViewController()
let repository = DataBaseRepository()
let service = Service(repository: repository)
basketViewController.service = Service()
```
基於屬性注入的方式也有不完美的地方:屬性的訪問許可權被放大,不能將他們定義為私有了。
不管是屬性注入,還是建構函式注入,都包含了2個工作:
- 建立
Service
和BasketViewController
的例項 - 完成
Service
與BasketViewController
的依賴關係
因此這裡又出現一個潛在的問題,就是當要更換Service
的時候,又需要去更改這些建立例項的程式碼。如果有多處地方跳轉到 BasketViewController
,那麼這類程式碼就得多處修改。因此可以將這兩個工作移交給一個獨立元件去完成,它的職責就是完成物件的建立以及物件之間的依賴關係的維護和管理。很多人想到這個元件可以用工廠模式進行設計,這是可取的,但是本文將封裝成類似SwiftUI中的 @Environment
的設計。
我們的設計目標就是:
```swift class BasketService { @Injected(.repository) var repository: Repository
func addArticles(to basket: Basket) {
let allArticles = repository.getAll()
basket.articles.append(contentsOf: allArticles)
}
}
class BasketViewController: UIViewController { private var basket = Basket() @Injected(.service) var service: BasketService
func loadArticles() {
service.addArticles(to: basket)
print(basket.articles)
}
}
let vc = BasketViewController() vc.loadArticles() ```
最終完整程式碼:
```swift struct Article: Equatable { let title: String }
class Basket { var articles = Article }
protocol Repository { func getAll() -> [Article] }
class DataBaseRepository: Repository { override func getAll() -> [Article] { // TODO:從資料庫中查詢資料 return [Article(title: "測試資料")] } }
public protocol InjectionKey { associatedtype Value static var currentValue: Self.Value {get set} }
/// 提供獲取依賴 struct InjectedValues { private static var current = InjectedValues()
static subscript<K>(key: K.Type) -> K.Value where K : InjectionKey {
get { key.currentValue }
set { key.currentValue = newValue }
}
static subscript<T>(_ keyPath: WritableKeyPath<InjectedValues, T>) -> T {
get { current[keyPath: keyPath] }
set { current[keyPath: keyPath] = newValue }
}
}
@propertyWrapper
struct Injected
init(_ keyPath: WritableKeyPath<InjectedValues, T>) {
self.keyPath = keyPath
}
}
private struct RepositoryKey: InjectionKey { static var currentValue: Repository = DataBaseRepository() }
private struct ServiceKey: InjectionKey { static var currentValue: BasketService = BasketService() }
extension InjectedValues { var repository: Repository { get {Self[RepositoryKey.self]} set {Self[RepositoryKey.self] = newValue} }
var service: BasketService {
get { Self[ServiceKey.self] }
set {Self[ServiceKey.self] = newValue}
}
}
class BasketService { @Injected(.repository) var repository: Repository
func addArticles(to basket: Basket) {
let allArticles = repository.getAll()
basket.articles.append(contentsOf: allArticles)
}
}
class BasketViewController: UIViewController { private var basket = Basket() @Injected(.service) var service: BasketService
func loadArticles() {
service.addArticles(to: basket)
print(basket.articles)
}
}
let vc = BasketViewController() vc.loadArticles() ```
結果輸出:
shell
[__lldb_expr_388.Article(title: "測試資料")]