Swift 5.1:颠覆!将你的代码减少一半



原创 读芯术 2019-08-05 17:01:00

全文共7044字,预计学习时长14分钟

Swift 5.1:颠覆!将你的代码减少一半

图片来源:unsplash.com/@max_duz

Swift 5.1增加了许多新功能,其中一些功能有望彻底改变编写和构建Swift代码的方式。那么,如何使用Swift 5.1 Property Wrappers(属性包装器)将依赖注入代码减少一半?

本文讨论了Swift Property Wrappers,并演示一种可大大简化代码的方法。

Swift 5.1:颠覆!将你的代码减少一半

背景

现代软件开发是一种有关项目管理复杂性的练习,架构是我们试图实现这一练习的方法之一。 反过来,架构实际上只是一个术语,用于描述如何将复杂的软件分解为易于了解的层和组件。

Swift 5.1:颠覆!将你的代码减少一半

因此,我们将软件分解为可以轻松编写的简化组件,只做一件事(单一职责原则,SRP),并且可以轻松测试。

然而,一旦拥有了一堆部件,就必须将所有部件重新连接在一起,才能形成一个工作应用程序。

以正确的方式将部件连接在一起,就能得到一个由松散耦合的组件组成的整洁架构。

但如果连接的方式出错了,最终只能得到一个紧密耦合的乱码,其中的大多数部件都包含许多子组件构建和在内部运作的方法的信息。

这使组件共享几乎不可能实现,并且同样无法轻松地将一个组件层换成另一个组件层。

这样的情况令人左右为难,在尝试简化代码时使用的工具和技术最终却使我们的生活变得更加复杂。

幸运的是,可以使用另一种技术来管理这个额外的复杂层,该技术被称为依赖注入,基于一个称为控制反转的原理。

Swift 5.1:颠覆!将你的代码减少一半

Swift 5.1:颠覆!将你的代码减少一半

依赖注入

本文无法对依赖注入作出完整且详尽的解释,简单来说,即依赖注入允许给定组件向系统要求连接到完成其工作所需的所有部件。

这些依赖项将返回到完全成型并准备使用的组件。

例如,ViewController(视图控制器)可能需要ViewModel(视图模型)。 ViewModel可能需要一个API组件来获取一些数据,这些数据又需要访问身份验证系统和当前的会话管理器。ViewModel还需要一个具有依赖关系的数据转换服务。

ViewController不涉及这些东西,也不应该涉及,只需与它完成工作所需的组件进行对话。

为了演示所涉及的技术,本文将使用一个称为Resolver的强大的轻量型依赖注入系统。如果你使用其它任何DI框架也可。

如果想了解更多信息,请参阅Resolver GitHub存储库上的Dependency Injection指南,以及Resolver本身的相关文档。

传送门:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------

Swift 5.1:颠覆!将你的代码减少一半

简单实例

使用依赖注入的非常基本的视图模型如下所示:

class XYZViewModel {
private var fetcher: XYZFetching
private var service: XYZService
init(fetcher: XYZFetching, service: XYZService) {
self.fetcher = fetcher
self.service = service
}
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}

以上列出的是view model需要的组件,以及一个初始化函数,其作用基本上是将传递给模型的任何组件分配给模型的实例变量。

这称为构造函数注入,使用该函数可确保在无法实例化给定组件时不用给它所需的一切。

现在有了view model之后,如何获得是view controller?

Resolver可以在几种模式下自动解决这个问题,这里最简单的方法是使用一种称为Service Locator的模式......这基本上是一些知道如何定位且请求服务的代码。

class XYZViewController: UIViewController {
private let viewModel: XYZViewModel = Resolver.resolve()
override func viewDidLoad() {
...
}
}

因此viewModel要求Resolver“resolve(解析)”依赖关系。解析器使用提供的类型信息来查找用于创建所请求类型的对象的实例工厂。

请注意,viewModel需要一个fetcher和一个提供给它的服务,但view controller完全不需要这些东西,只需依赖注入系统处理所有这些凌乱的小细节。

此外还有其他一些好处。例如,可以运行“Mock”方案,其中数据层被替换为来自应用程序中嵌入的JSON文件的mock数据,这样的数据在开发,调试和测试时都便于运行。

依赖系统可以在后台轻松处理这类事情,所有的view controller都知道它仍然拥有所需的视图模型。 

Resolver文档示例传送门:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------

最后请注意,在依赖注入术语中,依赖关系通常称为服务。

注册

为了使典型的依赖注入系统工作,服务必须注册,需要提供与系统可能要创建的每种类型相关联的工厂方法。

在某些系统中,依赖项被命名,而在其他系统中,必须指定依赖项类型。但是,解析器通常可以推断出所需的类型信息。

因此,解析器中的典型注册块可能如下所示:

func setupMyRegistrations {
register { XYZViewModel(fetcher: resolve(), service: resolve()) }
register { XYZFetcher(session: resolve()) as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}

注意第一个注册函数注册XYZViewModel并提供一个工厂函数来创建新的实例。 注册的类型由工厂的返回类型自动推断。

XYZViewModel初始化函数所需的每个参数也可以通过再次推断类型签名并依次解析来解决。

第二个函数注册XYZFetching协议,通过构建具有自己的依赖关系的XYZFetcher实例来满足该协议。

该过程以递归方式重复,直到所有部件都具有初始化所需的所有部件并执行他们需要的操作。

Swift 5.1:颠覆!将你的代码减少一半

问题

Swift 5.1:颠覆!将你的代码减少一半

化繁为简 图片来源:unsplash.com/@emileseguin

然而,大多数现实生活中的程序都是复杂的,因此初始化函数可能会开始失控。

class MyViewModel {
var userStateMachine: UserStateMachine
var keyValueStore: KeyValueStore
var bundle: BundleProviding
var touchIdService: TouchIDManaging
var status: SystemStatusProviding?
init(userStateMachine: UserStateMachine,
bundle: BundleProviding,
touchID: TouchIDManaging,
status: SystemStatusProviding?,
keyValueStore: KeyValueStore) {
self.userStateMachine = userStateMachine
self.bundle = bundle
self.touchIdService = touchID
self.status = status
self.keyValueStore = keyValueStore
}
...
}

初始化函数中有相当多的代码,这是是必需的,但所有代码都是样板文件。如何避免这种情况?

Swift 5.1:颠覆!将你的代码减少一半

Swift 5.1和Property Wrappers

幸运的是,Swift 5.1为我们提供了一个新工具称为Property Wrappers(正式称为“property delegates”),该工具作为提案SE-0258的一部分在Swift论坛上提出,并添加到Swift 5.1和Xcode 11中。

Property Wrapper的新功能使属性值能够使用自定义get / set实现自动包装,因此得名。

请注意,可以使用属性值上的自定义getter和setter来执行其中一些操作,但缺点是必须在每个属性上编写几乎相同的代码,即更多样板文件。如果每个属性都需要某种内部支持变量,那就更糟了。 (还有更多样板文件。)

@Injected Property Wrapper

因此,在get / set对中自动包装属性听起来并不令人兴奋,但属性包装器将对我们的Swift代码产生重大影响。

为了演示,我们将创建一个名为@Injected的Property Wrappers并将其添加到代码库中。 

现在,回到“失控”示例,看看全新的物业包装给我们带来了什么。

class MyViewModel {
@Injected var userStateMachine: UserStateMachine
@Injected var keyValueStore: KeyValueStore
@Injected var bundle: BundleProviding
@Injected var touchIdService: TouchIDManaging
@Injected var status: SystemStatusProviding?
...
}

就是这样。 只需将属性标记为@Injected,每个属性将根据需要自动解析(注入),由此初始化功能中的所有样板代码都消失了!

此外,现在从@Injected注释中可以清楚地看出依赖注入系统提供了哪些服务。

这种特殊类型的注释方案在其他语言上应用时,最明显的是在Android上的Kotlin中编程以及使用Dagger 2依赖注入框架。

履行

属性包装器实现很简单。 我们使用Service类型定义一个通用结构,并将其标记为@propertyWrapper。

@propertyWrapper
struct Injected<Service> {
private var service: Service?
public var container: Resolver?
public var name: String?
public var value: Service {
mutating get {
if service == nil {
service = (container ?? Resolver.root).resolve(
Service.self,
name: name
)
}
return service!
}
mutating set {
service = newValue
}
}
}

所有属性包装器都必须实现一个名为value的变量。

当从变量请求或赋值时,Value提供属性包装器使用的getter和setter实现。

在这种情况下,服务被请求时,我们的值“getter”将检查这是否是第一次被调用。 如果是这样,当访问包装器代码时,请求Resolver根据泛型类型解析所需服务的实例,将结果存储到私有变量中供以后使用,并返回该服务。

当想要手动分配服务时,我们还提供了一个setter。 在某些情况下,这可以派上用场,最值得注意的是在进行单元测试时。

该实现还公开了一些额外的参数,如名称和容器,更多的是在一秒钟内实现。

更多实例

属性包装器的实现很简单。使用服务类型定义一个通用结构,并将其标记为@propertyWrapper。

class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
override func viewDidLoad() {
...
}
}

将ViewModel精简到最基本的代码为: 

class XYZViewModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
func load() -> Image {
let data = fetcher.getData(token)
return service.decompress(data)
}
}

至注册码也被简化,因为构造函数参数被左右删除......

func setupMyRegistrations {
register { XYZViewModel() }
register { XYZFetcher() as XYZFetching }
register { XYZService() }
register { XYZSessionManager()
}

命名服务类型

解析器支持命名类型,它允许程序区分相同类型的服务或协议。

这也展示了一个有趣的property wrappers属性,让我们来看看这个。

常见的用例可能需要两种不同视图模型中的view controller,该选择取决于它是否已经传递数据,因此应该以“添加”或“编辑”模式操作。

注册可能如下所示,两个模型都符合XYZViewModel协议或基类。

func setupMyRegistrations {
register(name: "add") { NewXYZViewModel() as XYZViewModel }
register(name: "edit") { EditXYZViewModel() as XYZViewModel }
}

然后在view controller中:

class XYZViewController: UIViewController {
@Injected private var viewModel: XYZViewModel
var myData: MyData?
override func viewDidLoad() {
$viewModel.name = myData == nil ? "add" : "edit"
viewModel.configure(myData)
...
}
}

请注意viewDidLoad中引用的$ viewModel.name。

在大多数情况下,我们希望Swift假装包装的值是属性的实际值。但是,使用美元符号为属性包装器添加前缀使我们可以引用属性包装器本身,从而获得对可能公开的任何公共变量或函数的访问权限。

在这种情况下,设置name参数,第一次尝试使用视图模型时,该参数将传递给Resolver。解析器将在解析依赖关系时传递该名称。

简而言之,在属性包装器上使用$前缀可以让我们操纵和/或引用包装器本身。你会在SwiftUI中看到很多这样的东西。

Swift 5.1:颠覆!将你的代码减少一半

为什么是“注入”?

不少人会问:为什么使用“注入”一词?既然代码使用Resolver,为什么不将它标记为@Resolve?

理由很简单。我们现在正在使用Resolver,主要是因为我们写了它。但我们可能想在另一个应用程序中共享或使用我的一些模型或服务代码,并且该应用程序可能使用不同的系统来管理依赖注入。比如,Swinject Storyboard。

“注入“成为一个更中性的术语,需要做的就是提供一个新版本的@Injected属性包装器,使用Swinject作为后端,一旦使用,就可固定化。

其他用例

Property Wrappers将来的更多用途体现在Swift上。

SwiftUI广泛使用依赖注入,除此之外,Cocoa和UIKit中的标准类提供了一些额外的包装器也不足为奇。

我们会想到围绕用户默认值和钥匙串访问的常见包装器。想象一下用下列代码包装任何属性:

@Keychain(key: "username") var username: String?

并从钥匙串自动获取支持你的数据。

过度使用

然而,就像任何酷炫的新锤子一样,我们冒着过度使用它的风险,因为每个问题看起来都像钉子一样。

有一次所有东西都变成了协议,然后开始了解何时能最好地使用协议(比如数据层代码),然后再退出。 在此之前,C ++添加了自定义运算符,我们突然试图找出user1 + user2的结果可能是什么?

实现Property Wrappers时的关键问题是问自己:我是否会在所有代码库中广泛使用这个包装器? 如果是这样,那么Property Wrappers可能是个不错的选择。

或者至少减少其占用的空间。 如果创建一个如上所示的@Keychain包装器,可以在与KeychainManager类相同的文件中将它实现为fileprivate,从而避免在整个代码中到处随意穿插。

毕竟,现在使用它简单得就像:

@Injected var keychain: KeychainManager

我们不想要每个模型看起来都像这样的版本:

class MyModel {
@Injected private var fetcher: XYZFetching
@Injected private var service: XYZService
@Error private var error: String
@Constrain private var myInt: Int
@Status private var x = 0
@Status private var y = 0
}

然后让下一个查看代码的开发人员争先恐后地弄清楚每个包装器的作用。

Swift 5.1:颠覆!将你的代码减少一半

完成块

property wrappers只是Swift5.1和Xcode 11中引入的许多功能之一,有望彻底改变编写Swift应用程序的方式。

SwiftUI和Combine得到了媒体大幅的关注,但特别是在真正开始使用SwiftUI和Combine之前,property wrappers就将大大减少在日常编程中编写的样板代码量。

与SwiftUI和Combine不同,property wrappers可以在早期版本的iOS上使用! 不只是iOS 13。


0