有限狀態(tài)機FSM的幾種簡單實現(xiàn)

『代碼github地址』

標(biāo)簽: 有限狀態(tài)機,Akka fsm,squirrel-foundation帘营,java狀態(tài)模式、責(zé)任鏈模式


1. 有限狀態(tài)機的概念

有限狀態(tài)機(英語:finite-state machine,縮寫:FSM)又稱有限狀態(tài)自動機补鼻,簡稱狀態(tài)機胀葱,是表示有限個狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動作等行為的數(shù)學(xué)模型。

通常FSM包含幾個要素:狀態(tài)的管理唇牧、狀態(tài)的監(jiān)控罕扎、狀態(tài)的觸發(fā)、狀態(tài)觸發(fā)后引發(fā)的動作丐重。

這些關(guān)系的意思可以這樣理解:

  • State(S) x Event(E) -> Actions (A), State(S’)
  • 如果我們當(dāng)前處于狀態(tài)S腔召,發(fā)生了E事件, 我們應(yīng)執(zhí)行操作A,然后將狀態(tài)轉(zhuǎn)換為S’

下面展示最常見的表示:當(dāng)前狀態(tài)(B)和條件(Y)的組合指示出下一個狀態(tài)(C)扮惦。完整的動作信息可以只使用腳注來增加臀蛛。包括完整動作信息的FSM定義可以使用狀態(tài)表。

條件↓當(dāng)前狀態(tài)→ 狀態(tài)A 狀態(tài)B 狀態(tài)C
條件X
條件Y 狀態(tài)C
條件Z

2. 使用狀態(tài)機的應(yīng)用背景

在廣告投放項目中由于復(fù)雜的廣告投放邏輯崖蜜,存在著大量的if-else 判斷類似的硬編碼浊仆,希望能借助對fsm模型的調(diào)研,找出理想的實現(xiàn)方式豫领。

3.有限狀態(tài)機的幾種實現(xiàn)

3.1枚舉類實現(xiàn)java狀態(tài)模式

首先在枚舉類中 定義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) {}
    },
    ...
   
    //狀態(tài)模式 提取的接口  在常量實體類中實現(xiàn)抽象方法
    abstract void valid(JavaPlatformMachine pm);
    abstract void first(JavaPlatformMachine pm);
    abstract void businessLine(JavaPlatformMachine pm);
    abstract void district(JavaPlatformMachine pm); 
}      

在enum JavaPlatformState 中,除了狀態(tài)模式 提取的接口外等恐,添加了狀態(tài)機的各種動作action實現(xiàn)

//狀態(tài)機的各種動作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");}

建立狀態(tài)機實體,ContextData是封裝條件的bean類洲劣,初始化狀態(tài)OPEN,在狀態(tài)機里定義action,調(diào)用對應(yīng)state的相應(yīng)的方法。

public class ContextData {
    private Boolean isValid_;//廣告位是否有效
    private Boolean isFirst_;//是否第一次請求
    private Boolean isBusinessLine_;//是否屬于業(yè)務(wù)線廣告位
    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);}
}

測試方法课蔬,初始化狀態(tài)機囱稽,設(shè)置參數(shù),按次序調(diào)用對應(yīng)的Action

    JavaPlatformMachine pm = new JavaPlatformMachine();
    pm.data.setValid_(true);// 廣告位是否有效
    pm.data.setFirst_(false);// 是否第一次請求
    pm.data.setBusinessLine_(true);//是否屬于業(yè)務(wù)線廣告位
    pm.data.setDistrict_(true);//是否有地域
    pm.valid();
    pm.first();
    pm.businessLine();
    pm.district();

輸出結(jié)果

OPEN→ 
→STEP1
STEP1→ 
→STEP2
STEP2→ 
→STEP3
STEP3→ 
Top9
→OFF

在設(shè)置參數(shù)下二跋,最后狀態(tài)調(diào)用Top9战惊。
不過這種方式在枚舉類中實現(xiàn)抽象方法,每個state下都要覆寫(Override)action的抽象方法同欠,顯然不利于拓展样傍。

參考資料:
http://blog.csdn.net/yqj2065/article/details/39371487

3.2抽象類實現(xiàn)java狀態(tài)模式

當(dāng)一個類的某個成員變量的值變化時横缔,可能導(dǎo)致多個行為表現(xiàn)得不同。將該成員變量封裝成類型的模式衫哥,即為狀態(tài)模式(state pattern)茎刚。即用多態(tài)來重構(gòu)分支結(jié)構(gòu)。

首先抽象狀態(tài)類撤逢,定義一個接口以封裝與Context的一個特定狀態(tài)相關(guān)的行為

public abstract class State {
    public abstract void Handle(StateModeContext context );
    public abstract boolean isFinalflag();
}

Context類膛锭,維護(hù)一個State子類的實例,這個實例定義當(dāng)前的狀態(tài)蚊荣。

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的初始狀態(tài)
    public StateModeContext(State state , ContextData data )
    {
        this.data = data;
        this.state = state;
    }

    /// 對請求做處理初狰,并設(shè)置下一個狀態(tài)
    boolean  trueFlag = true;
    
    public void Request()
    {
        //如果當(dāng)前step 是最后一步  nextStep 不執(zhí)行
        if(state.isFinalflag()){
            trueFlag = false;
        }
        state.Handle(this);
    }
}

最后定義各個狀態(tài)子類

public class State404 extends State {
    @Override
    public void Handle(StateModeContext context) {
        System.out.println("當(dāng)前狀態(tài)是 404  do something");
    }
    @Override
    public boolean isFinalflag() {
        return true;
    }
}

其中設(shè)置一個FinalFlag 標(biāo)識 是否是最終狀態(tài)。
在下面state的子類里面進(jìn)行條件判斷互例,并設(shè)置下一個狀態(tài)奢入,這一點有點類似責(zé)任鏈模式(在抽象類中定義一個實例成員變量 next handler 的引用)

public class StateStep1 extends State {
    @Override
    public void Handle(StateModeContext context) {
        System.out.println("當(dāng)前狀態(tài)是 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;
    }
}

測試類:設(shè)置初始化數(shù)據(jù)和初始化狀態(tài)stateOpen,根據(jù)狀態(tài)樹的深度確定循環(huán)迭代次數(shù),進(jìn)行迭代媳叨。

public class StateModeTest {
    public static void main(String[] args) {
        // 設(shè)置Context的初始狀態(tài)為ConcreteStateA
        ContextData data = new ContextData(true,false,true,true);
        StateModeContext context = new StateModeContext(new StateOpen(),data);
        // 不斷地進(jìn)行請求腥光,同時更改狀態(tài)
        int size = 4;// 請求迭代數(shù)
        for(int i = 0 ; i< size+1; i++){
            if(context.trueFlag){
                context.Request();
            }
        }
    }
}

輸出結(jié)果:

當(dāng)前狀態(tài)是 open
open -> step1
當(dāng)前狀態(tài)是 step1
step1 -> step2
當(dāng)前狀態(tài)是 step2
step2 -> step3
當(dāng)前狀態(tài)是 step3
step3 -> Top9(返回9素材)
當(dāng)前狀態(tài)是 Top9(返回9素材)  do something

這種方式實現(xiàn)狀態(tài)模式,每個狀態(tài)要new一個狀態(tài)的子類糊秆,而且手動指定循環(huán)迭代次數(shù)通過迭代方式進(jìn)行事件的調(diào)用武福。

3.3 Akka FSM 實現(xiàn)狀態(tài)機

Akka的狀態(tài)機是非常簡潔的實現(xiàn),充分利用了Scala的許多先進(jìn)的語法糖讓代碼更加簡潔清晰痘番。是基于Akka Actor 實現(xiàn)捉片,封裝了很多自定義的API(實際上就是DSL)。

在底層汞舱,Akka FSM就是一個繼承了Actor的trait(scala的特征trait就相當(dāng)于java的接口)

trait FSM[S, D] extends Actor with Listeners with ActorLogging { ...}

FSM trait提供了一個包裝了常規(guī)Actor的DSL伍纫,讓我們能集中注意力在更快的構(gòu)建手頭的狀態(tài)機上。常規(guī)Actor只有一個receive方法兵拢,F(xiàn)SM trait包裝了receive方法的實現(xiàn)并將調(diào)用指向到一個特定狀態(tài)機的處理代碼塊翻斟。

class PlatformMachine extends FSM[PlatformState,PlatformData]

上面的 PlatformMachine 是一個FSM ACTOR。
PlatformState,PlatformData分別為我定義的狀態(tài)和數(shù)據(jù)说铃。在FSM中,有兩個東西是一直存在的嘹履,任何時間點都有狀態(tài) 腻扇,和在狀態(tài)中進(jìn)行共享的數(shù)據(jù)。 這代表所有的fsm的狀態(tài)繼承自PlatformState砾嫉,而所有在狀態(tài)間共享的數(shù)據(jù)就是PlatformData

在伴生對象中定義狀態(tài)幼苛,消息,數(shù)據(jù)焕刮。

在Scala的類中舶沿,與類名相同的對象(object)叫做伴生對象墙杯,類和伴生對象之間可以相互訪問私有的方法和屬性。在Scala中沒有靜態(tài)方法和靜態(tài)字段括荡,但是可以使用object這個語法結(jié)構(gòu)來達(dá)到同樣的目的高镐。object是單例模式的,一般也用于存放工具方法和常量,共享單個不可變的實例等畸冲。

在Scala中樣例類(case class)是一中特殊的類嫉髓,可用于模式匹配。case class是多例的邑闲,后面要跟構(gòu)造參數(shù)算行,case object是單例的。這點其實和class與object的區(qū)別是一樣的苫耸。

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 extends  UserMessage
  case object ToNotFound extends  UserMessage
  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 equals  false)  {
        println("goto NotFound!")
        service.notFound()
        goto(Off)
      }else{
        println("goto step1")
        goto(Step1)
      }
    }
  }

  when(Off){
    //接收off狀態(tài)下的所有Event
    case _ => {
      println("end !")
      stay()
    }
  }

  when(Step1){
    case Event(ToReturnDimension, PlatformData(_,isFirst,_,_))   => {
      //是否第一次請求 /是  返回廣告位大小尺寸數(shù)據(jù)
      if (isFirst equals  true){
        println("goto ReturnDimension!")
        service.returnDimension()
        goto(Off)
      }else{
        println("goto step2")
        goto(Step2)
      }
    }
  }

  when(Step2){
    case Event(ToPreciseAdvertising, PlatformData(_,_,isBusinessLine,_))   => {
      //是否業(yè)務(wù)線廣告位 /是  返回精準(zhǔn)投放
      if (isBusinessLine equals  false){
        println("goto PreciseAdvertising!")
        service.preciseAdvertising()
        goto(Off)
      }else{
        println("goto step3")
        goto(Step3)
      }
    }
  }

  when(Step3){
    case Event(ToTop9, PlatformData(_,_,_,district))   => {
      //是否有地域 /是  返回9素材
      if (district equals  true){
        println("goto Top9!")
        service.top9()
        goto(Off)
      }else{
        println("goto step4")
        goto(Step4)
      }
    }
  }

  when(Step4){
    //接收off狀態(tài)下的所有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   ! ")
  }

}

如上州邢,首先是定義初始化狀態(tài)為open和初始化數(shù)據(jù)。

startWith(Open, PlatformData(false,false,false,false))

接下來是狀態(tài)處理器褪子,根據(jù)各個業(yè)務(wù)來偷霉,例如:

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 equals  false)  {
        println("goto NotFound!")
        service.notFound()
        goto(Off)
      }else{
        println("goto step1")
        goto(Step1)
      }
    }
  }

我們有一個初始狀態(tài)(Open),when(open)代碼塊處理Open狀態(tài)的
收到的消息event褐筛,ToNotFound由when(ToNotFound)代碼塊來處理类少。我提到的消息與常規(guī)我們發(fā)給Actor的消息時一樣的,消息與數(shù)據(jù)一起包裝過渔扎。包裝后的叫做Event(akka.actor.FSM.Event)硫狞,看起來的樣例是這樣case Event(ToNotFound, PlatformData(isValid,_,_,_)),比如我這里獲得了isValid的參數(shù),在Open的狀態(tài)下模式匹配到了ToNotFound,在函數(shù)里面根據(jù)參數(shù)做業(yè)務(wù)判斷晃痴,調(diào)用業(yè)務(wù)方法或者通過調(diào)用goto方法残吩,調(diào)度到下一個狀態(tài)。

還有一個event是SetInitData,我這里是設(shè)置自定義初始化數(shù)據(jù)用的倘核,其中有幾個關(guān)鍵字stay,usingstateData泣侮。

每一個被阻塞的case都必須返回一個State。這個可以用stay來完成紧唱,含義是已經(jīng)在處理這條消息的最后了,以下是stay方法的源碼實現(xiàn):

 final def stay(): State = goto(currentState.stateName) // cannot directly use currentState because of the timeout field

其實也就是調(diào)用了goto活尊。
using方法可以讓我們把改過的數(shù)據(jù)傳給下個狀態(tài)。

  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 是如果沒有匹配到漏益,F(xiàn)SM Actor會嘗試將我們的消息與whenUnhandled塊中的模式進(jìn)行匹配蛹锰。
onTransition 是在狀態(tài)變化時做出反應(yīng)或得到通知。
測試類:

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 一個狀態(tài)機绰疤,對這個ActorRef 發(fā)送消息铜犬,按順序執(zhí)行。這里的和scala Actor 編程的模式一樣癣猾,表示發(fā)送異步消息敛劝,沒有返回值。
執(zhí)行結(jié)果:

方法耗時:36
SetInitData:true , false ,false ,true  
goto step1
-------------------------onTransition : from Open to Step1  ! 
goto step2
-------------------------onTransition : from Step1 to Step2 ! 
goto PreciseAdvertising!
精準(zhǔn)投放!
-------------------------onTransition : from Step2 to OFF   ! 
end !

由結(jié)果看出來纷宇,其也是異步調(diào)用的夸盟。
由于網(wǎng)上找的資料都是用繼承Akka的TestKit測試包來進(jìn)行測試的demo,現(xiàn)在我還沒找到實際能用于生產(chǎn)上的解決方案呐粘。
如我下面代碼:

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

其測試結(jié)果:

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調(diào)用actorOf的方式 獲得machine這個ActorRef满俗,進(jìn)行異步發(fā)送消息,我這里是先線程sleep 再關(guān)閉ActorSystem作岖。這塊我用的比較淺唆垃,還沒有找到其他更好的方法。

參考資料:
http://udn.yyuap.com/doc/akka-doc-cn/2.3.6/scala/book/chapter3/07_fsm.html
http://www.reibang.com/p/41905206b3b3
http://www.cnphp6.com/archives/29029

3.4 squirrel state machine 實現(xiàn)狀態(tài)機

squirrel-foundation是一款輕量級的java有限狀態(tài)機痘儡。既支持流式API又支持聲明式創(chuàng)建狀態(tài)機辕万,允許用戶以一種簡單方式定義操作方法。這里只介紹狀態(tài)機squirrel的初級用法沉删。

3.4.1簡單操作介紹

state machine(T), state(S), event(E) and context(C)

  • T代表實現(xiàn)的狀態(tài)機類型渐尿。
  • S代表實現(xiàn)的狀態(tài)類型。
  • E代表實現(xiàn)的事件類型矾瑰。
  • C代表實現(xiàn)的外部上下文類型砖茸。

首先得先創(chuàng)建一個狀態(tài)機

  • 通過StateMachineBuilderFactory創(chuàng)建的StateMachineBuilder用來定義狀態(tài)機。
  • 所有的狀態(tài)機實例會被同一個狀態(tài)機builder創(chuàng)建殴穴,該builder共享一份結(jié)構(gòu)化的數(shù)據(jù)凉夯,從而優(yōu)化內(nèi)存的使用。
  • 狀態(tài)機builder在生成狀態(tài)機的時候使用lazy模式采幌。當(dāng)builder創(chuàng)建第一個狀態(tài)機實例時劲够,包含時間消耗的狀態(tài)機定義才會被創(chuàng)建。但是狀態(tài)機定義生成之后休傍,接下來的狀態(tài)機創(chuàng)建將會非痴饕铮快。狀態(tài)機builder應(yīng)該盡量重用磨取。
UntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(FSMController.class);

狀態(tài)機builder創(chuàng)建之后人柿,定義狀態(tài)機的state,transition和action,執(zhí)行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來定義狀態(tài)機寝衫。

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 下觸發(fā)顷扩,滿足條件context!=null &&context.getValue()>80)后執(zhí)行該thisMethod方法。

這里的whenMvel使用MVEL來描述條件慰毅,字符:::用來分離條件名稱和條件表達(dá)式。context是預(yù)先定義好的指向當(dāng)前上下文的對象扎阶。

這樣的條件判斷 是在MyState.C 到MyState.D 在GoToD的event 下滿足MyCondition的情況下 才會CallMethod汹胃,但是這里when 只是內(nèi)部條件判斷婶芭,只是從from狀態(tài)C to 狀態(tài)D 轉(zhuǎn)移中進(jìn)行條件判斷 判斷不過則不執(zhí)行狀態(tài)轉(zhuǎn)移。
如果我要取代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的類,并在里面根據(jù)ContextData寫業(yè)務(wù)跳轉(zhuǎn)邏輯轨奄。stateMachine.fire(MyEvent.GoToD, context);用戶可以發(fā)送event以及context孟害,在狀態(tài)機內(nèi)部觸發(fā)transition。
如下代碼:接下來在測試類里面實例化這個對象挪拟,并且定義在狀態(tài)C入口時定義這個action挨务,這樣每次在進(jìn)入狀態(tài)C的時候就會執(zhí)行FSMDecisionMaker里面的execute方法了∮褡椋可以配合該方法參數(shù)列表內(nèi)的 Object to (即transition target state) 進(jìn)行各個狀態(tài)的條件判斷了谎柄。

FSMDecisionMaker decisionMaker = new FSMDecisionMaker("DecisionMaker");
builder.onEntry(MyState.C).perform(decisionMaker);

squirrel-foundation還提供注解方式來定義和擴展?fàn)顟B(tài)機。例子如下:

@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);
    ...
}

注解既可以定義在狀態(tài)機的實現(xiàn)類上也可以定義在狀態(tài)機需要實現(xiàn)的任何接口上面,流式API定義的狀態(tài)機也可以使用注解(但是接口中定義的方法必須要是public的)惯雳。

在接口或者實現(xiàn)類里面注冊相應(yīng)的method朝巫,在注解里面定義entry 或者 exit 該state調(diào)用的method,和對應(yīng)event 觸發(fā)Transitions 所調(diào)用的method。

3.4.2 根據(jù)業(yè)務(wù)編寫Demo

狀態(tài)流程圖如下(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
  1. 首先定義context 上下文數(shù)據(jù)對象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...
    }
  1. 其次定義一個FSMController類 恩商,其繼承AbstractUntypedStateMachine,實際上就抽象成了一個狀態(tài)機变逃,里面我注冊了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)
public  class 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("進(jìn)入"+to+".");
    }
    protected void ontoStep1(FSMState from, FSMState to, FSMEvent event, FSMContextData context) {
        System.out.println("進(jìn)入"+to+".");
    }
    protected void ontoStep2(FSMState from, FSMState to, FSMEvent event, FSMContextData context) {
        System.out.println("進(jìn)入"+to+".");
    }
   ...
   
    protected void ontoNotFound(FSMState from, FSMState to, FSMEvent event, FSMContextData context) {
        System.out.println("進(jìn)入"+to+".");
    }
    protected void ontoPublicDelivery(FSMState from, FSMState to, FSMEvent event, FSMContextData context) {
        System.out.println("進(jìn)入"+to+".");
    }
    ...
}

注解@StateMachineParameters用來聲明狀態(tài)機泛型參數(shù)類型,包括stateType怠堪、eventType揽乱、contextType。AbstractUntypedStateMachine是任何無狀態(tài)的狀態(tài)機的基類粟矿。 在這個類里面我用枚舉類定義了state 和 event凰棉,和執(zhí)行的方法即action。
在該類上方用注解方式@Transit@State陌粹,可以理解為將event 或 state 和 action 做了一次映射撒犀。whenMvel條件判斷一樣也可以用在注解里面實現(xiàn)。

  1. 然后創(chuàng)建一個決策類FSMDecisionMaker,在該類里定義業(yè)務(wù)條件或舞,其繼承UntypedAnonymousAction 荆姆,并在里面根據(jù)typedTo區(qū)分在哪個state下,再根據(jù) ContextData寫業(yè)務(wù)跳轉(zhuǎn)邏輯映凳,用stateMachine.fire方法進(jìn)行狀態(tài)跳轉(zhuǎn)胆筒。
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);//否:公有投放
            }
        }

    }
}
  1. 最后建立測試類
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和決策類,創(chuàng)建狀態(tài)機builder诈豌,定義transitions流程仆救,通過builder初始化狀態(tài)Open創(chuàng)建狀態(tài)機對象fsm。通過fire方法啟動狀態(tài)機執(zhí)行跳轉(zhuǎn)矫渔,如果有條件判斷并且一次定義多個transition的需要定義在FSMDecisionMaker 類里彤蔽。

測試結(jié)果

進(jìn)入Start.
進(jìn)入Step1.
進(jìn)入Step2.
進(jìn)入Step3.
進(jìn)入Step4.
進(jìn)入MeteriaMatchLogic2.
Current state is MeteriaMatchLogic2
方法耗時:432

這種方式實現(xiàn)雖然沒有Akka FSM 簡潔、方便蚌斩,是用java實現(xiàn)的铆惑,而且比Akka輕量級,用注解配合流式Api,可以把更多的注意力放在業(yè)務(wù)實現(xiàn)上面送膳。缺點是中文資料特別少员魏。

參考資料:
https://github.com/hekailiang/squirrel
http://www.yangguo.info/2015/02/01/squirrel/

4.總結(jié)

不管是squirrel-foundation還是Akka FSM,或者是各種狀態(tài)模式的實現(xiàn)叠聋,其實都是歸為幾個要素:狀態(tài)state撕阎,上下文數(shù)據(jù)contextDate,事件event,以及動作action碌补。 各種實現(xiàn)都需要初始化數(shù)據(jù)虏束,初始化狀態(tài),按照指定的event進(jìn)行條件判斷厦章,然后觸發(fā)相應(yīng)的action镇匀。
在項目中應(yīng)用java的狀態(tài)模式實際意義不大,在項目代碼里面就是用責(zé)任鏈模式實現(xiàn)的(繼承了handler類袜啃,持有對下一個對象的引用)汗侵,squirrel-foundation和Akka比會更加輕量級,而且更容易在java項目中使用群发,但是缺點同樣是資料偏少晰韵,怕踩坑。其他類似行為樹我沒找到實現(xiàn)方案熟妓,規(guī)則引擎學(xué)習(xí)成本高雪猪,而且偏重量級不適合項目業(yè)務(wù)場景。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末起愈,一起剝皮案震驚了整個濱河市只恨,隨后出現(xiàn)的幾起案子译仗,更是在濱河造成了極大的恐慌,老刑警劉巖坤次,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件古劲,死亡現(xiàn)場離奇詭異斥赋,居然都是意外死亡缰猴,警方通過查閱死者的電腦和手機铃彰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門跃须,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纪吮,“玉大人抗俄,你說我怎么就攤上這事喉前∑旆遥” “怎么了踱启?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵俏讹,是天一觀的道長弯菊。 經(jīng)常有香客問我纵势,道長,這世上最難降的妖魔是什么管钳? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任钦铁,我火速辦了婚禮,結(jié)果婚禮上才漆,老公的妹妹穿的比我還像新娘牛曹。我一直安慰自己,他們只是感情好醇滥,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布黎比。 她就那樣靜靜地躺著,像睡著了一般鸳玩。 火紅的嫁衣襯著肌膚如雪阅虫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天不跟,我揣著相機與錄音颓帝,去河邊找鬼。 笑死躬拢,一個胖子當(dāng)著我的面吹牛躲履,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播聊闯,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼工猜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了菱蔬?” 一聲冷哼從身側(cè)響起篷帅,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤史侣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后魏身,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惊橱,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年箭昵,在試婚紗的時候發(fā)現(xiàn)自己被綠了税朴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡家制,死狀恐怖正林,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颤殴,我是刑警寧澤觅廓,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站涵但,受9級特大地震影響杈绸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜矮瘟,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一瞳脓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芥永,春花似錦篡殷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至棘催,卻和暖如春劲弦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背醇坝。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工邑跪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人呼猪。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓画畅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宋距。 傳聞我的和親對象是個殘疾皇子轴踱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容