noem/state-machine-interface

构建和消费基于事件的有限状态机的合约

dev-master 2024-02-03 09:53 UTC

This package is auto-updated.

Last update: 2024-09-03 10:59:28 UTC


README

状态机接口

{: .no_toc }

此仓库包含构建和消费基于事件的有限状态机的合约。

目录

{: .no_toc .text-delta }

  1. 此文档的漂亮 GH Pages 版本在这里显示了目录 {:toc}

1 - 术语

  • 状态 - 应用状态的命名表示,可以通过直接转换或通过一系列高级超状态的链来隐式确定。
  • 转换 - 定义何时以及如何从一个状态移动到另一个状态
  • 触发器 - 会引发转换的事件。任何 PHP object 都可以用作触发器,这使得此接口与基于 PSR-14 的事件系统高度互操作。
  • 动作 - 动作再次是一个 object,其工作方式类似于事件。它的处理程序基于当前状态,但以非常灵活的方式实现有状态的行为。
  • 状态机 - 触发器和动作的主要入口点。负责跟踪当前状态,执行转换并通知订阅者。
  • 上下文 与当前状态相关的元数据
  • 转换提供者 - 为任何给定的 trigger 返回一个有效的 Transition 对象。
  • 状态机观察者 - 允许外部代码订阅状态更新。
  • 状态存储 - 一个抽象层,用于将加载和保存活动状态的过程与各种来源(例如,内存、数据库、Redis)接口。

2 - 概念

2.1 - StateMachineInterface

实现此接口的类的目的是跟踪活动状态,以及在扩展 ObservableStateMachineInterface 的情况下将外部应用程序的事件委派。很容易被诱惑在这个类中塞入大量的逻辑和职责,这就是为什么这里提供的接口故意将一些预期职责从类中分离出来。

interface StateMachineInterface
{

    /**
     * Implementing methods MUST receive a TransitionInterface object from a TransitionProviderInterface.
     * If a transition object is returned, its target state MUST be transitioned to.
     *
     * @see TransitionInterface::target()
     *
     * @param object $payload
     * @return StateMachineInterface
     */
    public function trigger(object $payload): self;
}

2.1.1 - 执行转换

因此,基本接口上唯一必需的方法是响应外部 trigger (-> 事件)。当状态机被触发时,必须从一个 TransitionProvider 请求一个 Transition 对象。如果没有返回有效的转换,则不需要进一步操作。

状态机可以接收一个 StateStorageInterface 作为注入的依赖项。如果使用此依赖项,则必须使用 StateStorageInterface::save() 调用新状态,以便持久化状态更改。

ObservableStateMachineInterface 必须在转换时通知所有订阅者。关于此的更多内容将在下一节中介绍。

2.1.2 - 处理事件

有两个与事件处理相关的接口,状态机可以可选实现

ObservableStateMachineInterface

此接口定义了一个分离成三个感兴趣区域的观察者模式

  • 进入状态
  • 退出状态
  • 执行动作
interface ObservableStateMachineInterface extends StateMachineInterface
{

    /**
     * Adds an observer to the stack.
     * When a state machine exits a state, all ExitStateObservers MUST be notified.
     * When a new state is entered, all EnterStateObservers MUST be notified
     * When a state action is triggered (on implementors of StatefulActorInterface),
     * all ActionObservers MUST be notified
     *
     * @see Observer\EnterStateObserver, Observer\ExitStateObserver, Observer\ActionObserver, ActorInterface
     *
     * @param Observer\StateMachineObserver $observer
     *
     * @return self
     */
    public function attach(Observer\StateMachineObserver $observer): self;

    /**
     * Removes an observer from the stack.
     * The observer MUST no longer be notified of state changes
     *
     * @param Observer\StateMachineObserver $observer
     *
     * @return self
     */
    public function detach(Observer\StateMachineObserver $observer): self;
}

因此,状态机可以编写而无需了解任何事件处理逻辑,更不用说提供自己的逻辑。使用 StateMachineObserver 的用例包括

  • 实现作为状态的 onEntryonExitaction 回调定义的内部有状态行为
  • 日志记录
  • 桥接到外部事件系统

ActorInterface

实现此接口的类提供了一种通过action($payload)方法处理任意object负载的方式。

interface ActorInterface
{

    /**
     * Carry out an action corresponding to the given payload object
     *
     * @param object $payload Arbitrary data relevant for the desired action.
     *
     * @return object The payload object - modified by the implementing method if applicable
     */
    public function action(object $payload): object;
}

这是将应用程序逻辑与状态机接口的主要方式。每当需要状态相关的行为或数据时,可以从状态机请求相应的操作。

它旨在作为任何用例的通用和灵活的入口点,因此将此方法包装在更具体的糖方法中,以直接针对特定状态机的使用场景是一个好主意。

namespace Noem\State;

class MyFSM implements StateMachineInterface, ActorInterface {

    public function trigger(object $payload): self {
        // ... implementation
        return $this;
    }
    
    public function action(object $payload): object {
        // ... implementation
    }
    
    public function buttonLabel(): string {
        $result = $this->action((object)['label' => '']);
        return (string) $result->label;
    }

}

ContextAwareStateMachineInterface

更复杂的实现可能希望存储属于特定状态的元数据。一个经典的例子是“按钮点击”

  • 用户点击按钮。发出一个ButtonClicked事件。我们进入button.held状态。
  • 现在可以过渡到button.pressedbutton.longPressed,每个状态都禁用,直到发生ButtonReleased事件
  • 在每次Update操作中,我们通过$machine->context($state)['timeHeld']++增加timeHeld
  • 按钮被释放,导致一个ButtonReleased事件
  • 守卫回调正在触发:如果timeHeld < 500ms,则启用过渡到button.pressed。如果是>500ms,则启用过渡到button.longPressed

ContextInterface提供了一种存储此类元信息的方法,而无需依赖外部框架或全局状态

2.2 - TransitionProviderInterface

转换提供者负责根据给定的操作和当前状态返回一个有效的转换。

interface TransitionProviderInterface
{

    /**
     * Return a Transition object that matches the given state and trigger
     *
     * @param StateInterface $state For comparing the source state against
     * @param object $trigger For evaluating whether the transition is enabled
     *
     * @return TransitionInterface|null
     */
    public function getTransitionForTrigger(StateInterface $state, object $trigger): ?TransitionInterface;
}

它在意图和功能上类似于PSR-14的ListenerProvider

  • 它通过屏蔽状态机和状态及转换来源的知识以及它们如何交互的复杂性来简化状态机对象的复杂性
  • 因此,它的责任可以简化为
    • 跟踪当前状态
    • 处理事件
  • 状态和转换的交互以及与状态图的外部交互允许多个包之间的互操作性:一个AggregateTransitionProvider可能从多个模块收集转换,从而构建一个由许多松散耦合的片段组成的状态机。