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());
}
}