当前位置: 首页 > 科技观察

Swift中依赖注入的风格

时间:2023-03-19 00:30:06 科技观察

前言在上一篇文章中,我们研究了使用依赖注入在Swift应用程序中实现更多解耦和可测试架构的一些不同方法。例如,在DependencyInjectionUsingFactoriesinSwift[1]中将依赖注入与工厂模式相结合,在AvoidingSingletonsinSwift[2]中将SimpleInterest替换为DependencyInjection。到目前为止,我的大部分文章和示例都使用了基于初始化程序的依赖注入。然而,像大多数编程技术一样,依赖注入有多种“风格”,每种都有自己的优点和缺点。本周,让我们来看看三种不同的依赖注入方法以及它们在Swift中的使用方式。基于初始化器让我们快速回顾一下最常见的依赖注入形式——基于初始化器的依赖注入,即对象在初始化时就应该被赋予它所需要的依赖项。这种方法的伟大之处在于它保证我们的对象拥有立即完成工作所需的一切。假设我们正在构建一个从磁盘加载文件的FileLoader。为此,它使用了两个依赖项——一个是系统提供的FileManager的实例,另一个是Cache。使用基于初始化的依赖注入,可以这样实现:fileManagerself.cache=cache}}请注意上面如何使用默认参数来避免在使用单例或新实例时总是创建依赖项。这使我们能够在生产代码中使用FileLoader()简单地创建一个文件加载器,同时仍然能够通过在测试代码中注入模拟数据或显式实例来进行测试。基于属性虽然基于初始化程序的依赖注入通常非常适合您自己的自定义类,但当您必须从系统类继承时,使用它有时会有点棘手。一个例子是在构建视图控制器时,尤其是当您使用XIB或Storyboard来定义它们时,因为那样您就无法再控制类的初始化程序。对于这些类型的情况,基于属性的依赖注入可能是一个很好的选择。与其在初始化器中注入对象的依赖关系,不如在之后简单地分配它。这种依赖注入的方式还可以帮助你减少模板文件,特别是当有一个很好的默认值不一定需要注入的时候。让我们看另一个例子——在这个例子中,我们将构建一个PhotoEditorViewController来让用户编辑他们图库中的照片。为了发挥作用,这个视图控制器需要一个系统提供的PHPhotoLibrary类的实例(它是一个单例),以及一个我们自己的PhotoEditorEngine类的实例。为了在没有自定义初始化器的情况下实现依赖注入,我们可以创建两个具有默认值的可变属性,如下所示:使用系统单例通过3个简单步骤测试Swift代码”使用协议为系统PhotoLibrary类提供更抽象的PhotoLibrary接口。这将使测试和数据模拟变得更加容易!上面的好处是,我们仍然可以通过重新分配视图控制器的属性,轻松地将模拟数据注入到我们的测试中:完全控制其中存储了哪些照片apply(filter:.blackAndWhite)viewController.savePhoto()//AssertresultistrueXCTAssertTrue(photoIsBlackAndWhite(library.photos[0]))}}基于参数最后,让我们看看基于参数的依赖注入。当您希望在不过多更改其现有结构的情况下轻松地使遗留代码更易于测试时,此类型特别有用。很多时候,我们只需要一次特定的依赖,或者我们只需要在特定条件下模拟它。我们可以公开一个接受依赖项作为参数的API,而不是更改对象初始值设定项或将属性公开为可变的(这并不总是一个好主意)。让我们看一下作为笔记记录应用程序一部分的NoteManager类。它的工作是管理用户写的所有笔记,并提供一个API,用于根据查询搜索笔记。由于这是一个可能需要一段时间的操作(可能如果用户有很多笔记),我们通常在后台队列中这样做:注意])->Void){DispatchQueue.global(qos:.userInitiated).async{letdatabase=self.loadDatabase()letnotes=database.filter{noteinreturnnote.matches(query:query)}completionHandler(notes)}}}虽然上述方法对于我们的生产代码是一个很好的解决方案,但在测试中我们通常希望尽可能避免异步代码和并行性以避免flakiness。虽然使用初始化程序或基于属性的依赖注入来指定NoteManager应该始终使用的显式队列会很好,但这可能需要对类进行重大修改,而我们目前不能/不愿意这样做.这就是基于参数的依赖注入的用武之地。与其重构我们的整个类,让我们直接注入哪个队列来运行loadNotes操作:Note])->Void){queue.async{letdatabase=self.loadDatabase()letnotes=database.filter{noteinreturnnote.matches(query:query)}completionHandler(notes)}}}这给了我们Being能够在测试代码中轻松使用自定义队列,我们??可以等待它。这几乎允许我们在测试中将上述API转换为同步API,这使事情变得更容易和更可预测。基于参数的依赖注入的另一个用例是当您想要测试静态API时。对于静态API,我们没有初始化器,最好不要静态地保持任何状态,因此基于参数的依赖注入成为一个不错的选择。让我们看一下当前依赖单例的静态MessageSender类:)letendpoint=Endpoint.sendMessage(to:user)NetworkManager.shared.post(data,to:endpoint.url)}}尽管理想的长期解决方案可能是将MessageSender重构为非静态的并使用,但是对于为了便于测试(例如,重现/验证错误),我们可以简单地将其依赖项作为参数注入,而不是依赖单例:classMessageSender{staticfuncsend(_message:Message,touser:User,database:数据库=.shared,networkManager:NetworkManager=.shared)throws{database.insert(message)letdata:Data=trywrap(message)letendpoint=Endpoint.sendMessage(to:user)networkManager.post(data,to:endpoint.url)}}我们再次使用默认参数,除了方便的原因,但更重要的是在这里能够o在我们的代码中添加测试支持,同时仍然保持100%向后兼容的方向。参考文献[1]在Swift中使用工厂进行依赖注入:https://www.swiftbysundell.com/articles/dependency-injection-using-factories-in-swift。[2]在Swift中避免单例:https://www.swiftbysundell.com/articles/avoiding-singletons-in-swift。