Swift 声明式程序设计

本文讲的是Swift 声明式程序设计,


在我第一份 iOS 开发工程师的工作中,我编写了一个 XML 解析器和一个简单的布局工具,两个东西都是基于声明式接口。XML 解析器是基于 .plist 文件来实现 Objective-C 类关系映射。而布局工具则允许你利用类似 HTML 一样标签化的语法来实现界面布局(不过这个工具使用的前提是已经正确使用AutoLayout & CollectionViews)。

尽管这两个库都不完美,它们还是展现了声明式代码的四大优点:

  • 关注点分离: 我们在使用声明式风格编写的代码时声明了意图,从而无需关注具体的底层实现,可以说这样的分离是自然发生的。
  • 减少重复的代码: 所有声明式代码都共用一套样式实现,这里面很多属于配置文件,这样可以减少重复代码所带来的风险。
  • 优秀的 API 设计: 声明式 API 可以让用户自行定制已有实现,而不是将已有实现做一种固定的存在看待。这样可以保证修改程度降至最小。
  • 良好的可读性: 讲真,按照声明式 API 所写出来的代码简直优美无比。 这些天我写的大多数 Swift 代码非常适用于声明式编程风格。

不管是对于某一种数据结构的描述,或者是对某个功能的实现,在编写过程中,我最常使用的类型还是一些简单的结构体。声明不同的类型,主要是基于泛型类,然后这些东西负责实现具体的功能或者完成必要的工作。我们在 PlanGrid 开发过程中采用这种方法来编写我们得 Swift 代码。这种开发方式已经对对代码可读性的提升还有开发人员的效率提升上产生了巨大的影响。

本文我想讨论的是 PlanGrid 应用中所使用的 API 设计,它原本使用 NSOperationQueue 实现,现在使用了一种更接近声明式的方法-讨论这个 API 应该可以展示声明式编程风格在各方面的好处。

在 Swift 中构建一个声明式请求序列

我们重新设计的 API 用来将本地变化(也可能是离线发生的)与 API 服务器进行同步。我不会讨论这种变化追踪方法的细节,而是将精力放在网络请求的生成和执行上。

在这篇文章里,我想专注于一个特定的请求类型上:上传本地生成的图片。出于多种因素的考虑(超出本文讨论范围),上传图片的操作包括三次请求:

  1. 向 API 服务器发起请求,API 服务器将会响应,响应内容为向 AWS 服务器上传图片所需信息。
  2. 上传图片至 AWS (使用上次请求得到的信息)。
  3. 向 API 服务器发起请求以确认图片上传成功。

既然我们有包括这些请求序列的上传任务,我们决定将其抽象成一个特殊的类型,并让我们的上传架构支持它。

定义请求序列协议

我们决定引入一个单独的类型来对网络请求序列进行描述。这个类型将被我们的上传者类使用,上传者类的作用是将描述转化为实在的网络请求(要提醒你们的是我们不会在本篇文章中讨论上传者类的实现)。

接下来这个类型是我们控制流的精髓:我们有一个请求序列,序列中的每个请求都可能依赖于前一个请求的结果。

小贴士: 接下来的代码里的一些类型的命名方式看起来有点奇怪,但是它们中大多数是根据应用专属术语集来命名的(如: Operation )。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16


public typealias PreviousRequestTuple = (

request: PushRequest,

response: NSURLResponse,

responseBody: JsonValue?

)

/// A sequence of push requests required to sync this operation with the server.

/// As soon as a request of this sequence completes,

/// `PushSyncQueueManager` will poll the sequence for the next request.

/// If `nil` is returned for the `nextRequest` then

/// this sequence is considered complete.

public protocol OperationRequestSequence: class {

/// When this method returns `nil` the entire `OperationRequestSequence`

/// is considered completed.

func nextRequest(previousRequest: PreviousRequestTuple?) throws -> PushRequest?

}

通过调用 nextRequest: 方法来让请求序列生成一个请求时,我们提供了一个对前一个请求的引用,包括 NSURLResponse 和 JSON 响应体(如果存在的话)。每一个请求的结果都可能在下一次请求时产生((将会返回一个 PushRequest 对象),除了没有下一次请求(返回 nil )或者在请求过程中发生了一些以外的情况导致没有返回必要的响应以外(请求序列在该情况下 throws )。

值得注意的是, PushRequest 并不是这个返回值类型的理想名。这个类型只是描述一个请求的详情(结束符,HTTP 方法等等),其并不参与任何实质性的工作。这是声明式设计中很重要的一个方面。

你可能已经注意到了这个协议依赖于一个特定 class ,我们这样做是因为我们意识到OperationRequestSequence 其是一个状态描述类型。它需要能够捕获并使用前面的请求所产生的结果(比如:在第三个请求里可能需要获取第一个请求的响应结果)。这个做法参考了 mutating 方法的结构,不得不说这样的行为貌似让这部分有关上传操作的代码变得更为复杂了(所以说重新赋值变化结构体并不是一件那么简单的事儿)

在基于 OperationRequestSequence 协议实现了我们第一个请求序列后,我们发现相比实现nextRequest 方法来说,简单地提供一个数组来保存请求链更合适。于是我们便添加了ArrayRequestSequence 协议来提供了一个请求数组的实现:


1

2

3

4

5

6

7

8

9

10

11

12

13

14


public typealias RequestContinuation = (previous: PreviousRequestTuple?) throws -> PushRequest?

public protocol ArrayRequestSequence: OperationRequestSequence {

var currentRequestIndex: Int { get set }

var requests: [RequestContinuation] { get }

}

extension ArrayRequestSequence {

public func nextRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {

let nextRequest = try self.requests[self.currentRequestIndex](previous: previous)

self.currentRequestIndex += 1

return nextRequest

}

}

这个时候,我们定义了一个新的上传序列,这只是很微小的一点工作。

实现请求序列协议

作为一个小例子,让我们看看用来上传快照的上传序列吧(在 PlanGrid 中,快照指的是在图片中绘制的可导出的蓝图或者注释):


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85


/// Describes a sequence of requests for uploading a snapshot.

final class SnapshotUploadRequestSequence: ArrayRequestSequence {

// Removed boilerplate initializer &

// instance variable definition code...

// This is the definition of the request sequence

lazy var requests: [RequestContinuation] = {

return [

// 1\. Get AWS Upload Package from API

self._allocationRequest,

// 2\. Upload Snapshot to AWS

self._awsUploadRequest,

// 3\. Confirm Upload with API

self._metadataRequest

]

}()

// It follows the detailed definition of the individual requests:

func _allocationRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {

// Generate an API request for this file upload

// Pass file size in JSON format in the request body

return PushInMemoryRequestDescription(

relativeURL: ApiEndpoints.snapshotAllocation(self.affectedModelUid.value),

httpMethod: .POST,

jsonBody: JsonValue(values:

[

"filesize" : self.imageUploadDescription.fullFileSize

]

),

operationId: self.operationId,

affectedModelUid: self.affectedModelUid,

requestIdentifier: SnapshotUploadRequestSequence.allocationRequest

)

}

func _awsUploadRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {

// Check for presence of AWS allocation data in response body

guard let allocationData = previous?.responseBody else {

throw ImageCreationOperationError.MissingAllocationData

}

// Attempt to parse AWS allocation data

self.snapshotAllocationData = try AWSAllocationPackage(json: allocationData["snapshot"])

guard let snapshotAllocationData = self.snapshotAllocationData else {

throw ImageCreationOperationError.MissingAllocationData

}

// Get filesystem path for this snapshot

let thumbImageFilePath = NSURL(fileURLWithPath:

SnapshotModel.pathForUid(

self.imageUploadDescription.modelUid,

size: .Full

)

)

// Generate a multipart/form-data request

// that uploads the image to AWS

return AWSMultiPartRequestDescription(

targetURL: snapshotAllocationData.targetUrl,

httpMethod: .POST,

fileURL: thumbImageFilePath,

filename: snapshotAllocationData.filename,

operationId: self.operationId,

affectedModelUid: self.affectedModelUid,

requestIdentifier: SnapshotUploadRequestSequence.snapshotAWS,

formParameters: snapshotAllocationData.fields

)

}

func _metadataRequest(previous: PreviousRequestTuple?) throws -> PushRequest? {

// Generate an API request to confirm the completed upload

return PushInMemoryRequestDescription(

relativeURL: ApiEndpoints.snapshotAllocation(self.affectedModelUid.value),

httpMethod: .PUT,

jsonBody: self.snapshotMetadata,

operationId: self.operationId,

affectedModelUid: self.affectedModelUid,

requestIdentifier: SnapshotUploadRequestSequence.metadataRequest

)

}

}

在实现的过程中你应该注意这样几件事情:

  • 这里面几乎没有命令式代码。大多数的代码都通过实例变量和前次请求的结果来描述网络请求。
  • 代码并不调用网络层,也没有任何上传操作的类型信息。它们只是对每个请求的详情进行了描述。事实上,这段代码没有能被观测到的副作用,它只更改了内部状态。
  • 这段代码里可以说没有任何的错误处理代码。这个类型只负责处理该请求序列中发生的特定错误(比如前次请求并未返回任何结果等)。而其余的错误通常都在网络层予以处理了。
  • 我们使用 PushInMemoryRequestDescription/AWSMultipartRequestDescription 来对我们对自己的 API 服务器或者是对 AWS 服务器发起请求的行为进行抽象。我们的上传代码将会根据情况在两者之前进行切换,对两者使用不同的 URL 会话配置,以免将我们自有 API 服务器的认证信息发送至 AWS 。

我不会详细讨论整个代码,但是我希望这个例子能充分展现我之前提到过的声明式设计方法的一系列优点:

  • 关注点分离: 上面编写的类型只有描述一系列请求这一单一功能。
  • 减少重复的代码: 上面编写的类型里面只包含对请求进行描述的代码,并不包含网络请求及错误处理的代码。
  • 优秀的 API 设计: 这样的 API 设计能有效的减轻开发者的负担,他们只需要实现一个简单的协议以确保后续产生的请求是基于前一个请求结果的即可。
  • 良好的可读性: 再次声明,以上代码非常集中;我们不需要在样板代码的海洋里游泳,就可以找到代码的意图。那也说明,为了更快地理解这段代码,你需要对我们的抽象方式有一定的了解。

现在可以想想如果利用 NSOperationQueue 来替代我们的方案会怎么样?

什么是 NSOperationQueue ?

采用 NSOperationQueue 的方案复杂了很多,所以在这篇文章里给出相对应的代码并不是一个很好的选择。不过我们还是可以讨论下这种方案。

关注点分离在这种方案中难以实现。和对请求序列进行简单抽象不同的是,NSOperationQueue 中的NSOperations 对象将负责网络请求的开关操作。这里面包含请求取消和错误处理等特性。在不同的位置都有相似的上传代码,同时这些代码很难进行复用。在大多数上传请求被抽象成一个NSOperation 的情况下,使用子类并不是一个好选择,虽然说我们得上传请求队列被抽象成为一个被 NSOperationQueue所装饰的 NSOperation 。

NSOperationQueue 中的无关信息相当多。。代码中随处可见对网络层的操作和调用 NSOperation 中的特定方法,比如 main 和 finish 方法。在没有深入了解具体的 API 调用规则前,很难知道具体操作是用来做什么的

这种 API 所采用的处理方式,某种意义上让开发者的开发体验变得更差了。和简单的实现相对应的协议不同的是,在 Swift 中如果采用上述的开发方式,人们需要去了解一些约定俗成的规定,尽管这些规定可能并不强制要求你遵守。

这种处理方式将会显著增加开发者的负担。与实现一个简单协议不同的是,在新版本的 Swift 中实现这样的代码的话,我们需要去理解一些特有的约定。尽管很多被记载下来的约定并不是与编程相关的。

由于一些其他原因,该 API 可能会导致一些与网络请求的错误报告相关的 bug 。为了避免每个请求操作都执行自己的错误报告代码,我们将其集中在一个地方进行处理。错误处理代码将会在请求结束之后开始执行。然后代码将会检查请求类型中的 error 属性的值是否存在。为了及时地反馈错误信息,开发者需要及时在操作完成之前设置 NSOperation 中的 error 属性的值。由于这是一个非强制性约定导致一堆新代码忘记设置其属性的值,可能会导致诸多错误信息的遗失。

所以啊,我们很期待我们介绍的这样一种新的方式能帮助开发者们在未来编写上传及其余功能的代码。

总结

声明式的编程方法已经对我们的编程技能和开发效率产生了巨大的影响。我们提供了一种受限的 API ,这种 API 用途单一且不会留下一堆迷之 Bug 。我们可以避免使用子类及多态等一系列手段,转而使用基于泛型类型的声明式风格代码来替代它。我们可以写出优美的代码。我们所编写的代码都是能很方便的进行测试的(关于这点,编程爱好者们可能觉得在声明式风格代码中测试可能不是必要的。)所以你可能想问:“别告诉我这是一种完美无瑕的编程方式?”

首先,在具体的抽象过程中,我们可能会花费一些时间与精力。不过,这种花费可以通过仔细设计 API ,并并通过提供一些测试,代替用例实现功能,为使用者提供参考。

其次,请注意,声明式编程并不是适用于任何时间任何业务的。要想适用声明式编程,你的代码库里至少要有一个用相似方法解决了多次的问题。如果你尝试在一个需要高度可定制化的应用里使用声明式编程, 然后你又对整个代码进行了错误的抽象,那么最后你会得到如同乱麻一般的声明式代码。对于任何的抽象过程而言,过早地进行抽象都会造成一大堆令人费解的问题。

声明式 API 有效地将 API 使用者身上的压力转移至 API 开发者身上,对于命令式 API 则不需要这样。为了提供一组优秀的声明式 API ,API 的开发者必须确保接口的使用与接口的实现细节进行严格的隔离。不过严格遵循这样要求的 API 是很少的。React 和 GraphQL 证明了声明式 API 能有效提升团队编码的体验。

其实我觉得,这只是一个开端,我们会慢慢发现在复杂的库中所隐藏复杂的细节和对外提供的简单易用的接口。期待有一天,我们能利用一个基于声明式编程的 UI 库来构建我们的 iOS 程序。





原文发布时间为:2016年10月30日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-09-22 05:44:28

Swift 声明式程序设计的相关文章

为MapReduce框架使用SQL类语言:使用高级声明式接口让Hadoop易于使用

简介 在过去二十年中,计算能力的稳步增强催生了铺天盖地的数据量,这反过来引起计算架构和大型数据处理机 制的范式转换.例如,天文学中的强大望远镜.物理学中的粒子加速器.生物学中的基因组测序系统都将海量数据交到了科 学家手中.Facebook 每天会收集 15TB 的数据到 PB 级的数据仓库中.在业界(例如,Web 数据分析.点击流分析和网络 监控日志分析)和科学界(例如,大规模模拟产生的数据的分析.传感器部署以及高吞吐量实验室设备),对大型数据挖掘 和数据分析应用的需求都在增加.尽管并行数据库系

JS声明式函数与赋值式函数实例分析_javascript技巧

本文实例讲述了JS声明式函数与赋值式函数.分享给大家供大家参考,具体如下: 引言 "程序是不会骗人的"我们项目中的一个哥们经常这样说,为什么他会有这样的感叹呢?就是有时候我么程序员会出现的这样的问题,当我们让别人来调试错误的时候,别人什么都没有说,在我们给人家复现错误的时候发现,错误竟然没有了,留下自己在风中凌乱.此处中枪的童鞋们请顶起来......下面说说小编给别人调BUG时候遇到的问题如下: 请听题:说出下面几段js脚本的结果是什么? <script type="t

使用decj简化Web前端开发(三) 声明式国际化

引言 本期将介绍decj的国际化(I18N)支持,包括声明式多语言支持和声明式CSS文件动态按需加载. 声明式CSS文件按需加载 使用decj框架,开发人员只需要在模块定义中声明模块所需的各个CSS文件,即可实现这些CSS在该模块被加载时而被动态加载,而无需事先在页面中添加link标签来引用各个CSS文件. 模块定义的css属性用于声明模块所需的各个CSS文件.该属性值是一个字符串数组,其各个元素为所要加载的CSS文件的URL.如果只需要加载一个CSS文件,css属性的值也可以是一个字符串.如清

使用decj简化Web前端开发(二) 声明式表单增强和页面初始化

引言 表单(Form)是Web应用中数据展现和收集常用的HTML元素.开发人员经常需要处理表单的数据填充.数据校验和格式化以及数据打包.另外,页面在加载完毕后往往需要执行一段初始化逻辑.本期将介绍decj对HTML表单的声明式增强和声明式页面/模块初始化这2个特性. 声明式表单功能增强 decj以声明式编程的方式对表单数据展现和收集功能进行增强.在数据展现方面,decj支持根据指定的数据自动将数据填充到表单中.对表单字段值进行自动格式化.在数据收集方面,decj支持对表单字段值进行自动校验.对表

WF从入门到精通(第十六章):声明式工作流

学习完本章,你将掌握: 1.理解过程式(imperative)工作流模型和声明式(declarative)工作流模型之间的主要区别 2.创建声明式工作流 3.使用XAML XML词汇来创建工作流 4.调入基于XAML的工作流并执行 许多开发者或许并不知道WF既能用基于过程化的定义来执行工作流(使用工作流视图设计器)也能用基于声明式的定义来执行工作流(工作流使用XML来进行定义). 每一种风格都有优点.当你使用我们贯穿本书已使用过的技术来创建你的工作流应用程序的时候,工作流模型实际上是被编译进了一

模块化Java:声明式模块化

前一篇文章,<模块化Java: 动态模块化>描述了如何通过使用服务 (service)给应用程序带来动态模块化特性.它们是通过输出的一个(或多个 )可以在运行时被动态发现的接口而实现的.尽管这种方式使得client和server 完全解耦,但是又带来一个如何(何时)启动服务的问题. 启动顺序 在彻头彻尾的动态系统里,服务不仅可以在系统运行的时候装卸,还可以以 不同的顺序启动.有时,这是个大问题:无论A和B的启动顺序如何,在系统达到 就绪状态并准备好接收事件之前,如果没有事件(或线程)出现,那么

Spring中的四种声明式事务的配置

Spring中的四种声明式事务的配置Spring容器中有两种思想很重要,也就是我们常用的Ioc和Aop,如果理解了这两种思想,对于我们学习设计模式和编程有很大的帮助,美国四人帮(GOF)写的设计模式中,有很多都用到了Ioc的思想.简单的说就是依赖注入的思想.常见的一种情况:如果一个类中要复用另外一个类中的功能时,我们可能会首先想到继承,如果你知道Ioc这种思想的话,我想你不会用继承,你会马上想到把要用到功能抽取出来,在我们要用到的类中只需通过set方法简单的注入就可以了,其实这里用到了对象的组合

Spring源代码解析(六):Spring声明式事务处理

我们看看Spring中的事务处理的代码,使用Spring管理事务有声明式和编程式两种方 式,声明式事务处理通过AOP的实现把事物管理代码作为方面封装来横向插入到业务代码 中,使得事务管理代码和业务代码解藕.在这种方式我们结合IoC容器和Spirng已有的 FactoryBean来对事务管理进行属性配置,比如传播行为,隔离级别等.其中最简单的方 式就是通过配置TransactionProxyFactoryBean来实现声明式事物: 在整个源代码分析中,我们可以大致可以看到Spring实现声明式事物

Spring声明式事务管理源码解读之事务提交

在下面的文章中,我讲会多次提到第一篇文章,第一篇文章是:Spring声明式事务管 理源码解读之事务开始 如果要理解事务提交的话,理解事务开始是一个前提条件,所以请先看第一篇文章,再 来看这篇 如果你仔细看下去,我想肯定是有很多收获,因为我们确实能从spring的代码和思想 中学到很多东西. 正文: 其实俺的感觉就是事务提交要比事务开始复杂,看事务是否提交我们还是要回到 TransactionInterceptor类的invoke方法 Java代码 public Object invoke(Met