C# Finite State Machine Design Evolution for

20987 ワード

概要
証券取引システムの両足套利機能を開発する際、各両足套利監視には、状態変遷メンテナンスが必要である.最初のインプリメンテーションではステータスマシンは使用されませんでしたが、停止、監視、実行、実行の4つのステータスを定義し、サポートしました.その後、設計変更に直面し、一時停止ステータスを追加する必要があります.コードを簡略化し,可読性を向上させるためにFSMステートマシンを実現した.
従来,通信下位埋め込みシステムではTCP/IPのプロトコルスタックのステートマシン処理に経験があるため,ステートマシンを独自に実装した.実現後、ネット上でC#実現ステータスマシンの方式について調査したところ、ステータスマシンの設計を簡素化し最適化するための新しい技術がたくさんあることが分かった.特に,状態機械と「挙動木Behavior Tree(BT)」の発展と関係を理解した.これまで行動木に触れたことがないが,この編ではステータスマシンにも重点を置き,次回は行動木の応用を検討する時間を見つけたい.
Introduction of arbitrage trading
An arbitrage trading has 2 legs, which acn be executed simultaneously, or one after another (successively). 各配当には2つの参照品種があり、価格差が所定の条件に達すると自動的に実行がトリガーされます.when created, it is in Stopped state, user can start Monitoring, or stop executing.

public enum ArbitrageStatus
{
Monitoring,
Executing,
Executed,
Stopped,
Paused
}

第一の実現方式

#region FSM
    private bool FsmSetStatus(Arbitrage arbitrage, ArbitrageStatus to)
    {
        switch (arbitrage.ArbitrageStatus)
        {
            case ArbitrageStatus.  :
                switch (to)
                {
                    //     ->     ,            
                    //      ,              
                    case ArbitrageStatus.    :
                        return FsmFrom  to    (arbitrage);
                    case ArbitrageStatus.  :
                        return FsmFrom  to  (arbitrage);
                    case ArbitrageStatus.   :
                        return FsmFrom  to   (arbitrage);
                }
                break;
            case ArbitrageStatus.  :
                switch (to)
                {
                    //                  
                    case ArbitrageStatus.    :
                        return FsmFrom  to    (arbitrage);
                }
                break;
            case ArbitrageStatus.   :
                switch (to)
                {
                    case ArbitrageStatus.  :
                        return FsmFrom   to  (arbitrage);
                }
                break;
            case ArbitrageStatus.    :
                switch (to)
                {
                    case ArbitrageStatus.    :
                    case ArbitrageStatus.   :
                        return FsmFrom    to       (arbitrage);
                    case ArbitrageStatus.  :
                        return FsmFrom    to  (arbitrage);
                    case ArbitrageStatus.  :
                        return FsmFrom    to  (arbitrage);
                }
                break;
            case ArbitrageStatus.    :
                switch (to)
                {
                    case ArbitrageStatus.  :
                        return FsmFrom    to  (arbitrage);
                    case ArbitrageStatus.  :
                        return FsmFrom    to  (arbitrage);
                    case ArbitrageStatus.    :
                        return FsmFrom    to    (arbitrage);
                    case ArbitrageStatus.    :
                        return true;
                }
                break;
        }
        return false;
    }


    /// 
    ///      ,              。
    /// 
    /// 
    /// 
    private bool FsmFrom  to    (Arbitrage arbitrage)
    {
        if (UnfinishedEntrustCounter(arbitrage) == 0)
        {
            arbitrage.ArbitrageStatus = ArbitrageStatus.    ;
            return true;
        }

        arbitrage.ArbitrageStatus = ArbitrageStatus.    ;
        return true;
    }
    private bool FsmFrom  to  (Arbitrage arbitrage)
    {
        //      1,        。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        arbitrage.ArbitrageStatus = ArbitrageStatus.  ;
        return true;
    }

    private bool FsmFrom  to   (Arbitrage arbitrage)
    {
        //      1,        。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        if(arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
            arbitrage.ArbitrageStatus = ArbitrageStatus.   ;
        return true;
    }

    private bool FsmFrom    to  (Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.  ;
        return true;
    }

    private bool FsmFrom    to  (Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.  ;
        return true;
    }

    private bool FsmFrom    to    (Arbitrage arbitrage)
    {
        if (!arbitrage.IgnoreCheckOnce)
        {
            if (!TsabUtility.CheckPriceContition(arbitrage))
                return false;
        }
        else
        {
            arbitrage.IgnoreCheckOnce = false;
        }

        List> legEntrusts;
        if (NextArbitrageBaskets(arbitrage, out legEntrusts))
        {
            arbitrage.ArbitrageStatus = ArbitrageStatus.    ;
            // issue 
            LegEntrustsHandler(arbitrage, legEntrusts);
        }
        return true;
    }

    /// 
    ///              “   ”,“    ”     。
    /// 
    /// 
    /// 
    private bool FsmFrom    to       (Arbitrage arbitrage)
    {
        if (!FailedEntrustCheck(arbitrage))
        {
            ManualStopArbitrage(arbitrage, true);
            _lastError = "           ,        !";
            if (AlarmHandler != null)
            {
                AlarmHandler(arbitrage);
            }
            ClearEntrustCounter(arbitrage);
            arbitrage.NeedWaitNextLeg = false;
            arbitrage.ArbitrageStatus = ArbitrageStatus.  ;
            return true;
        }

        if (UnfinishedEntrustCounter(arbitrage) == 0)
        {
            ClearEntrustCounter(arbitrage);
            if (!arbitrage.NeedWaitNextLeg)
            {
                arbitrage.ExecutionTimesDone++;
                if (arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
                {
                    ClearEntrustCounter(arbitrage);
                    arbitrage.ArbitrageStatus = ArbitrageStatus.   ;
                    return true;
                }
            }
            if (arbitrage.NeedWaitNextLeg || TsabUtility.CheckPriceContition(arbitrage))
            {
                //          
                List> legEntrusts;
                if (NextArbitrageBaskets(arbitrage, out legEntrusts))
                {
                    // issue 
                    LegEntrustsHandler(arbitrage, legEntrusts);
                }
            }
            else
            {
                arbitrage.ArbitrageStatus = ArbitrageStatus.    ;
            }
        }

        return true;
    }

    private bool FsmFrom    to  (Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.  ;
        return true;
    }

    private bool FsmFrom    to  (Arbitrage arbitrage)
    {
        //      1,        。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        arbitrage.ArbitrageStatus = ArbitrageStatus.  ;
        return true;
    }

    private bool FsmFrom   to  (Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.  ;
        return true;
    }

    private bool FsmFrom  to    (Arbitrage arbitrage)
    {
        if (arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
        {
            _lastError = "          !" + arbitrage;
            return false;
        }
        arbitrage.ArbitrageStatus = ArbitrageStatus.    ;
        return true;
    }


    #endregion

この はCommandやEventの を させず, び しインタフェースは の を して をトリガし,すなわち「 」をステータスマシン にカプセル していない.
び し :

public bool ManualStartArbitrage(Arbitrage arbitrage, bool forceIssueNow)
{
var exists = ArbitrageList.Where(p => p.Id == arbitrage.Id);
if (!exists.Any())
{
_lastError = " : Id:" + arbitrage.Id;
return false;
}
        if (arbitrage.ExecutionTimesPlanned <= arbitrage.ExecutionTimesDone)
        {
            _lastError = "                !";
            return false;
        }

        if (!FsmSetStatus(arbitrage, ArbitrageStatus.    ))
        {
            _lastError = GetLastError();
            return false;
        }

        //       ,     
        if (forceIssueNow || TsabUtility.CheckPriceContition(arbitrage))
        {
            arbitrage.IgnoreCheckOnce = true;
            var ret = FsmSetStatus(arbitrage, ArbitrageStatus.    );
            arbitrage.IgnoreCheckOnce = false;
            return ret;
        }

        return true;
    }
されたリファレンスインプリメンテーション
1のリファレンス
1の はStackoverflowから ており,Commandを に し, によってCommandによって をトリガする を する.Let's start with this simple state diagram:
![simple state machine diagram][1]
We have:
  • 4 states (Inactive, Active, Paused, and Exited)
  • 5 types of state transitions (Begin Command, End Command, Pause Command, Resume Command, Exit Command).

  • You can convert this to C# in a handful of ways, such as performing a switch statement on the current state and command, or looking up transitions in a transition table. For this simple state machine, I prefer a transition table, which is very easy to represent using a Dictionary :
    using System;
    using System.Collections.Generic;
    
    namespace Juliet
    {
        public enum ProcessState
        {
            Inactive,
            Active,
            Paused,
            Terminated
        }
    
        public enum Command
        {
            Begin,
            End,
            Pause,
            Resume,
            Exit
        }
    
        public class Process
        {
            class StateTransition
            {
                readonly ProcessState CurrentState;
                readonly Command Command;
    
                public StateTransition(ProcessState currentState, Command command)
                {
                    CurrentState = currentState;
                    Command = command;
                }
    
                public override int GetHashCode()
                {
                    return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
                }
    
                public override bool Equals(object obj)
                {
                    StateTransition other = obj as StateTransition;
                    return other != null && this.CurrentState == other.CurrentState && this.Command == other.Command;
                }
            }
    
            Dictionary transitions;
            public ProcessState CurrentState { get; private set; }
    
            public Process()
            {
                CurrentState = ProcessState.Inactive;
                transitions = new Dictionary
                {
                    { new StateTransition(ProcessState.Inactive, Command.Exit), ProcessState.Terminated },
                    { new StateTransition(ProcessState.Inactive, Command.Begin), ProcessState.Active },
                    { new StateTransition(ProcessState.Active, Command.End), ProcessState.Inactive },
                    { new StateTransition(ProcessState.Active, Command.Pause), ProcessState.Paused },
                    { new StateTransition(ProcessState.Paused, Command.End), ProcessState.Inactive },
                    { new StateTransition(ProcessState.Paused, Command.Resume), ProcessState.Active }
                };
            }
    
            public ProcessState GetNext(Command command)
            {
                StateTransition transition = new StateTransition(CurrentState, command);
                ProcessState nextState;
                if (!transitions.TryGetValue(transition, out nextState))
                    throw new Exception("Invalid transition: " + CurrentState + " -> " + command);
                return nextState;
            }
    
            public ProcessState MoveNext(Command command)
            {
                CurrentState = GetNext(command);
                return CurrentState;
            }
        }
        
    
        public class Program
        {
            static void Main(string[] args)
            {
                Process p = new Process();
                Console.WriteLine("Current State = " + p.CurrentState);
                Console.WriteLine("Command.Begin: Current State = " + p.MoveNext(Command.Begin));
                Console.WriteLine("Command.Pause: Current State = " + p.MoveNext(Command.Pause));
                Console.WriteLine("Command.End: Current State = " + p.MoveNext(Command.End));
                Console.WriteLine("Command.Exit: Current State = " + p.MoveNext(Command.Exit));
                Console.ReadLine();
            }
        }
    }
    

    As a matter of personal preference, I like to design my state machines with a GetNext function to return the next state deterministically , and a MoveNext function to mutate the state machine.
    2のリファレンス
    C# のラベルメカニズムを いて, なステートマシンコードを した.Sidneys1/GFSM
    A Generic Finite State Machine in C# Implementation is easy:
     
     

    // Entirely unnecessary, just here to implement a common DoWork()
    public abstract class MyStateBase : IState {
    public virtual void Enter() {}
    public abstract void DoWork();
    public virtual void Leave() {}
    }

    // Define a state and it's transitions
    [Transition("next", typeof(EndState))]
    public class StartState : MyStateBase {
    public override void DoWork() => Console.WriteLine("In StartState");
    public override void Leave() => Console.WriteLine("\tLeaving StartState");
    }

    [Transition("next", null)]
    public class EndState : MyStateBase {
    public override void Enter() => Console.WriteLine("\tEntered EndState");
    public override void DoWork() => Console.WriteLine("In EndState");
    public override void Leave() => Console.WriteLine("\tLeaving EndState");
    }

    internal class Program {
    private static void Main() {
    var fsm = new FiniteStateMachine();

    fsm.Transitioning += transition => Console.WriteLine($"Beginning transition: {transition}");
    fsm.Transitioned += transition => {
      Console.WriteLine($"Done transitioning: {transition}");
      if (transition.To == null)
        Console.WriteLine("
    Exited"); }; Console.WriteLine("Started
    "); fsm.GetCurrentState().DoWork(); fsm.DoTransition("next"); fsm.GetCurrentState().DoWork(); fsm.DoTransition("next"); Console.ReadLine();

    } }/* Will print: Started
    In StartState Beginning transition: DemoApp.StartState + 'next' = DemoApp.EndState Leaving StartState Entered EndState Done transitioning: DemoApp.StartState + 'next' = DemoApp.EndState In EndState Beginning transition: DemoApp.EndState + 'next' = null Leaving EndState Done transitioning: DemoApp.EndState + 'next' = null
    Exited */
    3のリファレンス
    eteeselink/YieldMachine
    Some shameless self-promo here, but a while ago I created a library called YieldMachine which allows a limited-complexity state machine to be described in a very clean and simple way. For example, consider a lamp:
    state machine of a lamp
    Notice that this state machine has 2 triggers and 3 states. In YieldMachine code, we write a single method for all state-related behavior, in which we commit the horrible atrocity of using goto for each state. A trigger becomes a property or field of type Action , decorated with an attribute called Trigger . I've commented the code of the first state and its transitions below; the next states follow the same pattern.
    public class Lamp : StateMachine
    {
        // Triggers (or events, or actions, whatever) that our
        // state machine understands.
        [Trigger]
        public readonly Action PressSwitch;
    
        [Trigger]
        public readonly Action GotError;
    
        // Actual state machine logic
        protected override IEnumerable WalkStates()
        {
        off:                                       
            Console.WriteLine("off.");
            yield return null;
    
            if (Trigger == PressSwitch) goto on;
            InvalidTrigger();
    
        on:
            Console.WriteLine("*shiiine!*");
            yield return null;
    
            if (Trigger == GotError) goto error;
            if (Trigger == PressSwitch) goto off;
            InvalidTrigger();
    
        error:
            Console.WriteLine("-err-");
            yield return null;
    
            if (Trigger == PressSwitch) goto off;
            InvalidTrigger();
        }
    }
    

    Short and nice, eh!
    This state machine is controlled simply by sending triggers to it:
    var sm = new Lamp();
    sm.PressSwitch(); //go on
    sm.PressSwitch(); //go off
    
    sm.PressSwitch(); //go on
    sm.GotError();    //get error
    sm.PressSwitch(); //go off
    

    Just to clarify, I've added some comments to the first state to help you understand how to use this.
        protected override IEnumerable WalkStates()
        {
        off:                                       // Each goto label is a state
    
            Console.WriteLine("off.");             // State entry actions
    
            yield return null;                     // This means "Wait until a 
                                                   // trigger is called"
    
                                                   // Ah, we got triggered! 
                                                   //   perform state exit actions 
                                                   //   (none, in this case)
    
            if (Trigger == PressSwitch) goto on;   // Transitions go here: 
                                                   // depending on the trigger 
                                                   // that was called, go to
                                                   // the right state
    
            InvalidTrigger();                      // Throw exception on 
                                                   // invalid trigger
    
            ...
    

    This works because the C# compiler actually created a state machine internally for each method that uses yield return . This construct is usually used to lazily create sequences of data, but in this case we're not actually interested in the returned sequence (which is all nulls anyway), but in the state behaviour that gets created under the hood.
    The StateMachine base class does some reflection on construction to assign code to each [Trigger] action, which sets the Trigger member and moves the state machine forward.
    But you don't really need to understand the internals to be able to use it.
    4のリファレンス
    OmerMor/StateMachineToolkit A generic state-machine framework, with support for active/passive machines, exposed events and rich exception handling.
    ステータスEntry/Exit Methodsは、 EntryHandlerおよびExitHandlerによって され、ステータス およびActionMethodsは、AddTransitionによって される.
    4のリファレンス
    Real-Serious-Games/Fluent-State-Machine
    サブステートネストをサポートします. を め する はなく、 により に です.
    デザイン
    C#の を しているかどうかにかかわらず、 も なのは なる から を つけることである.
    ステートマシンの (Attributeなどの を しない):
  • の :2つの な のマッピング( 、2 など)
  • を する.
  • active/passive machinesをサポートしているかどうか( 4 )
  • Command/Eventによって をトリガする:すべての ロジックを にカプセル し、Commandによって をトリガする.
  • hierarchical state machine?ステータス サブステータスがサポートされているかどうか.たとえば、 が を た 、 が に され、 の の が してから の が される があります.この もステータスは「 」で、「 の を つ」というIDのみが になります.

  • Eric LippertはSOの に えた.
    If you want to write a state machine, just write a state machine. It's not hard. If you want to write a lot of state machines, write a library of useful helper methods that let you cleanly represent state machines, and then use your library. But don't abuse a language construct intended for something completely different that just happens to use state machines as an implementation detail. That makes your state machine code hard to read, understand, debug, maintain and extend. (And incidentally, I did a double-take when reading your name. One of the designers of C# is also named Matt Warren!) ステータスマシンが なときは、そのまま きます.ステータスマシンを く がある は、 でライブラリを できます.ステータスマシンは ではないので、あまり にこだわらないでください.
    の のインプリメンテーションでは、Command/Eventは されず、「 の の 」を することで を します.ここではいくつかの があります.たとえば、ステータス び し に のターゲットステータスが ある 、 CallerによってSetStatus を に び す があります. いい は、 にCommandで を えることです.Commandを み む は,この び しロジックをカプセル している.