從狀態模式看 JavaScript 與 Java

天方夜發表於2016-10-31

這篇文章緣起於前幾天微博上有關動態語言與靜態語言的討論,因為有幾個程式設計高手參加,所以能看到一些特別有啟發性的發言。本文主要是下面這一條微博的讀後感,也是我的練習與思考。

@有個梨UGlee:如果你去看四人幫的Design Pattern裡,就有State Pattern;State Pattern用型別編碼State,就是我們說的問題;但是動態語言裡寫出來非常簡單,型別語言裡寫得極其繁瑣。

關於動態語言與靜態語言,有很多比較和討論它們的文章,但大部分都沒有抓住重點。而上面一條微博,提到了一個很好的切入點,那就是「狀態模式(State Pattern)」。

狀態模式:蝙蝠俠/布魯斯·韋恩

蝙蝠俠(英語:Batman)是一名出現於DC漫畫的虛構超級英雄角色,由鮑勃·凱恩和比爾·芬格創作。他的名字叫 Bruce,是一位美國億萬富翁,這是他的正常身份,用於正常生活,例如進行參加宴會之類的活動。他的另一個身份是 Batman,是打擊犯罪的黑暗騎士。

這是一個狀態模式的好示例,我用 Java 和 JavaScript 各寫了一個示例,體會體會「極其繁瑣」與「非常簡單」。

極其繁瑣:Java 版本

目錄結構:

com
  |-- tianfangye
    |-- Client.java
    |-- person
        |-- Person.java
    |-- state
        |-- BatmanState.java
        |-- BruceState.java
        |-- IState.java

Person.java

package com.tianfangye.person;

import com.tianfangye.state.IState;
import com.tianfangye.state.BruceState;

public class Person {

  // 當前狀態
  private IState state;

  public Person() {
    this.state = new BruceState();
  }

  // 設定當前狀態
  public void changeState(IState state) {
    this.state = state;
  }

  // 改變狀態(變身)
  public void convertState() {
    this.state.convertState(this);
  }

  // 開始行動
  public void takeAction() {
    this.state.doActivities();
  }
}

IState.java

package com.tianfangye.state;

import com.tianfangye.person.Person;

public interface IState {

  // 轉換狀態
  public void convertState(Person person);

  // 執行活動
  public void doActivities();
}

BruceState.java

package com.tianfangye.state;

import com.tianfangye.person.Person;

public class BruceState implements IState {

  private String name = "- Bruce -";

  // 轉換狀態
  public void convertState(Person person) {
    person.changeState(new BatmanState());
  }

  // 執行活動
  public void doActivities() {
    System.out.println(this.name + " <> " + "參加宴會");
  }
}

BatmanState.java

package com.tianfangye.state;

import com.tianfangye.person.Person;

public class BatmanState implements IState {

  private String name = "- Batman -";

  // 轉換狀態
  public void convertState(Person person) {
    person.changeState(new BruceState());
  }

  // 執行活動
  public void doActivities() {
    System.out.println(this.name + " <> " + "打擊犯罪");
  }
}

Client.java

package com.tianfangye;

import com.tianfangye.person.Person;

public class Client {
  public static void main(String[] args) {
    Person person = new Person();
    person.takeAction();
    person.convertState();
    person.takeAction();
  }
}

程式輸出:

  • - Bruce - <> 參加宴會
  • - Batman - <> 打擊犯罪

關於狀態模式的實現,GoF 的 Design Patterns 裡面提到過一些需要考慮的方面,其中之一是「誰來定義狀態的轉換」。可以由狀態的使用者(Person)實現,也可以由每個狀態各自實現,各有利弊。上例由每個狀態各自實現,接下來的 JavaScript 示例也是這種選擇(對於示例程式,另一種實現更簡單)。

非常簡單:JavaScript 版本

const state = {
  _Bruce: {
    name: "- Bruce -",
    convertState() {
      this.identity = state._Batman;
    },
    takeAction() {
      window.console.log(`${this.name} <> 參加宴會`);
    }
  },

  _Batman: {
    name: "- Batman -",
    convertState() {
      this.identity = state._Bruce;
    },
    takeAction() {
      window.console.log(`${this.name} <> 打擊犯罪`);
    }
  }
};

const person = {
  identity: state._Bruce,
  convertState() {
    this.identity.convertState.call(this);
  },
  takeAction() {
    this.identity.takeAction();
  }
};

person.takeAction();
person.convertState();
person.takeAction();

程式輸出:

  • - Bruce - <> 參加宴會
  • - Batman - <> 打擊犯罪

可以看到,相比靜態語言的版本,動態語言的版本竟是如此渾然天成!甚至不是在實現「設計模式」,只是對語言特性的正常使用而已。所以,什麼是設計模式?聰明人應該想通了——設計模式是一個衍生問題,不是本質問題。本質問題是程式設計,是靜態語言與動態語言,是靜態型別與動態型別,是程式語言抽象。

@vczh的一個知乎回答對這一點講得很透徹:

設計模式說白了就是在不允許使用dynamic_cast的情況下如何讓你的設計通過型別系統的檢驗,於是發明出了一大堆行之有效的做法。為什麼不能用dynamic_cast?因為通常如果你可以通過修改設計來避免所有dynamic_cast,那你就得到了最優的效能(通常指的是係數,不是複雜度)。

但是很多人都知其然不知其所以然,盲目的背誦卻不練習(這並不是讓你不要去背誦),喜歡過度設計控制不住自己,還可以拿它來裝逼等等。其實這都不是設計模式的問題,而是人的劣根性的問題,這些人不管學什麼都是一樣的,只不過設計模式把這些人的效果放大了。

顯而易見,型別是個大問題!動態語言能做到非常簡潔是由於動態型別系統的靈活性。它們在相當大的程度上簡化了程度設計,也就是所謂的「非常簡單」。要明白,這裡的「簡單」與「複雜」並不是指程式碼量的多少,也不是指語言特性使用的多少,而是指花費在程式設計上面的心思的多少。具體到本例,主要是下面兩點:

  • 動態型別:不需要花心思去搞定型別檢查。
  • 執行時繫結上下文的 this 物件:不需要將有狀態的物件傳來傳去。

當然,這些靈活性並非沒有代價(效能)。另外,關於動態語言與靜態語言的選擇,一直有很多工程問題上的爭議(從文章開頭的微博往下探索,可以找到大段這方面的爭論)。我個人對於動態語言、靜態語言沒有明顯偏好,只是喜歡把問題弄清楚——不論有沒有偏好,偏好強烈還是微弱,這都是首要的一步。

相關文章