在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上来看,它有点像我们之前讲到的组合模式。
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,还有分支逻辑法与查表法。今天,我们就详细讲讲这几种实现方式,并且对比一下它们的优劣和应用场景。

什么是有限状态机?

有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
对于刚刚给出的状态机的定义,我结合一个具体的例子,来进一步解释一下
“超级马里奥”游戏不知道你玩过没有?在游戏中,马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。
为了方便接下来的讲解,我们对游戏背景做了简化,只保留了部分状态和事件。简化之后的状态转移如下图所示:

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

我们如何编程来实现上面的状态机呢?最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以,我们把这种方法暂且命名为分支逻辑法。如下所示。其中,obtainMushRoom()、obtainCape()、obtainFireFlower()、meetMonster() 这几个函数,能够根据当前的状态和事件,更新状态和增减积分。

<?php
namespace DesignPatterns\State\example2\v1;

class MarioStateMachine
{
    const Mario = array(
        'SMALL'=>0,//小马里奥
        'SUPER'=>1,//超级马里奥
        'CAPE'=>2,//斗篷马里奥
        'FIRE'=>3//火焰马里奥
    );
    private $score;
    private $currentState;//当前状态

    public function __construct()
    {
        $this->score = 0;
        $this->currentState = self::Mario['SMALL'];
    }
    //获得蘑菇
    public function obtainMushRoom()
    {
        if ($this->currentState == self::Mario['SMALL']){
            $this->currentState = self::Mario['SUPER'];
            $this->score +=100;
        }

    }
    //获得斗篷
    public function obtainCape()
    {
        if ($this->currentState == self::Mario['SMALL'] || $this->currentState == self::Mario['SUPER']) {
            $this->currentState = self::Mario['CAPE'];
            $this->score +=200;
        }

    }
    //获得火焰
    public function obtainFireFlower()
    {
        if ($this->currentState == self::Mario['SMALL'] || $this->currentState == self::Mario['SUPER']) {
            $this->currentState = self::Mario['FIRE'];
            $this->score +=300;
        }

    }
    //遇到怪物
    public function meetMonster(){
        if ($this->currentState == self::Mario['SUPER']) {
            $this->currentState = self::Mario['SMALL'];
            $this->score -=100;
            return;
        }
        if ($this->currentState == self::Mario['CAPE']) {
            $this->currentState = self::Mario['SMALL'];
            $this->score -=200;
            return;
        }
        if ($this->currentState == self::Mario['FIRE']) {
            $this->currentState = self::Mario['SMALL'];
            $this->score -=300;
            return;
        }
    }

    /**
     * @return int
     */
    public function getScore(): int
    {
        return $this->score;
    }

    /**
     * @return int
     */
    public function getCurrentState()
    {
        return array_search($this->currentState,self::Mario);
    }
}

//clientCode
$mario = new MarioStateMachine();
$mario->obtainMushRoom();//获取蘑菇
$mario->obtainCape();//获取斗篷
$mario->meetMonster();//遇到怪物
$score = $mario->getScore();
$state = $mario->getCurrentState();
echo "mario score: ".$score." state: ".$state;

对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug。

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

实际上,上面这种实现方法有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,我们就一块儿来看下,如何利用查表法来实现状态机。实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。

相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。具体的代码如下所示:

<?php
namespace DesignPatterns\State\example2\v2;

class MarioStateMachine
{
    const Mario = array(
        'SMALL'=>0,//小马里奥
        'SUPER'=>1,//超级马里奥
        'CAPE'=>2,//斗篷马里奥
        'FIRE'=>3//火焰马里奥
    );
    const GOT = array(
        'GOT_MUSHROOM'=>0,
        'GOT_CAPE'=>1,
        'GOT_FIRE'=>2,
        'MET_MONSTER'=>3
    );
    private $transitionTable = [
        ['SUPER', 'CAPE', 'FIRE', 'SMALL'],
        ['SUPER', 'CAPE', 'FIRE', 'SMALL'],
        ['CAPE','CAPE','CAPE','SMALL'],
        ['FIRE', 'FIRE', 'FIRE', 'SMALL'],];

    private $actionTable = [
        [+100, +200, +300, +0],
        [+0, +200, +300, -100],
        [+0, +0, +0, -200],
        [+0, +0, +0, -300]];
    private $score;
    private $currentState;//当前状态

    private $stateValue;
    private $eventValue;

    public function __construct()
    {
        $this->score = 0;
        $this->currentState = self::Mario['SMALL'];
    }
    //获得蘑菇
    public function obtainMushRoom()
    {
        $this->executeEvent( self::GOT['GOT_MUSHROOM']);

    }
    //获得斗篷
    public function obtainCape()
    {
        $this->executeEvent( self::GOT['GOT_CAPE']);

    }
    //获得火焰
    public function obtainFireFlower()
    {
        $this->executeEvent( self::GOT['GOT_FIRE']);

    }
    //遇到怪物
    public function meetMonster(){
        $this->executeEvent( self::GOT['MET_MONSTER']);
    }
    public function executeEvent($eventValue){
        $stateValue = $this->currentState;
        $this->currentState = self::Mario[$this->transitionTable[$stateValue][$eventValue]];
        $this->score += $this->actionTable[$stateValue][$eventValue];

    }
    /**
     * @return int
     */
    public function getScore(): int
    {
        return $this->score;
    }

    /**
     * @return int
     */
    public function getCurrentState()
    {
        return array_search($this->currentState,self::Mario);
    }
}

//clientCode
$mario = new MarioStateMachine();
$mario->obtainMushRoom();//获取蘑菇
$mario->obtainFireFlower();//获取斗篷
$mario->meetMonster();//遇到怪物
$score = $mario->getScore();
$state = $mario->getCurrentState();
echo "mario score: ".$score." state: ".$state;

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

在查表法的代码实现中,事件触发的动作只是简单的积分加减,所以,我们用一个 int 类型的二维数组 actionTable 就能表示,二维数组中的值表示积分的加减值。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),我们就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性。虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。实际上,针对分支逻辑法存在的问题,我们可以使用状态模式来解决。我们先来了解一下状态模式

状态模式

状态(State) 模式在GOF《设计模式》中给出定义为:允许一个对象在其内部状态改变时改变它的行为。这个对象看起来似乎修改了它的类。看起来,状态模式好像是神通广大——居然能够“修改自身的类”!

能够让程序根据不同的外部情况来做出不同的响应,最直接的方法就是在程序中将这些可能发生的外部情况全部考虑到,使用 if else 语句来进行代码响应选择。但是这种方法对于复杂一点的状态判断,就会显得杂乱无章,容易产生错误;而且增加一个新的状态将会带来大量的修改。这个时候“能够修改自身”的状态模式的引入也许是个不错的主意。状态模式可以有效的替换充满在程序中的 if else 语句:将不同条件下的行为封装在一个类里面,再给这些类一个统一的父类来约束他们。我们来看一下状态模式的结构

状态模式的结构与实现
结构

  • 上下文(Context)角色:客户程序是通过它来满足自己的需求。它定义了客户程序需要的接口;并且维护一个具体状态角色的实例,这个实例来决定当前的状态。
  • 状态(State)角色:定义一个接口以封装与使用环境角色的一个特定状态相关的行为。
  • 具体状态(Concrete State)角色:实现状态角色定义的接口。
代码实现

State.php

<?php
namespace DesignPatterns\State\example1;
/**
 * State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为。
 * Class State
 * @package DesignPatterns\State\example1
 */
abstract class State
{
    abstract public function Handle(Context $context);
}

ConcreteStateA.php

<?php
namespace DesignPatterns\State\example1;
/**
 * ConcreteState类,具体状态,每一个子类实现一个与Context的一个状态相关的行为。
 * Class ConcreteStateA
 * @package DesignPatterns\State\example1
 */
class ConcreteStateA extends State
{
    public function Handle(Context $context)
    {
        echo "ConcreteStateA Handler<br>";
        //设置ConcreteStateA的下一状态是ConcreteStateB
        $context->setState(new ConcreteStateB()) ;
    }
}

ConcreteStateB.php

<?php
namespace DesignPatterns\State\example1;
/**
 * Class ConcreteStateB
 * @package DesignPatterns\State\example1
 */
class ConcreteStateB extends State
{
    public function Handle(Context $context)
    {
        echo "ConcreteStateB Handler<br>";
        //设置ConcreteStateA的下一状态是ConcreteStateA
        $context->setState(new ConcreteStateA());
    }
}

Context.php

<?php
namespace DesignPatterns\State\example1;
/**
 * Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态。
 * Class Context
 * @package DesignPatterns\State\example1
 */
class Context
{
    private $state;

    //定义context的初始状态
    public function __construct(State $state)
    {
        $this->state = $state;
    }

    /**
     * @return State
     */
    public function getState(): State
    {
        return $this->state;
    }

    /**
     * @param State $state
     */
    public function setState(State $state): void
    {
        $this->state = $state;
    }
    //对请求做处理,并设置下一状态
    public function Request()
    {
        $this->state->Handle($this);
    }
}

运行 Client.php

<?php
namespace DesignPatterns\State\example1;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run()
    {
         $context = new Context(new ConcreteStateA());
         $context->request();
         $context->request();
         $context->request();
         $context->request();
    }
}

$worker = new Client();
$worker->run();

运行结果:

状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。我们回到这个例子,利用状态模式,我们来补全 MarioStateMachine 类,补全后的代码如下所示。
其中,IMario 是状态的接口,定义了所有的事件。SmallMario、SuperMario、CapeMario、FireMario 是 IMario 接口的实现类,分别对应状态机中的 4 个状态。原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 类中,现在,这些代码逻辑被分散到了这 4 个状态类中。
IMario.php

<?php
namespace DesignPatterns\State\example2\v3;
/**
 * Interface IMario
 * @package DesignPatterns\State\example2\v3
 */
interface IMario
{
    public function getName();
    //获取蘑菇
    public function obtainMushRoom();
    //获取斗篷
    public function obtainCape();
    //获得火焰
    public function obtainFireFlower();
    //遇到怪物
    public function  meetMonster();
}

SmallMario.php

<?php
namespace DesignPatterns\State\example2\v3;
/**
 * Class SmallMario
 * @package DesignPatterns\State\example2\v3
 */
class SmallMario implements IMario
{
    private $stateMachine;

    public function __construct(MarioStateMachine $stateMachine)
    {
        $this->stateMachine = $stateMachine;
    }
    public function getName()
    {
        return $this->stateMachine::Mario['SMALL'];
    }

    public function obtainMushRoom()
    {
        $this->stateMachine->setCurrentState(new SuperMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore()+100);
    }

    public function obtainCape()
    {
        $this->stateMachine->setCurrentState(new CapeMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore()+200);
    }

    public function obtainFireFlower()
    {
        $this->stateMachine->setCurrentState(new FireMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore()+300);
    }

    public function meetMonster()
    {
        // do nothing...
    }
}

SuperMario.php

<?php
namespace DesignPatterns\State\example2\v3;
class SuperMario implements IMario
{
    private $stateMachine;

    public function __construct(MarioStateMachine $stateMachine)
    {
        $this->stateMachine = $stateMachine;
    }
    public function getName()
    {
        return $this->stateMachine::Mario['SUPER'];
    }


    public function obtainMushRoom()
    {
        // do nothing...
    }

    public function obtainCape()
    {
        $this->stateMachine->setCurrentState(new CapeMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore() + 200);
    }

    public function obtainFireFlower()
    {
        $this->stateMachine->setCurrentState(new FireMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore() + 300);
    }

    public function meetMonster()
    {
        $this->stateMachine->setCurrentState(new SmallMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore() - 100);
    }
}

CapeMario.php

<?php
namespace DesignPatterns\State\example2\v3;

class CapeMario implements IMario
{
    private $stateMachine;

    public function __construct(MarioStateMachine $stateMachine)
    {
        $this->stateMachine = $stateMachine;
    }
    public function getName()
    {
        return $this->stateMachine::Mario['CAPE'];
    }

    public function obtainMushRoom()
    {
        //do nothing...
    }

    public function obtainCape()
    {
        //do nothing...
    }

    public function obtainFireFlower()
    {
        //do nothing...
    }

    public function meetMonster()
    {
        $this->stateMachine->setCurrentState(new SmallMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore()-200);
    }
}

FireMario.php

<?php
namespace DesignPatterns\State\example2\v3;

class FireMario implements IMario
{

    private $stateMachine;

    public function __construct(MarioStateMachine $stateMachine)
    {
        $this->stateMachine = $stateMachine;
    }
    public function getName()
    {
        return $this->stateMachine::Mario['FIRE'];
    }

    public function obtainMushRoom()
    {
        //do nothing...
    }

    public function obtainCape()
    {
        //do nothing...
    }

    public function obtainFireFlower()
    {
        //do nothing...
    }

    public function meetMonster()
    {
        $this->stateMachine->setCurrentState(new SmallMario($this->stateMachine));
        $this->stateMachine->setScore($this->stateMachine->getScore()-300);
    }
}

MarioStateMachine.php

<?php
namespace DesignPatterns\State\example2\v3;
class MarioStateMachine
{
    private $score;
    private $currentState;
    const Mario = array(
        'SMALL'=>0,//小马里奥
        'SUPER'=>1,//超级马里奥
        'CAPE'=>2,//斗篷马里奥
        'FIRE'=>3//火焰马里奥
    );

    public function __construct()
    {
        $this->score = 0;
        $this->currentState = new SmallMario($this);
    }
    public function obtainMushRoom()
    {
        $this->currentState->obtainMushRoom();
    }

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

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

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

    /**
     * @return int
     */
    public function getScore(): int
    {
        return $this->score;
    }

    public function getCurrentState()
    {
        return array_search($this->currentState->getName(),self::Mario);
    }

    /**
     * @param int $score
     */
    public function setScore(int $score): void
    {
        $this->score = $score;
    }

    /**
     * @param IMario $currentState
     */
    public function setCurrentState(IMario $currentState): void
    {
        $this->currentState = $currentState;
    }
}

运行 Client.php

<?php
namespace DesignPatterns\State\example2\v3;
require dirname(dirname(__DIR__)).'/../vendor/autoload.php';
class Client
{
    public function run()
    {
        $mario = new MarioStateMachine();
        $mario->obtainMushRoom();//获取蘑菇
        $mario->obtainCape();//获取斗篷
        $mario->meetMonster();//遇到怪物
        $score = $mario->getScore();
        $state = $mario->getCurrentState();
        echo "mario score: ".$score." state: ".$state;
    }
}
$worker = new Client();
$worker->run();

运行结果:

上面的代码实现不难看懂,我只强调其中的一点,即 MarioStateMachine 和各个状态类之间是双向依赖关系。MarioStateMachine 依赖各个状态类是理所当然的,但是,反过来,各个状态类为什么要依赖 MarioStateMachine 呢?这是因为,各个状态类需要更新 MarioStateMachine 中的两个变量,score 和 currentState。
实际上,上面的代码还可以继续优化,我们可以将状态类设计成单例,毕竟状态类中不包含任何成员变量。但是,当将状态类设计成单例之后,我们就无法通过构造函数来传递 MarioStateMachine 了,而状态类又要依赖 MarioStateMachine,那该如何解决这个问题呢?实际上我们可以通过函数参数将 MarioStateMachine 传递进状态类。根据这个设计思路,我们对上面的代码进行重构。重构之后的代码如下所示:
IMario.php

<?php
namespace DesignPatterns\State\example2\v4;
/**
 * Interface IMario
 * @package DesignPatterns\State\example2\v4
 */
interface IMario
{
    public function getName(MarioStateMachine $stateMachine);
    //获取蘑菇
    public function obtainMushRoom(MarioStateMachine $stateMachine);
    //获取斗篷
    public function obtainCape(MarioStateMachine $stateMachine);
    //获得火焰
    public function obtainFireFlower(MarioStateMachine $stateMachine);
    //遇到怪物
    public function  meetMonster(MarioStateMachine $stateMachine);
}

SmallMario.php

<?php
namespace DesignPatterns\State\example2\v4;
/**
 * Class SmallMario
 * @package DesignPatterns\State\example2\v3
 */
class SmallMario implements IMario
{
    private $stateMachine;
    private static $instances = null;

    private function __construct(){}

    public static function getInstance()
    {
        if (!isset(self::$instances)) {
            self::$instances = new static;
        }
        return self::$instances;
    }

    public function getName(MarioStateMachine $stateMachine)
    {
        return $stateMachine::Mario['SMALL'];
    }

    public function obtainMushRoom(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(SuperMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore()+100);
    }

    public function obtainCape(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(CapeMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore()+200);
    }

    public function obtainFireFlower(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(FireMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore()+300);
    }

    public function meetMonster(MarioStateMachine $stateMachine)
    {
        // do nothing...
    }
}

SuperMario.php

<?php
namespace DesignPatterns\State\example2\v4;
class SuperMario implements IMario
{
    private $stateMachine;

    private static $instances = null;

    private function __construct(){}

    public static function getInstance()
    {
        if (!isset(self::$instances)) {
            self::$instances = new static;
        }
        return self::$instances;
    }

    public function getName(MarioStateMachine $stateMachine)
    {
        return $stateMachine::Mario['SUPER'];
    }

    public function obtainMushRoom(MarioStateMachine $stateMachine)
    {
        // do nothing...
    }

    public function obtainCape(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(CapeMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore() + 200);
    }

    public function obtainFireFlower(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(FireMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore() + 300);

    }

    public function meetMonster(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(SmallMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore() -100);
    }
}

CapeMario.php

<?php
namespace DesignPatterns\State\example2\v4;

class CapeMario implements IMario
{
    private $stateMachine;

    private static $instances = null;

    private function __construct(){}

    public static function getInstance()
    {
        if (!isset(self::$instances)) {
            self::$instances = new static;
        }
        return self::$instances;
    }

    public function getName(MarioStateMachine $stateMachine)
    {
        return $stateMachine::Mario['CAPE'];
    }

    public function obtainMushRoom(MarioStateMachine $stateMachine)
    {
        //do nothing...
    }

    public function obtainCape(MarioStateMachine $stateMachine)
    {
        //do nothing...
    }

    public function obtainFireFlower(MarioStateMachine $stateMachine)
    {
        //do nothing...
    }

    public function meetMonster(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(SmallMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore() -200);
    }
}

FireMario.php

<?php
namespace DesignPatterns\State\example2\v4;

class FireMario implements IMario
{

    private $stateMachine;

    private static $instances = null;

    private function __construct(){}

    public static function getInstance()
    {
        if (!isset(self::$instances)) {
            self::$instances = new static;
        }
        return self::$instances;
    }

    public function getName(MarioStateMachine $stateMachine)
    {
        return $stateMachine::Mario['FIRE'];
    }

    public function obtainMushRoom(MarioStateMachine $stateMachine)
    {
        //do nothing...
    }

    public function obtainCape(MarioStateMachine $stateMachine)
    {
        //do nothing...
    }

    public function obtainFireFlower(MarioStateMachine $stateMachine)
    {
        //do nothing...
    }

    public function meetMonster(MarioStateMachine $stateMachine)
    {
        $stateMachine->setCurrentState(SmallMario::getInstance());
        $stateMachine->setScore($stateMachine->getScore() -300);
    }
}

MarioStateMachine.php

<?php
namespace DesignPatterns\State\example2\v4;
class MarioStateMachine
{
    private $score;
    private $currentState;
    const Mario = array(
        'SMALL'=>0,//小马里奥
        'SUPER'=>1,//超级马里奥
        'CAPE'=>2,//斗篷马里奥
        'FIRE'=>3//火焰马里奥
    );

    public function __construct()
    {
        $this->score = 0;
        $this->currentState = SmallMario::getInstance();
    }
    public function obtainMushRoom()
    {
        $this->currentState->obtainMushRoom($this);
    }

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

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

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

    /**
     * @return int
     */
    public function getScore(): int
    {
        return $this->score;
    }

    public function getCurrentState()
    {
        return array_search($this->currentState->getName($this),self::Mario);
    }

    /**
     * @param int $score
     */
    public function setScore(int $score): void
    {
        $this->score = $score;
    }

    /**
     * @param IMario $currentState
     */
    public function setCurrentState(IMario $currentState): void
    {
        $this->currentState = $currentState;
    }
}

运行 Client.php

<?php
namespace DesignPatterns\State\example2\v3;
require dirname(dirname(__DIR__)).'/../vendor/autoload.php';
class Client
{
    public function run()
    {
        $mario = new MarioStateMachine();
        $mario->obtainMushRoom();//获取蘑菇
        $mario->obtainCape();//获取斗篷
        $mario->meetMonster();//遇到怪物
        $score = $mario->getScore();
        $state = $mario->getCurrentState();
        echo "mario score: ".$score." state: ".$state;
    }
}
$worker = new Client();
$worker->run();

运行结果:

实际上,像游戏这种比较复杂的状态机,包含的状态比较多,优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。

总结

今天我们讲解了状态模式。虽然网上有各种状态模式的定义,但是你只要记住状态模式是状态机的一种实现方式即可。状态机又叫有限状态机,它有 3 个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。针对状态机,
今天我们总结了三种实现方式。
第一种实现方式叫分支逻辑法。利用 if-else 或者 switch-case 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。
第二种实现方式叫查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
第三种实现方式叫状态模式。对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。

参考文章:设计模式之美 --状态模式:游戏、工作流引擎中常用的状态机是如何实现的?

github示例:https://github.com/yangpanyao/design-patterns/tree/master/State

Last modification:July 11th, 2020 at 05:01 pm