Swift中依賴注入的解耦策略

大神Q發表於2019-05-05

原文地址:Dependency Injection Strategies in Swift

簡書地址:Swift中依賴注入的解耦策略

今天我們將深入研究Swift中的依賴注入,這是軟體開發中最重要的技術之一,也是許多程式語言中使用頻繁的概念。 具體來說,我們將探索可以使用的策略/模式,包括Swift中的Service Locator模式。

依賴注入背後的意圖是通過讓一個物件提供另一個物件的依賴關係來解耦。它用於為模組提供不同的配置,尤其對於為(單元)測試模組和/或應用程式提供模擬依賴性非常有用。我們將在本文中使用術語依賴注入僅作為描述一個物件如何為其他物件提供依賴關係的設計模式。 不要將依賴注入與幫助你注入依賴項的框架或庫混淆。

Why should I use it?

依賴注入有助於我們在不同的環境中使我們的元件更少耦合和更可重用。總的來說,它是分離關注的一種形式,因為它使用從初始化和配置的依賴性來分離。為實現這一目標,我們可以使用不同的技術將依賴項注入到我們的模組中。

如上所述,依賴注入的一個非常重要的方面是它使我們的程式碼更易於測試。 我們可以為我們想要測試的類/模組的依賴項注入模擬例項。這使我們可以將測試集中在模組中的單元測試程式碼上,並確保這部分按預期工作,而不會產生導致測試失敗不明確的模糊副作用,因為其中一個依賴項不符合預期。這些依賴項應該自行測試,以便更容易地發現真正的錯誤並加快開發工作流程。

我們在之前的一篇文章中已經描述了我們的測試策略。 如果您想了解有關我們測試設定的更多資訊,請務必閱讀該文章Testing Mobile Apps

此外,依賴注入允許我們繞過軟體開發中最常見的錯誤之一:在程式碼庫中濫用單例。 如果你想更多地瞭解為什麼單例不好,請看看Are Singletons BadSingletons Are Evil

Different strategies to do Dependency Injection in Swift

在Swift中我們有很多方式使用依賴注入,大多數原則也適用於其他程式語言,即使在大多數其他環境中(特別是在Java社群中),人們傾向於使用特殊的依賴注入框架來為它們做繁重的工作。

是的,Swift中也有Dependency Injection框架。 最受歡迎的是Swinject,具有豐富的功能和大型社群。但今天我們將向你展示一些注入依賴項的簡單技巧,而不會引入另一個巨大的第三方框架。

要看看在實際中如何使用改技術,我們可以看一下簡短的使用一個service使用repository物件獲取資料的案例。

class BasketService {
    private let repository: Repository<Article>

    init(repository: Repository<Article>) {
        self.repository = repository
    }

    func addAllArticles(to basket: Basket) {
        let allArticles = repository.getAll()
        basket.articles.append(contentsOf: allArticles)
    }
}

複製程式碼

我們為BasketService注入了一個repository,這樣我們的service就不需要知道如何提供所用的商品了。它們可以來自repository,該repository從本地JSON檔案獲取資料,或從本地資料庫檢索,甚至從伺服器獲取。

這允許我們在不同的環境中使用我們的BasketService,如果我們想為這個類編寫單元測試,我們可以注入我們的模擬的repository,通過使用始終相同的測試資料使我們的測試更加可預測。


class BasketServiceTests: XCTestCase {
    func testAddAllArticles() {
        let expectedArticle = Article(title: "Article 1")
        let mockRepository = MockRepository<Article>(objects: [expectedArticle])
        let basketService = BasketService(repository: mockRepository)
        let basket = Basket()

        basketService.addAllArticles(to: basket)

        XCTAssertEqual(basket.articles.count, 1)
        XCTAssertEqual(basket.articles[0], expectedArticle)
    }
}

複製程式碼

好了,我們可以向模擬repository中放入模擬商品,再向service注入這個模擬repository來測試service是否按與其工作,並將測試商品新增到購物袋中。

Property-based Dependency Injection

xBildschirmfoto-2018-12-07-um-10.31.31-10.png.pagespeed.ic.yAytv_X3m0.png

好吧,initializer-based dependency injection 似乎是一個很好的解決方案,但有些情況下它不適合,例如在ViewControllers中,使用初始化程式並不是那麼容易,特別是如果你使用XIB或storyboard檔案。

我們都知道這個錯誤訊息和Xcode提供的煩人的解決方案。 但是如何在不覆蓋所有預設初始值設定項的情況下使用依賴注入?

這就是property-based Dependency Injection發揮作用的地方。我們在初始化後分配模組的屬性。

讓我們看一下我們的BasketViewController,它將我們的BasketService類作為依賴。

class BasketViewController: UIViewController {
    var basketService: BasketService! = nil
}

let basketViewController = BasketViewController()
basketViewController.basketService = BasketService()


複製程式碼

我們被迫在這裡強制解包一個optional的屬性,以確保在之前未正確注入basketService屬性時程式崩潰。

如果我們想要擺脫對optional屬性的強制解包,可以在宣告屬性時提供預設值。

class BasketViewController: UIViewController {
    var basketService: BasketService = BasketService()
}


複製程式碼

property-based Dependency Injection也有一些缺點:首先,我們的類需要處理依賴項的動態更改;其次,我們需要使屬性可以從外部訪問和變化,並且不能再將它們定義為私有。

Factory Classes

到目前為止,我們看到的兩種解決方案都將注入依賴關係的責任轉移到建立新模組的類。這可能比將依賴項硬編碼到模組中更好,但將此責任轉移到自己的型別通常是更好的解決方案。它還確保我們不需要在程式碼中為初始化模組寫重複程式碼。

這些型別處理類的建立並設定其所有依賴項。這些所謂的Factory類還解決了傳遞依賴關係的問題。我們之前必須使用所有其他解決方案執行此操作,如果您的類具有大量依賴項,或者您具有多個依賴項層級(例如上面的示例),它可能會變得混亂:BasketViewController - > BasketService - > Repository。

讓我們看一下Basket的Factory

protocol BasketFactory {
    func makeBasketService() -> BasketService
    func makeBasketViewController() -> BasketViewController
}

複製程式碼

通過讓工廠成為協議,我們可以有多個實現,例如測試用例的特殊工廠。

Factory-based Dependency Injection與我們之前看到的解決方案密切配合,允許我們混合使用不同的技術,但是我們如何保持建立類的例項介面清晰。

除了向你展示一個例子,沒有更好的方法來解釋它:

class DefaultBasketFactory: BasketFactory {

    func makeBasketService() -> BasketService {
        let repository = makeArticleRepository()
        return BasketService(repository: repository)
    }

    func makeBasketViewController() -> BasketViewController {
        let basketViewController = BasketViewController()
        basketViewController.basketService = makeBasketService()
        return basketViewController
    }

    // MARK: Private factory methods
    private func makeArticleRepository() -> Repository<Article> {
        return DatabaseRepository()
    }

}

複製程式碼

我們的DefaultBasketFactory實現了上面定義的協議,並具有公共工廠方法和私有方法。 工廠方法可以而且應該使用類中的其他工廠方法來建立較低的依賴項。

上面的例子很好地展示了我們如何組合initializer-based and property-based Dependency Injection,同時具有優雅和簡單的介面來建立依賴關係的優勢。

要初始化我們的BasketViewController例項,我們只需編寫一行單一且自解釋的程式碼。

let basketViewController = factory.makeBasketViewController()

複製程式碼

The Service Locator Pattern

根據我們目前看到的解決方案,我們將使用所謂的Service Locator設計模式構建更通用,更靈活的解決方案。 讓我們從定義Service Locator的相關實體開始:

  • Container:儲存用來建立已註冊型別例項的配置。
  • Resolver:通過使用Container的配置建立類的例項,解決一個型別的實際實現。
  • ServiceFactory:用於建立通用型別例項的通用工廠。

Resolver

我們首先為Service Locator Pattern定義一個Resolver協議。它是一個簡單的協議,只有一種方法可用於建立符合傳遞的ServiceType型別的例項。

protocol Resolver {
    func resolve<ServiceType>(_ type: ServiceType.Type) -> ServiceType
}


複製程式碼

我們可以通過以下方式使用符合該協議的物件:

let resolver: Resolver = ...
let instance = resolver.resolve(SomeProtocol.self)

複製程式碼

ServiceFactory

接下來,我們使用關聯型別ServiceType定義ServiceFactory協議。 我們的工廠將建立符合ServiceType協議的型別例項。

protocol ServiceFactory {
    associatedtype ServiceType
    func resolve(_ resolver: Resolver) -> ServiceType
}


複製程式碼

這看起來與我們之前看到的Resolver協議非常相似,但它引入了額外的關聯型別,以便為我們的實現新增更多型別安全性。

讓我們定義符合這個協議的第一個型別BasicServiceFactory。此工廠類使用注入的工廠方法生成ServiceType型別的類/結構的例項。 通過將Resolver作為引數傳遞給工廠閉包,我們可以使用它來建立建立該型別例項所需的更低階別的依賴關係。

struct BasicServiceFactory<ServiceType>: ServiceFactory {
    private let factory: (Resolver) -> ServiceType

    init(_ type: ServiceType.Type, factory: @escaping (Resolver) -> ServiceType) {
        self.factory = factory
    }

    func resolve(_ resolver: Resolver) -> ServiceType {
        return factory(resolver)
    }
}

複製程式碼

這個BasicServiceFactory結構體可以獨立使用,比我們上面看到的Factory類更通用。但我們還沒有完成。我們在Swift中實現Service Locator Pattern所需的最後一件事是Container

Container

在我們開始寫Container類之前 讓我們重複一下它應該為我們做些什麼:

  • 它應該允許我們為某種型別註冊新工廠
  • 它應該儲存ServiceFactory例項
  • 它應該被用作任何儲存型別的Resolver

為了能夠以型別安全的方式儲存ServiceFactory類的例項,我們需要能夠在Swift中實現可變引數化泛型。這在Swift中尚不可能,但是它是GenericsManifesto的一部分,將在未來版本中新增到Swift中。與此同時,我們需要使用名為AnyServiceFactory的型別擦除版本來消除泛型型別。

為了簡單起見,我們不會向你展示它的實現,但如果您對它感興趣,請檢視下面連結。

struct Container: Resolver {

    let factories: [AnyServiceFactory]

    init() {
        self.factories = []
    }

    private init(factories: [AnyServiceFactory]) {
        self.factories = factories
    }

    ...

複製程式碼

我們將Container定義為充當resolver解析器的結構體並儲存已擦除型別的工廠。接下來,我們將新增用於在工廠中註冊新型別的程式碼。

// MARK: Register
    func register<T>(_ type: T.Type, instance: T) -> Container {
        return register(type) { _ in instance }
    }

    func register<ServiceType>(_ type: ServiceType.Type, _ factory: @escaping (Resolver) -> ServiceType) -> Container {
        assert(!factories.contains(where: { $0.supports(type) }))

        let newFactory = BasicServiceFactory<ServiceType>(type, factory: { resolver in
            factory(resolver)
        })
        return .init(factories: factories + [AnyServiceFactory(newFactory)])
    }

    .

複製程式碼

第一種方法允許我們為ServiceTyp註冊一個類的某個例項。這對於注入Singleton(類似)類(如UserDefaultsBundle)特別有用。

第二個甚至更重要的方法是建立一個新factory(工廠)並返回一個新的不可container(容器),包括該新factory

最後一個缺失的部分是實際符合我們的Resolver協議並使用我們儲存的工廠解析例項。

// MARK: Resolver
    func resolve<ServiceType>(_ type: ServiceType.Type) -> ServiceType {
        guard let factory = factories.first(where: { $0.supports(type) }) else {
            fatalError("No suitable factory found")
        }
        return factory.resolve(self)
    }

複製程式碼

我們使用一個guard語句來檢查它是否包含一個能夠解決依賴關係的工廠,否則會丟擲一個fatal error。最後,我們返回第一個支援此型別的工廠建立的例項。

Usage of Service Locators

讓我們從之前開始我們的basket示例,併為所有basket相關類定義一個容器:

let basketContainer = Container()
    .register(Bundle.self, instance: Bundle.main)
    .register(Repository<Article>.self) { _ in DatabaseRepository() }
    .register(BasketService.self) { resolver in
        let repository = resolver.resolve(Repository<Article>.self)
        return BasketService(repository: repository)
    }
    .register(BasketViewController.self) { resolver in
        let basketViewController = BasketViewController()
        basketViewController.basketService = resolver.resolve(BasketService.self)
        return basketViewController
    }


複製程式碼

這顯示了我們超級簡單解決方案的強大和優雅。我們可以使用鏈式register方法儲存所有工廠,同時混合使用我們之前看到的所有不同的依賴注入技術。

最後,但同樣重要的是,我們用於建立例項的介面保持簡單和優雅。


let basketViewController = basketContainer.resolve(BasketViewController.self)
複製程式碼

Conclusion

我們已經看到了在Swift中使用依賴注入的不同技術。更重要的是,我們已經看到你不需要決定一個單一的解決方案。它們可以混合以獲得每種技術的綜合優勢。為了將所有內容提升到新的水平,我們在Swift中引入了Factory``類和更通用的ServiceLocator模式解決方案。這可以通過新增對多個引數的額外支援或通過在Swift引入可變引數泛型時新增更多型別安全性來改進。

為簡單起見,我們忽略了諸如範圍,動態依賴和迴圈依賴之類的東西 所有這些問題都是可以解決的,但超出了本文的範圍。 你可以在DependencyInjectionPlayground檢視在此展示的所有內容。

最後個人補充

依賴注入OC的比較不錯的庫有:
objectiontyphoon
Swift版本的有: TyphoonSwiftSwinjectCleanseneedle
比較不錯的中文文章:
使用objection來模組化開發iOS專案
Objection原始碼分析
iOS 元件通訊方案
Swinject原始碼解析

相關文章