iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
1
Software Development

為什麼世界需要Typescript系列 第 20

狀態模式(State Pattern) - 20

實踐好每一種狀態物件之後, 接下來在VendingMachine 類別物件中, 初始化現在的狀態

class VendingMachine {
   private _inventory: number;   //販賣機裡面剩餘飲料的庫存數量
   private _price: number;       //飲料價錢
   private _amount: number = 0;      //已投幣多少金額

   private _state: State;

   constructor (inventory: number, price: number) {
      this._inventory = inventory; 
      this._price = price;
      this.ledScreen = `庫存${this._inventory} 瓶, 請投幣購買`;

      _state = new VendingState(this);
   }

   insertMoney(money: number): number {
      return this._state.insertMoney(money);
   }

   buyButton(): [boolean, number] {
      return this._state.buyButton();
   }

   cancel(): number {
      return this._state.cancel();
   }
}

你可以發現原本每一個動作方法insertMoney, buyButton, cancel 都是呼叫 _state 狀態物件的對應的方法. 如此一來, 未來只要加入一種新的狀態, 也只需要實作新的狀態類別物件, 不必修改到 VendingMachine 物件.

狀態模式就是將行為用一個介面封裝起來, 針對不同的狀態去改變其行為.

playFun 遊戲企劃

小明收到一個企劃遊戲需求, 按照要求設計了一個 playFun 方法,

class MyService {
   playFun(player: Player): boolean {
      let gameInfo = this.getPlayerGameInfo(player);
      if( gameInfo == null ) {
         this.logError("紀錄ERROR LOG");
         return false;
      }

      if(!this.decreaseLifePoint(player) ){
         this.logError("扣體力點數失敗");
         return false;
      }

      if( !this.notifyPlayFunOtherServer(player, "通知其他人玩家加入作戰") ) {
         this.addLivePoint(player);
         this.logError("紀錄 Game ERROR LOG");
         return false;
      }

      return true;
   }
}

這是一個遊戲服務, playFun 方法中, 第一步取得玩家遊戲資料, 如果找不到遊戲資料就紀錄log 錯誤, 如果找到遊戲紀錄就扣除體力值, 然後透過 notifyPlayFunOtherServer 通知其他遊戲伺服器有新玩家加入遊戲作戰.

過了兩天, 小明又收到企劃修改需求, 要求 playFun 再加一個 "加倍送體力" 活動. 所以小明又修改同一個 playFun 的方法, 裡面再加一個....

class MyService {
   playFun(player: Player): boolean {
      let gameInfo = this.getPlayerGameInfo(player);
      if( gameInfo == null ) {
         this.logError("紀錄ERROR LOG");
         return false;
      }

      if(!this.decreaseLifePoint(player) ){
         this.logError("扣體力點數失敗");
         return false;
      }

      if( !this.notifyPlayFunOtherServer(player, "通知其他人玩家加入作戰") ) {
         this.addLivePoint(player);
         this.logError("紀錄 Game ERROR LOG");
         return false;
      }

      if( !this.addDoubleLivePoint(player) ) {
         this.addLivePoint(player);
         this.logError("加倍送體力活動失敗");
         return false;
      }

      return true;
   }
}

分析程式碼, 你可以發現這是一條一整道流程, 只要流程當中有一個動作有問題就紀錄LOG 並回傳失敗. 還要把先前扣掉的體力值加回去, 每次辦活動, 都要修改原來的 playFun 方法內容. 這樣加下去 playFun 內容長度越來越長....

在 playFun 方法中

this.getPlayerGameInfo(player)
this.decreaseLifePoint(player)
this.notifyPlayFunOtherServer(player, ...)
this.addDoubleLivePoint(player)
// 增加 Code 

我們的目標是讓 playFun 容易擴充, 在不修改現有 playFun 程式碼的情形就能增加新的行為, 或是減少行為, 這樣的設計具有彈性, 可以接受行銷企劃們經常修改需求的變動. 這看起來根本不可能?

裝飾者模式(Decorator Pattern)

首先我們要設計一個裝飾者共同介面

abstract class MyService {
   protected _service: MyService;
   constructor(service: MyService) {
      this._service = service;
   }
   abstract playFun(player: Player): boolean;
}

在 playFun 方法中

this.getPlayerGameInfo(player)
this.decreaseLifePoint(player)
this.notifyPlayFunOtherServer(player, ...)
this.addDoubleLivePoint(player)

最主要是核心動作是

this.getPlayerGameInfo(player)
this.decreaseLifePoint(player)

然後建立 PlayFunService 類別物件, 這物件只需要專心進行 playFun 真正的那兩個動作就好

class PlayFunService extends MyService {
   constructor() {
      super(null);
   }
   playFun(player: Player): boolean {
      let gameInfo = this.getPlayerGameInfo(player);
      if( gameInfo == null ) {
         return false;
      }

      if(!this.decreaseLifePoint(player) ){
         return false;
      }

      return true;
   }

   ...
}

然後我們設計例外處理錯誤類別物件, 在設計例外處理類別之前, 我先介紹一下Javascript 的錯誤處理介紹


上一篇
狀態模式(State Pattern) - 19
下一篇
錯誤種類 - 21
系列文
為什麼世界需要Typescript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言