網頁

2017/12/1

Java 設計模式 狀態模式 State Pattern

本篇介紹設計模式的狀態模式(State Pattern)。

狀態模式屬於設計模式中的行為模式(Behavior Patterns)的一種

狀態模式要處理的問題是當物件的狀態(state)會影響行為時的情況,換句話說就是行為會依賴物件的狀態而改變。

如果你發現程式碼中經常出現重複的條件判斷語句(if elseswitch case),而這些條件是用來判斷某個物件屬性來執行對應的方法,那或許就是使用狀態模式的時機,例如下面這種情況。

    if(obj.getState() == 1) {
      doA()
    } else if (obj.getState() == 2) {
      doB();
    } else if (obj.getState() == 3) {
      doC();
    }

上面的問題是這種判斷狀態而執行不同行為的程式碼可能會散布在業務邏輯的各處,例如有多個類別的程式碼中都出現這樣的判斷式,導致當如果狀態有增減時,或當某個狀態的行為有更動時,你就必須找出所有有此判斷式的程式碼來增減狀態並賦與對應的行為。如果少修改到一個狀態可能會影響程式執行的結果,更糟的是可能直到有人反映才會發現錯誤,所以狀態模式就是為了解決這種問題。

狀態模式透過將不同狀態所要執行的行為用狀態類別封裝起來,而這些狀態類別都繼承一個共同的狀態介面,所以這些狀態類別會有統一的方法名稱,但擁有不同的實作。而當物件屬性的狀態改變時,就會依照該狀態來執行該類別的實作方法。

這樣的好處是各種狀態的判斷及所要執行的行為被集中在同一個類別,如果要增減時就容易多了。但缺點是程式碼不如if elseswitch case那樣直覺,程式碼結構會比較複雜,而多了狀態類別所以系統負擔會多一些。

光看上面的敘述通常很難懂想睡覺,建議先看下面範例。


一開始的程式碼很簡單,一個用來執行程式的類別Demo,及一個物件類別Person

public class Person {
  
  public Person(String name) {
    this.name = name;
  }
  
  private String name;

  // getters and setters...
}

Demo類別中建立一個Person並執行一個簡單工作。

public class Demo {

  public static void main(String[] args) {

    Person person = new Person("matt");
    
    System.out.println(person.getName() + "工作");
  }
}

上面是最開始的模樣,狀態還不存在。


假設今天Person多了一個心情屬性mood會影響執行的工作內容,目前mood只有開心(happy)和難過(sad)兩種狀態。

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
  }
  
  private String name;
  private String mood;

  // getters and setters...
}

所以Demo執行時要依照Person的心情mood來執行不同的行為。

public class Demo {

  public static void main(String[] args) {

    Person person = new Person("matt", "sad");
    
    if (person.getMood().equals("happy")) {
      System.out.println(person.getName() + "開心地去工作");
    } else if (person.getMood().equals("sad")) {
      System.out.println(person.getName() + "心情不好不想上班");
    }
  }
}

因為Demo執行的行為與人有關,所以進一步把行為封裝到Person中的doWork()方法,修改如下。

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
  }
  
  private String name;
  private String mood;
  
  public void doWork() {
    if (mood.equals("happy")) {
      System.out.println(name + "開心地去工作");
    } else if (mood.equals("sad")) {
      System.out.println(name + "心情不好不想上班");
    }
  }

  // getters and setters...
}

public class Demo {

  public static void main(String[] args) {

    Person person = new Person("matt", "sad");
    
    person.doWork();
  }
}

如果Person多了一種行為叫運動(doExercise),此行為也一樣依心情有不同的結果,則修改如下。

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
  }
  
  private String name;
  private String mood;
  
  public void doWork() {
    if (mood.equals("happy")) {
      System.out.println(name + "開心地去工作");
    } else if (mood.equals("sad")) {
      System.out.println(name + "心情不好不想上班");
    }
  }
  
  public void doExercise() {
    if (mood.equals("happy")) {
      System.out.println(name + "心情好多做幾組");
    } else if (mood.equals("sad")) {
      System.out.println(name + "心情不好肌肉拉傷");
    }
  }

  // getters and setters...
}

public class Demo {

  public static void main(String[] args) {

    Person person = new Person("matt", "sad");
    
    person.doWork();
    person.doExercise();
  }
}

如果Person多了一種心情叫放鬆(relax),則受心情影響的行為也要因此多增加放鬆狀態的執行內容。

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
  }
  
  private String name;
  private String mood;
  
  public void doWork() {
    if (mood.equals("happy")) {
      System.out.println(name + "開心地去工作");
    } else if (mood.equals("sad")) {
      System.out.println(name + "心情不好不想上班");
    } else if (mood.equals("relax")) {
      System.out.println(name + "心情放鬆乾脆請假去玩");
    }
  }
  
  public void doExercise() {
    if (mood.equals("happy")) {
      System.out.println(name + "心情好多做幾組");
    } else if (mood.equals("sad")) {
      System.out.println(name + "心情不好肌肉拉傷");
    } else if (mood.equals("relax")) {
      System.out.println(name + "心情放鬆隨便做就好");
    }
  }

  // getters and setters...
}

Demo不用改變

public class Demo {

  public static void main(String[] args) {

    Person person = new Person("matt", "sad");
    
    person.doWork();
    person.doExercise();
  }
}

此時你會發現你可能會漏了某個方法判斷該新增狀態的行為,目前只有兩種方法doWork()doExercise()而已,如果今天會依狀態改變的行為有十幾個,狀態也是幾十個,那在沒有使用狀態模式的情況下,你必須在每個行為中添加該狀態的判斷,這會是一件很麻煩的問題,所以下面範例改以狀態模式來修改。

狀態模式就是把物件的狀態屬性改為狀態類別,而每個狀態都有共同要實作的方法,所以會有一個狀態介面。Person原本代表狀態的屬性是mood,所以新增一個MoodState介面,並在介面中定義各狀態共同的方法。

public interface MoodState {
  
  public void doWork(Person person);
  public void doExercise(Person person);
  
}

doWork()doExercise()因為可能會用到Person物件的其他屬性,所以將Person物件傳入,在狀態模式中將具有狀態的物件稱作為Context,所以此範例的Context就是指Person

MoodState介面建好後,接著實作各種具體的狀態,目前有三種狀態分別為開心(happy),難過(sad)和放鬆(relax),所以建立HappySadRelax三個實作MoodState介面的具體狀態類別。(本範例的具體狀態類別是定義在狀態介面中。)

public interface MoodState {
  
  public void doWork(Person person);
  public void doExercise(Person person);
  
  class Happy implements MoodState {
    @Override
    public void doWork(Person person) {
      
    }
    @Override
    public void doExercise(Person person) {
      
    }
  }
  
  class Sad implements MoodState {
    @Override
    public void doWork(Person person) {
      
    }
    @Override
    public void doExercise(Person person) {
      
    }
  }
  
  class Relax implements MoodState {
    @Override
    public void doWork(Person person) {
      
    }
    @Override
    public void doExercise(Person person) {
      
    }
  }
  
}

具體類別建立好後接著實作每個狀態執行的行為細節。


public interface MoodState {
  
public void doWork(Person person);
 public void doExercise(Person person);
  
  class Happy implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "開心地去工作");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情好多做幾組");
    }
  }
  
  class Sad implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情不好不想上班");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情不好肌肉拉傷");
    }
  }
  
  class Relax implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情放鬆乾脆請假去玩");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情放鬆隨便做就好");
    }
  }
}

接著在Person新增一個MoodState moodS屬性負責紀錄具體的狀態類別,並在物件建構時根據mood來產生具體狀態。

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
    this.moodState = genMoodState(); // 建構Person時根據mood產生對應的具體狀態類別
  }
  
  private String name;
  private String mood;
  private MoodState moodState; // 狀態類別
  
  public MoodState genMoodState () { // 根據mood屬性產生MoodState的具體狀態
    if (mood.equals("happy")) {
      return new MoodState.Happy();
    } else if (mood.equals("sad")) {
      return new MoodState.Sad();
    } else if (mood.equals("relax")) {
      return new MoodState.Relax();
    }
    return null;
  }
  
  public void doWork() {
    if (mood.equals("happy")) {
      System.out.println(name + "開心地去工作");
    } else if (mood.equals("sad")) {
      System.out.println(name + "心情不好不想上班");
    } else if (mood.equals("relax")) {
      System.out.println(name + "心情放鬆乾脆請假去玩");
    }
  }
  
  public void doExercise() {
    if (mood.equals("happy")) {
      System.out.println(name + "心情好多做幾組");
    } else if (mood.equals("sad")) {
      System.out.println(name + "心情不好肌肉拉傷");
    } else if (mood.equals("relax")) {
      System.out.println(name + "心情放鬆隨便做就好");
    }
  }

  // getters and setters...
}

接著把物件中各行為中用來判斷狀態的判斷式移除,改為呼叫具體狀態類別中的行為。

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
    this.moodState = genMoodState();
  }
  
  private String name;
  private String mood;
  private MoodState moodState;
  
  public MoodState genMoodState () {
    if (mood.equals("happy")) {
      return new MoodState.Happy();
    } else if (mood.equals("sad")) {
      return new MoodState.Sad();
    } else if (mood.equals("relax")) {
      return new MoodState.Relax();
    }
    return null;
  }
  
  public void doWork() {
    moodState.doWork(this); // 改呼叫狀態類別的方法
  }
  
  public void doExercise() {
    moodState.doExercise(this); // 改呼叫狀態類別的方法
  }

  // getters and setters...
}

以上大致就完成狀態模式的設計了。


由於狀態的種類是在已知且有限的情況(開心(happy),難過(sad),放鬆(relax)),所以使用列舉(enum)來做進一步修改。

新增MoodEnum列舉。(本範例將列舉放在MoodState介面中。)

public interface MoodState {
  
  public void doWork(Person person);
  public void doExercise(Person person);
  
  class Happy implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "開心地去工作");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情好多做幾組");
    }
  }
  
  class Sad implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情不好不想上班");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情不好肌肉拉傷");
    }
  }
  
  class Relax implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情放鬆乾脆請假去玩");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情放鬆隨便做就好");
    }
  }
  
  // 心情的列舉
  enum MoodEnum {
    Happy("happy"),
    Sad("sad"),
    Relax("relax");
    
    String mood;
    
    private MoodEnum(String mood) {
      this.mood = mood;
    }
    
    public static MoodEnum getMoodEnum(String mood) {
      for(MoodEnum moodEnum : values()) {
        if (moodEnum.getMood().equals(mood)) {
          return moodEnum;
        }
      }
      return null;
    }

    // getters and setters...
  }
  
}

所以Person建構時根據mood產生具體狀態的判斷式可修改如下,且建構式可改傳入MoodState.MoodEnum

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
    this.moodState = genMoodState();
  }
  
  private String name;
  private String mood;
  private MoodState moodState;

  public Person(String name, MoodState.MoodEnum moodEnum) {
    this.name = name;
    this.mood = moodEnum.getMood();
    this.moodState = genMoodState();
  }
  
  public MoodState genMoodState () {
    MoodState.MoodEnum moodEnum = MoodState.MoodEnum.getMoodEnum(mood);
    switch (moodEnum) {
      case Happy: return new MoodState.Happy();
      case Sad: return new MoodState.Sad();
      case Relax:return new MoodState.Relax();
      default: return null;
    }
    
  }
  
  public void doWork() {
    moodState.doWork(this);
  }
  
  public void doExercise() {
    moodState.doExercise(this);
  }

  // getters and setters...
}

Demo中建立Person物件時可改傳入列舉。

public class Demo {

  public static void main(String[] args) {

    Person person = new Person("matt", MoodState.MoodEnum.Sad);
    
    person.doWork();
    person.doExercise();
  }
}

假如今天又多一種心情狀態-痛苦(pain),則新增該狀態的行為時,只要新增一個實作MoodState介面的具體狀態類別Pain及方法,並在MoodEnum及Context(即Person)中判斷狀態的地方加上該狀態即可。

public interface MoodState {
  
  public void doWork(Person person);
  public void doExercise(Person person);
  
  class Happy implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "開心地去工作");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情好多做幾組");
    }
  }
  
  class Sad implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情不好不想上班");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情不好肌肉拉傷");
    }
  }
  
  class Relax implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情放鬆乾脆請假去玩");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情放鬆隨便做就好");
    }
  }
  
  // 新增的具體狀態類別
  class Pain implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情痛苦想辭職");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情痛苦胸口鬱悶");
    }
  }
  
  enum MoodEnum {
    Happy("happy"),
    Sad("sad"),
    Relax("relax"),
    Pain("pain"); // 新增的狀態
    
    String mood;
    
    private MoodEnum(String mood) {
      this.mood = mood;
    }
    
    public static MoodEnum getMoodEnum(String mood) {
      for(MoodEnum moodEnum : values()) {
        if (moodEnum.getMood().equals(mood)) {
          return moodEnum;
        }
      }
      return null;
    }

    // getters and setters...
  }
  
}
public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
    this.moodState = genMoodState();
  }
  
  public Person(String name, MoodState.MoodEnum moodEnum) {
    this.name = name;
    this.mood = moodEnum.getMood();
    this.moodState = genMoodState();
  }
  
  private String name;
  private String mood;
  private MoodState moodState;
  
  public MoodState genMoodState () {
    MoodState.MoodEnum moodEnum = MoodState.MoodEnum.getMoodEnum(mood);
    switch (moodEnum) {
      case Happy: return new MoodState.Happy();
      case Sad: return new MoodState.Sad();
      case Relax:return new MoodState.Relax();
      case Pain: return new MoodState.Pain(); // 新增的狀態
      default: return null;
    }
    
  }
  
  public void doWork() {
    moodState.doWork(this);
  }
  
  public void doExercise() {
    moodState.doExercise(this);
  }

  // getters and setters...
}

使用新增的心情狀態Pain來測試。

public class Demo {

  public static void main(String[] args) {

    Person person = new Person("matt", MoodState.MoodEnum.Pain);
    
    person.doWork();
    person.doExercise();

  }
}

假如今天Person多了一種受心情影響的行為叫做跳舞(doDance),則只要在MoodState介面中新增該方法即可,因為Compiler會警告有哪寫實作MoodState介面的狀態類別尚未完成該方法實作。


所以狀態模式的設計中,會有幾個重要的組件:

  • 第一個是具有狀態屬性的物件,又稱Context,在範例中為Person
  • 第二個是狀態的State介面,在範例中為MoodState
  • 第三個為狀態下面的各種具體類別,在範例中為HappySadRelaxPain


建立狀態模式的第一步驟為將Context中代表狀態的屬性定為State介面,
第二步為建立代表各種狀態的具體類別並實作State介面,
第三步將Context中的行為改由狀態的方法來執行。



後來想一想,把判斷狀態的方法由Person搬到MoodEum應該比較好。

public interface MoodState {
  
  public void doWork(Person person);
  public void doExercise(Person person);
  
  class Happy implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "開心地去工作");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情好多做幾組");
    }
  }
  
  class Sad implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情不好不想上班");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情不好肌肉拉傷");
    }
  }
  
  class Relax implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情放鬆乾脆請假去玩");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情放鬆隨便做就好");
    }
  }
  
  class Pain implements MoodState {
    @Override
    public void doWork(Person person) {
      System.out.println(person.getName() + "心情痛苦想辭職");
    }
    @Override
    public void doExercise(Person person) {
      System.out.println(person.getName() + "心情痛苦胸口鬱悶");
    }
  }
  
  enum MoodEnum {
    Happy("happy",new Happy()),
    Sad("sad", new Sad()),
    Relax("relax", new Relax()),
    Pain("pain", new Pain());
    
    String mood;
    MoodState moodState;
    
    private MoodEnum(String mood, MoodState moodState) {
      this.mood = mood;
      this.moodState = moodState;
    }
    
    public static MoodEnum getMoodEnum(String mood) {
      for(MoodEnum moodEnum : values()) {
        if (moodEnum.getMood().equals(mood)) {
          return moodEnum;
        }
      }
      return null;
    }
    
    public static MoodState getMoodState(String mood){  // 判斷狀態的方法由Person移到這裡
      for(MoodEnum moodEnum : values()) {
        if (moodEnum.getMood().equalsIgnoreCase(mood)) {
          return moodEnum.getMoodState();
        }
      }
      return null;
    }

    // getters and setters...
  }
  
}

public class Person {
  
  public Person(String name, String mood) {
    this.name = name;
    this.mood = mood;
    this.moodState = MoodState.MoodEnum.getMoodState(mood);
  }
  
  public Person(String name, MoodState.MoodEnum moodEnum) {
    this.name = name;
    this.mood = moodEnum.getMood();
    this.moodState = MoodState.MoodEnum.getMoodState(mood);
  }
  
  private String name;
  private String mood;
  private MoodState moodState;
  
  public void doWork() {
    moodState.doWork(this);
  }
  
  public void doExercise() {
    moodState.doExercise(this);
  }
  
  public void doDance() {
    moodState.doDance(this);
  }

  // getters and setters...
}

1 則留言: