Swift中依賴注入的解耦策略

原文地址:Dependency Injection Strategies in Swifthtml

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

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

依賴注入背後的意圖是經過讓一個對象提供另外一個對象的依賴關係來解耦。它用於爲模塊提供不一樣的配置,尤爲對於爲(單元)測試模塊和/或應用程序提供模擬依賴性很是有用。咱們將在本文中使用術語依賴注入僅做爲描述一個對象如何爲其餘對象提供依賴關係的設計模式。 不要將依賴注入與幫助你注入依賴項的框架或庫混淆。github

Why should I use it?

依賴注入有助於咱們在不一樣的環境中使咱們的組件更少耦合和更可重用。總的來講,它是分離關注的一種形式,由於它使用從初始化和配置的依賴性來分離。爲實現這一目標,咱們可使用不一樣的技術將依賴項注入到咱們的模塊中。數據庫

如上所述,依賴注入的一個很是重要的方面是它使咱們的代碼更易於測試。 咱們能夠爲咱們想要測試的類/模塊的依賴項注入模擬實例。這使咱們能夠將測試集中在模塊中的單元測試代碼上,並確保這部分按預期工做,而不會產生致使測試失敗不明確的模糊反作用,由於其中一個依賴項不符合預期。這些依賴項應該自行測試,以便更容易地發現真正的錯誤並加快開發工做流程。編程

咱們在以前的一篇文章中已經描述了咱們的測試策略。 若是您想了解有關咱們測試設置的更多信息,請務必閱讀該文章Testing Mobile Appsswift

此外,依賴注入容許咱們繞過軟件開發中最多見的錯誤之一:在代碼庫中濫用單例。 若是你想更多地瞭解爲何單例很差,請看看Are Singletons BadSingletons Are Evil 設計模式

Different strategies to do Dependency Injection in Swift

在Swift中咱們有不少方式使用依賴注入,大多數原則也適用於其餘編程語言,即便在大多數其餘環境中(特別是在Java社區中),人們傾向於使用特殊的依賴注入框架來爲它們作繁重的工做。api

是的,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源碼解析

相關文章
相關標籤/搜索