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囉。
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中很基礎的一種,小弟在這斗膽在這耍下小刀,高人見到莫笑啊....
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()); } }