Day 25: Behavioral patterns - State

目的

如果物件内的方法,会依据物件内的状态,使用多个 if - else if - elseswitch case 来判定不同状态下该执行的内容时,可以将 switch case 的逻辑抽取出多个独立的物件,负责状态的变化以及执行的内容。

说明

通常发生在物件内的方法会依据自身的状态,产生 if - else if - elseswitch case 的逻辑判断,如果判断的情境数量太多会丧失程序的弹性,导致之後不易於维护以及修改。因此可以将所有的情境独自转换成单一物件,每个物件将执行各自的任务,而且还能够在需要时更改物件的状态,让下次物件执行相同方法时,会自动执行新状态下的相关程序码。

作法是:

  1. 观察、找出有多个逻辑判断的物件(称作:Context)。
  2. 建立不同状态下独立物件的虚拟层亲代,开规格告知每个情境都会拥有的方法。
  3. 建立不同状态下独立物件的子代(称作:State 物件),除了将原本逻辑判断内实作细节的程序码搬移过来,还可以依据需求,更改 Context 的状态。
  4. 修改 Context
    1. Context 的状态更改为 State 物件。
    2. 逻辑判断的部分,改成呼叫 State 的方法。

以下范例以「简易型电视游乐器」为核心制作。

UML 图

State Pattern UML Diagram

使用 Java 实作

制作独立物件的虚拟层亲代:State

public interface State {
    public abstract void tryToShot();

    public abstract void tryToKick();

    public abstract void tryToJump();

    public abstract void tryToMovingToRight();

    public abstract void tryToMovingToLeft();

    public abstract void tryToDoSomething();

    public abstract void tryToBecomeSnipperMode();

    public abstract void tryToBecomeStandingMode();

    public abstract void tryToBecomeBoostMode();
}

制作具有多个逻辑判断的物件:Hero(Context 物件)

public class Hero {
    private State state;

    public Hero() {
        this.state = new StandingState(this);
    }

    public void changeState(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void tryToShot() {
        state.tryToShot();
    }

    public void shot() {
        System.out.println("英雄射出一发子弹");
    }

    public void tryToKick() {
        state.tryToKick();
    }

    public void kick() {
        System.out.println("英雄向前方踢了一脚");
    }

    public void tryToJump() {
        state.tryToJump();
    }

    public void jump() {
        System.out.println("英雄往上跳");
    }

    public void tryToMovingToRight() {
        state.tryToMovingToRight();
    }

    public void moveToRight() {
        System.out.println("英雄往右走");
    }

    public void tryToMovingToLeft() {
        state.tryToMovingToLeft();
    }

    public void moveToLeft() {
        System.out.println("英雄往左走");
    }

    public void tryToDoSomething() {
        state.tryToDoSomething();
    }

    public void doNothing() {
        System.out.println("什麽事也没发生");
    }

    public void tryToBecomeSnipperMode() {
        state.tryToBecomeSnipperMode();
    }

    public void tryToBecomeStandingMode() {
        state.tryToBecomeStandingMode();
    }

    public void tryToBecomeBoostMode() {
        state.tryToBecomeBoostMode();
    }
}

制作独立物件的子代:StandingStateSnipperStateBoostState(State 物件)

public class StandingState implements State {
    private Hero hero;

    public StandingState(Hero hero) {
        this.hero = hero;
    }

    @Override
    public void tryToShot() {
        hero.shot();
    }

    @Override
    public void tryToKick() {
        hero.kick();
    }

    @Override
    public void tryToJump() {
        hero.jump();
    }

    @Override
    public void tryToMovingToRight() {
        hero.moveToRight();
    }

    @Override
    public void tryToMovingToLeft() {
        hero.moveToLeft();
    }

    @Override
    public void tryToDoSomething() {
        hero.doNothing();
    }

    @Override
    public void tryToBecomeSnipperMode() {
        hero.changeState(new SnipperState(hero));
    }

    @Override
    public void tryToBecomeStandingMode() {
        hero.changeState(new StandingState(hero));
    }

    @Override
    public void tryToBecomeBoostMode() {
        hero.changeState(new BoostState(hero));
    }
}

public class SnipperState implements State {
    private Hero hero;

    public SnipperState(Hero hero) {
        this.hero = hero;
    }

    @Override
    public void tryToShot() {
        hero.shot();
    }

    @Override
    public void tryToKick() {
        hero.doNothing();
    }

    @Override
    public void tryToJump() {
        hero.doNothing();
    }

    @Override
    public void tryToMovingToRight() {
        hero.doNothing();
    }

    @Override
    public void tryToMovingToLeft() {
        hero.doNothing();
    }

    @Override
    public void tryToDoSomething() {
        hero.doNothing();
    }

    @Override
    public void tryToBecomeSnipperMode() {
        hero.changeState(new SnipperState(hero));
    }

    @Override
    public void tryToBecomeStandingMode() {
        hero.changeState(new StandingState(hero));
    }

    @Override
    public void tryToBecomeBoostMode() {
        hero.changeState(new BoostState(hero));
    }
}

public class BoostState implements State {
    private Hero hero;

    public BoostState(Hero hero) {
        this.hero = hero;
    }

    @Override
    public void tryToShot() {
        hero.doNothing();
    }

    @Override
    public void tryToKick() {
        hero.doNothing();
    }

    @Override
    public void tryToJump() {
        hero.jump();
    }

    @Override
    public void tryToMovingToRight() {
        hero.moveToRight();
    }

    @Override
    public void tryToMovingToLeft() {
        hero.moveToLeft();
    }

    @Override
    public void tryToDoSomething() {
        hero.doNothing();
    }

    @Override
    public void tryToBecomeSnipperMode() {
        hero.changeState(new BoostState(hero));
    }

    @Override
    public void tryToBecomeStandingMode() {
        hero.changeState(new StandingState(hero));
    }

    @Override
    public void tryToBecomeBoostMode() {
        hero.changeState(new BoostState(hero));
    }
}

制作掌机的摇杆:VideoGameController

public class VideoGameController {
    private Hero hero;

    public VideoGameController(Hero hero) {
        this.hero = hero;
    }

    public void pushButtonY() {
        hero.tryToShot();
    }

    public void pushButtonX() {
        hero.tryToKick();
    }

    public void pushButtonB() {
        hero.tryToJump();
    }

    public void pushButtonA() {
        hero.tryToDoSomething();
    }

    public void pushButtonRightArrow() {
        hero.tryToMovingToRight();
    }

    public void pushButtonLeftArrow() {
        hero.tryToMovingToLeft();
    }

    public void holdButtonR() {
        hero.tryToBecomeSnipperMode();
    }

    public void releaseButtonR() {
        hero.tryToBecomeStandingMode();
    }

    public void holdButtonL() {
        hero.tryToBecomeBoostMode();
    }

    public void releaseButtonL() {
        hero.tryToBecomeStandingMode();
    }
}

测试,模拟玩家不断在不同模式间尝试按钮:VideoGameStateSample

public class VideoGameStateSample {
    public static void main(String[] args) {
        Hero hero = new Hero();
        VideoGameController controller = new VideoGameController(hero);

        System.out.println("---尝试基本按钮---");
        standardOperation(controller);
        System.out.println("\n---按住按钮 R---");
        controller.holdButtonR();
        standardOperation(controller);
        System.out.println("\n---放开按钮 R---");
        controller.releaseButtonR();
        standardOperation(controller);
        System.out.println("\n---按住按钮 L---");
        controller.holdButtonL();
        standardOperation(controller);
        System.out.println("\n---放开按钮 L---");
        controller.releaseButtonL();
        standardOperation(controller);
    }

    private static void standardOperation(VideoGameController controller) {
        controller.pushButtonY();
        controller.pushButtonX();
        controller.pushButtonB();
        controller.pushButtonA();
        controller.pushButtonRightArrow();
        controller.pushButtonLeftArrow();
    }
}

使用 JavaScript 实作

制作独立物件的虚拟层亲代:State

/** @abstract */
class State {
  /** @param {Hero} hero */
  constructor(hero) {
    this.hero = hero;
  }

  tryToShot() { return; }

  tryToKick() { return; }

  tryToJump() { return; }

  tryToMovingToRight() { return; }

  tryToMovingToLeft() { return; }

  tryToDoSomething() { return; }

  tryToBecomeSnipperMode() { return; }

  tryToBecomeStandingMode() { return; }

  tryToBecomeBoostMode() { return; }
}

制作具有多个逻辑判断的物件:Hero(Context 物件)

class Hero {
  constructor() {
    /** @type {State} */
    this.state = new StandingState(this);
  }

  /** @param {State} state */
  changeState(state) {
    this.state = state;
  }

  getState() {
    return this.state;
  }

  tryToShot() {
    this.state.tryToShot();
  }

  shot() {
    console.log("英雄射出一发子弹");
  }

  tryToKick() {
    this.state.tryToKick();
  }

  kick() {
    console.log("英雄向前方踢了一脚");
  }

  tryToJump() {
    this.state.tryToJump();
  }

  jump() {
    console.log("英雄往上跳");
  }

  tryToMovingToRight() {
    this.state.tryToMovingToRight();
  }

  moveToRight() {
    console.log("英雄往右走");
  }

  tryToMovingToLeft() {
    this.state.tryToMovingToLeft();
  }

  moveToLeft() {
    console.log("英雄往左走");
  }

  tryToDoSomething() {
    this.state.tryToDoSomething();
  }

  doNothing() {
    console.log("什麽事也没发生");
  }

  tryToBecomeSnipperMode() {
    this.state.tryToBecomeSnipperMode();
  }

  tryToBecomeStandingMode() {
    this.state.tryToBecomeStandingMode();
  }

  tryToBecomeBoostMode() {
    this.state.tryToBecomeBoostMode();
  }
}

制作独立物件的子代:StandingStateSnipperStateBoostState(State 物件)

class StandingState extends State {
  /** @param {Hero} hero */
  constructor(hero) {
    super(hero);
  }

  /** @override */
  tryToShot() {
    this.hero.shot();
  }

  /** @override */
  tryToKick() {
    this.hero.kick();
  }

  /** @override */
  tryToJump() {
    this.hero.jump();
  }

  /** @override */
  tryToMovingToRight() {
    this.hero.moveToRight();
  }

  /** @override */
  tryToMovingToLeft() {
    this.hero.moveToLeft();
  }

  /** @override */
  tryToDoSomething() {
    this.hero.doNothing();
  }

  /** @override */
  tryToBecomeSnipperMode() {
    this.hero.changeState(new SnipperState(this.hero));
  }

  /** @override */
  tryToBecomeStandingMode() {
    this.hero.changeState(new StandingState(this.hero));
  }

  /** @override */
  tryToBecomeBoostMode() {
    this.hero.changeState(new BoostState(this.hero));
  }
}

class SnipperState extends State {
  /** @param {Hero} hero */
  constructor(hero) {
    super(hero);
  }

  /** @override */
  tryToShot() {
    this.hero.shot();
  }

  /** @override */
  tryToKick() {
    this.hero.doNothing();
  }

  /** @override */
  tryToJump() {
    this.hero.doNothing();
  }

  /** @override */
  tryToMovingToRight() {
    this.hero.doNothing();
  }

  /** @override */
  tryToMovingToLeft() {
    this.hero.doNothing();
  }

  /** @override */
  tryToDoSomething() {
    this.hero.doNothing();
  }

  /** @override */
  tryToBecomeSnipperMode() {
    this.hero.changeState(new SnipperState(this.hero));
  }

  /** @override */
  tryToBecomeStandingMode() {
    this.hero.changeState(new StandingState(this.hero));
  }

  /** @override */
  tryToBecomeBoostMode() {
    this.hero.changeState(new BoostState(this.hero));
  }
}

class BoostState extends State {
  /** @param {Hero} hero */
  constructor(hero) {
    super(hero);
  }

  /** @override */
  tryToShot() {
    this.hero.doNothing();
  }

  /** @override */
  tryToKick() {
    this.hero.doNothing();
  }

  /** @override */
  tryToJump() {
    this.hero.jump();
  }

  /** @override */
  tryToMovingToRight() {
    this.hero.moveToRight();
  }

  /** @override */
  tryToMovingToLeft() {
    this.hero.moveToLeft();
  }

  /** @override */
  tryToDoSomething() {
    this.hero.doNothing();
  }

  /** @override */
  tryToBecomeSnipperMode() {
    this.hero.changeState(new BoostState(this.hero));
  }

  /** @override */
  tryToBecomeStandingMode() {
    this.hero.changeState(new StandingState(this.hero));
  }

  /** @override */
  tryToBecomeBoostMode() {
    this.hero.changeState(new BoostState(this.hero));
  }
}

制作掌机的摇杆:VideoGameController

class VideoGameController {
  /** @param {Hero} hero */
  constructor(hero) {
    this.hero = hero;
  }

  pushButtonY() {
    this.hero.tryToShot();
  }

  pushButtonX() {
    this.hero.tryToKick();
  }

  pushButtonB() {
    this.hero.tryToJump();
  }

  pushButtonA() {
    this.hero.tryToDoSomething();
  }

  pushButtonRightArrow() {
    this.hero.tryToMovingToRight();
  }

  pushButtonLeftArrow() {
    this.hero.tryToMovingToLeft();
  }

  holdButtonR() {
    this.hero.tryToBecomeSnipperMode();
  }

  releaseButtonR() {
    this.hero.tryToBecomeStandingMode();
  }

  holdButtonL() {
    this.hero.tryToBecomeBoostMode();
  }

  releaseButtonL() {
    this.hero.tryToBecomeStandingMode();
  }
}

测试,模拟玩家不断在不同模式间尝试按钮:VideoGameStateSample

/** @param {VideoGameController} controller */
const standardOperation = (controller) => {
  controller.pushButtonY();
  controller.pushButtonX();
  controller.pushButtonB();
  controller.pushButtonA();
  controller.pushButtonRightArrow();
  controller.pushButtonLeftArrow();
};

const videoGameStateSample = () => {
  const hero = new Hero();
  const controller = new VideoGameController(hero);

  console.log("---尝试基本按钮---");
  standardOperation(controller);
  console.log("\n---按住按钮 R---");
  controller.holdButtonR();
  standardOperation(controller);
  console.log("\n---放开按钮 R---");
  controller.releaseButtonR();
  standardOperation(controller);
  console.log("\n---按住按钮 L---");
  controller.holdButtonL();
  standardOperation(controller);
  console.log("\n---放开按钮 L---");
  controller.releaseButtonL();
  standardOperation(controller);
};

videoGameStateSample();

总结

State 模式有趣的地方,在於要改善方法内因为过多的 if - else if - elseswitch case 导致程序码过多的情况,因为过多的程序码通常暗示着「难以改动」的可能,违反追求高弹性、易於修改的理念。

实作时观察到有三点要多加注意:

  • 如何抽取?
  • 如何让 State 的虚拟层规范的方法足以用於所有的 State
  • 如何让 Context 在呼叫方法时,可以连带更改自己的状态?

然而,就我自己的开发经验,使用的 if - else if - elseswitch case 不一定是糟糕的做法,假如逻辑判断的数量只有几个的话,反而不用特地抽离、制作成 State 物件,避免会增加开发上的麻烦。

讲来讲去可以发现,什麽情况下需要转换,会是更重要的问题。

  • 转换後是否增加开发上的困难?
  • 该段程序码会不会有全部翻新、重构的可能?

我想一个好的开发者如果能了解现在以及往後的可能需求,将更有把握判断要不要转换。

最後,尝试使用後明显感受到,那些依赖状态的变换而有不同程序码要执行的方法们,不用再写一大堆判断了,单单一、两行就做到相同的事情,这感觉真棒!

这感觉真棒

明天将介绍 Behavioural patterns 的第九个模式:Strategy 模式。


<<:  [DAY 27] _看门狗简介_视窗看门狗(1)

>>:  Day26:今天来聊一下使用资料连接器将资料连接到Azure Sentinel

Day2 跟着官方文件学习Laravel-环境设定

我们今天要把环境给设定好,并且尝试将laravel专案启动,而在我写这篇文章之前,我已经有利用Hom...

第30天 - 文件审核系统(8)_审核端END

延续昨天要讲的 DS.php https://ithelp.ithome.com.tw/articl...

<Day29> 实战!!投资小白的出击!!!!

时间过好快,不知不觉的已经要迈入铁人赛的最後一天了 回顾开赛到现在,除了学习Shioaji API的...

RISC-V on Rust 从零开始(8) - 实作instruction decoder

这次要来实作指令decoder,负责pipeline中的decode stage。计组教科书上常见的...

Linkedin - Java检定题库 private 概念

前言 在更新Linkedkin 个人档案的时候 偶然发现他有技术检定测验 如果总成绩在前30%,会发...