更多优质内容
请关注公众号

面向对象和设计模式(二十四) 如何实现有限状态机之状态模式——PHP实现-张柏沛IT博客

正文内容

面向对象和设计模式(二十四) 如何实现有限状态机之状态模式——PHP实现

栏目:PHP 系列:面向对象与设计模式 发布时间:2023-06-06 14:03 浏览量:1299
本系列文章目录
展开/收起

一、什么是有限状态机

在介绍状态模式之前,需要介绍“有限状态机”的概念。通俗的来说,如果一个对象拥有不同且有限个数的状态,且在特定的情况下,这些状态可以相互切换,那么我们就可以说这个对象是一个有限状态机。


在具体的业务中,有限状态机非常常见,例如 任务、单据和工作流等对象他们都有未处理、处理中、已完成等状态。


状态机有 3 个组成部分:状态、事件、动作。事件触发状态的转移,而状态的转移会引发动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。


反过来,无限状态机就是状态个数无限,例如我们说一个商品的库存,它可以是100个,也可以是200个,也可以是321个,商品的库存数量是无限的,因此库存对象就是一个无限状态机。


举个有限状态机的具体例子:马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。




二、状态模式

状态模式是一种实现有限状态机对象的设计模式,用来描述一个状态机对象的事件、状态转化过程以及引发的动作。

它的使用场景就是实现一个对象的状态转变。


实际上,除了状态模式之外,还有分支逻辑法查表法可以实现状态机。下面,我们分别用分支逻辑法、查表法和状态模式这三种方式来实现上述的马里奥状态变化。


无论是哪种方法,状态机的实现都是以事件作为驱动的,说人话就是对象的方法名应该是一个事件(即图中的线条),而方法内容应该是状态的转移和动作的执行。


因此无论使用哪种实现方法,状态机的实现框架都应该是如下所示的代码:

class MarioStateMachine {	// 马里奥状态机
    // 省略其他方法和属性

    // 获得蘑菇事件
    public function obtainMushRoom() {
        //TODO
    }

    // 获得斗篷事件
    public function obtainCape() {
        //TODO
    }

    // 获得火焰事件
    public function obtainFireFlower() {
        //TODO
    }

    // 遇到怪兽事件
    public function meetMonster() {
        //TODO
    }
  }


此外强烈推荐实现状态机之前,先像上面那样画出状态转移图,方便分析。



状态机实现方式一:分支逻辑法


状态机最简单直接的实现方式是,参照状态转移图,将每一个状态转移,直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,因此称为分支逻辑法。


代码如下所示:

<?php

class MarioStateMachine {
    const SMALL = 0;
    const SUPER = 1;
    const FIRE = 2;
    const CAPE = 3;
    private $score;
    private $currentState;
  
    public function __construct()
    {
      $this->score = 0;
      $this->currentState = self::SMALL;
    }
  
    public function obtainMushRoom() {
      if ($this->currentState == self::SMALL) {
        $this->currentState = self::SUPER;
        $this->score += 100;
      }
    }
  
    public function obtainCape() {
      if ($this->currentState == self::SMALL || $this->currentState == self::SUPER ) {
        $this->currentState = self::CAPE;
        $this->score += 200;
      }
    }
  
    public function obtainFireFlower() {
      if ($this->currentState == self::SMALL || $this->currentState == self::SUPER ) {
        $this->currentState = self::FIRE;
        $this->score += 300;
      }
    }
  
    public function meetMonster() {
      if ($this->currentState == self::SUPER) {
        $this->currentState = self::SMALL;
        $this->score -= 100;
        return;
      }
  
      if ($this->currentState == self::CAPE) {
        $this->currentState = self::SMALL;
        $this->score -= 200;
        return;
      }
  
      if ($this->currentState == self::FIRE) {
        $this->currentState = self::SMALL;
        $this->score -= 300;
        return;
      }
    }
  
    public function getScore() {
      return $this->score;
    }
  
    public function getCurrentState() {
      return $this->currentState;
    }
  }


分支逻辑法可以处理简单的状态转移和动作变更逻辑。但是,对于复杂的状态机来说,这种实现方式极易漏写错写某个状态转移。

除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易出错,引入 bug。



状态机实现方式二:查表法


我们知道,状态机有3个维度:状态、动作 与 事件。

查表法本质是将状态、动作 与 事件这三个维度用一个二维表进行映射。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。


此时我们需要在代码中维护1个二维数组statusActionMap,存储状态的转移与动作的映射关系即可。

相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 statusActionMap 这个二维数组即可。


具体的代码如下所示:

<?php

class MarioStateMachine {
    // 常量省略

    protected static $statusActionMap = [
        self::SMALL => [
            self::CAPE => 200,
            self::FIRE => 300,
            self::SUPER => 100,
        ],
        self::SUPER => [
            self::CAPE => 200,
            self::FIRE => 300,
            self::SMALL => -100,
        ],
        self::CAPE => [
            self::SMALL => -100,
        ],
        self::FIRE => [
            self::SMALL => -100,
        ],
    ];

    private $score;
    private $currentState;
  
    public function __construct()
    {
      $this->score = 0;
      $this->currentState = self::SMALL;
    }

    public function doEvent($newStatus){
        if(
            !isset(self::$statusActionMap[$this->currentState]) || 
            !isset(self::$statusActionMap[$this->currentState][$newStatus])
        ){
            return ;
        }

        $this->score += self::$statusActionMap[$this->currentState][$newStatus];
    }
  
    public function obtainMushRoom() {
        $this->doEvent(self::SUPER);
    }
  
    public function obtainCape() {
        $this->doEvent(self::CAPE);
    }
  
    public function obtainFireFlower() {
        $this->doEvent(self::FIRE);
    }
  
    public function meetMonster() {
        $this->doEvent(self::SMALL);
    }
  }



状态机实现方式三:状态模式


在查表法的代码实现中,事件触发的动作只是简单的积分加减,所以,我们用一个二维数组就能表示动作。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作,比如加减积分、写数据库,还有可能发送消息通知等等,我们就没法用如此简单的二维数组来表示了。


虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。


实际上,针对复杂动作变更,分支逻辑法存在的问题,我们可以使用状态模式来解决。状态模式通过将事件触发和动作执行,以不同状态的维度拆分到不同的状态类中,来避免分支判断逻辑。


我们还是结合代码来理解这句话。利用状态模式,我们来补全 MarioStateMachine 类,补全后的代码如下所示。其中,IMario 是状态的接口,定义了所有的事件。SmallMario、SuperMario、CapeMario、FireMario 是 IMario 接口的实现类,分别对应状态机中的 4 个状态。


原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 类中,现在,这些代码逻辑被拆分到了这 4 个状态类中。

<?php

interface MarioEvent{
    public function obtainMushRoom();
    public function obtainCape();
    public function obtainFireFlower();
    public function meetMonster();
}

/**
 * @property MarioStateMachine $stateMachine
 */
  class Mario implements MarioEvent{
    private $score = 0;
    protected $stateMachine;// 状态机对象来表示马里奥的状态
  
    private function __construct($initState = MarioStateMachine::SMALL) {
        $this->stateMachine = MarioStateMachine::instance($this, $initState);
    }
  
    public function getState() {
        return $this->stateMachine->getState();
    }

    public function getScore() {
        return $this->score;
    }

    public function addScore($update) {
        $this->score += $update;
    }

    public function setStateMachine(MarioStateMachine $stateMachine) {
        $this->stateMachine = $stateMachine;
    }

    public function obtainMushRoom(){
        $this->stateMachine->obtainMushRoom();
    }

    public function obtainCape(){
        $this->stateMachine->obtainCape();
    }

    public function obtainFireFlower(){
        $this->stateMachine->obtainFireFlower();
    }

    public function meetMonster(){
        $this->stateMachine->meetMonster();
    }

    // Mario对象不暴露 setState 这种直接改变状态的方法,因为状态往往和分数等其它属性联动变化
  }

  // 马里奥状态机
  abstract class MarioStateMachine implements MarioEvent{
    const SMALL = 0;
    const SUPER = 1;
    const FIRE = 2;
    const CAPE = 3;
    const STATE_MACHINE_MAP = [
        self::SMALL => SmallMario::class,
        self::SUPER => SuperMario::class,
        self::FIRE => FireMario::class,
        self::CAPE => CapeMario::class,
    ];

    protected $mario;       // 马里奥对象
    protected static $stateMap = [];        // 多例模式,存储4种马里奥状态

    public static function instance(Mario $mario, $currentState){
        if(empty(self::$stateMap[$currentState])){
            $stateClass = self::STATE_MACHINE_MAP[$currentState];
            self::$stateMap[$currentState] = new $stateClass($mario);
        }
        return self::$stateMap[$currentState];
    }

    private function __construct(Mario $mario){
        $this->mario = $mario;
    }

    abstract public function getState();
  }
  
  class SmallMario extends MarioStateMachine{
    public function getState(){
        return self::SMALL;
    }
    
    public function obtainMushRoom() {
        $this->mario->addScore(100);
        $this->mario->setStateMachine(MarioStateMachine::instance($this->mario, self::SUPER));
    }

    public function obtainCape() {
        $this->mario->addScore(200);
        $this->mario->setStateMachine(MarioStateMachine::instance($this->mario, self::CAPE));
    }

    public function obtainFireFlower() {
        $this->mario->addScore(300);
        $this->mario->setStateMachine(MarioStateMachine::instance($this->mario, self::FIRE));
    }

    public function meetMonster() {
        echo "Game over!";
    }
  }

  class SuperMario extends MarioStateMachine{
    public function getState(){
        return self::SUPER;
    }
    
    public function obtainMushRoom() {
        $this->mario->addScore(100);
    }

    public function obtainCape() {
        $this->mario->addScore(200);
        $this->mario->setStateMachine(MarioStateMachine::instance($this->mario, self::CAPE));
    }

    public function obtainFireFlower() {
        $this->mario->addScore(300);
        $this->mario->setStateMachine(MarioStateMachine::instance($this->mario, self::FIRE));
    }

    public function meetMonster() {
        $this->mario->addScore(-100);
        $this->mario->setStateMachine(MarioStateMachine::instance($this->mario, self::SMALL));
    }
  }

  // 省略CapeMario、FireMario类...

  class ApplicationDemo {
    public static function main() {
      $mario = new Mario();
      $mario->obtainMushRoom();
      $score = $mario->getScore();
      $state = $mario->getState();
      echo "mario score: " + $score + "; state: " + $state;
    }
  }


上面的代码,Mario对象需要重度依赖StateMachine对象,直接将Mario的状态以对象的形式保存。


实际上,我们完全可以将和状态相关的逻辑从Mario对象中抽离出来,Mario对象可以保留整型的state属性,并且有最基础的setState方法。但是和状态转移和落库的相关逻辑可以只交给状态机负责,也就是说状态机和Mario对象可以完全没有关系,不过状态机需要保存和状态相关的其他属性,例如score分数。Mario对象也有score和state,因为这是Mario对象自身的属性,Machine对象也有score和state,因为Machine做状态转移时需要用到。Mario的score、state与Machine的score、state可以不一致,Machine完成状态转移后再刷新Mario对象即可。

<?php

interface MarioEvent{
    public function getState();
    public function obtainMushRoom();
    public function obtainCape();
    public function obtainFireFlower();
    public function meetMonster();
}

class Mario{
    const SMALL = 0;
    const SUPER = 1;
    const FIRE = 2;
    const CAPE = 3;

    // 省略Mario自身的属性和方法
}

abstract class MarioState {

    const STATE_MACHINE_MAP = [
        self::SMALL => SmallMario::class,
        self::SUPER => SuperMario::class,
        self::FIRE => FireMario::class,
        self::CAPE => CapeMario::class,
    ];
    protected static $stateMap = [];        // 多例模式,存储4种马里奥状态

    public static function instance(int $state){
        if(empty(self::$stateMap[$state])){
            $stateClass = self::STATE_MACHINE_MAP[$state];
            self::$stateMap[$state] = new $stateClass();
        }
        return self::$stateMap[$state];
    }

    abstract public function getState();
}

class SmallState extends MarioState{
    public function getState() {
        return Mario::SMALL;
    }

    public function obtainMushRoom(MarioStateMachine $stateMachine){
        $stateMachine->setState(Mario::SUPER);
        $stateMachine->setScore($stateMachine->getScore() + 100);
    }

    public function obtainCape(MarioStateMachine $stateMachine){
        $stateMachine->setState(Mario::CAPE);
        $stateMachine->setScore($stateMachine->getScore() + 200);
    }

    public function obtainFireFlower(MarioStateMachine $stateMachine){
        $stateMachine->setState(Mario::FIRE);
        $stateMachine->setScore($stateMachine->getScore() + 300);
    }

    public function meetMonster(MarioStateMachine $stateMachine){
        echo "Game Over";
    }
}

// 省略CapeState、FireState、SuperState类...

/**
 * 马里奥状态机
 * @property MarioState $marioState
 */
class MarioStateMachine implements MarioEvent{
    protected $marioState;       // 马里奥状态
    private $score = 0;

    public function __construct(int $initState){
        $this->setState($initState);
    }

    public function setState(int $state){
        $this->marioState = MarioState::instance($state);
    }

    public function getScore(){
        return $this->score;
    }

    public function setScore($score){
        $this->score = $score;
    }

    public function getState(){
        return $this->marioState->getState();
    }

    public function obtainMushRoom(){
        $this->marioState->obtainMushRoom($this);
    }
    public function obtainCape(){
        $this->marioState->obtainCape($this);
    }
    public function obtainFireFlower(){
        $this->marioState->obtainFireFlower($this);
    }
    public function meetMonster(){
        $this->marioState->meetMonster($this);
    }
}

class ApplicationDemo {
    public static function main() {
        $stateMachine = new MarioStateMachine(Mario::SMALL);
        $stateMachine->obtainMushRoom();
        $score = $stateMachine->getScore();
        $state = $stateMachine->getState();
        echo "mario score: " + $score + "; state: " + $state;
    }
}


这个写法是真正的状态模式的写法和思维,前一种写法不算正规的状态模式,因为将复杂的状态转移行为放到了Mario对象中,与Mario对象耦合了起来,职责不够单一。


这种写法中,State和Machine也是双向依赖,不过Machine对State是强依赖,而State对Machine是一种弱依赖(因为State依赖Machine只是为了改变Machine内部的属性而已,而Machine对State的依赖是依赖State的行为,行为比操作属性更加的重),所以在obtainMushRoom这样的方法中通过传递Machine对象的方式让State对象可以使用到它,而非将Machine对象作为State对象的属性。


最后总结:

状态模式会引入非常多的状态类,会导致代码比较难维护,建议只有当三要素中的动作逻辑复杂的时候才使用状态模式。像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现;

对于状态比较多而动作逻辑简单的状态机,则优先推荐使用查表法,如果这种情况使用状态模式会产生很多状态类,而每个状态类中又没有什么实际内容。

如果状态和动作逻辑都很简单,则使用分支逻辑法即可。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 面向对象和设计模式(二十四) 如何实现有限状态机之状态模式——PHP实现

热门推荐
推荐新闻