过载(overloaded)
对象需要表示的信息: 类型、身份、行为、状态、值
对象具有的用来表示信息的方法: 类、实例化、方法、属性
这就带来了一个问题,属性需要用来表示状态和值两种信息。
下面是一段喜闻乐见的代码:
|
|
这段代码的意思是:如果现在网络请求,就发起一个。
看起来可能没有什么问题,但这确会是一场灾难。比如说我们需要添加更多的 CRUD 操作,比如说做一个保存的功能,这时候需要发起一个 POST 请求。我们并不需要在请求完成之前在获取到新的数据:
|
|
这段代码虽然变丑了,但依然是可控的。但是如果再增加一些操作呢?这个判断可能最多会变成 2^n 个分支了。
补足
这个问题的核心问题在于 fetchTask
在这里其实是有两种用途的。我们使用它要表示一个值(网络请求任务),也用它来表示一种状态(当前是否在发起请求)。这两种的关联程度是很高的,这种事情其实经常会发生。
有 savaTask
的时候,这个属性在表示了一个任务的同时也表示了另外的一种状态,这时候,就不仅仅是值和状态在同一个属性的问题了,在这种情况下,状态还分散在了多个不同的属性中。
解决的办法就是抽象,因为我们只有属性这一个锤子,值和状态就像是两个钉子,如果我们再引入一个新的东西来表示状态这个问题就解决了。
这个抽象长什么样呢?
状态机
Wiki 百科有状态机的介绍, 这篇文章将不会在概念上做过多的描述。总的来说,状态机是一种抽象的数据结构。他是一组状态,和这些状态之间的转换规则。
举个例子,现在有 5 种状态: Ready
、Fetching
、Saving
、Success
、failure
我们需要一个机器来模型化这 5 中可能的状态,以及这五种状态之间的转换规则。(比如: 可以从 Ready 转换到 Fetching 但不能从 Saving 转换到 Fetching, 也不能从 Fetching 转到 Fetching)
然后我们不仅有了一个规范的位置来找到对象不同的状态,而且我们在状态机种构建出来的规则也能避免对象进入无效的状态。这样的话,管理某个对象的状态就会变的更简单。
Swift 状态机
使用现有的状态机库的最大障碍是让他们可配置 。在让我们从这些东西中解放出来之前,我们首先需要定义所有的状态,以及这些状态之间的转换。这需要很多很丑的工厂,多余的样板,甚至是 XML。
首先我们来定义这个状态机:
|
|
接下来我们会定义一些代理,首先我们需要知道定义这个代理是用来做什么的:
- 告诉我们当前的状态是什么
- 决定新的状态是否合理
- 在状态转换之后执行什么任务(可以没有任务)
|
|
需要注意的是⚠️,
didTransitionFrom(to:)
方法可能需要被标记为optional
。- 这个协议应该是 class-only 的。这也是代理协议的习惯写法。从哲学层面上将,值语义的代码并不会有什么意义。
- 代码属性几乎全部都应该用
weak
或者unowned
修饰。
接下来我们把这个协议加入状态机中:
|
|
需要切换状态,以及成功切换状态之后就必须要调用代理的方法。
Swift 不能在储存属性的 setter 方法中设置这个属性的值,所以我们还需要一个公有的计算属性来做类似的操作。
|
|
然后再修改切换状态之后调用另外一个代理方法
|
|
最后,由于设置这个状态的初始值并不是我们概念中的状态转换,所以并不需要在这个时候调用 shouldTransitionFrom(to)
方法。所以在状态机的初始化方法中,我们直接给 _state
这个储存属性复制。最后代码长这样:
|
|
传统观念
上面的状态机看起来还不错吧,但是应该怎么用呢?
由于这里的代理方法是很通用的,如何使用其实是没有标准答案的,但是从我稍前的经验看来, Swift 有很多还不错的工具来帮我们实现这个,如枚举、元组、switch 语句。
首先要做的是定义我们需要的状态:
|
|
然后,通过Swift 以及元组,来实现 shouldTransitionFrom(to:)
方法
|
|
我们先看下面的代码,default 语句直接让那些没有明确定义好的状态之间的切换永远不会发生。
再看上面一点,从准备状态到其他任何异步状态都是允许的,然后从任何的异步状态转换到结果状态也都是允许的。在这里我把他们分别写在不同的case
里面只是为了代码的可读性。
最后所有其他状态转换到准备状态都是允许的。
使用状态
我们使用类似的方法来实现另外一个方法:
|
|
这这里,当状态从准备转换到任何的异步状态的时候,会分别发起对应的请求。还记得最开始的情况吗?我们现在已经把状态的表示从 task
这个属性中剥离出来了。现在我们其实可以在 fetching 的事件中写一行:
|
|
因为上文中我们已经提到过从 .fetching
到 .fetching
是不允许的转换,所以无论这个方法被调用了多少次,也只可能有一个网络请求被发起。
还有一个好处是,我们可以使用枚举的关联值来传递数据,进一步改造,相应的方法应该是这样:
|
|
网络请求的结果回调,跟其他的事件一样,也能够被简化成状态的转换。这样的话,程序的交互就会变得非常的简单了,我们不需要再去关注是什么行为导致了界面的更新,看看到底是什么状态更新了UI是很简单的问题。