有限狀態機 FSM 的幾種簡單實現
標籤: 有限狀態機,Akka fsm,squirrel-foundation,java狀態模式、責任鏈模式
1. 有限狀態機的概念
有限狀態機(英語:finite-state machine,縮寫:FSM)又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。
通常FSM包含幾個要素:狀態的管理、狀態的監控、狀態的觸發、狀態觸發後引發的動作。
這些關係的意思可以這樣理解:
- State(S) x Event(E) -> Actions (A), State(S’)
- 如果我們當前處於狀態S,發生了E事件, 我們應執行操作A,然後將狀態轉換為S’
下面展示最常見的表示:當前狀態(B)和條件(Y)的組合指示出下一個狀態(C)。完整的動作資訊可以只使用腳註來增加。包括完整動作資訊的FSM定義可以使用狀態表。
條件↓當前狀態→ | 狀態A | 狀態B | 狀態C |
---|---|---|---|
條件X | … | … | … |
條件Y | … | 狀態C | … |
條件Z | … | … | … |
2. 使用狀態機的應用背景
在廣告投放專案中由於複雜的廣告投放邏輯,存在著大量的if-else 判斷類似的硬編碼,希望能借助對fsm模型的調研,找出理想的實現方式。
3.有限狀態機的幾種實現
3.1列舉類實現java狀態模式
首先在列舉類中 定義state 和定義的抽象方法。
public enum JavaPlatformState { //定義state OPEN{ @Override void exit(JavaPlatformMachine pm){super.exit(pm);} @Override void valid(JavaPlatformMachine pm){ this.exit(pm); if(pm.data.getValid_()){ pm.state =STEP1; }else{ NotFound(); pm.state =OFF; } pm.state.entry(pm); } @Override void first(JavaPlatformMachine pm) {} @Override void businessLine(JavaPlatformMachine pm) {} @Override void district(JavaPlatformMachine pm) {} }, STEP1{ @Override void exit(JavaPlatformMachine pm){super.exit(pm);} @Override void valid(JavaPlatformMachine pm) {} @Override void first(JavaPlatformMachine pm){ this.exit(pm); if(!pm.data.getFirst_()){ pm.state =STEP2; }else{ ReturnDimension(); pm.state =OFF; } pm.state.entry(pm); } @Override void businessLine(JavaPlatformMachine pm) {} @Override void district(JavaPlatformMachine pm) {} }, ... //狀態模式 提取的介面在常量實體類中實現抽象方法 abstract void valid(JavaPlatformMachine pm); abstract void first(JavaPlatformMachine pm); abstract void businessLine(JavaPlatformMachine pm); abstract void district(JavaPlatformMachine pm); }
在enum JavaPlatformState 中,除了狀態模式 提取的介面外,添加了狀態機的各種動作action實現
//狀態機的各種動作action methode void entry(JavaPlatformMachine pm){System.out.println("→"+pm.state.name());} void exit(JavaPlatformMachine pm){System.out.println(pm.state.name()+"→ ");} void NotFound(){System.out.println("NotFound");} void ReturnDimension(){System.out.println("ReturnDimension");} void PreciseAdvertising(){System.out.println("PreciseAdvertising");} void Top9(){System.out.println("Top9");}
建立狀態機實體,ContextData是封裝條件的bean類,初始化狀態OPEN,在狀態機裡定義action,呼叫對應state的相應的方法。
public class ContextData { private Boolean isValid_;//廣告位是否有效 private Boolean isFirst_;//是否第一次請求 private Boolean isBusinessLine_;//是否屬於業務線廣告位 private Boolean district_;//是否有地域 ... } public class JavaPlatformMachine { ContextData data = new ContextData(); JavaPlatformState state = JavaPlatformState.OPEN; //Action public void valid(){state.valid(this);} public void first(){state.first(this);} public void businessLine(){state.businessLine(this);} public void district(){state.district(this);} }
測試方法,初始化狀態機,設定引數,按次序呼叫對應的Action
JavaPlatformMachine pm = new JavaPlatformMachine(); pm.data.setValid_(true);// 廣告位是否有效 pm.data.setFirst_(false);// 是否第一次請求 pm.data.setBusinessLine_(true);//是否屬於業務線廣告位 pm.data.setDistrict_(true);//是否有地域 pm.valid(); pm.first(); pm.businessLine(); pm.district();
輸出結果
OPEN→ →STEP1 STEP1→ →STEP2 STEP2→ →STEP3 STEP3→ Top9 →OFF
在設定引數下,最後狀態呼叫Top9。
不過這種方式在列舉類中實現抽象方法,每個state下都要覆寫(Override)action的抽象方法,顯然不利於拓展。
參考資料: http://blog.csdn.net/yqj2065/article/details/39371487
3.2抽象類實現java狀態模式
當一個類的某個成員變數的值變化時,可能導致多個行為表現得不同。將該成員變數封裝成型別的模式,即為狀態模式(state pattern)。即用多型來重構分支結構。
首先抽象狀態類,定義一個介面以封裝與Context的一個特定狀態相關的行為
public abstract class State { public abstract void Handle(StateModeContext context ); public abstract boolean isFinalflag(); }
Context類,維護一個State子類的例項,這個例項定義當前的狀態。
public class StateModeContext { private State state; private ContextData data ; public ContextData getData() { return data; } public void setData(ContextData data) { this.data = data; } public State getState() { return state; } public void setState(State state) { this.state = state; } /// 定義Context的初始狀態 public StateModeContext(State state , ContextData data ) { this.data = data; this.state = state; } /// 對請求做處理,並設定下一個狀態 booleantrueFlag = true; public void Request() { //如果當前step 是最後一步nextStep 不執行 if(state.isFinalflag()){ trueFlag = false; } state.Handle(this); } }
最後定義各個狀態子類
public class State404 extends State { @Override public void Handle(StateModeContext context) { System.out.println("當前狀態是 404do something"); } @Override public boolean isFinalflag() { return true; } }
其中設定一個FinalFlag 標識 是否是最終狀態。
在下面state的子類裡面進行條件判斷,並設定下一個狀態,這一點有點類似責任鏈模式(在抽象類中定義一個例項成員變數 next handler 的引用)
public class StateStep1 extends State { @Override public void Handle(StateModeContext context) { System.out.println("當前狀態是 step1"); ContextData data = context.getData(); if(data.getFirst_()){ System.out.println("step1 -> dimension(返回尺寸)"); context.setState(new StateDimension()); }else{ System.out.println("step1 -> step2"); context.setState(new StateStep2()); } } @Override public boolean isFinalflag() { return false; } }
測試類:設定初始化資料和初始化狀態stateOpen,根據狀態樹的深度確定迴圈迭代次數,進行迭代。
public class StateModeTest { public static void main(String[] args) { // 設定Context的初始狀態為ConcreteStateA ContextData data = new ContextData(true,false,true,true); StateModeContext context = new StateModeContext(new StateOpen(),data); // 不斷地進行請求,同時更改狀態 int size = 4;// 請求迭代數 for(int i = 0 ; i< size+1; i++){ if(context.trueFlag){ context.Request(); } } } }
輸出結果:
當前狀態是 open open -> step1 當前狀態是 step1 step1 -> step2 當前狀態是 step2 step2 -> step3 當前狀態是 step3 step3 -> Top9(返回9素材) 當前狀態是 Top9(返回9素材)do something
這種方式實現狀態模式,每個狀態要new一個狀態的子類,而且手動指定迴圈迭代次數通過迭代方式進行事件的呼叫。
3.3 Akka FSM 實現狀態機
Akka的狀態機是非常簡潔的實現,充分利用了Scala的許多先進的語法糖讓程式碼更加簡潔清晰。是基於Akka Actor 實現,封裝了很多自定義的API(實際上就是DSL)。
在底層,Akka FSM就是一個繼承了Actor的trait(scala的特徵trait就相當於java的介面)
trait FSM[S, D] extends Actor with Listeners with ActorLogging { ...}
FSM trait提供了一個包裝了常規Actor的DSL,讓我們能集中注意力在更快的構建手頭的狀態機上。常規Actor只有一個receive方法,FSM trait包裝了receive方法的實現並將呼叫指向到一個特定狀態機的處理程式碼塊。
class PlatformMachine extends FSM[PlatformState,PlatformData]
上面的 PlatformMachine 是一個FSM ACTOR。
PlatformState,PlatformData分別為我定義的狀態和資料。在FSM中,有兩個東西是一直存在的,任何時間點都有狀態 ,和在狀態中進行共享的資料。 這代表所有的fsm的狀態繼承自PlatformState,而所有在狀態間共享的資料就是PlatformData
在伴生物件中定義狀態,訊息,資料。
在Scala的類中,與類名相同的物件(object)叫做伴生物件,類和伴生物件之間可以相互訪問私有的方法和屬性。在Scala中沒有靜態方法和靜態欄位,但是可以使用object這個語法結構來達到同樣的目的。object是單例模式的,一般也用於存放工具方法和常量,共享單個不可變的例項等。
在Scala中樣例類(case class)是一中特殊的類,可用於模式匹配。case class是多例的,後面要跟構造引數,case object是單例的。這點其實和class與object的區別是一樣的。
object PlatformMachine{ sealed trait PlatformState //定義6個State case object Open extends PlatformState case object Off extends PlatformState case object Step1 extends PlatformState case object Step2 extends PlatformState case object Step3 extends PlatformState case object Step4 extends PlatformState // state data container case class PlatformData(isValid: Boolean, isFirst: Boolean, isBusinessLine: Boolean,district:Boolean) //定義互動訊息 sealed trait UserMessage case object ToHoldOn extendsUserMessage case object ToNotFound extendsUserMessage case object ToReturnDimension extends UserMessage case object ToPreciseAdvertising extends UserMessage case object ToTop9 extends UserMessage case class SetInitData(isValid: Boolean, isFirst: Boolean, isBusinessLine: Boolean,district:Boolean) extends UserMessage }
以下是FSM 類的程式碼
class PlatformMachine extends FSM[PlatformState,PlatformData]{ val service = new PlatformMachineService() startWith(Open, PlatformData(false,false,false,false)) //Handlers of State when(Open){ case Event(SetInitData(isValid_,isFirst_,isBusinessLine_,district_), _) =>{ println(s"SetInitData:$isValid_ , $isFirst_ ,$isBusinessLine_ ,$district_") stay using stateData.copy(isValid = isValid_,isFirst=isFirst_,isBusinessLine=isBusinessLine_,district=district_) } case Event(ToNotFound, PlatformData(isValid,_,_,_))=> { if (isValid equalsfalse){ println("goto NotFound!") service.notFound() goto(Off) }else{ println("goto step1") goto(Step1) } } } when(Off){ //接收off狀態下的所有Event case _ => { println("end !") stay() } } when(Step1){ case Event(ToReturnDimension, PlatformData(_,isFirst,_,_))=> { //是否第一次請求 /是返回廣告位大小尺寸資料 if (isFirst equalstrue){ println("goto ReturnDimension!") service.returnDimension() goto(Off) }else{ println("goto step2") goto(Step2) } } } when(Step2){ case Event(ToPreciseAdvertising, PlatformData(_,_,isBusinessLine,_))=> { //是否業務線廣告位 /是返回精準投放 if (isBusinessLine equalsfalse){ println("goto PreciseAdvertising!") service.preciseAdvertising() goto(Off) }else{ println("goto step3") goto(Step3) } } } when(Step3){ case Event(ToTop9, PlatformData(_,_,_,district))=> { //是否有地域 /是返回9素材 if (district equalstrue){ println("goto Top9!") service.top9() goto(Off) }else{ println("goto step4") goto(Step4) } } } when(Step4){ //接收off狀態下的所有Event case _ => { println("Step4 end !") stay() } } whenUnhandled { case _ => { goto(Open) } } onTransition { case Open-> Step1 => println("-------------------------onTransition : from Open to Step1! ") case Open-> Off=> println("-------------------------onTransition : from Open to OFF! ") case Step1 -> Step2 => println("-------------------------onTransition : from Step1 to Step2 ! ") case Step1 -> Off=> println("-------------------------onTransition : from Step1 to OFF! ") case Step2 -> Step3 => println("-------------------------onTransition : from Step2 to Step3 ! ") case Step2 -> Off=> println("-------------------------onTransition : from Step2 to OFF! ") case Step3 -> Step4 => println("-------------------------onTransition : from Step3 to Step4 ! ") case Step3 -> Off=> println("-------------------------onTransition : from Step3 to OFF! ") } }
如上,首先是定義初始化狀態為open和初始化資料。
startWith(Open, PlatformData(false,false,false,false))
接下來是狀態處理器,根據各個業務來,例如:
when(Open){ case Event(SetInitData(isValid_,isFirst_,isBusinessLine_,district_), _) =>{ println(s"SetInitData:$isValid_ , $isFirst_ ,$isBusinessLine_ ,$district_") stay using stateData.copy(isValid = isValid_,isFirst=isFirst_,isBusinessLine=isBusinessLine_,district=district_) } case Event(ToNotFound, PlatformData(isValid,_,_,_))=> { if (isValid equalsfalse){ println("goto NotFound!") service.notFound() goto(Off) }else{ println("goto step1") goto(Step1) } } }
我們有一個初始狀態(Open),when(open)程式碼塊處理Open狀態的
收到的訊息event,ToNotFound由when(ToNotFound)程式碼塊來處理。我提到的訊息與常規我們發給Actor的訊息時一樣的,訊息與資料一起包裝過。包裝後的叫做Event(akka.actor.FSM.Event)
,看起來的樣例是這樣case Event(ToNotFound, PlatformData(isValid,_,_,_))
,比如我這裡獲得了isValid的引數,在Open的狀態下模式匹配到了ToNotFound,在函式裡面根據引數做業務判斷,呼叫業務方法或者通過呼叫goto
方法,排程到下一個狀態。
還有一個event是SetInitData,我這裡是設定自定義初始化資料用的,其中有幾個關鍵字stay
,using
和stateData
。
每一個被阻塞的case都必須返回一個State。這個可以用stay
來完成,含義是已經在處理這條訊息的最後了,以下是stay
方法的原始碼實現:
final def stay(): State = goto(currentState.stateName) // cannot directly use currentState because of the timeout field
其實也就是呼叫了goto
。
using
方法可以讓我們把改過的資料傳給下個狀態。
whenUnhandled { case _ => { goto(Open) } } onTransition { case Open-> Step1 => println("-------------------------onTransition : from Open to Step1! ") case Open-> Off=> println("-------------------------onTransition : from Open to OFF! ") case Step1 -> Step2 => println("-------------------------onTransition : from Step1 to Step2 ! ") case Step1 -> Off=> println("-------------------------onTransition : from Step1 to OFF! ") case Step2 -> Step3 => println("-------------------------onTransition : from Step2 to Step3 ! ") case Step2 -> Off=> println("-------------------------onTransition : from Step2 to OFF! ") case Step3 -> Step4 => println("-------------------------onTransition : from Step3 to Step4 ! ") case Step3 -> Off=> println("-------------------------onTransition : from Step3 to OFF! ") }
whenUnhandled
是如果沒有匹配到,FSM Actor會嘗試將我們的訊息與whenUnhandled塊中的模式進行匹配。
onTransition
是在狀態變化時做出反應或得到通知。
測試類:
class PlatformSpec extends TestKit(ActorSystem("platform-system")) with MustMatchers//must描述assertion,比如"hello" must (contain("hello")) with FunSpecLike with ImplicitSender { val begin: Long = System.currentTimeMillis() describe("just 4 test") { it("TestKit Demo") { val platformMachine = TestActorRef(Props(new PlatformMachine())) platformMachine ! SetInitData(true,false,false,true) platformMachine ! ToNotFound platformMachine ! ToReturnDimension platformMachine ! ToPreciseAdvertising platformMachine ! ToTop9 } } val end: Long = System.currentTimeMillis() System.out.println("方法耗時:"+(end-begin)); }
new 一個狀態機,對這個ActorRef 傳送訊息,按順序執行。這裡的!
和scala Actor 程式設計的模式一樣,表示傳送非同步訊息,沒有返回值。
執行結果:
方法耗時:36 SetInitData:true , false ,false ,true goto step1 -------------------------onTransition : from Open to Step1! goto step2 -------------------------onTransition : from Step1 to Step2 ! goto PreciseAdvertising! 精準投放! -------------------------onTransition : from Step2 to OFF! end !
由結果看出來,其也是非同步呼叫的。
由於網上找的資料都是用繼承Akka的TestKit
測試包來進行測試的demo,現在我還沒找到實際能用於生產上的解決方案。
如我下面程式碼:
object PlatformTest extends App{ private val begin: Long = System.currentTimeMillis() val system = ActorSystem() val machine: ActorRef = system.actorOf(Props[PlatformMachine],"plantformTest") machine ! SetInitData(true,false,true,true) machine ! ToNotFound machine ! ToReturnDimension machine ! ToPreciseAdvertising machine ! ToTop9 Thread.sleep(100) system.shutdown() private val end: Long = System.currentTimeMillis() System.out.println("方法耗時:"+(end-begin)); //system.awaitTermination() }
其測試結果:
SetInitData:true , false ,true ,true goto step1 -------------------------onTransition : from Open to Step1! goto step2 -------------------------onTransition : from Step1 to Step2 ! goto step3 -------------------------onTransition : from Step2 to Step3 ! goto Top9! 返回9素材! -------------------------onTransition : from Step3 to OFF! 方法耗時:638
通過ActorSystem呼叫actorOf的方式 獲得machine這個ActorRef,進行非同步傳送訊息,我這裡是先執行緒sleep 再關閉ActorSystem。這塊我用的比較淺,還沒有找到其他更好的方法。
參考資料: http://udn.yyuap.com/doc/akka-doc-cn/2.3.6/scala/book/chapter3/07_fsm.html http://www.jianshu.com/p/41905206b3b3 http://www.cnphp6.com/archives/29029
3.4 squirrel state machine 實現狀態機
squirrel-foundation是一款輕量級的java有限狀態機。既支援流式API又支援宣告式建立狀態機,允許使用者以一種簡單方式定義操作方法。這裡只介紹狀態機squirrel的初級用法。
3.4.1簡單操作介紹
state machine(T), state(S), event(E) and context(C)
- T代表實現的狀態機型別。
- S代表實現的狀態型別。
- E代表實現的事件型別。
- C代表實現的外部上下文型別。
首先得先建立一個狀態機
- 通過StateMachineBuilderFactory建立的StateMachineBuilder用來定義狀態機。
- 所有的狀態機例項會被同一個狀態機builder建立,該builder共享一份結構化的資料,從而優化記憶體的使用。
- 狀態機builder在生成狀態機的時候使用lazy模式。當builder建立第一個狀態機例項時,包含時間消耗的狀態機定義才會被建立。但是狀態機定義生成之後,接下來的狀態機建立將會非常快。狀態機builder應該儘量重用。
UntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(FSMController.class);
狀態機builder建立之後,定義狀態機的state,transition和action,執行external Transition。
builder.externalTransition().from(MyState.C).to(MyState.D).on(MyEvent.GoToD).when( new Condition<MyContext>() { @Override public boolean isSatisfied(MyContext context) { return context!=null && context.getValue()>80; } @Override public String name() { return "MyCondition"; } }).callMethod("thisMethod");
也可以使用流式API來定義狀態機。
builder.externalTransition().from(MyState.C).to(MyState.D).on(MyEvent.GoToD) .whenMvel("MyCondition:::(context!=null &&context.getValue()>80)") .callMethod("thisMethod");
即從MyState.C 到MyState.D,並且在事件MyEvent.GoToD 下觸發,滿足條件context!=null &&context.getValue()>80)
後執行該thisMethod
方法。
這裡的whenMvel
使用MVEL來描述條件,字元:::
用來分離條件名稱和條件表示式。context
是預先定義好的指向當前上下文的物件。
這樣的條件判斷 是在MyState.C 到MyState.D 在GoToD的event 下滿足MyCondition的情況下 才會CallMethod,但是這裡when 只是內部條件判斷,只是從from狀態C to 狀態D 轉移中進行條件判斷 判斷不過則不執行狀態轉移。
如果我要取代if-else 條件判斷的需求的話,如果按這種寫法 是不是該這麼寫:
builder.externalTransition().from(MyState.C).to(MyState.D).on(MyEvent.GoToD) .whenMvel("MyCondition:::(context!=null &&context.getValue()>80)") .callMethod("thisMethod");
builder.externalTransition().from(MyState.C).to(MyState.E).on(MyEvent.GoToE) .whenMvel("MyCondition:::(context!=null &&context.getValue()=<80)") .callMethod("OtherMethod");
這樣子肯定不行,其實還有API,可以一次定義多個transition,如下:
builder.transitions().from(MyState.C).toAmong(MyState.D, MyState.E) .onEach(MyEvent.GoToD, MyEvent.GoToE).callMethod("thisMethod|OtherMethod");
不過這樣子,首先在之前就要定義條件才行
public class FSMDecisionMaker extends UntypedAnonymousAction { @Override public void execute(Object from, Object to, Object event, Object context, UntypedStateMachine stateMachine) { MyState typedTo = (MyState)to; FSMContextData typedContext = (FSMContextData)context; if(typedTo == MyState.C){ if(typedContext.getValue()>80) { stateMachine.fire(MyEvent.GoToD, context); } else { stateMachine.fire(MyEvent.GoToE, context); } } }
定義完這個繼承隱式匿名action的類,並在裡面根據ContextData寫業務跳轉邏輯。stateMachine.fire(MyEvent.GoToD, context);
使用者可以傳送event以及context,在狀態機內部觸發transition。
如下程式碼:接下來在測試類裡面例項化這個物件,並且定義在狀態C入口時定義這個action,這樣每次在進入狀態C的時候就會執行FSMDecisionMaker裡面的execute方法了。可以配合該方法引數列表內的 Object to (即transition target state) 進行各個狀態的條件判斷了。
FSMDecisionMaker decisionMaker = new FSMDecisionMaker("DecisionMaker"); builder.onEntry(MyState.C).perform(decisionMaker);
squirrel-foundation
還提供註解方式來定義和擴充套件狀態機。例子如下:
@States({ @State(name="A", entryCallMethod="entryStateA", exitCallMethod="exitStateA"), @State(name="B", entryCallMethod="entryStateB", exitCallMethod="exitStateB") }) @Transitions({ @Transit(from="A", to="B", on="GoToB", callMethod="stateAToStateBOnGotoB"), @Transit(from="A", to="A", on="WithinA", callMethod="stateAToStateAOnWithinA", type=TransitionType.INTERNAL) }) interface MyStateMachine extends StateMachine<MyStateMachine, MyState, MyEvent, MyContext> { void entryStateA(MyState from, MyState to, MyEvent event, MyContext context); void stateAToStateBOnGotoB(MyState from, MyState to, MyEvent event, MyContext context) void stateAToStateAOnWithinA(MyState from, MyState to, MyEvent event, MyContext context) void exitStateA(MyState from, MyState to, MyEvent event, MyContext context); ... }
註解既可以定義在狀態機的實現類上也可以定義在狀態機需要實現的任何介面上面,流式API定義的狀態機也可以使用註解(但是介面中定義的方法必須要是public的)。
在介面或者實現類裡面註冊相應的method,在註解裡面定義entry 或者 exit 該state呼叫的method,和對應event 觸發Transitions 所呼叫的method。
3.4.2 根據業務編寫Demo
狀態流程圖如下(ps:簡書的markdown 太坑,還沒有畫流程圖的功能,文字也不能高亮,下面只有流程圖markdown的原文,還是能看明白的...):
graph TB Open(Open)-->Start(Start) Start --> a{isValid<br/>廣告位是否有效} a --> |N|NotFound(NotFound<br/>404) a --> |Y|Step1(Step1) Step1 --> b{isPrivateAD<br/>是否私有廣告位} b --> |N|PublicDelivery(PublicDelivery<br/>公有投放) b --> |Y|Step2(Step2) PublicDelivery-->MeteriaMatchLogicDiagram(MeteriaMatchLogicDiagram<br/>物料匹配邏輯圖) Step2 --> c{isPrivateAdvertiser<br/>是否私有廣告主} c --> |N|MeteriaMatchLogic3(MeteriaMatchLogic3<br/>物料匹配邏輯) c --> |Y|Step3(Step3) Step3 --> d{isFixedDelivery<br/>廣告位是否有定投廣告計劃} d --> |Y|MeteriaMatchLogic1(MeteriaMatchLogic1<br/>物料匹配邏輯) d --> |N|Step4(Step4) MeteriaMatchLogic1 --> |isNoMaterialDelivery<br/>沒有素材可投|Step4 Step4 --> e{isUnFixedPmp<br/>是否有非定投PMP廣告計劃} e --> |Y|MeteriaMatchLogic2(MeteriaMatchLogic2<br/>物料匹配邏輯) e --> |N|Step5(Step5) MeteriaMatchLogic2 --> |isNoMaterialDelivery<br/>沒有素材可投|Step5 Step5 --> f{isUnFixedUnPmp<br/>是否有非定投非PMP廣告計劃} f --> |Y|MeteriaMatchLogic3 f --> |N|PublicDelivery
- 首先定義context 上下文資料物件FSMContextData ,作為StateMachineParameters 裡的contextType。
public class FSMContextData { private Boolean valid;//廣告位是否有效 private Boolean privateAD;//是否私有廣告位 private Boolean privateAdvertiser;//是否私有廣告主 private Boolean fixedDelivery;//廣告位是否有定投廣告計劃 private Boolean unFixedPmp;//是否有非定投PMP廣告計劃 private Boolean unFixedUnPmp;//是否有非定投非PMP廣告計劃 private Boolean noMaterialDelivery;//是否沒有素材可投 ... Getter and Construstor with Fields... }
- 其次定義一個FSMController類 ,其繼承AbstractUntypedStateMachine,實際上就抽象成了一個狀態機,裡面我註冊了event,state,還有action等。
@Transitions({ @Transit(from="Open", to="Start", on="ToStart"), @Transit(from="PublicDelivery", to="MeteriaMatchLogicDiagram", on="ToMeteriaMatchLogicDiagram"), @Transit(from="MeteriaMatchLogic1", to="Step4", on="ToStep4",whenMvel="MyCondition:::(context!=null && context.isNoMaterialDelivery())"), @Transit(from="MeteriaMatchLogic2", to="Step5", on="ToStep5",whenMvel="MyCondition:::(context!=null && context.isNoMaterialDelivery())"), }) @States({ @State(name="Start", entryCallMethod="ontoStart"), @State(name="Step1", entryCallMethod="ontoStep1"), @State(name="Step2", entryCallMethod="ontoStep2"), @State(name="Step3", entryCallMethod="ontoStep3"), @State(name="Step4", entryCallMethod="ontoStep4"), @State(name="Step5", entryCallMethod="ontoStep5"), @State(name="NotFound", entryCallMethod="ontoNotFound"), @State(name="PublicDelivery", entryCallMethod="ontoPublicDelivery"), @State(name="MeteriaMatchLogicDiagram", entryCallMethod="ontoMeteriaMatchLogicDiagram"), @State(name="MeteriaMatchLogic1", entryCallMethod="ontoMeteriaMatchLogic1"), @State(name="MeteriaMatchLogic2", entryCallMethod="ontoMeteriaMatchLogic2"), @State(name="MeteriaMatchLogic3", entryCallMethod="ontoMeteriaMatchLogic3") }) @StateMachineParameters(stateType=FSMState.class, eventType=FSMEvent.class, contextType=FSMContextData.class) publicclass FSMController extends AbstractUntypedStateMachine { public enum FSMEvent { ToStart,ToStep1,ToStep2,ToStep3,ToStep4,ToStep5, ToNotFound, ToPublicDelivery,ToMeteriaMatchLogicDiagram, ToMeteriaMatchLogic1,ToMeteriaMatchLogic2,ToMeteriaMatchLogic3 } public enum FSMState { Open,Start,Step1,Step2,Step3,Step4,Step5, NotFound,PublicDelivery,MeteriaMatchLogicDiagram, MeteriaMatchLogic1,MeteriaMatchLogic2,MeteriaMatchLogic3 } protected void ontoStart(FSMState from, FSMState to, FSMEvent event, FSMContextData context) { System.out.println("進入"+to+"."); } protected void ontoStep1(FSMState from, FSMState to, FSMEvent event, FSMContextData context) { System.out.println("進入"+to+"."); } protected void ontoStep2(FSMState from, FSMState to, FSMEvent event, FSMContextData context) { System.out.println("進入"+to+"."); } ... protected void ontoNotFound(FSMState from, FSMState to, FSMEvent event, FSMContextData context) { System.out.println("進入"+to+"."); } protected void ontoPublicDelivery(FSMState from, FSMState to, FSMEvent event, FSMContextData context) { System.out.println("進入"+to+"."); } ... }
註解@StateMachineParameters
用來宣告狀態機泛型引數型別,包括stateType、eventType、contextType。AbstractUntypedStateMachine
是任何無狀態的狀態機的基類。在這個類裡面我用列舉類定義了state 和 event,和執行的方法即action。
在該類上方用註解方式@Transit
和@State
,可以理解為將event 或 state 和 action做了一次對映。whenMvel
條件判斷一樣也可以用在註解裡面實現。
- 然後建立一個決策類FSMDecisionMaker,在該類裡定義業務條件,其繼承UntypedAnonymousAction ,並在裡面根據typedTo區分在哪個state下,再根據 ContextData寫業務跳轉邏輯,用stateMachine.fire方法進行狀態跳轉。
public class FSMDecisionMaker extends UntypedAnonymousAction { final String name; FSMDecisionMaker(String name) { this.name = name; } @Override public String name() { return name; } @Override public void execute(Object from, Object to, Object event, Object context, UntypedStateMachine stateMachine) { FSMState typedTo = (FSMState)to; FSMContextData typedContext = (FSMContextData)context; if(typedTo == FSMState.Start){ if(typedContext.isValid()) {//廣告位是否有效 stateMachine.fire(FSMEvent.ToStep1, context);//有效:step1 } else { stateMachine.fire(FSMEvent.ToNotFound, context);//無效:404 } } else if(typedTo == FSMState.Step1){ if(typedContext.isPrivateAD()) {//是否私有廣告位 stateMachine.fire(FSMEvent.ToStep2, context);//是:step2 } else { stateMachine.fire(FSMEvent.ToPublicDelivery, context);//否:公有投放 } } else if(typedTo == FSMState.Step2){ if(typedContext.isPrivateAdvertiser()) {//是否私有廣告主 stateMachine.fire(FSMEvent.ToStep3, context);//是:step3 } else { stateMachine.fire(FSMEvent.ToMeteriaMatchLogic3, context);//否:物料匹配邏輯3 } } else if(typedTo == FSMState.Step3){ if(typedContext.isFixedDelivery()) {//廣告位是否有定投廣告計劃 stateMachine.fire(FSMEvent.ToMeteriaMatchLogic1, context);//是:物料匹配邏輯1 } else { stateMachine.fire(FSMEvent.ToStep4, context);//否:step4 } } else if(typedTo == FSMState.Step4){ if(typedContext.isUnFixedPmp()) {//是否有非定投PMP廣告計劃 stateMachine.fire(FSMEvent.ToMeteriaMatchLogic2, context);//是:物料匹配邏輯2 } else { stateMachine.fire(FSMEvent.ToStep5, context);//否:step5 } } else if(typedTo == FSMState.Step5){ if(typedContext.isUnFixedUnPmp()) {//是否有非定投非PMP廣告計劃 stateMachine.fire(FSMEvent.ToMeteriaMatchLogic3, context);//是:物料匹配邏輯3 } else { stateMachine.fire(FSMEvent.ToPublicDelivery, context);//否:公有投放 } } } }
- 最後建立測試類
public class QuickStartTest { public static void main(String[] args) { long begin=System.currentTimeMillis(); FSMContextData contextData = new FSMContextData(true,true,true,false,true,false,false); FSMDecisionMaker decisionMaker = new FSMDecisionMaker("DecisionMaker"); //Build State Transitions UntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(FSMController.class); // Start -> Step1 ; Start -> NotFound builder.onEntry(FSMController.FSMState.Start).perform(decisionMaker); builder.transitions().from(FSMController.FSMState.Start).toAmong(FSMController.FSMState.NotFound, FSMController.FSMState.Step1) .onEach(FSMController.FSMEvent.ToNotFound, FSMController.FSMEvent.ToStep1); // Step1 -> Step2 ; Step1 -> PublicDelivery builder.onEntry(FSMController.FSMState.Step1).perform(decisionMaker); builder.transitions().from(FSMController.FSMState.Step1).toAmong(FSMController.FSMState.PublicDelivery, FSMController.FSMState.Step2) .onEach(FSMController.FSMEvent.ToPublicDelivery, FSMController.FSMEvent.ToStep2); // Step2 -> Step3 ; Step2 -> MeteriaMatchLogic3 builder.onEntry(FSMController.FSMState.Step2).perform(decisionMaker); builder.transitions().from(FSMController.FSMState.Step2).toAmong(FSMController.FSMState.MeteriaMatchLogic3, FSMController.FSMState.Step3) .onEach(FSMController.FSMEvent.ToMeteriaMatchLogic3, FSMController.FSMEvent.ToStep3); // Step3 -> Step4 ; Step3 -> MeteriaMatchLogic1 builder.onEntry(FSMController.FSMState.Step3).perform(decisionMaker); builder.transitions().from(FSMController.FSMState.Step3).toAmong(FSMController.FSMState.MeteriaMatchLogic1, FSMController.FSMState.Step4) .onEach(FSMController.FSMEvent.ToMeteriaMatchLogic1, FSMController.FSMEvent.ToStep4); // Step4 -> Step5 ; Step4 -> MeteriaMatchLogic2 builder.onEntry(FSMController.FSMState.Step4).perform(decisionMaker); builder.transitions().from(FSMController.FSMState.Step4).toAmong(FSMController.FSMState.MeteriaMatchLogic2, FSMController.FSMState.Step5) .onEach(FSMController.FSMEvent.ToMeteriaMatchLogic2, FSMController.FSMEvent.ToStep5); // Step5 -> PublicDelivery ; Step5 -> MeteriaMatchLogic3 builder.onEntry(FSMController.FSMState.Step5).perform(decisionMaker); builder.transitions().from(FSMController.FSMState.Step5).toAmong(FSMController.FSMState.MeteriaMatchLogic3, FSMController.FSMState.PublicDelivery) .onEach(FSMController.FSMEvent.ToMeteriaMatchLogic3, FSMController.FSMEvent.ToPublicDelivery); //Use State Machine UntypedStateMachine fsm = builder.newStateMachine(FSMController.FSMState.Open); //Open -> Start fsm.fire(FSMController.FSMEvent.ToStart, contextData); //PublicDelivery -> MeteriaMatchLogicDiagram fsm.fire(FSMController.FSMEvent.ToMeteriaMatchLogicDiagram, contextData); //MeteriaMatchLogic1 -> Step4沒有素材可投 fsm.fire(FSMController.FSMEvent.ToStep4, contextData); //MeteriaMatchLogic2 -> Step5沒有素材可投 fsm.fire(FSMController.FSMEvent.ToStep5, contextData); System.out.println("Current state is "+fsm.getCurrentState()); fsm.terminate(); long end=System.currentTimeMillis(); System.out.println("方法耗時:"+(end-begin)); } }
先初始化contextData和決策類,建立狀態機builder,定義transitions流程,通過builder初始化狀態Open建立狀態機物件fsm。通過fire方法啟動狀態機執行跳轉,如果有條件判斷並且一次定義多個transition的需要定義在FSMDecisionMaker 類裡。
測試結果
進入Start. 進入Step1. 進入Step2. 進入Step3. 進入Step4. 進入MeteriaMatchLogic2. Current state is MeteriaMatchLogic2 方法耗時:432
這種方式實現雖然沒有Akka FSM 簡潔、方便,是用java實現的,而且比Akka輕量級,用註解配合流式Api,可以把更多的注意力放在業務實現上面。缺點是中文資料特別少。
參考資料: https://github.com/hekailiang/squirrel http://www.yangguo.info/2015/02/01/squirrel/
4.總結
不管是squirrel-foundation還是Akka FSM,或者是各種狀態模式的實現,其實都是歸為幾個要素:狀態state,上下文資料contextDate,事件event,以及動作action。 各種實現都需要初始化資料,初始化狀態,按照指定的event進行條件判斷,然後觸發相應的action。
在專案中應用java的狀態模式實際意義不大,在專案程式碼裡面就是用責任鏈模式實現的(繼承了handler類,持有對下一個物件的引用),squirrel-foundation和Akka比會更加輕量級,而且更容易在java專案中使用,但是缺點同樣是資料偏少,怕踩坑。其他類似行為樹我沒找到實現方案,規則引擎學習成本高,而且偏重量級不適合專案業務場景。