有限状态机(FSM)模型用于为包括Android在内的各种平台编写代码。 它使您可以减少代码的繁琐度,使其完全适合于Model-View-Presenter(MVP)范例,并适合进行简单的测试。 开发人员Vladislav Kuznetsov告诉Droid Party,该模型如何帮助Yandex.Disk应用程序的开发。
-首先,让我们谈谈理论。 我想你们每个人都听说过MVP和状态机,但我们将重复一遍。

让我们谈谈动机,为什么需要所有这些以及如何对我们有帮助。 让我们继续我们所做的,通过一个真实的例子,我将展示代码片段。 最后,我们将讨论测试,以及这种方法如何帮助您方便地测试所有内容。
每个人都使用状态机和MVP或类似的东西(可能是MVI)。
有很多状态机。 这是可以给他们提供的最简单的定义:这是一种数学抽象,以状态,事件的有限集合的形式出现,并根据事件从当前状态到新状态的转变。

这是一些抽象的程序员的简单示意图,这些程序员有时会睡觉,有时会吃东西,但主要是编写代码。 这对我们来说足够了。 有限状态机的种类繁多,但这对我们来说已经足够了。

状态机的范围很大。 对于每个项目,它们都会被使用并成功应用。

像任何方法一样,MVP将我们的应用程序分为几层。 查看-通常是活动或片段,其任务是将某些操作转发给用户,以标识演示者用户已做某事。 我们认为Model是数据提供者。 它可以像数据库一样,如果我们谈论的是干净的体系结构或Interactor,那么一切都可以。 Presenter是连接View和模型的中介,同时它可以从模型中获取和更新View。 这对我们来说足够了。
谁能用一句话说什么是程序? 可执行代码? 太笼统了,比较详细。 算法? 算法是一系列动作。
这是一个数据集和某种控制流。 谁操作此数据无关紧要:无论用户是不是。 遵循这样的思想:在任何时候,应用程序的状态都取决于其所有数据的总数。 而且,应用程序中的数据越多,管理它们的难度就越大,当出现问题时,情况就越难以预测。

想象一个带有三个布尔标志的简单类。 为了确保涵盖组合这些标志的所有方案,您需要2³方案。 必须涵盖八个场景,并保证我正在确定要处理所有标志组合。 如果添加另一个标志,则它按比例增加。
我们面临着类似的问题。 这似乎是一个简单的任务,但是随着我们的开发和工作,我们开始意识到出了点问题。 我将谈论我们推出的功能。 这称为删除本地照片。 关键是用户以自动模式将一些数据上传到云。 这些很可能是他在手机上拍摄的照片和视频。 事实证明,这些文件似乎在云中。 删除这些照片后,为什么要占用手机上的宝贵空间?

设计师提出了这样的概念。 似乎只是一个对话,它的标题是绘制我们可以释放的空间量,消息文本和带有两个清理模式的复选标记:删除用户上传的所有照片,或仅删除一个月以上的照片。

我们看了看,似乎没有什么复杂的。 对话框,两个TextViews,复选框,按钮。 但是,当我们开始详细研究此问题时,我们意识到获取有关可以删除多少个文件的数据是一项长期的任务。 因此,我们必须向用户显示某种存根。 这是一个伪代码,在现实生活中看起来不同,但含义相同。

我们检查一些状态,检查我们是否正在计算,然后画一个“等待”插头。

当计算结束时,我们有几个选项可以显示给用户。 例如,我们可以删除的文件数为零。 在这种情况下,我们会向用户发送一条消息,提示您没有要删除的内容,所以下次再来。 然后设计人员来找我们,说我们必须区分用户已经清除文件或不清除任何东西,没有加载任何东西的情况。 因此,出现另一种情况,我们正在等待启动并向他发送另一条消息。

在某些情况下,尽管如此,某些情况下仍然可行,例如,用户使用复选标记不删除新文件。 在这种情况下,还有两个选择。 可以清除文件,也不能清除文件,即已经清除了所有文件,因此我们警告您已删除所有新文件。


当我们确实可以删除某些内容时,还有另一种条件。 未选中,并且有一个可以删除某些内容的选项。 您看这段代码,似乎出了点问题。 我还没有列出所有内容,我们进行了permishin检查,因为没有它们,一切都将无法工作,我们无法触摸卡上的文件,此外,我们还需要检查用户是否启用了自动加载功能,因为如果没有自动加载功能,这些功能将无用,我们将清洁。 还有更多条件。 该死的,这似乎是一件简单的事情,因此,出现了许多问题。
显然,立即出现了几个问题。 首先,此代码不可读。 这里描绘了某个伪代码,但是在实际项目中,它散布在不同的功能,代码段中,因此用肉眼很难理解。 对此类代码的支持也相当复杂。 尤其是当您进入一个新项目时,会被告知您需要进行这样的功能,添加一些条件,检查肯定的情况,一切正常,但是测试人员会说在某些条件下一切都破裂了。 发生这种情况是因为您根本没有考虑任何情况。
另外,从某种意义上说,这是多余的,因为我们有很多条件分支,所以我们必须事先检查所有不适合我们的条件。 它们事先是负数,但是由于它们是用这样的分支编写的,因此我们必须检查它们。 事实是,在该示例中,我具有某种布尔标志,但是在实践中,您可能会调用到数据库更深处的函数。 一切皆有可能,由于冗余,还会有额外的制动器。
最可悲的是,在测试阶段错过了一些意外的行为,没有任何反应,在生产中的某个地方,用户充其量也没有发生,某种UI曲线,在最坏的情况下,它跌落了或者数据丢失了。 只是应用程序的行为不一致。
如何解决这个问题? 由状态机的力量。

状态机处理的主要任务是承担一项大的复杂任务,并将其分解为易于与之交互和管理的小的离散状态。 坐下来思考之后,既然我们正在尝试做一些MVP,那么如何将我们的状态与所有这些联系起来? 我们大致得出了这样的计划。 读GOF书的人都是经典的状态模式,即所谓的上下文,我称其为状态提示者,实际上是演示者。 演示者具有此状态,知道如何切换它们,并且如果他们想知道某些信息(例如,文件大小或想要请求异步请求),仍可以向我们的状态提供一些数据,请选择。

这里没有超级傻瓜,下一张幻灯片更重要。

这样,当您开始制作状态机时,您需要开始开发。 您坐在计算机上或桌子旁的某个地方,可以在一张纸上或使用专用工具绘制状态图。 也没有什么复杂的,但是此阶段有很多优点。 首先,在早期阶段,您可以立即检测到业务逻辑中的一些不一致之处。 您的产品可能来了,表达了他们的愿望,一切都很好,但是当您开始编写代码时,您会了解到某些东西并不合适。 我认为每个人都有这样的情况。 但是,当您绘制图表时,您可以在早期阶段看到某些东西没有对接。 它的绘制非常简单,有一些特殊的工具,例如PlantUML,在其中甚至不需要绘制,也不需要编写伪代码,并且它本身会生成图形。
我们的图表如下所示,它描述了此对话框的状态。 有几种状态以及它们之间过渡的逻辑。

让我们继续进行代码。 声明本身并没有什么重要,主要是它具有三种方法:onEnter,在进入时调用第一个invalidateView。 为什么要这样做? 因此,一旦进入状态,UI就会更新。 加上有invalidateView方法和onExit方法,如果需要对UI进行某些操作,则可以重载;而onExit方法,可以在退出状态时进行某些操作。

国有企业。 提供点击状态功能的界面。 我们发现,它将是未来的演示者。 这些是提供对数据的额外访问的方法。 如果状态之间的数据比较混乱,我们可以将其保存在presenter中并通过此接口提供。 在这种情况下,我们可以提供可以清理的文件大小,并提供进行某种请求的机会。 我们处于一种状态,我们想请求一些东西,通过StateOwner我们可以调用一个方法。
另一个这样的实用性是,他也可以将链接返回到视图。 这样做是为了,如果您有一个状态并且有一些数据到达,您不想切换到新状态,那只是多余的,您可以直接更新视图,文本。 我们使用它来更新用户在查看对话时看到的位数。 我们正在运行时下载文件,他看着对话,并更新了数字。 我们并没有进入新的状态,我们只是在更新当前的View。

这是标准的MVP,一切都应该非常简单,没有逻辑,简单的方法可以画些东西。 我坚持这个概念。 应该没有逻辑,至少应该采取某种行动。 我们完全采用一些“文本视图”,仅对其进行更改。

主讲人 还有更多有趣的事情。 首先,我们可以通过它获取某些状态的数据,我们有两个标有State注释的变量。 谁使用过Icepick很熟悉。 我们不会在Partible中手动编写序列化,而是使用现成的库。
以下是初始状态。 设置初始状态总是有用的,即使它什么也不做。 有用之处在于您不需要执行空检查,但是如果我们说它可以做某事。 例如,您需要为应用程序的生命周期执行一次操作,当我们启动应用程序时,您需要执行一次该过程,而无需再次执行。 当我们退出初始状态时,我们总是可以做这样的事情,而我们永远都不会回到这种状态。 键入以便绘制状态图。 尽管谁知道谁会画画,也许您可以回来。
我赞成尽量减少对Null的检查,等等,因此在这里我保留了指向简单视图实现的链接。 我们不需要同步任何东西,只是在发生分离的某个时候,我们用一个空的视图替换视图,演示者可以在状态中切换到某个位置,认为有一个视图,它可以更新它,但是实际上它可以工作与空的实现。
为了保存状态,还有其他几种方法,但是我们希望幸免于Activity的剧变,在这种情况下,这都是通过构造函数完成的。 一切都有些复杂,这是一个夸大的例子。

有必要转发saveState,如果有人使用类似的库,那么一切都是微不足道的。 您可以用手写字。 有两种方法非常重要:attach(在onStart上调用)和detach(在onStop上调用)。

它们的重要性是什么? 最初,我们计划在onCreateView和onDestroyView中进行附加和分离,但这还不够。 如果您有一个视图,则您的文本可能会更新,或者可能会出现一个对话框片段。 而且,如果您没有陷入onStop,然后尝试显示该片段,您将捕获一个众所周知的异常,即当我们仍然处于状态时,您无法提交事务。 要么使用提交状态丢失,要么不使用。 因此,我们在onStop中对此进行了详细介绍,而演示者将继续在那里工作,切换状态,捕获事件。 在开始发生的那一刻,我们将触发视图附加事件,演示者将更新UI以匹配当前状态。


有一个释放方法,通常在onDestroy中调用,您进行分离并另外释放资源。

另一个重要的setState方法。 由于我们计划在onEnter和onExit中更改UI,因此需要检查主线程。 这给我们带来了一个限制,即我们在这里不会做任何繁重的工作,所有请求都必须是对UI的,或者必须是异步的。 这个地方的优点是,这里我们可以保留状态的进入和退出,这在调试时非常有用,例如,当出现问题时,您可以查看系统如何单击并了解问题所在。

条件的几个例子。 有一个初始状态,它仅触发计算在视图可用时需要释放多少空间。 这将在onStart之后发生。 一旦onStart发生,我们就进入新状态,系统开始请求数据。


状态的一个示例是计算中,我们将使用stateOwner声明文件的大小,以某种方式爬到数据库中,然后仍然存在inValidateView,我们将更新当前用户UI。 如果重新附加视图,则调用viewAttached。 如果我们在后台,而计算在后台,则再次返回到Activity,将调用此方法并更新所有数据。

一个事件的示例,我们询问stateOwner可以释放多少个文件,并调用filesSizeUpdated方法。 在这里,我太懒了,可以编写三种单独的方法,例如update,存在许多旧文件,以及如何分开不同的事件。 但是您必须了解,一旦对您来说很困难,那么它将变得简单得多。 不必过度设计每个事件都是一个单独的方法。 如果我没有发现任何问题,可以通过一个简单的方法解决。

我看到了一些潜在的改进。 我不喜欢我们被迫抛弃这些方法,例如onStart,on Stop,onCreate,onSave等。 您可以附加到生命周期,但是尚不清楚该如何处理saveState。 例如,有一个想法可以制作演示者片段。 为什么不呢 没有UI的片段会捕获整个生命周期,通常,我们将不需要任何东西,所有东西都会自动飞向我们。
另一个有趣的点:每次都重新创建此演示者,并且如果您在演示者中存储了大数据,则您进入数据库并按住一个巨大的光标,那么每次旋转屏幕时都无法请求。 因此,您可以缓存演示者,例如,从Architecture Components中获取ViewModule,制作一些片段来保存演示者缓存并为每个视图返回它们。
您可以使用表格方式指定状态机,因为我们使用的状态模式有一个明显的缺点:只要您需要向新事件中添加一种方法,就必须向所有后代添加实现。 至少是空的。 或者在基本条件下进行。 这不是很方便。 因此,在所有库中都使用了表述状态机的表格方式-如果您在GitHub上搜索单词FSM,则会发现大量的库为您提供了一种构建器,您可以在其中设置初始状态,事件和最终状态。 扩展和维护这种状态机要容易得多。
另一个有趣的问题是:如果使用状态模式,并且状态机开始增长,则很可能将必须以相同的方式处理某些事件,以使代码不会被复制,从而创建了一个基本状态。 事件越多,开始出现的基本条件就越多,层次结构也越来越多,并且出现了问题。
众所周知,继承必须由委派代替,而分层状态机有助于解决此问题。 您具有不依赖于继承级别的状态-只需构建通过上述处理程序的状态树即可。 您也可以单独阅读,这是非常有用的。 例如,在Android中,WatchDog Wi-Fi使用了分层状态机,它可以监视网络状态,它们就在Android源中。

最后但并非最不重要的。 如何测试? 首先,可以测试确定性状态。 有一个单独的状态,我们创建一个实例,拉出onEnter方法,然后看到在视图中调用了相应的值。 因此,我们验证我们的状态正确更新了View。 如果您的View没什么大不了的,那么很可能您将涉及大量场景。

您可以使用返回大小的函数锁定某些方法,在onEnter之后调用另一个事件,并查看特定状态如何响应特定事件。 在这种情况下,当filesSizeUpdated事件发生并且AllFilesSize大于零时,我们必须转换为新的CleanAllFiles状态。 借助布局,我们检查了所有这些。

最后-我们可以测试整个系统。 我们构造状态,向其发送事件,然后检查系统的行为。 我们有三个测试阶段。
我们分别测试UI的更新方式,测试过渡逻辑,状态切换的方式,以及测试整个系统。我们将视频播放器重写为这种概念,覆盖率超过70%。这些测试涵盖了不到80%的说明。我认为这是一个非常酷的指标。
我们使用这个概念得到了什么?首先,进行测试。状态机和演示者很容易在生命周期中结交朋友。可扩展性。这种方法使您可以限制在某个概念中。您可以强化某些内容,但是最有可能的是,当有人检查您的代码时,他会说-您为什么要这样做,何时您只需添加一个新状态就可以了。- , , , . , , . , , . - , , . , , . lock . - , .
— . , , , , . , - , , -, , . , . , .