当前位置: 首页 > 编程日记 > 正文

Swift中依赖注入的解耦策略

原文地址:Dependency Injection Strategies in Swift

简书地址: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() -> BasketServicefunc 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 methodsprivate 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 ServiceTypefunc resolve(_ resolver: Resolver) -> ServiceType
}复制代码

这看起来与我们之前看到的Resolver协议非常相似,但它引入了额外的关联类型,以便为我们的实现添加更多类型安全性。

让我们定义符合这个协议的第一个类型BasicServiceFactory。此工厂类使用注入的工厂方法生成ServiceType类型的类/结构的实例。 通过将Resolver作为参数传递给工厂闭包,我们可以使用它来创建创建该类型实例所需的更低级别的依赖关系。

struct BasicServiceFactory<ServiceType>: ServiceFactory {private let factory: (Resolver) -> ServiceTypeinit(_ 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: Registerfunc 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 infactory(resolver)})return .init(factories: factories + [AnyServiceFactory(newFactory)])}.复制代码

第一种方法允许我们为ServiceTyp注册一个类的某个实例。这对于注入Singleton(类似)类(如UserDefaultsBundle)特别有用。

第二个甚至更重要的方法是创建一个新factory(工厂)并返回一个新的不可container(容器),包括该新factory

最后一个缺失的部分是实际符合我们的Resolver协议并使用我们存储的工厂解析实例。

// MARK: Resolverfunc 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 inlet repository = resolver.resolve(Repository<Article>.self)return BasketService(repository: repository)}.register(BasketViewController.self) { resolver inlet 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源码解析

相关文章:

Eclipse mac 下的快捷键

2019独角兽企业重金招聘Python工程师标准>>> Eclipse&#xff0c;MyEclipse 的preference 在“windows”下边&#xff0c;mac下在左上角苹果图标边上 win下我们都习惯了ctrl c&#xff0c;在Mac 下使用标准键盘变成了win键c 系统的偏好设定 -> 键盘 -> 修饰…

Ubuntu上使终端显示Git分支(oh-my-zsh)

oh-my-zsh是基于Zsh(Zsh是一个Linux用户很少使用的power-shell&#xff0c;这是由于大多数Linux产品安装&#xff0c;以及默认使用bash shell)的功能作了一个扩展&#xff0c;方便插件管理、主体自定义等。oh-my-zsh源码在 https://github.com/robbyrussell/oh-my-zsh &#x…

天哪!我的十一假期被AI操控了

&#xff08;图片付费下载自视觉中国&#xff09;导语&#xff1a;这个假期&#xff0c;除了脑海一直在唱歌&#xff0c;庆祝祖国成立的 70 周年&#xff0c;当然也闲不住&#xff0c;要乘机出去浪一浪。目前小长假进度条已经进行到 71.4% 了&#xff0c;有没有发现这个假期与以…

使用SVN+Axure RP 8.0创建团队项目

一、使用到的工具&#xff1a;VisualSVN Server --SVN服务器&#xff1a;https://www.visualsvn.com/server/ Axure RP 8.0 &#xff1a;http://www.downcc.com/soft/103078.html 二、VisualSVN Server 安装以及操作1、安装 &#xff1a; 默认安装即可 2、操作&#xff1a; &a…

no no no.不要使用kill -9.

2019独角兽企业重金招聘Python工程师标准>>> no no no.不要使用kill -9. 它没有给进程留下善后的机会&#xff1a; 1) 关闭socket链接 2) 清理临时文件 3) 将自己将要被销毁的消息通知给子进程 4) 重置自己的终止状态 等等。 通常&#xff0c;应该发送15&#xff0c…

人工智能的“天罗地网”

&#xff08;图片付费下载自视觉中国&#xff09;整理 | 弯月编辑 | 郭芮来源 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;人工智能&#xff08;AI&#xff09;技术正在全球迅速崛起。不断涌现的最新发展令世人瞩目&#xff0c;从以假乱真的深度伪造视频&#xff0c…

Ubuntu下安装Cppcheck源码操作步骤

Cppcheck是用在C、C中对code进行静态检查的工具。它的源码在 https://github.com/danmar/cppcheck 。它的License是GPL-3.0。Cppcheck可以检查不通过编译的文件&#xff0c;执行的检查包括&#xff1a;(1)、自动变量检查&#xff1b;(2)、数组的边界检查&#xff1b;(3)、clas…

用“脸”打卡,抬头就能签到!

科技正在飞速改变我们的生活&#xff0c;以前我们上班的时候&#xff0c;脖子上总会挂一个IC卡用来验证身份和签到打卡&#xff0c;后来指纹识别出现了&#xff0c;我们又逐渐习惯了指纹打卡&#xff0c;到如今&#xff0c;随着人脸识别技术的出现&#xff0c;我们开始用“脸”…

OC基础第四讲--字符串、数组、字典、集合的常用方法

OC基础第四讲--字符串、数组、字典、集合的常用方法 字符串、数组、字典、集合有可变和不可变之分。以字符串为例&#xff0c;不可变字符串本身值不能改变&#xff0c;必须要用相应类型来接收返回值&#xff1b;而可变字符串调用相应地方法后&#xff0c;本身会改变&#xff1b…

分类、检测、分割任务均有SOTA表现,ACNet有多强?

&#xff08;图片付费下载自视觉中国&#xff09;作者 | 路一直都在来源 | 知乎专栏Abstract本文提出了一种新的自适应连接神经网络(ACNet)&#xff0c;从两个方面对传统的卷积神经网络(CNNs)进行了改进。首先&#xff0c;ACNet通过自适应地确定特征节点之间的连接状态&#xf…

CUDA Samples: approximate prior vbox layer

以下CUDA sample是分别用C和CUDA实现的类似prior vbox layer的操作&#xff0c;并对其中使用到的CUDA函数进行了解说&#xff0c;各个文件内容如下&#xff1a;common.hpp:#ifndef FBC_CUDA_TEST_COMMON_HPP_ #define FBC_CUDA_TEST_COMMON_HPP_#include <typeinfo> #inc…

如何成为一名成功的 iOS 程序员?

前言&#xff1a; 编程是一个仅靠兴趣仍不足以抵达成功彼岸的领域。你必须充满激情&#xff0c;并且持之以恒地不断汲取更多有关编程的知识。只是对编程感兴趣还不足以功成名就——众所周知&#xff0c;我们工作起来像疯子。 编程是一个没有极限的职业&#xff0c;所以要成为一…

C#之委托与事件

委托与事件废话一堆&#xff1a;网上关于委托、事件的文章有很多&#xff0c;一千个哈姆雷特就有一千个莎士比亚&#xff0c;以下内容均是本人个人见解。1. 委托1.1 委托的使用这一小章来学习一下怎么简单的使用委托&#xff0c;了解一些基本的知识。这里先看一下其他所要用到的…

24式加速你的Python

作者 | 梁云1991来源 Python与算法之美一、分析代码运行时间第1式&#xff0c;测算代码运行时间平凡方法快捷方法&#xff08;jupyter环境&#xff09;第2式&#xff0c;测算代码多次运行平均时间平凡方法快捷方法&#xff08;jupyter环境&#xff09;第3式&#xff0c;按调用函…

pip、NumPy、Matplotlib在Windows上的安装过程

Windows上Python 3.6.2 64位的安装步骤&#xff1a;1. 从 https://www.python.org/downloads/windows/ 下载Windows x86-64 executable installer(即python-3.6.2-amd64.exe)&#xff1b;2. 直接以管理员身份运行安装&#xff0c;勾选添加到环境变量、pip等即可。可以同时在Wi…

分享:个人是怎么学习新知识的

为什么80%的码农都做不了架构师&#xff1f;>>> 挺多童鞋问我是怎么学习新知识的&#xff0c;干脆写篇文章总结一下&#xff0c;希望对大家有所帮助。对照书、技术博客、极客时间等学习的方式我就不说了。 一、早期 在15年及更早&#xff0c;由于知识储备少&#x…

easyui的datagrid

datagrid数据的绑定方式&#xff1a; 1&#xff09;data 后跟数据行的json串 2&#xff09;url 后跟{"total":,"rows":,"foot":},其中total代码返回总行数&#xff0c;rows为数据行json串 .NET MVC&#xff0c;controll控制类方法中获取datagrid…

线性回归介绍及分别使用最小二乘法和梯度下降法对线性回归C++实现

回归&#xff1a;在这类任务中&#xff0c;计算机程序需要对给定输入预测数值。为了解决这个任务&#xff0c;学习算法需要输出函数f:Rn→R。除了返回结果的形式不一样外&#xff0c;这类问题和分类问题是很像的。这类任务的一个示例是预测投保人的索赔金额(用于设置保险费)&am…

4种最常问的编码算法面试问题,你会吗?

导语&#xff1a;面试是测查和评价人员能力素质的一种考试活动。最常问的编码算法面试问题你知道多少呢&#xff1f;作者 | Rahul Sabnis译者 | 苏本如&#xff0c;编辑 | 刘静来源 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;在许多采访中&#xff0c;我经常被要求…

[小梅的体验课堂]Microsoft edge canary mac版本体验

简介 华硕微软越来越没有自己的JC了&#xff0c;不经在windows里面加了wsl而且还废弃了自己的老edge浏览器&#xff0c;重新基于chromium内核开发了新的edge浏览器了&#xff0c;不管怎么说mac上又多了一款新的浏览器&#xff0c;对于一个爱好新鲜的我来说那就简单安装体验下 下…

SQL Server用户自定义函数

用户自定义函数不能用于执行一系列改变数据库状态的操作&#xff0c;但它可以像系统 函数一样在查询或存储过程等的程序段中使用&#xff0c;也可以像存储过程一样通过EXECUTE 命令来执行。在 SQL Server 中根据函数返回值形式的不同将用户自 定义函数分为三种类型&#xff1a;…

C++11中std::initializer_list的使用

initializer_list是一种标准库类型&#xff0c;用于表示某种特定类型的值的数组。和vector一样&#xff0c;initializer_list也是一种模板类型&#xff0c;定义initializer_list对象时&#xff0c;必须说明列表中所含元素的类型。和vector不一样的是&#xff0c;initializer_li…

WijmoJS 2019V1正式发布:全新的在线 Demo 系统,助您快速上手,开发无忧

2019独角兽企业重金招聘Python工程师标准>>> 下载WijmoJS 2019 v1 WijmoJS是为企业应用程序开发而推出的一系列包含HTML5和JavaScript的开发控件。其中包含了金融图表、FlexSheet、先进的JavaScript控件&#xff08;Wijmo 5&#xff09;和经典的jQuery小部件&#x…

最后3天,BDTC 2019早鸟票即将售罄,超强阵容及议题抢先曝光!

大会官网&#xff1a;https://t.csdnimg.cn/U1wA2019 年12月5-7 日&#xff0c;由中国计算机学会主办&#xff0c;CCF 大数据专家委员会承办&#xff0c;CSDN、中科天玑数据科技股份有限公司协办的 2019 中国大数据技术大会&#xff0c;将于北京长城饭店隆重举行。届时&#xf…

php_mongo.dll下载(php操作mongoDB需要)

php_mongo.dll下载&#xff08;php操作mongoDB需要&#xff09;如果PHP连接操作mongoDB就必须要加入此扩展&#xff1a;php_mongo.dll&#xff0c;放到你对应php的扩展目录在你的php.ini中加入&#xff1a;extensionphp_mongo.dll重启apache&#xff0c;在phpinfo()中查看是否有…

十大机器智能新型芯片:华为抢占一席,Google占比最多

&#xff08;图片付费下载自视觉中国&#xff09;整理 | 胡巍巍来源 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;当年&#xff0c;阿基米德爷爷说出“给我一个支点&#xff0c;我就能撬动地球”这句话时&#xff0c;估计没少遭受嘲讽。然而后来的我们&#xff0c;都…

C++/C++11中头文件numeric的使用

<numeric>是C标准程序库中的一个头文件&#xff0c;定义了C STL标准中的基础性的数值算法(均为函数模板)&#xff1a; (1)、accumulate: 以init为初值&#xff0c;对迭代器给出的值序列做累加&#xff0c;返回累加结果值&#xff0c;值类型必须支持””算符。它还有一个…

Spring基础16——使用FactoryBean来创建

1.配置bean的方式 配置bean有三种方式&#xff1a;通过全类名&#xff08;class反射&#xff09;、通过工厂方法&#xff08;静态工厂&实例工厂&#xff09;、通过FactoryBean。前面我们已经一起学习过全类名方式和工厂方法方式&#xff0c;下面通过这篇文章来学习一下Fact…

查看进程 端口

2019独角兽企业重金招聘Python工程师标准>>> 一 进程 ps -ef 1.UID 用户ID2.PID 进程ID3.PPID 父进程ID4.C CPU占用率5.STIME 开始时间6.TTY 开始此进程的TTY7.TIME 此进程运行的总时间8.CMD 命令名 二端口 netstat Linux下如果我…

深度学习中的欠拟合和过拟合简介

通常情况下&#xff0c;当我们训练机器学习模型时&#xff0c;我们可以使用某个训练集&#xff0c;在训练集上计算一些被称为训练误差(training error)的度量误差&#xff0c;目标是降低训练误差。机器学习和优化不同的地方在于&#xff0c;我们也希望泛化误差(generalization …