iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Software Development

也該是時候學學 Design Pattern 了系列 第 25

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 24: Behavioral patterns - Observer
下一篇
Day 26: Behavioral patterns - Strategy
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言