原文地址:Dependency Injection Strategies in Swifthtml
简书地址:Swift中依赖注入的解耦策略ios
今天咱们将深刻研究Swift中的依赖注入,这是软件开发中最重要的技术之一,也是许多编程语言中使用频繁的概念。 具体来讲,咱们将探索可使用的策略/模式,包括Swift中的Service Locator
模式。git
依赖注入背后的意图是经过让一个对象提供另外一个对象的依赖关系来解耦。它用于为模块提供不一样的配置,尤为对于为(单元)测试模块和/或应用程序提供模拟依赖性很是有用。咱们将在本文中使用术语依赖注入仅做为描述一个对象如何为其余对象提供依赖关系的设计模式。 不要将依赖注入与帮助你注入依赖项的框架或库混淆。github
依赖注入有助于咱们在不一样的环境中使咱们的组件更少耦合和更可重用。总的来讲,它是分离关注的一种形式,由于它使用从初始化和配置的依赖性来分离。为实现这一目标,咱们可使用不一样的技术将依赖项注入到咱们的模块中。数据库
如上所述,依赖注入的一个很是重要的方面是它使咱们的代码更易于测试。 咱们能够为咱们想要测试的类/模块的依赖项注入模拟实例。这使咱们能够将测试集中在模块中的单元测试代码上,并确保这部分按预期工做,而不会产生致使测试失败不明确的模糊反作用,由于其中一个依赖项不符合预期。这些依赖项应该自行测试,以便更容易地发现真正的错误并加快开发工做流程。编程
咱们在以前的一篇文章中已经描述了咱们的测试策略。 若是您想了解有关咱们测试设置的更多信息,请务必阅读该文章Testing Mobile Apps。swift
此外,依赖注入容许咱们绕过软件开发中最多见的错误之一:在代码库中滥用单例。 若是你想更多地了解为何单例很差,请看看Are Singletons Bad或Singletons Are Evil 。设计模式
在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
是否按与其工做,并将测试商品添加到购物袋中。
好吧,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类还解决了传递依赖关系的问题。咱们以前必须使用全部其余解决方案执行此操做,若是您的类具备大量依赖项,或者您具备多个依赖项层级(例如上面的示例),它可能会变得混乱: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()
复制代码
根据咱们目前看到的解决方案,咱们将使用所谓的Service Locator
设计模式构建更通用,更灵活的解决方案。 让咱们从定义Service Locator
的相关实体开始:
Container
的配置建立类的实例,解决一个类型的实际实现。咱们首先为Service Locator Pattern
定义一个Resolver
协议。它是一个简单的协议,只有一种方法可用于建立符合传递的ServiceType
类型的实例。
protocol Resolver {
func resolve<ServiceType>(_ type: ServiceType.Type) -> ServiceType
}
复制代码
咱们能够经过如下方式使用符合该协议的对象:
let resolver: Resolver = ...
let instance = resolver.resolve(SomeProtocol.self)
复制代码
接下来,咱们使用关联类型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类以前 让咱们重复一下它应该为咱们作些什么:
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
(相似)类(如UserDefaults
和Bundle
)特别有用。
第二个甚至更重要的方法是建立一个新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
。最后,咱们返回第一个支持此类型的工厂建立的实例。
让咱们从以前开始咱们的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)
复制代码
咱们已经看到了在Swift中使用依赖注入的不一样技术。更重要的是,咱们已经看到你不须要决定一个单一的解决方案。它们能够混合以得到每种技术的综合优点。为了将全部内容提高到新的水平,咱们在Swift中引入了Factory``类和更通用的ServiceLocator
模式解决方案。这能够经过添加对多个参数的额外支持或经过在Swift引入可变参数泛型时添加更多类型安全性来改进。
为简单起见,咱们忽略了诸如范围,动态依赖和循环依赖之类的东西 全部这些问题都是能够解决的,但超出了本文的范围。 你能够在DependencyInjectionPlayground查看在此展现的全部内容。
依赖注入OC的比较不错的库有:
objection和typhoon
Swift版本的有: TyphoonSwift,Swinject,Cleanse和needle
比较不错的中文文章:
使用objection来模块化开发iOS项目
Objection源码分析
iOS 组件通讯方案
Swinject源码解析