iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 16
0

[Design Pattern] State 狀態模式

今天要介紹的 pattern 是 state pattern。在現實生活中,我們的服務或產品很常會需要根據 state 的不同而有不同的行為表現。
例如,今天當你設計一個購票網站,一個「空的」座位是可以被任何人加到購物車的,但是當某個人把那個座位加到購物車的時候,這個位子的狀態變成「付款中」,這個位子就不應該可以被其他人加到購物車。

這時候可以考慮用 if 等判斷式來處理,但是如果 state 很多的時候,就會有很多的判斷式,這會造成程式碼很難維護和很難理解。這時候就可以考慮使用今天要介紹的 state pattern

state UML

State pattern 的概念是把每一個 state 以及相關的邏輯抽到各自的 class 裡,然後找出這些 state 之間的共通點把它變成 state interface,其他的非 state 相關的邏輯會保留在 context 裡,外部的程式主要是使用 context,通常不會直接跟 state 接觸。

為了讓 context 可以把 state 相關的請求,轉交給 state object 來處理,context 會有一個 reference 連到現在的 state object。
State object 也會有一個 reference 連到相對應的 context object,因此它可以透過這個 reference 去通知 context object 改變 state。

簡化的例子

以剛剛的購票為例,我們用 Typescript 來示範。原本的程式碼可能像這樣。addToCart()裡面是用判斷式來區別不同狀態的不同行為。當狀態只有少數幾個的時候,這種做法是可以接受的,但是當狀態變多的時候,就會變得很複雜。

class Ticket {
  static readonly openState = 'open';
  static readonly inCartState = 'inCart';

  seatId: number;
  userId: number|null;
  state: string;

  constructor(seatId: number) {
    this.seatId = seatId;
    this.state = 'open';
  }

  public printTicket() {
    console.log(`This ticket's seat id is ${this.seatId}.`);
  }

  public addToCart(userId: number): boolean {
    if (this.state == Ticket.openState) {
      this.state = Ticket.inCartState;
      this.userId = userId;
      return true;
    }
    else if (this.state == Ticket.inCartState) {
      if (this.userId == userId) {
        console.log('You have already added this ticket to your cart.');
        return true;
      } else {
        console.log('This ticket has been added by someone.');
        return false;
      }
    }
  }
}

const ticket = new Ticket(0)
const user0 = 0;
const user1 = 1;

 // Output: This ticket's seat id is 0.
ticket.printTicket();

console.log('It should be able to be added by user 0.');
ticket.addToCart(user0); // Return: true

console.log('It should return true and print the information.');
// Output: You have already added this ticket to your cart.
ticket.addToCart(user0); // Return: true

console.log('It should not be able to be added to cart by user 1.');
// Output: This ticket has been added by someone.
ticket.addToCart(user1);  // Return: false

現在讓我們改用 state pattern 的方式來處理,我們把 addToCart() 裡面的邏輯抽出來變成兩個 state classes (OpenStateInCartState),並且抽出共同的部分(addToCart())放到 State interface裡。接下來,我們讓 OpenStateInCartState reference 到 context (Ticket),並且讓 Ticket 也可以 reference 回 state。

這樣我們就可以讓 state 呼叫或更改 Ticket 的內容以及 Ticket 現在的 state,而 Ticket 也可以把 state 相關的事情 (addToCart()) 經由 reference 交給相對應的 state 來處理。

state UML

// The common interface between OpenState and InCartState.
interface State {
  addToCart(userId: number): boolean;
}

class OpenState implements State {
  context: Ticket;

  constructor(ticket: Ticket) {
    this.context = ticket;
  }

  // Logic of addToCart() when the state is OpenState.
  public addToCart(userId: number): boolean {
    this.context.userId = userId;
    this.context.setState(new InCartState(this.context));
    return true;
  }
}

class InCartState implements State {
  context: Ticket;

  constructor(ticket: Ticket) {
    this.context = ticket;
  }

  // Logic of addToCart() when the state is InCartState.
  public addToCart(userId: number): boolean {
    if (this.context.userId == userId) {
      console.log('You have already added this ticket to your cart.');
      return true;
    } else {
      console.log('This ticket has been added by someone.');
      return false;
    }
  }
}

class Ticket {
  seatId: number;
  userId: number|null;
  state: State;

  constructor(seatId: number) {
    this.seatId = seatId;
    this.state = new OpenState(this);
  }

  // A method which is not related to states.
  public printTicket() {
    console.log(`This ticket's seat id is ${this.seatId}`);
  }

  public setState(state: State) {
    this.state = state;
  }

  // Delegate the state specific logic to current state object.
  public addToCart(userId: number): boolean {
    return this.state.addToCart(userId);
  }
}

const ticket = new Ticket(0)
const user0 = 0;
const user1 = 1;

 // Output: This ticket's seat id is 0.
ticket.printTicket();

console.log('It should be able to be added by user 0.');
ticket.addToCart(user0); // Return: true

console.log('It should return true and print the information.');
// Output: You have already added this ticket to your cart.
ticket.addToCart(user0); // Return: true

console.log('It should not be able to be added to cart by user 1.');
// Output: This ticket has been added by someone.
ticket.addToCart(user1);  // Return: false

State Pattern 的優缺點

State pattern 的優點是可以把 state 相關的邏輯切割得很乾淨,每個 state 只負責這個 state 的邏輯,這也讓新增 state 變得很輕鬆,也可以去除掉那些 if 判斷式。

但是它的缺點也很明顯,就像我們的例子一樣,導入 state pattern 之後,程式碼行數變成兩倍,如果當我們的 state 很少或是不常更動的時候,其實導入 state pattern 會增加程式碼的複雜度。

State Pattern vs Strategy Pattern

Strategy UML

State pattern 從 UML 上來看,其實跟 strategy pattern 很像。他們都是利用 composition 的概念,把部分的邏輯交給其他 object (state 和 strategy) 處理。

主要的差異是,strategy 通常不會知道其他 strategy,也不會有方式可以 reference context 來修改 context。
但是 state pattern 通常知道其他 state 的存在,也能夠通知 context 去修改 context 的內容以及改變 context 的 state。

作者:Maso


上一篇
[Design Pattern] Abstract Factory 抽象工廠模式
下一篇
[Design Pattern] Bridge 橋樑模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言