2015年12月21日月曜日

[C#]有限ステートマシン(finite state machine)を作ってみた

ネット上で様々なステートマシンを見てきてシンプルだったり、高機能だったり色々あったけど、
でも俺流はこうだ!っていうのがあったので、今回記事にしました。


考え方は、ステートマシンクラスを生成するときにコンテキストクラスを渡しておいて、イベントFireするというもの。


実装は拡張メソッドで書き記す。

 ・Class名=State名
 ・Method名=Event名、ChangeStateAttribute属性を用いて状態遷移を記述する。
 

となるように記述する。


下がステートマシンクラスの本体。


 public class ChangeStateAttribute : Attribute
 {
  public object DoingState { get; } = null;
  public object SuccessState { get; } = null;
  public object FailedState { get; } = null;

  public ChangeStateAttribute(object success, object failed)
  {
   SuccessState = success;
   FailedState = failed;
  }

  public ChangeStateAttribute(object before, object success, object failed)
  {
   DoingState = before;
   SuccessState = success;
   FailedState = failed;
  }
 }

 public class StateMachine<STATE, EVENT>
 {
  public STATE State { get; private set; }
  public object targetObject { get; }
  Dictionary<STATE, Dictionary<EVENT, MethodInfo>> states { get; } = new Dictionary<STATE, Dictionary<EVENT, MethodInfo>>();

  public StateMachine(object targetObject, STATE InitState)
  {
   State = InitState;
   this.targetObject = targetObject;
   var type = targetObject.GetType();
   var asm = type.Assembly;
   var types = asm.GetTypes();
   var enums = Enum.GetValues(typeof(STATE));
   var events = Enum.GetValues(typeof(EVENT));

   foreach (STATE state in enums)
   {
    var sname = state.ToString();
    var stype = types.Where(_ => _.Name == sname).First();
    var edic = new Dictionary<EVENT, MethodInfo>();

    foreach (EVENT evt in events)
    {
     var ename = evt.ToString();
     var methodInfo = stype.GetMethod(ename);
     if (null != methodInfo)
     {
      edic[evt] = methodInfo;
     }
    }
    states[state] = edic;
   }
  }

  /// <summary>
  /// 実行
  /// </summary>
  /// <param name="trigger"></param>
  /// <param name="parameters"></param>
  public bool Fire(EVENT trigger, params object[] parameters)
  {
   var minfo = states[State][trigger];
   var before = minfo.GetCustomAttribute<ChangeStateBeforeAttribute>();
   var after = minfo.GetCustomAttribute<ChangeStateAfterAttribute>();
   var prms = parameters.ToList();
   prms.Insert(0, targetObject);
   var old = State;

   if (null != before) State = (STATE)before.State;
   if ((bool)minfo.Invoke(null, prms.ToArray()))
   {
    if (null != after) State = (STATE)after.State;
    return true;
   }
   else
   {
    State = old;
    return false;
   }
  }
  public Task<bool> FireASync(EVENT trigger, params object[] parameters)
  {
   return Task.Run(() => Fire(trigger, parameters));
  }
 }



次に実装サンプルで通信プログラムを実装してみる。


Step1. enum型でStateとEventを定義する。

 public enum CommState
 {
  NotConnected,
  Connecting,
  Idle,
  Busy,
  Disconnecting,
  Error,
 }

 public enum CommEvent
 {
  Connect,
  Disconnect,
  Request,
  Notify,
  ReadLine,
 }

Step2. Stateクラス、Eventメソッドを実装する。

 public static class NotConnected
 {
  [ChangeState(CommState.Idle, CommState.Error)]
  public static bool Connect(this CommPort self)
  {
   self.Open();
   return true;
  }
 }

 public static class Connecting
 {
 }

 public static class Disconnecting
 {
 }

 public static class Idle
 {
  [ChangeState(CommState.Busy, CommState.Idle, CommState.Error)]
  public static bool Request(this CommPort self, string message)
  {
   self.Write(message);
   Console.WriteLine("R:{0}", self.Read());
   return true;
  }

  [ChangeState(CommState.Busy, CommState.Idle, CommState.Error)]
  public static bool Notify(this CommPort self, string message)
  {
   self.Write(message);
   return true;
  }

  [ChangeState(CommState.Busy, CommState.Idle, CommState.Error)]
  public static bool ReadLine(this CommPort self)
  {
   Console.WriteLine("R:{0}", self.Read());
   return true;
  }

  [ChangeState(CommState.Disconnecting, CommState.NotConnected, CommState.Error)]
  public static bool Disconnect(this CommPort self)
  {
   self.Close();
   return true;
  }
 }

 public static class Busy
 {
 }

 public static class Error
 {
  [ChangeState(CommState.Idle, CommState.Error)]
  public static bool Connect(this CommPort self)
  {
   self.Open();
   return true;
  }
 }

Step3. ステートマシンを生成する。

  static void Main(string[] args)
  {
   var port = new CommPort();
   var fsm = new StateMachine<CommState, CommEvent>(port, CommState.NotConnected);

   fsm.Fire(CommEvent.Connect);
   fsm.Fire(CommEvent.Request, "HELLO");
   fsm.Fire(CommEvent.Notify, "HELLO");
   fsm.Fire(CommEvent.ReadLine);
   fsm.Fire(CommEvent.Disconnect);

   Console.ReadKey();
  }
 //CommAPI
 public class CommPort
 {
  public void Open() { Console.WriteLine("Open"); }
  public void Close() { Console.WriteLine("Close"); }
  public void Write(string message) { Console.WriteLine("Send:{0}", message); }
  public string Read() { Console.WriteLine("Read"); return "World"; }
 }



もうちょっとシンプルな例題だとすっきり書けたかなあ。



コメントを投稿

Androider