23 2月 2010

State Pattern

State Pattern這個入門的Design Pattern,我想大多數人都不陌生,只是我工作這麼久,幾乎沒有看別人使用過,多半都是用if {} else {}, switch case來處理,這是我比較不能理解的情形。
一個程式有多種State,而且需要依目前不同State做出不同的反應,幾乎是每個系統一定會有的,
像電信業的使用者多半有prepaid, postpaid, unsubscribe,np等多種State;
還有所謂的Task,可能有Init, Scheduled, Triggered, Success, Fail等State,我看到的程式幾乎都是將State開放交給在外部程式利用setter來改變。
這樣也不是說不好,如果控制得當,系統當然也不會有什麼大問題,但是在每次系統要做事之前,一定要經過一大堆的State判斷,可能是if else,也可能是switch,才能決定能不能做,要做些什麼。
開發時間一長或是人員替換,再來看這個程式,就會有點傷腦筋,到底有多少種State,在什麼情形下會改變,又是誰改了這個State,很多問號就一直浮出來。

先說說我自己的常遇到的情形好了。

所謂的State多半有三種,Initial State, Alive State, Final State。
Initial State就是最開始的State(廢話..),而且幾乎只有一種,一開始都是處於這個State之下,再來會進入Alive或Final State,只要離開了Initial State就幾乎不會再回到Initial State了。
Alive State則是運作中的State,像是Scheduled, Triggered這種State,Alive State間可能會互相轉換,最終應該要進入Final State。
Final State是程式最後運行的終點,進入Final State之後就不應該再有任何改變,像是Success, Fail, Cancel這種詞意所代表的通常就是Final State。
也很有可能沒有Final State,像是User這種類型的Class,可能就僅在Active, Inactive, Suspended之類的State間轉換。
State間的轉換不應該是依靠setter來處理,而應該是一個Action(Method),這樣才能確保State是依照我們的控制在運作,而不會被天外飛來之物所改變。

這裡舉個簡單的例子,用常見的Task來看看吧。先利用enum列出可能的State。

public enum TaskStateEnum {
 Init, //Initial State
 Scheduled, //Alive State
 Triggered, //Alive State
 Retry, //Alive State
 Success, //Final State
 Fail, //Final State
 Canceled //Final State
}

這時候如果有個State Diagram的話就會很容易理解各個State間轉換的情形,但原讓諒小弟懶人一個....
一般寫出來的Task可能會像下面這樣

public class Task {
 private String oid;
 private Date scheduleTime;
 private TaskStateEnum taskStateEnum = TaskStateEnum.Init;
 
 public Task(String oid) {
  this.oid = oid;
 }

 public void schedule(Date scheduleTime) {
  if (null == scheduleTime) {
   throw new RuntimeException("scheduleTime can't be null value.");
  }
  //檢查State,僅有Init跟Scheduled的Task才能改變scheduleTime
  if (!(TaskStateEnum.Init == taskStateEnum || TaskStateEnum.Scheduled == taskStateEnum)) {
   System.out.println("Task["+oid+"] can't schedule from "+taskStateEnum);
   return;
  }
  this.taskStateEnum = TaskStateEnum.Scheduled;
  this.scheduleTime = scheduleTime;
 }

 public void execute() throws Exception {
  //檢查State,僅有Scheduled的Task才能進行execute
  if (!(TaskStateEnum.Scheduled == taskStateEnum)) {
   System.out.println("Task["+oid+"] can't execute from "+taskStateEnum);
   return;
  }
  //改變State為Trigger
  this.taskStateEnum = TaskStateEnum.Triggered;
  
  //oid為SuccessTask就直接進入Success,其餘則進入Retry。丟出Exception告訴TaskRunner執行有問題。
  if ("SuccessTask".equals(this.oid)) {
   this.taskStateEnum = TaskStateEnum.Success;
   System.out.println("Task["+this.oid+"] success at ["+this.scheduleTime+"].");
  } else {
   this.taskStateEnum = TaskStateEnum.Retry;
   throw new Exception("Task["+this.oid+"] fail.");
  }
 }

 public void retry() {
  //檢查State,僅有Retry的Task才需要再次處理,這使用switch並非必要,僅是為了demo麻煩的switch
  switch(taskStateEnum) {
  case Retry:
   //oid為RetryTask就直接進入Success,其餘則進入Fail。
   //這小Demo僅retry一次,若再失敗就直接Fail不再處理。
   if ("RetryTask".equals(this.oid)) {
    this.taskStateEnum = TaskStateEnum.Success;
    System.out.println("Task["+this.oid+"] success at ["+this.scheduleTime+"].");
   } else {
    this.taskStateEnum = TaskStateEnum.Fail;
    System.out.println("Task["+this.oid+"] fail at ["+this.scheduleTime+"].");
   }
   break;
  case Init:
  case Scheduled:
  case Triggered:
  case Success:
  case Fail:
  case Canceled:
  default:
   System.out.println("Task["+oid+"] can't retry from "+taskStateEnum);
  }
 }

 public void cancel() {
  //檢查State,僅有Init跟Scheduled等尚未被trigger的Task才能cancel。
  if (!(TaskStateEnum.Init == taskStateEnum || TaskStateEnum.Scheduled == taskStateEnum)) {
   System.out.println("Task["+oid+"] can't cancel from "+taskStateEnum);
   return;
  }
  taskStateEnum = TaskStateEnum.Canceled;
 }
 
 //盡量不要將setTaskStateEnum設為public,
 //否則將來出了問題就要找整個系統,但是如果用JDBC DAO之類的程式可能無法避免。
 protected void setTaskStateEnum(TaskStateEnum taskStateEnum) {
  this.taskStateEnum = taskStateEnum;
 }
 
 public String getOid() {
  return oid;
 }
 
 public Date getScheduleTime() {
  return scheduleTime;
 }

 public TaskStateEnum getTaskStateEnum() {
  return taskStateEnum;
 }
 
 protected void setScheduleTime(Date scheduleTime) {
  this.scheduleTime = scheduleTime;
 }
 
}

其中的schedule、execute、retry、cancel等就是所謂的Action,Action在執行時會造成TaskStateEnum的轉換,這裡使用最常見的if else 與 switch方式來檢查TaskStateEnum,然後直接使用this.taskStateEnum = XXX來改變TaskStateEnum。
要想將這個if else 跟 switch去除,就要靠State Pattern囉。

第一步先將這些Action抽出來,訂成一個Interface,就命名為State吧
public interface State {

 void schedule(Date scheduleTime);
 void execute() throws Exception;
 void retry();
 void cancel();
 
 //讓外界明白目前是什麼State
 TaskStateEnum getStateEnum();
}
第二步再訂一個基本的實做AbstractState,利用這個AbstractState將所有的Action預設為無作用,可以簡化後面的開發。
public abstract class AbstractState implements State {
 protected NewTask task;
 protected TaskStateEnum stateEnum;
 
 public AbstractState(NewTask task, TaskStateEnum stateEnum) {
  this.task = task;
  this.stateEnum = stateEnum;
 }
 
 @Override
 public TaskStateEnum getStateEnum() {
  return stateEnum;
 }
 
 @Override
 public void cancel() {
  System.out.println("Task["+task.getOid()+"] can't cancel from "+stateEnum);
 }

 @Override
 public void execute() throws Exception {
  System.out.println("Task["+task.getOid()+"] can't execute from "+stateEnum);
 }

 @Override
 public void retry() {
  System.out.println("Task["+task.getOid()+"] can't retry from "+stateEnum);
 }

 @Override
 public void schedule(Date scheduleTime) {
  System.out.println("Task["+task.getOid()+"] can't schedule from "+stateEnum);
 }

}
第三步再依據TaskStateEnum中的Initial、Alive、Final State建立Class,通常Alive State會自行擁有一個Class,而Final State可以共用一個Class,各個State implementation間會互相認識,並且利用task 的switchState()來要求task轉換State,
public class InitState extends AbstractState {

 public InitState(NewTask task) {
  super(task, TaskStateEnum.Init);
 }

 @Override
 public void schedule(Date scheduleTime) {
  this.task.switchState(new ScheduledState(task));
  this.task.setScheduleTime(scheduleTime);
 }
 
}

//因為Final State什麼事都不能做,所以幾乎等於AbstractState
public class FinalState extends AbstractState {

 public FinalState(NewTask task, TaskStateEnum stateEnum) {
  super(task, stateEnum);
 }

}

public class ScheduledState extends AbstractState {

 public ScheduledState(NewTask task) {
  super(task, TaskStateEnum.Scheduled);
 }
 
 @Override
 public void schedule(Date scheduleTime) {
  this.task.setScheduleTime(scheduleTime);
 }

 @Override
 public void execute() throws Exception {
  this.task.switchState(new TriggeredState(task));
  
  if ("SuccessTask".equals(this.task.getOid())) {
   this.task.switchState(new FinalState(task, TaskStateEnum.Success));
   System.out.println("Task["+this.task.getOid()+"] success at ["+this.task.getScheduleTime()+"].");
  } else {
   this.task.switchState(new RetryState(task));
   throw new Exception("Task["+this.task.getOid()+"] fail.");
  }
 }
 
}

public class TriggeredState extends AbstractState {

 public TriggeredState(NewTask task) {
  super(task, TaskStateEnum.Triggered);
 }

}

public class RetryState extends AbstractState {

 public RetryState(NewTask task) {
  super(task, TaskStateEnum.Retry);
 }
 
 @Override
 public void retry() {
  if ("RetryTask".equals(this.task.getOid())) {
   this.task.switchState(new FinalState(task, TaskStateEnum.Success));
   System.out.println("Task["+this.task.getOid()+"] success at ["+this.task.getScheduleTime()+"].");
  } else {
   this.task.switchState(new FinalState(task, TaskStateEnum.Fail));
   System.out.println("Task["+this.task.getOid()+"] fail at ["+this.task.getScheduleTime()+"].");
  }
 }
}
最後就修改Task,將TaskStateEnum轉換的機制改一下
 //加入State做為Action的delegater。
 private State state = null;
 
 public Task(String oid) {
  this.oid = oid;
  this.initState();
 }
 
 //無論是誰要改變taskStateEnum都必需提供State的實做,
 //以免Task與taskStateEnum的行為不一致
 public void switchState(State newState) {
  this.state = newState;
  this.taskStateEnum = newState.getStateEnum();
 }
 
 //利用目前的taskStateEnum來取得對應的State implementation
 public void initState() {
  switch(taskStateEnum) {
  case Scheduled:
   this.state = new ScheduledState(this);
   break;
  case Triggered:
   this.state = new TriggeredState(this);
   break;
  case Retry:
   this.state = new RetryState(this);
   break;
  case Success:
  case Fail:
  case Canceled:
   this.state = new FinalState(this, taskStateEnum);
   break;
  case Init:
  default:
   this.state = new InitState(this);
  }
 }
簡化後的Task就長得像下面這樣囉
public class Task {
 private String oid;
 private Date scheduleTime;
 private TaskStateEnum taskStateEnum = TaskStateEnum.Init;
 private State state = null;
 
 public Task(String oid) {
  this.oid = oid;
  this.initState();
 }

 public void schedule(Date scheduleTime) {
  this.state.schedule(scheduleTime);
 }

 public void execute() throws Exception {
  this.state.execute();
 }

 public void retry() {
  this.state.retry();
 }

 public void cancel() {
  this.state.cancel();
 }
 
 public String getOid() {
  return oid;
 }
 
 public Date getScheduleTime() {
  return scheduleTime;
 }

 public TaskStateEnum getTaskStateEnum() {
  return taskStateEnum;
 }
 
 protected void setScheduleTime(Date scheduleTime) {
  this.scheduleTime = scheduleTime;
 }

 protected void setTaskStateEnum(TaskStateEnum taskStateEnum) {
  this.taskStateEnum = taskStateEnum;
 }
 
 public void switchState(State newState) {
  this.state = newState;
  this.taskStateEnum = newState.getStateEnum();
 }
 
 public void initState() {
  switch(taskStateEnum) {
  case Scheduled:
   this.state = new ScheduledState(this);
   break;
  case Triggered:
   this.state = new TriggeredState(this);
   break;
  case Retry:
   this.state = new RetryState(this);
   break;
  case Success:
  case Fail:
  case Canceled:
   this.state = new FinalState(this, taskStateEnum);
   break;
  case Init:
  default:
   this.state = new InitState(this);
  }
 }
}

去除了惱人的if else 跟 switch,更容易聚焦在要修改的Action,State的轉換更是清楚(有State Diagram的話)。
只是多了很多Class....所以好不好也是見仁見智啦,有的人也就是不喜歡這麼多Class吧。
State Pattern是Design Pattern中很基礎的一種,小弟在這斗膽在這耍下小刀,高人見到莫笑啊....

什麼,看了這麼長的程式碼還想要看TestCase!?真是同道中人啊...
public class TaskTest {
 Task task = null;
 
 @Test
 public void testSuccessTask() {
  task = new Task("SuccessTask");
  task.schedule(new Date());
  try {
   task.execute();
  } catch (Exception e) {
   Assert.fail();
  }
  Assert.assertEquals(TaskStateEnum.Success, task.getTaskStateEnum());
 }
 
 @Test
 public void testRetryTask() {
  task = new Task("RetryTask");
  task.schedule(new Date());
  try {
   task.execute();
   Assert.fail(); //task must throw exception
  } catch (Exception e) {
   task.retry();
  }
  Assert.assertEquals(TaskStateEnum.Success, task.getTaskStateEnum());
 }
 
 @Test
 public void testFailTask() {
  task = new Task("FailTask");
  task.schedule(new Date());
  try {
   task.execute();
   Assert.fail(); //task must throw exception
  } catch (Exception e) {
   task.cancel(); //Triggered Task can't be canceled.
   Assert.assertEquals(TaskStateEnum.Retry, task.getTaskStateEnum());
   task.retry();
  }
  Assert.assertEquals(TaskStateEnum.Fail, task.getTaskStateEnum());
 }
}

沒有留言: