状态机很少被移动开发人员使用。 尽管大多数人都知道工作原理,并且可以轻松地独立实施。 在本文中,我们将以iOS应用程序为例,说明状态机可以解决哪些任务。 这个故事在自然界中得到应用,并致力于作品的实际方面。
在剪辑后,您会发现Alexander
Sychev在
AppsConf上的演讲全文(
Brain89 ),其中他分享了他在使用状态机开发非游戏应用程序时的选择。
关于演讲者: Alexander Sychev从事iOS开发已有八年了,在此期间,他参与了社交网络和金融部门的简单应用程序和复杂客户端的创建。 目前,是Sberbank的技术主管。
他们来自许多领域,具有不同的背景和经验来进行编程,因此首先我们回顾一下基本理论。
问题陈述

状态机是一种数学抽象,它包含三个主要元素:
- 许多内部状态
- 确定从当前状态到下一个状态的过渡的一组输入信号,
- 最终状态集,在此状态下自动机完成其工作(“允许输入单词x”)。
条件
状态指的是一个变量或一组确定对象行为的变量。 例如,在标准的iOS应用程序“ Settings
”中,有一个“ Bold”项(“ Basic→Universal access”)。 此项的值使您可以在两个选项之间切换,以在设备显示屏上显示文本。

通过发送相同的信号“更改拨动开关的值
” ,我们对系统产生了不同的反应:普通字体还是粗体-一切都很简单。 处于不同状态并接收相同信号的对象对状态变化的反应不同。
传统任务
实际上,程序员经常遇到有限状态机。
游戏应用
这是我想到的第一件事-作为游戏的一部分,几乎所有内容都由当前游戏状态决定。 因此,Apple假设主要在游戏应用程序中使用状态机(我们将在后面详细讨论)。
下面的示例说明了当处理相同信号但内部状态不同时系统的行为。 例如:
●游戏角色可以具有不同的优势:一种使用机械装甲并使用激光枪,而另一种使用弱泵。 根据这种状态,确定敌人的行为:他们攻击或逃跑。

●游戏已暂停-无需绘制当前帧; 玩家在菜单模式或游戏过程中-呈现方式完全不同。
文字分析
与状态机的使用相关的最流行的文本分析任务之一是垃圾邮件过滤器。 设置一组停用词和一个输入序列。 您必须过滤此序列或根本不显示它。

正式地,这是在字符串中查找子字符串的任务。 为了解决这个问题,使用了Knut-Morris-Pratt算法,其软件实现是一个有限状态机。 状态是输入序列的偏移量和在模式中找到的字符数-停用词。
同样,
在分析正则表达式时 ,经常使用有限状态机。
并行查询处理
状态机是用于实现请求处理和执行严格的指令集的选项之一。

例如,在Nginx Web服务器中,使用状态机处理各种协议的输入请求。 根据特定的协议,选择状态机的特定实现,并相应地执行一组众所周知的指令。
通常,获得两类问题:
- 管理具有复杂内部状态的复杂对象的逻辑,
- 控制和数据流的形成(算法说明)。
显然,在任何程序员的实践中都会遇到这样的常见任务。 因此,可以使用状态机,包括在非游戏内容应用程序中使用的状态机,大多数移动开发人员都涉及到该状态机。
接下来,我们将分析在何时何地可以使用状态机创建典型的iOS应用程序。
大多数移动应用程序具有分层体系结构。 有三个基础层。
- 表示层。
- 业务逻辑层。
- 一组帮助程序,网络客户端等(核心层)。
如上所述,状态机控制具有复杂行为的对象,即 条件复杂。 这样的对象肯定在表示层中,因为它通过处理用户输入或来自操作系统的消息来做出决定。 让我们看一下执行它的不同方法。

在Model-View-Controller的经典体系结构隐喻中,状态将在控制器中:状态决定了View中显示的内容以及如何响应输入信号:按下按钮,更改滑块等等。 逻辑上,控制器的一种实现是状态机。

在VIPER中,状态位于演示者中:由他确定当前屏幕的特定导航转换以及View中的数据显示。

在Model-View-ViewModel中,状态位于ViewModel中。 无论我们是否具有反应性粘合剂,MVVM隐喻中定义的模块的行为都将记录在ViewModel中。 显然,通过状态机来实现是可以接受的选择。

在应用程序的业务逻辑层上还会遇到状态非常复杂的复杂对象。 例如,根据是否与服务器建立连接的网络客户端发送或阻止请求。 或者是用于数据库的对象,需要将语言功能转换为SQL查询,执行,获取响应,将其转换为对象等。

在更具体的任务(例如支付模块)中,使用更广泛的状态集,复杂的逻辑,使用状态机也是正确的。
结果,我们发现在移动应用程序中,有许多对象的行为状态和逻辑描述比用一个句子更复杂。 他们必须能够管理。
考虑一个
真实的例子,并了解在什么时候真正需要有限状态机,以及在何处不合理地应用它。

考虑Champion iOS应用程序中的ViewController,这是一种流行的体育资源。 该控制器以表格形式显示一组注释。 用户输入比赛描述,查看照片,阅读新闻并发表评论。 屏幕非常简单:基础层提供数据,数据被处理并显示在屏幕上。

实际数据或错误都可以传输到显示器。 因此出现了第一个条件运算符,即第一个分支,它确定了应用程序的进一步行为。
下一个问题是如果没有数据该怎么办。 这种情况是错误的吗? 很有可能不是:并非每个新闻都有用户评论。 例如,埃及的曲棍球对任何人都不感兴趣;在这样的文章中,通常没有评论。 这是正常的行为,是您需要能够显示的屏幕的正常状态。 因此,出现了第二个条件运算符。
逻辑上假设用户还有一个期望数据的开始状态(例如,当注释屏幕仅出现在屏幕上时)。 在这种情况下,请正确显示加载指示器。 这是第三个条件语句。
因此,我们已经在一个简单的屏幕上显示了四种状态,通过if-else-if-else构造描述了其显示逻辑。

但是,如果还有更多这样的状态呢? 屏幕的迭代发展导致条件构造,一堆标志或繁琐的多重开关情况的复杂纠结。 这段代码很吓人。 想象一下,将支持他的开发人员知道您的住所,并且他总是随身携带一个电锯。 而且您真的想辜负您的少量但当之无愧的退休金。
我认为在这种情况下,值得考虑是否值得在应用程序中保留这样的实现。
缺点
让我们了解一下我们对这段代码不满意的地方。

首先,它
很难阅读 。
由于代码阅读不佳,这意味着新开发人员将很难弄清楚在项目的特定位置具体实现了什么。 因此,将花费大量时间来分析应用程序行为逻辑-
支持和开发的
成本将增加 。
此代码不
灵活 。 如果您需要添加一个新状态,该状态不遵循当前阶梯,则可能根本无法成功! 如果您需要通过通道-突然退出此梯子上的通过检查-怎么做? 几乎没有。
同样,使用这种方法,
无法免受虚拟国家的侵害 。 通过切换案例描述过渡时,很可能实现了默认行为。 就程序行为而言,此状态是逻辑的,但就应用程序的人员或业务逻辑而言,此状态几乎是不逻辑的。
针对所指出的缺点的解决方案是什么? 当然,这是每个模块/控制器/复杂对象的逻辑构造,不是基于直觉,而是使用一种良好的形式化方法。 例如,有限状态机。
游戏套件
例如,我们以Apple提供的产品
为例 。 在GameplayKit框架的框架内,有两个类可以帮助我们使用状态机。
框架的名称清楚地表明Apple希望在游戏中使用它。 但是
在非游戏应用程序中,它将很有用。
GKState类定义状态。 要对其进行描述,您需要执行简单的步骤。 我们从此类继承,设置状态名称,并定义三个方法。
- isValidNextState-基于前一个状态,当前状态是否有效。
- didEnterFrom-转换到此状态时的操作。
- willExitTo-退出此状态时的动作。
GKStateMachine是状态机类。 甚至更容易。 只需执行两个操作即可。
- 我们通过初始化程序将输入状态集传递给类型化数组。
- 我们使用enter方法根据输入信号进行转换。 通过它,还可以设置初始状态。
将任何类作为参数传递给
enter方法可能令人困惑。 但应注意,不能在状态数组中定义任何类的对象
-这禁止严格的输入。 因此,如果将任意类设置为下一个状态类,则不会发生任何事情,而enter方法将返回false。
状态和它们之间的过渡
熟悉Apple的框架后,让我们回到示例中。 有必要描述状态和它们之间的过渡。 这必须以最易理解的形式完成。 有两个常用选项:一个表或一个过渡图。 我认为过渡图是一个更容易理解的选择。 它以UML的标准化方式出现。 因此,我们选择它。
在过渡图中,存在用名称描述的状态,以及将这些状态连接起来以描述过渡的箭头。 在该示例中,有一个初始状态
-我们期望数据
-从初始状态可以达到三种状态:已接收数据,无数据和错误。

在实现中,我们得到四个小类。

让我们分析“数据待处理”状态。 在入口处,值得显示下载指示器。 而当您退出此状态时
,请将其隐藏。 为此,您需要有一个到ViewController的弱链接,该链接由创建的状态机控制。

机器参数
需要完成的第二步
是设置状态机的参数。 为此,创建状态并将其传输到自动机对象。

另外请务必设置初始状态

原则上,机器已准备就绪。 现在有必要处理对外部事件的反应,从而改变自动机的状态。

回顾问题陈述。 我们从if-else那里得到了一个阶梯,在此基础上决定了应该执行什么动作。 作为简单自动机的控件,这样的实现选项可能是(实际上是一个简单的开关
-这是有限状态机的原始实现),但实际上我们并未摆脱前面提到的缺点。
还有另一种方法可以让您摆脱这些阶梯。 它是由编程经典提出的
-所谓的“四人组”。

有一种特殊的设计模式,称为“状态”。

这是一种行为模式,类似于描述状态机抽象的策略。 它允许对象根据状态更改其行为。 该应用程序的主要目的
是将与特定状态关联的行为和数据封装在单独的类中。 因此,最初决定要引起哪个状态的状态机现在将发送信号,将其转换为状态,然后该状态将做出决定。 因此,部分卸载梯子,代码将变得更加令人愉快。
标准框架不知道如何。 他建议
GKStateMachine做出决定。 因此,我们用新方法扩展了有限状态机,其中,通过传递唯一确定下一个状态的所有条件变量的描述作为配置。 在此方法内部,可以将对下一个状态的选择委派给当前状态。

优良作法是用一个对象描述状态并始终将其传递,而不是编写许多输入参数。 接下来,我们将下一个状态的选择委托给当前状态。 这就是整个升级。
GameplayKit的优点。- 标准库。 无需下载任何内容,使用cocoapods或迦太基。
- 该库很容易学习。
- 一次有两种实现:在Objective-C和Swift上。
缺点:- 状态和过渡的实现紧密相关。
违反了唯一责任的原则:国家知道它的去向和方式。 - 重复状态不受任何方式控制。
数组传递给状态机,状态不多。 如果传输几个相同的状态,将使用列表中的最后一个。
有限状态机的实现还有什么? 看看GitHub。
Objective-C实施

TransitionKit
这是长期以来最受欢迎的Objective-C库,没有GamePlayKit中发现的缺陷。 它使我们能够在块上实现状态机以及与其关联的所有操作。
状态与转换是分开的 。
在TransitionKit中有2个类。
- TKState-用于设置状态以及输入,输出操作。
- TKEvent是描述过渡的类。
TKEvent将某些状态绑定到其他状态。 事件本身仅由字符串定义。
此外,还有其他好处。
您可以在过渡期间传输有用的数据 。 这与使用NSNotificationCenter时的作用相同。 所有有用的有效负载都以userInfo字典的形式出现,并且用户解析信息。
错误的过渡有一个描述 。 当我们尝试进行一个不存在的-不可能的转换时-我们不仅从转换方法返回时获得了NO值,而且还获得了错误的详细描述,这在调试状态机时非常有用。

TransitionKit用于流行的RestKit网络采集器。 这是一个很好的例子,说明了在实现网络操作时如何在应用程序内核中使用状态机。

RestKit有一个特殊的类-RKOperationStateMachine-用于管理并发操作。 在输入处,它接受正在处理的操作及其执行队列。

在内部,状态机非常简单:三个状态(就绪,已执行,已完成)和两个转换:开始和结束执行。 在开始处理(以及任何转换)之后,状态机开始控制在创建队列时指定的队列中用户定义的代码块。
与它的自动机关联的操作将外部事件转移到自动机,并且它在状态和所有相关动作之间执行转换。 状态机照顾
- 异步代码执行,
- 过渡期间执行原子代码,
- 过渡控制
- 取消操作
- 操作状态变量更改的正确性:isReady,isExecuting,isFinished。
换档
除了TransitionKit之外,还值得一提的是
Shift-一个实现为NSObject类别的微型库。 这种方法允许您将任何对象变成状态机,以字符串常量的形式描述状态,并在转换期间以块形式进行操作。 当然,这更多是一个培训项目,但是非常有趣,并且可以让您以最小的成本尝试一下状态机。
迅捷的实现

Swift上有许多有限状态机实现。 我将单挑一个(
备注 :很遗憾,该项目在报告发布后的最近两年中并未进行开发,但是其中包含的想法值得在文章中讲述)。
SwiftyStateMachine
在SwiftyStateMachine中,状态机由非稳定结构表示;通过属性的didSet方法,您可以轻松捕获状态更改。
在该库中,状态机是通过状态和状态之间的对应关系表定义的。 与机器将控制的对象分开描述该方案。 这是通过嵌套的开关盒实现的。

, .
- .
, . - .
state machine , state machine. - , , .
- , DOT.
state- — DOT. , , .

结论
.
- .
, . , . , .
- .
( ).
- .
, , . - . , SwiftyStateMachine , , . .
- .
, . , , . .
.

. , . , , switch case: , , — .

. . , . , , , . .

, , . .

—
. : , — .


«-»
.

, . .
app coordinators — , , : , . , .
, app coordinator , state machine. . , app coordinators state machine, , , ,
. , , , . .

, state machine , , .
state machine , if-else. , .
今年,在10月8日至9日举行的Apps Conf 2018上,Alexander 计划讨论面向对象编程的五项基本原则及其适用范围。
有关移动发展的更多报告,请访问我们的YouTube频道。如果您想接收有关新成绩单和有趣报道的信息,请订阅新闻通讯。