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

12 2月 2010

Guice Injector and Spring AnnotationConfigApplicationContext

目前Dependency Injection Framework比較活躍的除了Spring外就是Google的Guice了,Guice以Annotation為主,不需要複雜的設定檔,很容易就能上手,而且所需要的Library很小,對於一些比較小的系統,不希望使用太多Library的開發者而言(例如Android),Guice提供了一個較Spring有利的DI Framework。

Spring在使用Annotation上有些舊包袱,但在JSR-299,JSR-330後也逐漸為大家接受,但是在3.0之前,仍需要一個XML設定檔,相較Guice完全不用的情形下是有些許的不便(當然2.5自己加工一下也是可以達成不用讀取XML而直接使用Annotaion)。

Spring3.0多了個AnnotationConfigApplicationContext,可以讓我們完全不用讀取任何XML的檔案就能依Annotation完成DI的組裝工作,下面就簡單列一下兩種DI Framework的做法吧。

package org.elliot.di;

public interface Module {
 public String getModuleName();
}
package org.elliot.di;

import org.springframework.stereotype.Component;

@Component //Spring component => a bean
public class DefaultModule implements Module{
 public String getModuleName() {
  return "Default";
 }
}
訂了一個非常沒用的Interface,再實作一個很無聊的Implementation,DefaultModule上訂的@Component是Spring自定的,也可以改用JSR-299所定的@Resource,這個的做用基本上就是將它當做是之前Spring xml configuration中所訂的一個bean

package org.elliot.di;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.inject.Inject;

@Component //Spring component => a bean
public class Service {
 @Autowired //Spring Autowired
 @Inject //Guice Inject
 private Module module;
 
 public Module getModule() {
  return module;
 }

 public void setModule(Module module) {
  this.module = module;
 }

 public void showModuleName() {
  System.out.println(this.module.getModuleName());
 }
}
Service提供一個被注入的標的module,@Autowired是Spring的Annotaion,@Inject則是Guice的Annotaion,做用雷同,代表這是一個可以被注入的Field。

再來是Spring與Guice想法不同之處,Spring必需把Service也定為一個Component,這樣才可以透過BeanFactory或是Context取得,但Guice則不用,你可以留到你程式要用時再透過Guice Container來組裝,Spring目前似乎沒有這樣的想法(不確定...)。我比較想要的是可以自行new 一個Service instance,再丟給DI Container來將所需要的東西注入。


Guice雖然沒有設定檔,但你還是需要一個AbstractModule來指出一個組裝的需求,就像下列這樣,

package org.elliot.guice;

import org.elliot.di.DefaultModule;
import org.elliot.di.Module;

import com.google.inject.AbstractModule;

public class GuiceConfigModule extends AbstractModule {

 @Override
 protected void configure() {
  bind(Module.class).to(DefaultModule.class);
 }

}
必需要extends AbstractModule,實做protected void configure();這裡指定了只要Field型態是Module的都用DefaultModule的instance來注入。


再來就是簡單的測試,順便展示基本的用法

package org.elliot.guice;

import static org.junit.Assert.assertNotNull;

import org.elliot.di.Service;
import org.junit.Before;
import org.junit.Test;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class GuiceDITest {
 private Service service;
 
 @Before
 public void setUp() throws Exception {
  Injector injector = Guice.createInjector(new GuiceConfigModule());
  service = injector.getInstance(Service.class);
 }
 
 @Test
 public void testGuice() {
  assertNotNull(service.getModule());
  service.showModuleName();
 }
}
這是Guice的簡單測試,例用Guice.createInjector來產生一個Injector,這個Injector就同於Spring的Context,你需要相關的instance都跟Injector要。


Spring的也很簡單

package org.elliot.spring;

import static org.junit.Assert.assertNotNull;

import org.elliot.di.Service;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringDITest {
 private Service service;

 @Before
 public void setUp() throws Exception {
  AnnotationConfigApplicationContext context = 
   new AnnotationConfigApplicationContext("org.elliot");
  service = context.getBean(Service.class);
 }

 @Test
 public void testGuice() {
  assertNotNull(service.getModule());
  service.showModuleName();
 }
}
基本上就是將之前常用的ClassPathXmlApplicationContext, FileSystemXmlApplicationContext換成AnnotationConfigApplicationContext。

兩個TestCase要做的事完全一樣,看得出來Spring也能縮減相當程度的複雜度,但是Guice在速度跟耗用記憶體上還是具有優勢,只是我又少了一個用Guice的理由...