StateKit 0.2.1

StateKit 0.2.1

测试测试
Lang语言 Obj-CObjective C
许可 MIT
发布最新发布2017年10月
SwiftSwift 版本3.2

Shaheen GhiassyDaniel Beard 维护。



StateKit 0.2.1

  • Shaheen Ghiassy





StateKit 是一个用于 iOS、OSX、tvOS 或 watchOS 的框架,用于捕获、记录和管理状态,以便保持您的应用程序代码冷静和理智。

快速示例

让我们看看使用 StateKit 来管理加载状态和常规状态的快速、小型示例。

我们创建状态图如下

NSDictionary *chart = @{@"root":@{
                                @"enterState":^(SKStateChart *sc) {
                                    [sc goToState:@"loading"];
                                },
                                SKDefineTransition_Event2State(@"apiRespondedSuccessfully", @"regularView"),
                                @"subStates":@{
                                        @"regularView":@{
                                                @"enterState":^(SKStateChart *sc) {
                                                    // setup the regular view
                                                }},
                                        @"loading":@{
                                                @"enterState":^(SKStateChart *sc) {
                                                    // fetch data from the api
                                                    // show the loading spinner
                                                },
                                                @"exitState":^(SKStateChart *sc) {
                                                    // remove loading spinner
                                                }}}}};

SKStateChart *stateChart = [[SKStateChart alloc] initWithStateChart:chart];

或者,在 Swift 中

let chart: Dictionary<String,AnyObject> = [
	"root" : [
		"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
			sc.goToState("loading")
			}),
		"apiRespondedSuccessfully" : SKStateBlockify({(sc: SKStateChart) -> Void in
			sc.goToState("regularView")
			}),
		"subStates" : [
			"regularView" : [
				"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
					// setup the regular view
					})
			],
			"loading" : [
				"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
					// fetch data from the api
					// show the loading spinner
					}),
				"exitState" : SKStateBlockify({(sc: SKStateChart) -> Void in
					// remove loading spinner
					}),
			]
		]
	]
]

let stateChart: SKStateChart = SKStateChart(stateChart: chart)

注意:如果您需要集成 Swift,您需要在 Podfile 中添加 use_frameworks!,并将您的 Pod 路径修改为 pod "StateKit/Swift",以便访问辅助 typealias(类型别名)/ 函数。

上述字典被解释为一个类似这样的树形数据结构

让我们跟随这个特定的状态图发生的故事

init 时,状态图始终以 root 作为当前状态开始。当我们进入 root 状态时,状态图看到 root 状态中有一个 enterState 块,因此运行相关的块。该块将状态图导向 loading 状态。因此,状态图继续遍历树到 loading 状态。当我们进入 loading 状态时,运行 loading 状态上的 enterState 块。在这里,我们可以让应用程序从 API 获取数据并将指示器渲染到视图中。注意,状态图正在指导应用程序做什么。

当 API 成功响应时,我们向状态图发送适当的消息 [stateChart sendMessage:@"apiRespondedSuccessfully"]。从应用程序代码的角度来看,我们无需做任何其他事情,我们假设状态图将在必要时指导任何后续步骤。

当状态图收到消息 apiRespondedSuccessfully 时,状态图会查找适当的消息处理程序并运行消息处理程序的块。在这个例子中,块将状态图导向 regularView 状态。

当状态图从 loading 状态遍历到 regularView 状态时,我们可以以高精度处理 alloc/dealloc 对象。当我们退出 loading 状态时,我们清理加载 UI(例如移除/dealloc 指示器等),当我们进入 regularView 状态时,我们设置适当的 UI。

这是一个基本的状态图示例,但它展示了应用程序流程可以通过状态图智能地管理和控制。

为什么使用状态图?

他们说可以通过开发人员对应用程序流程控制的掌握程度来衡量他们的能力。流程控制可以通过多种方式处理,但尤其是在前端应用程序中,准确捕获和操作状态来管理应用程序的流程控制是必不可少的。

StateKit赋予您以易于阅读和维护的方式轻松创建复杂流程控制的能力。

优势

  • 减少循环复杂度 - 因为绝大多数甚至是所有分支逻辑都可以被描述和捕获在状态图中,您的函数可以安全地假设只有在需要时才被调用。这种保证允许在您的函数中进行更少的错误Checking和更少的逻辑分支,从而减少循环复杂度
  • 让你的代码更平静 - 使用适当的状态图管理流程控制后,函数现在可以开始变得更加平静。它们的代码,以前需要做好准备以防不适当地被调用或需要检查以了解之前发生了什么,现在可以摆脱这些担忧。函数现在可以安全地假设(给定适当的状态图结构)它们只有在实际需要时才会被调用,并且可以安全地假设在它们被调用之前,任何必要的分配或设置动作都已由父应用程序的状态处理。
  • 垃圾进,理智出 - 随着应用程序的增长,代码工作的环境变得更加复杂。NSNotificationEvents、用户事件、计时器事件和损坏的代码都导致干净的流程控制和开发者的理智度下降。通过将事件/消息委托给状态图,您可以使用合适的数据结构来解释混乱,并产生干净、纯净的流程控制。
  • 更好的内存管理 - 通过创建适当的树结构,我们可以精确地定义在何时何地分配和释放对象。嵌套状态不需要担心对象尚未创建,因为父状态已经处理了这一点。
  • 自文档化 - 通过在树中捕获状态,您可以在一个地方查看一个文件的逻辑分支的概述,这以视觉上描述正在发生的事情。
  • 单一事实来源 - 状态的唯一事实来源,还有什么更好的吗。

文档

语法

创建状态图的语法如下

根状态

所有状态图都必须有一个根状态 - 否则StateKit会抛出异常。以下是最小可行状态图。

NSDictionary *chart = @{@"root":@{}};
SKStateChart *stateChart = [[SKStateChart alloc] initWithStateChart:chart];

状态的消息

消息在状态字典条目的顶层定义。要将消息apiRespondedSuccessapiRespondedError添加到根状态,我们会这样做

NSDictionary *chart = @{@"root":@{
                                @"apiRespondedSuccess":^(SKStateChart *sc) {
                                    // show the page
                                },
                                @"apiRespondedError":^(SKStateChart *sc) {
                                    // show the error page
                                }}};

当根状态收到这两个消息之一时,将运行相关的块。

注意:消息的键/值对必须是字符串/块的类型。

注意,状态图的引用作为sc传递给函数。虽然您可能会从字典之外的位置引用状态图实例,但仍建议使用传递的引用。

子状态

子状态在关键字subStates下定义为字典。如果我们想在上面示例中添加一个loading子状态,我们会写

NSDictionary *chart = @{@"root":@{
                                @"apiRespondedSuccess":^(SKStateChart *sc) {
                                    // show the page
                                },
                                @"apiRespondedError":^(SKStateChart *sc) {
                                    // show the error page
                                },
                                @"subStates":@{
                                        @"loading":@{
                                                // put the loading's state messages and substates here
                                                }}}};

注意:子状态的键/值对必须是字符串/字典的类型。

消息

事件是状态图的核心。创建状态图后,外部世界可以开始向状态图发送消息,以了解正在发生的事情。状态图将解释消息并运行相应的接收器块(如果有)。

[stateChart sendMessage:@"userPressedTheRedButton"]

消息冒泡

消息首先发送到当前状态,以查看是否存在接收该消息的接收器。如果当前状态对消息没有响应,状态图将开始向上冒泡到树中,寻找任何响应消息的父状态。如果当前状态加上任何当前状态的父状态都没有对消息做出响应,则消息将被静默忽略。

示例

以下是一个虽然编造但有用的示例,说明在各种情况下消息冒泡是如何工作的。假设以下状态图

NSDictionary *chart = @{@"root":@{
                          @"subStates":@{
                                  @"a":@{
                                          @"subStates":@{
                                                  @"d":@{
                                                          @"userPressedButton":^(SKStateChart *sc) {
                                                              NSLog(@"state d says hi");
                                                          },
                                                          @"subStates":@{
                                                                  @"j":@{}
                                                                  }
                                                          }
                                                  }
                                          },
                                  @"b":@{
                                          @"userPressedButton":^(SKStateChart *sc) {
                                              NSLog(@"state b says hi");
                                          },
                                          @"subStates":@{
                                                  @"e":@{},
                                                  @"f":@{
                                                          @"subStates":@{
                                                                  @"g":@{
                                                                          @"userPressedButton":^(SKStateChart *sc) {
                                                                              NSLog(@"state g says hi");
                                                                          },
                                                                          @"subStates":@{
                                                                                  @"h":@{},
                                                                                  @"i":@{}
                                                                                  }}}}}},
                                  @"c":@{
                                          @"subStates":@{
                                                  @"k":@{}
                                                   }}}}};

这将转换为以下树

将消息userPressedButton发送到起始图,其意义将取决于状态图当前所在的状态。

以下表格显示了发送消息userPressedButton到状态图并给定各种起始当前状态时的输出。

当前状态 发送userPressedButton会输出
root
A
B state b says hi
C
D state d says hi
E state b says hi
F state b says hi
G state g says hi
H state g says hi
I state g says hi
J state d says hi
K
由于同一消息可以根据状态图的数据结构执行不同的操作,因此这非常有用,并被认为是良好的实践。

状态遍历

状态遍历是状态图的另一个重要方面。当从一个状态转换到另一个状态时,状态图不会直接从一个状态转换到另一个状态。相反,状态图遍历树来从一个状态转换到另一个状态。这种树遍历与状态事件的结合是一种非常强大的组合,可以精确管理应用程序的流程控制。

从一个状态过渡到另一个状态的逻辑采取以下步骤

  1. 状态图对其底层树数据结构进行广度优先搜索,以找到要转换到的状态。
  2. 然后状态图找到两个状态之间的最低公共祖先
  3. 然后状态图从当前状态遍历到最低公共祖先。当状态图遍历树时,它将运行每个接触到的状态的exitState块(如果存在)。
  4. 一旦状态图到达最低公共祖先,它就开始遍历树到目标状态。对于每个接触到的状态,它将运行如果存在的enterState块。
  5. 操作在一次状态图到达目标状态后完成。

在图形上,这看起来像

状态事件

当状态图遍历状态时,它将检查接触到的每个状态,如果存在,则在该状态下运行事件块。向你的状态图中添加事件块不是必需的。

当状态图进入状态时,它会运行如果存在的enterState块。而且当状态图遍历树时,它会运行如果存在的exitState块。

在上面的示例中,将运行状态GDexitState块。将运行状态EHenterState块。

请注意,对状态B没有运行任何操作,因为我们没有进入或退出该状态。

该做与不该做的事

不要告诉状态图要转换到哪个状态

许多初次了解状态图的开发者自然倾向于告知状态图要移动到哪个状态。不要这样做。外部世界通过发送消息来告诉状态图发生了什么,而状态图的职责是解释这些消息并根据需要(如果有的话)相应地操纵状态。

不要

[statechart goToState:"red"];

[stateChart sendMessage:@"userPressedTheRedButton"]

然后在你的状态图中这样做

@"userPressedTheRedButton":^(SKStateChart *sc) {
    [sc goToState:@"red"];
}

或者甚至更简单

SKDefineTransition_Event2State(@"userPressedTheRedButton", @"red")

不要害怕发送大量消息 -甚至是垃圾消息

向状态图发送消息是其关键操作之一。不要害怕发送大量消息。并且不要害怕发送当前无效的消息 -很多时候这是良好的编程实践。

例如,想想看一个计时器。每一分钟,它在整分钟时向状态图发送消息 minuteUpdated。如果状态图处于它关心的接收消息 minuteUpdated 的状态,它可以选择采取行动。如果状态图处于它不关心的 minuteUpdated 消息的状态,那么什么都不会发生 —— 这是一件好事。

不要把所有逻辑都塞进状态图中

状态图就像交响乐团的指挥。它知道所有的演奏者,并告诉他们何时演奏乐器。但不会代他们演奏乐器。同样,状态图应该调用函数,但详细逻辑应该不包含在状态图中。

@"enterState":(SKStateChart *sc) {
  [view setupUI];
}

不要

@"enterState":(SKStateChart *sc) {
  UILabel *label = [[UILabel alloc] init];
  label = [[UILabel alloc] init];
  label.text = @"StateKit!!!";
  label.font = [UIFont systemFontOfSize:26];
  label.textColor = [UIColor blueColor];
  [self.view addSubview:label];
  // ... more view logic
}

保持状态名称唯一

当到达新状态时,状态图从根状态开始,执行广度优先搜索,直到找到目标状态。虽然状态名称不会被强制要求是唯一的,但 bfs 搜索的结果可能会产生意外的结果。因此,最好避免重复的状态名称。

小心在状态事件块中放置 goToStates 指令

当在状态间转换时,状态图会在遍历树的期间调用事件块。小心在这些块中放置 goToState 指令,因为可能导致状态图在到达最终目标之前经历一段意外的旅程。

但正如提供的示例所示,有时在 enterState 块中放置 goToState 指令是合适的 —— 只需小心。

不要在状态图中引用self

为了避免保留周期,在状态图中使用 weak-self 引用。

__weak UIViewController *weakSelf = self;
NSDictionary *chart = @{@"root":@{
                                @"enterState":^(SKStateChart *sc) {
                                    weakSelf.title = @"hi";
                                }}};
let chart: Dictionary<String,AnyObject> = [
	"root" : [
		"enterState" : SKStateBlockify({ [weak self] (sc: SKStateChart) -> Void in
			self?.title = "hi"
			}),
		]
]

不要

NSDictionary *chart = @{@"root":@{
                                @"enterState":^(SKStateChart *sc) {
                                    self.title = @"hi";
                                }}};
let chart: Dictionary<String,AnyObject> = [
	"root" : [
		"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
			self.title = "hi"
			}),
		]
]

需要注意的问题

无限循环

有可能创建一个有效的状态图,使其能够无限期地转换状态。这显然是错误的 - 不要这样做。

为了辅助,StateKit 如果在一次操作中有超过100个状态转换将抛出异常

StateKit 不是一个有限状态机

StateKit 是一种有限状态机,但不是在传统意义上的 FSM。

FSMs 以图的形式管理状态 - StateKit 以树的形式管理状态。虽然所有树都是图,但并非所有图都是树。这个区别虽然微乎其微,但非常重要。

此外,StateKit 在基本数据结构中增加了事件和树遍历逻辑,从而允许快速应用程序开发。

随着经验的积累,你将能够识别哪些问题更适合用状态图来表示,而不是用有限状态机

如果您正在寻找适用于 iOS 或 OS X 开发的经典 有限状态机,请查看

单元测试

StateKit 完全测试并通过。

要运行单元测试,请打开文件 ./Example/StateKit.xcworkspace。然后按 command-U 运行测试。

安装

如前所述,如果您想包含 Swift 支持,并获得在闭包和 Objective-C 块之间桥梁的辅助函数,请修改 Podfile 的 StateKit 引用为:

pod "StateKit/Swift"
如果您喜欢 StateKit,请在 GitHub 上给它点星以帮助传播信息

StateKit 导入必要的类

#import <SKStateChart.h>

并创建/实例化您的事件图表

NSDictionary *chart = @{@"root":@{}};
SKStateChart *stateChart = [[SKStateChart alloc] initWithStateChart:chart];

作者

Shaheen Ghiassy, [email protected]

许可

StateKit 在 BSD 许可协议下可用。有关更多信息,请参阅 LICENSE 文件。