在这篇文章中我将描述状态机(State Machine)的使用,展示一个简单的实现,一个使用State模式的实现。值得一提的是,如果状态少于三个,则不宜使用 State 模式,因为这通常会导致代码可读性和相关支持问题不必要的复杂性——凡事都要有个度。

旗帜之王
假设我们正在为民用飞机的媒体系统开发视频播放器屏幕,播放器必须能够加载视频流、播放视频、允许用户停止下载过程、倒带以及执行其他通常的操作一名玩家。
假设播放器缓存了视频流的下一个块,检查是否有足够的块用于播放,开始向用户播放片段,同时继续下载下一个。
此时,用户快退到视频的中间,即现在需要停止播放当前片段并从新位置开始加载。然而,在某些情况下这是无法做到的——用户在观看有关航空安全的视频时无法控制视频流的播放。让我们检查 isSafetyVideoPlaying 标志来检查这种情况。
系统还必须能够暂停当前视频并通过播放器广播船长和船员的警报。让我们添加另一个 isAnnouncementPlaying 标志。另外,要求在显示有关使用播放器的帮助时不要暂停播放,另一个标志是HelpPresenting。
媒体播放器示例伪代码:
class MediaPlayer {
public var isHelpPresenting = false
public var isCaching = false
public var isMediaPlaying: Bool = false
public var isAnnouncementPlaying = false
public var isSafetyVideoPlaying = false
public var currentMedia: Media = null
fun play(media: Media) {
if isMediaPlaying == false, isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
if isCaching == false {
if isHelpPresenting == false {
media.playAfterHelpClosed()
}
else {
media.playAfterCaching()
}
}
}
fun pause() {
if isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
currentMedia.pause()
}
}
}
由于高可变性(熵),上面的示例难以阅读且难以维护。此示例基于我使用不使用状态机的*许多*项目的代码库的经验。
每个复选框必须专门“控制”应用程序的界面元素和业务逻辑;添加另一个复选框的开发人员必须能够处理它们,使用所有可能的选项多次检查和重新检查所有内容。
代入公式“2 ^ 复选框数量”,只需 6 个复选框即可获得 2 ^ 6 = 64 个应用程序行为选项,所有这些复选框组合都需要手动检查和维护。
从开发人员的角度来看,使用这样的系统添加新功能如下所示:
–我们需要添加显示航空公司浏览器页面的功能,并且如果机组人员宣布某些内容,它应该像电影一样最小化。
–好的,我会做的。 (哦该死,我必须添加另一个标志并仔细检查标志相交的所有位置,有很多东西需要更改!)
这也是标志系统的一个弱点–更改应用程序的行为。很难想象如何快速/灵活地基于标志更改行为,如果只更改一个标志后您必须仔细检查所有内容。这种开发方法会导致很多问题、时间和金钱的损失。
进入机器
如果您仔细查看这些标志,您就会明白,实际上我们正在尝试处理现实世界中发生的特定进程。我们列出了它们:正常模式、显示安全视频、广播船长或船员的消息。对于每个进程,一组已知的规则会改变应用程序的行为。
根据状态机(state machine)模式的规则,我们将所有进程列为枚举中的状态,将状态这样的概念添加到播放器代码中,通过删除标志上的组合来实现基于状态的行为。这样我们就可以将测试的选项减少到精确的状态数量。
伪代码:
enum MediaPlayerState {
mediaPlaying,
mediaCaching,
crewSpeaking,
safetyVideoPlaying,
presentingHelp
}
class MediaPlayer {
fun play(media: Media) {
media.play()
}
func pause() {
media.pause()
}
}
class MediaPlayerStateMachine {
public state: MediaPlayerState
public mediaPlayer: MediaPlayer
public currentMedia: Media
//.. init (mediaPlayer) etc
public fun set(state: MediaPlayerState) {
switch state {
case mediaPlaying:
mediaPlayer.play(currentMedia)
case mediaCaching, crewSpeaking,
safetyVideoPlaying, presentingHelp:
mediaPlayer.pause()
}
}
}
标志系统和状态机的巨大区别在于 set(state: ..) 方法中的逻辑状态切换漏斗,它允许你将人类对状态的理解转化为程序代码,而无需玩逻辑当进一步的代码支持时将标志转换为状态的游戏。
模式状态
接下来我将展示状态机的简单实现和状态模式之间的区别。假设我们需要添加 10 个状态;结果,状态机类将增长到一个 godobject 的大小,这将变得难以维护且成本高昂。当然,这个实现比flag实现要好(使用flag系统,开发者会先开枪自杀,如果没有,那么看到2^10=1024个变体,QA就会挂掉自己,但如果他们都*不注意*任务的复杂性,那么应用程序简单的用户会注意到它将拒绝使用特定的标志组合)
如果状态数量较多,就需要使用State模式。
让我们向状态协议添加一组规则:
protocol State {
func playMedia(media: Media, context: MediaPlayerContext)
func shouldCacheMedia(context: MediaPlayerContext)
func crewSpeaking(context: MediaPlayerContext)
func safetyVideoPlaying(context:MediaPlayerContext)
func presentHelp(context: MediaPlayerContext)
}
让我们将一组规则的实现移至单独的状态,例如一个状态的代码:
class CrewSpeakingState: State {
func playMedia(context: MediaPlayerContext) {
showWarning(“Can’ t play media - listen to announce!”)
}
func mediaCaching(context: MediaPlayerContext) {
showActivityIndicator()
}
func crewSpeaking(context: MediaPlayerContext) {
set(volume: 100)
}
func safetyVideoPlaying(context: MediaPlayerContext) {
set(volume: 100)
}
func presentHelp(context: MediaPlayerContext) {
showWarning(“Can’ t present help - listen to announce!”)
}
}
接下来,让我们创建一个每个状态都可以工作的上下文,并集成状态机:
final class MediaPlayerContext {
private
var state: State
public fun set(state: State) {
self.state = state
}
public fun play(media: Media) {
state.play(media: media, context: this)
}
…
остальные возможные события
}
应用程序组件通过公共方法与上下文一起工作;状态对象本身决定使用上下文内的状态机从哪个状态转换到哪个状态。
因此,我们实现了上帝对象分解,维护变化的状态会容易得多,这得益于编译器跟踪协议的变化,由于代码行数的减少而降低了理解状态的复杂度,并且专注于解决特定的状态问题。您现在还可以在团队中共享工作,为团队成员提供特定状态的实现,而不必担心需要“解决”冲突,这种情况在使用一个大型状态机类时会发生。
来源
https://refactoring.guru/ru/design-patterns/state