paint-brush
状态机可以帮助您解决复杂的编程问题经过@pragativerma
9,236 讀數
9,236 讀數

状态机可以帮助您解决复杂的编程问题

经过 Pragati Verma18m2022/09/26
Read on Terminal Reader
Read this story w/o Javascript

太長; 讀書

“状态”是所有开发人员从初级编程到中级编程时都会经历的一个常见编程术语。在计算机科学中,程序的状态被定义为它相对于先前存储的输入的位置。例如,控制变量(例如循环中使用的控制变量)会在每次迭代中改变程序的状态。检查程序的当前状态可用于测试或分析代码库。添加另一个状态很困难,因为它需要重写许多不同类的代码。

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - 状态机可以帮助您解决复杂的编程问题
Pragati Verma HackerNoon profile picture
0-item


“状态”是所有开发人员从初级编程进入中级编程时都会经历的一个常见编程术语。那么,“国家”一词究竟是什么意思呢?

通常,对象的状态只是对象的当前快照或其一部分。同时,在计算机科学中,程序的状态被定义为它关于先前存储的输入的位置。在这种情况下,“状态”一词的使用方式与科学中的使用方式相同:物体的状态,如气体、液体或固体,代表其当前的物理性质,而计算机程序的状态反映其当前的价值或内容。

存储的输入作为变量或常量保存在计算机程序中。在评估程序状态时,开发人员可能会检查这些输入中包含的值。程序运行时的状态可能会改变——变量可能会改变,内存值可能会改变。例如,控制变量(例如循环中使用的控制变量)会在每次迭代中改变程序的状态。检查程序的当前状态可用于测试或分析代码库。


在更简单的系统中,状态管理经常使用 if-else、if-then-else、try-catch 语句或布尔标志来处理;但是,当程序中有太多可以想象的状态时,这是无用的。它们可能导致难以理解、维护和调试的笨重、复杂的代码。


if-else-clauses 或 booleans 的一个缺点是它们可能会变得相当广泛,并且添加另一种状态很困难,因为它需要重写许多不同类的代码。假设您要制作一个具有主菜单、游戏循环和完成屏幕的游戏。


让我们构建一个视频播放器,例如:


 class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_stopped = True # A video can only be played when paused or stopped def play(self): if not self.is_playing or self.is_paused: # Make the call to play the video self.is_playing = True self.is_paused = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing def pause(self): if self.is_playing: # Make the call to pause the video self.is_playing = False self.is_paused = True else: raise Exception( 'Cannot pause a video that is not playing' ) # A video can only be stopped when it is playing or paused def stop(self): if self.is_playing or self.is_paused: # Make the call to stop the video self.is_playing = False self.is_paused = False else: raise Exception( 'Cannot stop a video that is not playing or paused' )


上面的代码片段是一个简单的视频播放器应用程序的 if-else 实现,其中三个基本状态是 - 播放、暂停和停止。但是,如果我们尝试添加更多状态,代码将迅速变得复杂、臃肿、重复,并且难以理解和测试。让我们看看添加另一个状态“rewind”时的代码是什么样的:


 class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_rewinding = False self.is_stopped = True # A video can only be played when it is paused or stopped or rewinding def play(self): if self.is_paused or self.is_stopped or self.is_rewinding: # Make the call to play the video self.is_playing = True self.is_paused = False self.is_stopped = False self.is_rewinding = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing or rewinding def pause(self): if self.is_playing or self.is_rewinding: # Make the call to pause the video self.is_playing = False self.is_paused = True self.is_rewinding = False self.is_stopped = False else: raise Exception( 'Cannot pause a video that is not playing or rewinding' ) # A video can only be stopped when it is playing or paused or rewinding def stop(self): if self.is_playing or self.is_paused or self.is_rewinding: # Make the call to stop the video self.is_playing = False self.is_paused = False self.is_stopped = True self.is_rewinding = False else: raise Exception( 'Cannot stop a video that is not playing or paused or rewinding' ) # 4. A video can only be rewinded when it is playing or paused. def rewind(self): if self.is_playing or self.is_paused: # Make the call to rewind the video self.is_playing = False self.is_paused = False self.is_stopped = False self.is_rewinding = True else: raise Exception( 'Cannot rewind a video that is not playing or paused' )


如果没有状态模式,您将不得不在整个代码中检查程序的当前状态,包括更新和绘制方法。如果要添加第四种状态,例如设置屏幕,则必须更新许多不同类的代码,这很不方便。这就是状态机的想法派上用场的地方。


什么是状态机?

状态机在计算机科学中并不是一个新概念。它们是软件业务中使用的基本设计模式之一。它比面向编码更面向系统,用于围绕用例建模。

让我们看一个通过 Uber 租用出租车的简单现实示例:

  1. 当您最初启动该程序时,它会将您带到主屏幕,您可以在其中在搜索区域中输入您的目的地。
  2. 一旦确定了正确的位置,Uber 就会显示推荐的旅行选项,例如 Pool、Premier、UberGo、Uber XL 等,以及定价估算。
  3. 一旦您选择付款选项并在必要时使用指定的行程时间按“确认”按钮,行程即被确认并指定司机。
  4. 优步现在会显示一张地图,您可以在上面找到您的司机。


屏幕 1 是此用例中所有用户看到的第一个屏幕,它是独立的。屏幕 2 依赖于屏幕 1,在您在屏幕 1 上提供准确数据之前,您将无法进入屏幕 2。同样,屏幕 3 依赖于屏幕 2,而屏幕 4 依赖于屏幕 3。如果您都没有您的司机也不会取消您的行程,您将被移至屏幕 4,您将无法在当前行程结束之前计划另一次行程。


假设下大雨,没有司机接受您的行程,或者在您所在地区找不到可用的司机来完成您的行程;将显示一条错误通知,警告您驱动程序不可用,并且您仍停留在屏幕 3 上。您仍可以返回屏幕 2、屏幕 1,甚至是第一个屏幕。

您处于出租车预订流程的不同步骤,只有在当前阶段的指定操作成功后,您才能进入下一个级别。例如,如果您在屏幕 1 上输入了错误的位置,您将无法进入屏幕 2,并且您将无法进入屏幕 3,除非您在屏幕 2 上选择旅行选项,但您可以除非您的行程已经预订,否则请始终返回上一阶段。


在上面的示例中,我们将出租车预订流程划分为多个活动,每个活动可能会或可能不会根据预订状态调用另一个活动。使用状态机对此进行建模。原则上,这些阶段/状态中的每一个都应该是自主的,只有在当前阶段(无论是否成功)完成后,才会召唤下一个阶段/状态。


用更专业的术语来说,状态机使我们能够将一个大而复杂的动作分解为一系列独立的较小活动,例如前面示例中的出租车预订活动。


事件连接较小的任务,从一种状态转移到另一种状态称为转换。我们通常会在从一种状态切换到另一种状态后执行一些操作,例如在后端创建预订、开具发票、保存用户分析数据、将预订数据捕获到数据库中、行程结束后触发付款等等.


因此,状态机的一般公式可以如下给出:


当前状态 + 一些动作 / 事件 = 另一个状态


让我们看看为一个简单的视频播放器应用程序设计的状态机是什么样子的:





我们可以使用转换在代码中实现它,如下所示:


 from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass


现在,如果我们想添加另一个状态,比如倒带,我们可以很容易地做到这一点,如下所示:





 from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' REWINDING = 'rewinding' # new def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.REWINDING, 'dest': self.PLAYING}, # new # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, {'trigger': 'pause', 'source': self.REWINDING, 'dest': self.PAUSED}, # new # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.REWINDING, 'dest': self.STOPPED}, # new # 4. A video can only be rewinded when it is playing or paused. {'trigger': 'rewind', 'source': self.PLAYING, 'dest': self.REWINDING}, #new {'trigger': 'rewind', 'source': self.PAUSED, 'dest': self.REWINDING}, # new ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass def rewind(self): pass


因此,我们可以看到状态机如何简化复杂的实现并避免我们编写不正确的代码。了解了状态机的功能后,现在重要的是要了解为什么以及何时使用状态机。


为什么以及何时使用状态机?

状态机可用于具有不同状态的应用程序。每个阶段都可以导致一个或多个后续状态,以及结束流程流。状态机使用用户输入或状态内计算来选择接下来要进入的状态。


许多应用程序需要一个“初始化”阶段,然后是允许各种操作的默认状态。先前和当前的输入以及状态都会对执行的操作产生影响。然后可以在系统“关闭”时执行清理措施。


如果我们可以将一个非常复杂的工作分解成更小的独立单元,状态机可以帮助我们更抽象地概念化和管理这些单元,我们只需要描述一个状态何时可以转换到另一个状态以及转换发生时会发生什么。我们不需要关心设置后转换是如何发生的。在那之后,我们只需要考虑何时和什么,而不是如何。

此外,状态机让我们以非常可预测的方式查看整个状态过程;一旦设置了转换,我们就不必担心管理不善或错误的状态转换;只有正确配置了状态机,才可能发生不正确的转换。我们全面了解状态机中的所有状态和转换。


如果我们不使用状态机,我们要么无法在各种可能的状态下可视化我们的系统,要么我们有意或无意地将我们的组件紧密耦合在一起,或者我们正在编写许多 if-else 条件来模拟状态转换,这使单元测试和集成测试变得复杂,因为我们必须确保编写所有测试用例来验证所有条件和使用的分支的可能性。


状态机的优点

状态机除了能够开发决策算法外,还是应用规划的功能形式。随着应用程序变得越来越复杂,对有效设计的需求也在增长。


状态图和流程图在整个设计过程中都很有用,有时甚至是必不可少的。状态机不仅对应用程序规划很重要,而且易于创建。


以下是现代计算中状态机的一些主要优势:

  • 它可以帮助您消除硬编码条件。状态机代表您抽象与状态和转换相关的所有逻辑。
  • 状态机通常具有有限数量的具有明确转换的状态,这使得识别哪个转换/数据/事件触发了请求的当前状态变得简单。
  • 建立状态机后,开发人员可以专注于构建动作和前提条件。通过充分的验证和预处理,状态机可以限制乱序操作。与优步的示例一样,在航程完成之前无法奖励司机。
  • 状态机可以很容易维护。在每个过渡期间采取的行动在逻辑上彼此独立。这样就可以隔离对应的代码了。
  • 状态机更不容易改变并且更稳定。如果当前和未来的用例非常明显,那么维护这样的系统就会变得容易得多。


状态机的缺点

并非关于状态机的一切都是好的,它们有时也会导致缺点和挑战。以下是状态机的一些常见问题:

  • 状态机通常是同步的。因此,如果您需要异步后台 API 调用/作业执行,则必须在决定最佳选择之前仔细权衡利弊。
  • 代码很快就会变得混乱。因为状态机是数据驱动的,您的产品团队可能会要求您根据不同的数据/输入参数从同一状态执行不同的转换。因此,这种类型的需求可能会导致带有笨拙的前置条件检查的多个转换。这完全取决于产品和机器的当前配置。
  • 如果您需要对状态机实例进行负载平衡,请使用启用了持久性的状态机实例;否则,您将需要实现持久层和必要的验证,以确保在不同的状态机实例上触发的多个请求产生一致的结果。
  • 因为很少有资源或社区专门用于不同的状态机实现,所以一旦您选择了一个库,帮助可能会受到限制。


使用状态机时的注意事项

使用状态机时,理想情况下您的系统应该有两个逻辑组件:

  1. 状态机/工作流系统本身
  2. 包含在一项或多项服务中的业务逻辑。


状态机可以被认为是驱动状态转换的基础设施。它验证状态转换并在转换之前、期间和之后执行配置的操作;但是,它不应该知道在这些操作中执行了什么业务逻辑。


因此,一般来说,使用正确的抽象将状态机与核心业务逻辑隔离是一个好主意;否则,管理代码将是一场噩梦。


以下是我们需要谨慎使用状态机逻辑的其他一些现实场景:

  • 状态机不仅仅是状态、转换和动作。它还应该能够定义状态变化的边界。只有在由可信赖的系统或用户触发时,转换才能在特定情况下成功。可能有多种类似的情况。因此,我们应该能够开发适当的状态转换保护逻辑。
  • 我们通常会为同一个业务实体创建许多可以同时运行的流程。在这种情况下,一个流程不会妨碍其他工作流程;它们可能会同时触发,也可能不会同时触发,但它们可以共存;第二个工作流可以从第一个工作流的一个合格阶段开始,之后它可以分支并独立工作。这种用例是由业务建立的;并非每个组织都会拥有它。
  • 原则上,工作流系统独立于业务领域。因此,在同一个工作流系统中,可以建立许多与同一个业务组织没有链接的流程。它们可能有一个共享的或不同的起点,这取决于工作流系统是否启用多个起点。
  • 当在同一个工作流系统中形成多个单独的工作流时,您可以获得跨系统中各种业务实体运行的所有业务流程的全局图。根据业务用例,不同的流程也可能具有某些相同的阶段。


状态机的实际或现实用例:

以下是我们日常生活中受益于状态机概念的一些实际应用:

  • 单页或选项卡对话框 对话框中的选项卡代表每个状态。用户可以通过选择某个选项卡来启动状态转换。每个选项卡的状态包括用户可以执行的任何操作。
  • 自助银行机(ATM)。在这个应用程序中,诸如等待用户输入、根据账户余额确认所需金额、分发货币、打印收据等状态都是可以想象的。
  • 一种软件,它进行一次测量,将其存储在内存中,然后等待用户采取另一个行动。该程序的步骤包括等待用户输入、执行测量、记录数据、显示结果等。例如,配置 ETL 作业。
  • 状态机通常用于设计用户界面。在设计用户界面时,不同的用户操作将用户界面转移到单独的处理段中。这些元素中的每一个都将作为状态机中的一个状态。这些段可以导致另一个段进行进一步处理或等待另一个用户事件。在这种情况下,状态机监视用户以确定他们下一步应该做什么。


例如,当您从在线电子商务网站购买商品时,它会经历多个阶段,例如订购、包装、发货、取消、已交付、已付款、退款等。当物品通过仓库或物流中心并在各个阶段进行扫描时,这种转换会自动发生,例如当用户取消或想要退款时。

  • 过程测试是状态机的另一个典型应用。在此示例中,过程的每个阶段都由一个状态表示。根据每个州的考试结果,可能会宣布一个单独的州。如果对所检查的过程进行彻底研究,这种情况可能会经常发生。


结论

状态机的概念在编程中非常有用。它不仅简化了开发更复杂用例应用程序的过程,还减少了必要的开发工作。它提供了对现代事件的更简单和优雅的掌握,如果正确应用,可能会创造奇迹。