原文地址:Dependency Injection Strategies in Swift
今天我们将深入研究Swift中的依赖注入,这是软件开发中最重要的技术之一,也是许多编程语言中使用频繁的概念。 具体来说,我们将探索可以使用的策略/模式,包括Swift中的Service Locator
模式。
依赖注入背后的意图是通过让一个对象提供另一个对象的依赖关系来解耦。它用于为模块提供不同的配置,尤其对于为(单元)测试模块和/或应用程序提供模拟依赖性非常有用。我们将在本文中使用术语依赖注入仅作为描述一个对象如何为其他对象提供依赖关系的设计模式。 不要将依赖注入与帮助你注入依赖项的框架或库混淆。
Why should I use it?
依赖注入有助于我们在不同的环境中使我们的组件更少耦合和更可重用。总的来说,它是分离关注的一种形式,因为它使用从初始化和配置的依赖性来分离。为实现这一目标,我们可以使用不同的技术将依赖项注入到我们的模块中。
如上所述,依赖注入的一个非常重要的方面是它使我们的代码更易于测试。 我们可以为我们想要测试的类/模块的依赖项注入模拟实例。这使我们可以将测试集中在模块中的单元测试代码上,并确保这部分按预期工作,而不会产生导致测试失败不明确的模糊副作用,因为其中一个依赖项不符合预期。这些依赖项应该自行测试,以便更容易地发现真正的错误并加快开发工作流程。
我们在之前的一篇文章中已经描述了我们的测试策略。 如果您想了解有关我们测试设置的更多信息,请务必阅读该文章Testing Mobile Apps。
此外,依赖注入允许我们绕过软件开发中最常见的错误之一:在代码库中滥用单例。 如果你想更多地了解为什么单例不好,请看看Are Singletons Bad或Singletons 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
好吧,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
(类似)类(如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
。最后,我们返回第一个支持此类型的工厂创建的实例。
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的比较不错的库有:
objection和typhoon
Swift版本的有:
TyphoonSwift,Swinject,Cleanse和needle
比较不错的中文文章:
使用objection来模块化开发iOS项目
Objection源码分析
iOS 组件通信方案
Swinject源码解析