如何正确使用状态模式?

时间:2011-02-08 16:48:28

标签: oop design-patterns

我在编程经历中遇到过一些状态模式的实现,并做了一些。我已经看到它们在各种场景中使用(主要是UI和解析)。麻烦的是,所有这些都在快速发展的压力下变成了难以维护和可理解的代码块。我正在考虑重构其中一个,但我很难在网上找到好的资源。在线状态模型有很多简单的例子,但我需要更深入的资源。

所以我在寻找:

  • 常见陷阱的例子 实现状态模式以及如何实现 避免它们,
  • 真实世界的例子 状态模式正确完成(如 一些开源项目/框架)
  • 与国家的个人经历 模式也欢迎

感谢您的时间

9 个答案:

答案 0 :(得分:25)

@Ivan:网络上有许多可用于Hierarchical State Machines(HSM)的资源。 Miro Samek撰写了大量有关此设计模式的文章,并提供了大量有用的信息。

一些值得关注的文章:

使用HSM而不是Mealy和Moore描述的平坦FSM状态图表的最大好处是层次结构创造了责任分离。子状态只需要处理它们明确设计要处理的那些条件 - 未处理的事件被传递到父状态,如果父状态没有明确地设计来处理它,那么它被传递到下一个更高的状态父母等。它允许您创建小型,可管理的状态机,每个状态机都有一个用途 - 一个可以放在一个对象中的状态机。随着新功能的添加,或者添加新类,他们只需要处理自己的小部分世界,并将未处理的事件传递给各自的父母。

如果正确实施,您将获得一个具有低圈复杂度的强大程序,可以根据需要轻松修改或升级。

答案 1 :(得分:24)

正如您可能已经阅读过的那样,当状态改变某个对象的行为时,State Design Pattern非常有用,该对象的构成包括该状态。这意味着State抽象类,接口或enumerated type的想法 - 尽管取决于语言Duck Typing也会这样做 - 定义任何常见行为和/或所需方法。

关键方面

在处理状态模式时,有两个关键方面需要考虑:枚举和转换。枚举仅仅意味着识别可能状态的集合(例如,一周中的几天),或者更抽象地表示状态的类型(即元状态),例如工作流引擎的开始,结束和中间。转换意味着决定如何建模运动在状态之间,通常通过捕获表格表示中的所有可能的转换(即Finite State Machine)或使每个状态知道其可能的“转换”到其他状态来完成。

通常,转换与元状态密切相关,因为在这样的动态系统中不可能提前知道所有状态和关系,其中可以在运行时添加新状态,从而可以添加转换。此外,通过转换方法,某些行为(例如通知)成为转换的一部分,而不是状态本身。

实施例

有几种情况我已经参与或讨论了这是一个使用工具:

  1. 工作流
  2. 电脑游戏对手A.I。
  3. 流程编排
  4. 工作流程我的意思是jBPM。像这样的系统关注的是在合适的时间给予合适的人正确的关注。他们通常会发送大量电子邮件或其他类型的通知。而且,他们所代表的流程需要能够随着组织的变化而变化,而被管理的数据通常变化要慢得多。

    电脑游戏对手A.I。是不言自明的。这不是我写过的东西,但在与那些人的谈话中,这些系统通常是自成一体的。换句话说,与工作流程不同,游戏通常没有改变用于控制计算机对手的过程的工具。

    流程编排与工作流类似,但侧重于系统集成,而不是人员交互。 Apache Mule框架就是一个例子。这里状态可以描述状态(例如,开始,进行中,结束)和类型(例如ftp集成点,sql集成点)。

    结论

    与其他答案不同,我认为状态封装是管理软件系统变更的绝佳方式。做得好,它促进了这些更改或使用户能够在运行时这样做。您需要权衡更多的灵活性,以换取更高的实现复杂性。因此,这种方法可能对购物车没有用,例如,行为可能是众所周知的并且不喜欢改变。另一方面,当过程发生变化时,它非常适合。

答案 2 :(得分:8)

仅仅是我的2美分,状态模式总是变得难以维护,因为那些没有编码它的人很难理解。我通常回退到旧的标准函数/方法指针数组,就像我在旧的C体验中一样。您只需构建一个二维函数指针数组,其中包含行/列的状态/信号。更容易理解。你有一个管理它的类,你委托给其他类来处理复杂性......

MY2C

答案 3 :(得分:6)

大多数情况下,状态模式设计中的状态处理的是一个状态(或状态的子状态),这使得它们更难维护。

如果一个州有任何选择,那么它主要处理多个州。

我采取了很多纪律来保持国家清洁。

可能的解决方案是使更复杂的状态机本身(HSM)。 这使得它在更高层次上更具可读性,因为它必须处理更少的状态。

答案 4 :(得分:4)

看看Finite State Machine。几乎每种成熟的语言都有很好的例子。由于您尚未指定首选语言,因此我将为您提供C ++示例:Boost FSM library。最有可能的是它比你需要的要复杂得多,但它可以给你一些肯定的设计提示

答案 5 :(得分:2)

  

所以我在寻找:

     
      
  • 实施状态模式时常见陷阱的示例以及如何避免它们,
  •   

状态模式不能很好地扩展。想象一下具有10个状态和10种不同转换类型的状态机。添加新状态意味着状态必须定义所有10个转换。添加新转换意味着所有10个州都必须定义它。简而言之,如果您的状态机不稳定和/或您有很多状态/转换,请不要使用状态模式。

  
      
  • 正确完成状态模式的真实世界示例(如在某些开源项目/框架中)
  •   

正确定义 :-) https://stackoverflow.com/a/2707195/1168342中引用的Java示例适用于JSF生命周期,但我认为只有一个转换。其他答案都没有引用国家的任何内容。

  
      
  • 欢迎个人体验国家模式
  •   

Head First Design Patterns uses a Gumball machine example来说明国家。具有讽刺意味的是,每次他们扩展设计(添加新的状态或转换)时,都会有很多重复的代码(特别是对于特定状态下的无效转换)。此外,根据谁决定下一个状态是什么,各个状态类可以相互耦合(状态间依赖性)。请参阅本答案末尾的解释:https://stackoverflow.com/a/30424503/1168342

GoF书提到table-based alternatives有优势,即他们的规律性。更改转换条件需要更改表(而不是代码)。

答案 6 :(得分:1)

如果每种状态都有不同的行为,则应使用状态模式。也许您需要在运行时重新配置转换。使用它的另一个原因是您可能需要稍后添加更多状态。

想象一下像中国跳棋这样的棋盘游戏你有不同的GUI状态来挑选一个Pawn,选择目标插槽等等。在每个状态中,GUI的行为应该不同,某些输入应该被处理,其他输入应该被忽略。使用简单的开关/外壳是可能的,但是状态模式在封装逻辑时很方便,相关的代码也是如此。这样可以更容易地引入新状态而不会影响大多数/所有其他状态(取决于谁负责设置转换:状态是否知道其传出转换,或者它们可以在运行时给出,例如使用构造函数)。 / p>

正如您在this example中所看到的,GuiController使用IGuiState接口来按需更改其行为。实施can be seen here

当您需要灵活时,主要的缺陷是使用开关/外壳。由于间接需要花费更多时间,我建议使用固定数量的相当简单的staates。我必须实现一个相当快速的低级网络协议,这通常需要很多开销。

答案 7 :(得分:0)

我正在构建一个表达式求值程序,它能够评估元素集。我发现状态模式对于区分根据其状态可以和不能对集合进行的操作非常有用。即:打开,关闭,不活动,活动等。 FSM非常容易绘制,并且通过消除对大量ifelse语句块的需要来定义代码的复杂性,以根据其所附属性定义功能应该执行的操作。它通过将条件转换为类来使这些条件更加明确。到目前为止,这是我最喜欢的模式之一。

答案 8 :(得分:0)

这只是一个很难用好的模式,但如果用得好,它可以非常有效。这是一个例子。 假设您正在为不同的集合实现迭代器。您希望迭代器中包含条件逻辑,以便您可以轻松更改其状态和行为。您在迭代器中嵌入了一个状态对象。集合中的每个对象都会将其类传递给迭代器,迭代器又将类传递给 State 对象。

假设您正在设计一款网络观察者家长监控应用。这个应用程序接收来自网络的所有数据——图片、文本(输入文本和解析的 HTML 文本)、GPS 坐标(它是一个电话应用程序)……所有这些来自活动的不同数据都被接收并存储在一个多样的收藏。

您需要以多种方式迭代这个多样化的集合,以多种方式对其进行分析,触发家长标志,以及做其他事情。

在某种模式下,状态对象检查搜索到的文本字符串,并将它们的文本与标记关键字列表进行比较。 在另一种模式下,状态对象检查 GPS 坐标并确定它们是否在特定时间范围内位于家或学校的特定半径内。 在另一种模式下,状态对象获取任何图像并将它们传递给可以识别裸体的机器学习算法。 等等。

将所有这些逻辑封装在这个状态对象中,并且能够像这样切换模式,这是很好的和模块化的。你切换模式,状态对象 - 和迭代器 - 表现得像一个完全不同的类。也许父母目前希望扫描他们的孩子是否在凌晨 4 点在家。所以......应用程序中有一个按钮,允许他们指定时间范围并点击“搜索”。状态对象将更改其状态以仅检查 GPS 坐标及其时间戳,它将跳过其他数据。

也许在应用程序中定义了一个状态......其中每一条信息都被分析。完整的监控。所以在状态对象中,状态本身被实现为一串位,并作为位掩码读取。要监视一切……好吧,您只需将整个位串设置为 1。现在您的状态对象将检查所有内容。 另一方面,也许父母觉得这太具有侵略性了。因此,父母可以配置应用程序,使其不监视位置。也许他们将其配置为仅监视裸体。好的...所以所有这些位都设置为 0,除了指定用于裸体的位。现在您的应用仅监控裸体。

这真的很可配置,不是吗?您喜欢的每一种行为组合都可以轻松设置。

所以父母只是在监视裸体……但是等等!这孩子行为不端。现在也许父母改变了主意,并决定……你知道,这个孩子失控了。我想更仔细地看看他们过去一个月在互联网上做了什么。他们是否一直在使用任何明确的搜索词?

没问题。你确实收集了那些数据,你只是跳过了它。太好了,只需按几个按钮...重新配置该状态对象,它就会做你想做的事。

另一个常见用途是实现动态命令。所以让我们说在魔兽世界...你有这些按钮。他们是命令。你组合按下它们。所以如果你按下攻击...然后在 0.5 秒或更短的时间内攻击...你会得到一个特殊的第二次攻击。实际上,屏幕上有一个小图标,在第一次按下后会发生变化,短暂的一秒钟,然后又切换回来。 因此,Command 将使用 State 对象实现其功能。第一个命令...将短暂改变该状态,并在 0.5 秒到期后默认返回。整个魔兽世界的界面——一系列的按钮、攻击等等——都以这种方式充满活力。所有命令都非常依赖于状态。
如果你的角色升级了,学会了新的攻击怎么办?修改状态对象非常容易。但是如果他换了武器呢……?并且再也无法进行那只针对剑的攻击?好吧,斧头的状态对象,他的新武器,没有额外的功能......它都是非常模块化的。

是的,有了这种根据状态更改功能的能力,您就可以获得很多选项和可配置性,这些选项和可配置性可以立即在此应用的 UI 中实现。在正确的上下文中使用时,它确实非常强大。您的原始评论确实涉及到这一点 - 解析和 UI 似乎是一个可以使用它的好地方。

除了使用流之外,您还可以仅使用它来指示 API 的类型。因此它可用于指示支持哪些功能。从那里,您可以按照您认为合适的方式做出回应。 假设您设计了一个可与 iPhone 游戏配合使用的远程 PS4 控制器。原生,iphones 游戏引擎有这个 State 对象,它封装了游戏原生控件,并定义了原生控件支持哪些按钮/操作。某些按钮被按下,游戏控制状态会相应地改变(类似于 WOW 示例,但更广泛)。也许有些游戏会在按下某些按钮时使用手机的振动反馈,而其他游戏则不会。也许你想在你的 PS4 控制器中实现双重冲击。所以一款特定的游戏不支持任何按钮状态的振动。嗯......你可以通过阅读状态来判断。如果游戏支持按钮状态的振动……您知道在控制器上激活双重冲击。见鬼,如果游戏不支持振动,您甚至可以通过将状态对象包装在一个装饰器中来解决这个问题,该装饰器以适当地与某些状态接口的方式实现双重冲击。

设计模式的问题,根据我的经验......如果使用得当,它们非常强大,它们可以增强您的应用程序支持的功能,从而使它们变得更好。但是,如果使用不当,它们只会使代码完全复杂。使整个设计脱轨,使维护变得麻烦...

一般来说,我更喜欢适合特定问题的设计模式的简单应用。如果设计太复杂,尤其是当您看不到它如何适合问题域时……那么您必须开始问自己……我是否正确实现了这一点?增加的复杂性真的值得我在这里做的吗?这实际上向应用添加了什么...?

这就是我所拥有的。祝您在设计工作中好运