用于在单个应用程序中创建不同有限状态机的简洁架构

Clean Architecture For Creating Different Finite State Machines Within Single Application

在单个应用程序中创建不同的、灵活的 FSM 时寻找避免重复的方法。

下面我有一个概念,标题是0: BEFORE Requirements Change。这个概念展示了如何创建不同产品的 FSM,以及如何创建一个 FSM 运行。在任何给定时间,只有一个产品的 FSM 可以在 station/computer 上 运行,但一个站可以允许多个产品(在不同时间)。就上下文而言,这是一个制造环境,有许多产品要经过扫描过程。有些产品在他们的过程中有共同点,比如产品 A 和 B(为产品设置批次 -> 扫描一个零件 -> 应用业务逻辑 -> 重复多个零件直到批次完成,打印标签 -> 设置下一批次...... ).但是其他产品有不同的流程,比如产品 C。产品的流程也可以 require/include/exclude 不同的组件(不同的设备、数据库、业务逻辑);这都显示在 0: BEFORE Requirements Change.

现在,假设需求发生变化(过去发生过多次),需要一个新步骤 in-between 多个 产品的 FSM 的现有步骤(例如,需要触发相机并处理图像)。此外,这个额外的步骤可能只是一个试用阶段,需要禁用。我现在必须去更改每个 FSMCreator,如标题 1: AFTER Requirements Change 下所示。当有很多产品(很多超过 3 个)时,像这样的大流程更改已经 error-prone 并且难以管理。

是否有 better/cleaner 组织架构或创建 FSM 的方法,以避免这种重复?

问题源于不同的 FSM 如何共享 一些 共同步骤,或具有 一些 共同组件,但不是 100%相同的。本质上,组件(设备、数据库、业务逻辑)、状态和转换有许多不同的 mixing-and-matching 变体。最终,定义 FSM 的是产品的流程,因此每个产品都需要知道如何创建其 FSM。这就是为什么我为每个产品使用不同的 FSMCreator class 来处理每个产品的不同过程。但是如图所示,这会导致重复。

0:需求变更前

/* FSM definition */
public class FSM
{
   private Dictionary<IState, Dictionary<string, IState>> _transitions = new Dictionary<IState, Dictionary<string, IState>>();
   private IState _startState;
   private IState _currentState;

   public FSM(IState startState)
   {
      _startState = startState;
   }

   // Instead of State pattern, doing it this way to keep states decoupled, allow for different transitions when creating FSM
   public void Add(IState state, string event, IState nextState)
   {
      Dictionary<string, IState> transition = new Dictionary<string, IState>();
      transition.Add(event, nextState);

      _transitions.Add(state, transition);
   }

   // Using Observer-like pattern to notify FSM from an IState, so FSM knows which next state to transition to
   public void Notify(string event)
   {
      _currentState.Unsubscribe(this); // Unsubscribe from previous state (makes sure FSM is only listening to one state below)

      _currentState = _transitions[currentState][event]; // Move to next state

      _currentState.Subscribe(this); // Subscribe to next state

      _currentState.Run(); // Execute next state
   }

   public void Start()
   {
      _currentState = _startState;

      _currentState.Subscribe(this); // Subscribe to starting state, listening for state to call Notify()
      _currentState.Run();
   }
}

/* Interface definitions */
public interface IState
{
   void Run(); // Executes the logic within state
   void Subscribe(FSM fsm); // FSM listens for state's Notify() call
   void Unsubscribe(FSM fsm); // FSM stops listening for state's Notify() call
}

public interface IFSMCreator
{
   FSM CreateFSM(); // How FSM is created depends on different products' process
}

/* Definitions to create FSM for different products */

// Create FSM for Product A
public class FSMCreatorForProductA implements IFSMCreator
{
   public FSM CreateFSM()
   {
      /* Devices needed for Product A process */
      IScanner scanner = new Scanner_Brand1();
      IPrinter printer = new Printer_Brand1();
      
      /* Databases needed for Product A process */
      IPartsDatabase partsDB = new PartsDB_Oracle();
      IShipmentsDatabase inventoryDB = new InventoryDatabase_MySql();

      /* Business logic needed for Product A process */
      IParser parser = new Parser1ForProductA(); // a way to parse the scan
      IProductLogic productLogic = new ProductLogic1ForProductA(partsDB); // business logic to apply to scan for Product A
      IShipmentLogic batchCompleteLogic = new BatchCompleteLogic1(inventoryDB, printer); // general logic when batch is completed, uses inventory database and prints label

      /* Create the states of Product A's process, which use above components */
      IState stateSetup = new SetupState(partsDB);
      IState stateWaitScan = new WaitScanState(scanner);
      IState stateProcessScan = new ProcessScanState(parser, productLogic);
      IState stateCount = new CountState(partsDB); 
      IState stateComplete = new CompleteState(batchCompleteLogic);

      /* THIS is the actual FSM creation. Needed the above states to be defined first, which needed the components (devices, databases, business logic) defined. */
      FSM fsm = new FSM(stateSetup);
      fsm.Add(stateSetup, "OK", stateWaitScan); // sets up batch; if successful, waits for scan (there would be error state if not successful; omitted for brevity)
      fsm.Add(stateWaitScan, "SCAN", stateProcessScan); // when scan occurs, process scan data
      fsm.Add(stateProcessScan, "OK", stateCount); // if processing successful, update/check count within batch
      fsm.Add(stateCount, "CONTINUE", stateWaitScan); // if batch count not complete, wait for next part
      fsm.Add(stateCount, "COMPLETE", stateComplete); // if batch count complete, finalize batch activities
      fsm.Add(stateComplete, "OK", stateSetup); // if final activities successful, set up for next batch
   }
}

// Create FSM for Product B
public class FSMCreatorForProductB implements IFSMCreator
{
   public FSM CreateFSM()
   {
      IScanner scanner = new Scanner_Brand1();
      IPrinter printer = new Printer_Brand1();
      
      IPartsDatabase partsDB = new PartsDB_Oracle();
      IShipmentsDatabase inventoryDB = new InventoryDatabase_MySql();

      /* v DIFFERENT FROM PRODUCT A v */
      IParser parser = new Parser1ForProductB(); // scan has different content, needs to be parsed differently
      IProductLogic productLogic = new ProductLogic1ForProductB(partsDB, inventoryDB); // Scan data needs to be processed differently. Note how Product B's single part logic also uses inventoryDB, whereas Product A did not
      IShipmentLogic batchCompleteLogic = new BatchCompleteLogic2(printer); // Note how Product B's batch completion logic does not do anything with inventory database; only prints label
      /* ^ DIFFERENT FROM PRODUCT A ^ */

      IState stateSetup = new SetupState(partsDB);
      IState stateWaitScan = new WaitScanState(scanner);
      IState stateProcessScan = new ProcessScanState(parser, productLogic);
      IState stateCount = new CountState(partsDB); 
      IState stateComplete = new CompleteState(batchCompleteLogic) 

      /* THIS is the actual FSM creation (same as Product A). Needed the above states to be defined first, which needed the components (devices, databases, business logic) defined. */
      FSM fsm = new FSM(stateSetup);
      fsm.Add(stateSetup, "OK", stateWaitScan);
      fsm.Add(stateWaitScan, "SCAN", stateProcessScan);
      fsm.Add(stateProcessScan, "OK", stateCount);
      fsm.Add(stateCount, "CONTINUE", stateWaitScan);
      fsm.Add(stateCount, "COMPLETE", stateComplete);
      fsm.Add(stateComplete, "OK", stateSetup);
   }
}

// Create FSM for Product C
public class FSMCreatorForProductC implements IFSMCreator
{
   public FSM CreateFSM()
   {
      /* Product C's station has different scanner brand, different communication method */
      /* Product C's process also does not need a printer */
      IScanner scanner = new Scanner_Brand2(); 
      
      /* Product C uses different partsDB (in Access) */
      IPartsDatabase partsDB = new PartsDB_Access();

      /* Product C using same inventoryDB */
      IShipmentsDatabase inventoryDB = new InventoryDatabase_MySql();

      /* Product C's process has 2 scans instead of 1 */
      IParser parser1 = new Parser1ForProductC();
      IParser parser2 = new Parser2ForProductC();
      IProductLogic productLogic1 = new ProductLogic1ForProductC(partsDB);
      IProductLogic productLogic2 = new ProductLogic2ForProductC(partsDB);

      /* Product C's process has no setup, count, or batch complete states! */
      IState stateWaitScan1 = new WaitScanState(scanner);
      IState stateProcessScan1 = new ProcessScanState(parser1, productLogic1);
      IState stateWaitScan2 = new WaitScanState(scanner);
      IState stateProcessScan2 = new ProcessScanState(parser2, productLogic2)

      /* Product C has different FSM / transitions */
      FSM fsm = new FSM(stateWaitScan1);
      fsm.Add(stateWaitScan1, "SCAN", stateProcessScan1); // when scan of part's first barcode happens, processes scan data
      fsm.Add(stateProcessScan1, "OK", stateWaitScan2); // if processing successful, waits for second barcode scan
      fsm.Add(stateWaitScan2, "SCAN", stateProcessScan2); // when scan of part's second barcode happens, processes scan data
      fsm.Add(stateProcessScan2, "OK", stateWaitScan1); // if processing successful, waits for next/new part scan
   }
}

/* Running FSM */
public void Main()
{
   // GetFSMCreator chooses FSMCreatorForProductA, FSMCreatorForProductB, FSMCreatorForProductC, etc.
   // from user input/selection, or could be configuration file on the station, or some other way. 
   // The implementation of GetFSMCreator() is irrelevant for the question.
   FSM fsm = GetFSMCreator().CreateFSM(); 

   // After getting/creating the right FSM, start the process
   fsm.Start();
}

1:需求变更后

/* Definitions to create FSM for different products */

// Create FSM for Product A
public class FSMCreatorForProductA implements IFSMCreator
{
   public FSM CreateFSM()
   {
      IScanner scanner = new Scanner_Brand1();
      IPrinter printer = new Printer_Brand1();
      
      /* Need new device now */
      ICamera camera = new Camera_Brand1(); 
      camera.SetEnabled(GetCameraEnabledSetting()); // Enable/disable based on some setting (GetCameraEnabledSetting() returns true or false)

      IPartsDatabase partsDB = new PartsDB_Oracle();
      IShipmentsDatabase inventoryDB = new InventoryDatabase_MySql();

      IParser parser = new Parser1ForProductA();
      IProductLogic productLogic = new ProductLogic1ForProductA(partsDB);
      IShipmentLogic batchCompleteLogic = new BatchCompleteLogic1(inventoryDB, printer);

      /* Need logic to do something with image */
      IProcessor processor = new ImageProcessorForProductA(partsDB)

      IState stateSetup = new SetupState(partsDB);
      IState stateWaitScan = new WaitScanState(scanner);
      IState stateProcessScan = new ProcessScanState(parser, productLogic);
      IState stateComplete = new CompleteState(batchCompleteLogic) 

      /* Added states */
      IState stateTriggerCamera = new TriggerCameraState(camera);
      IState stateProcessImage = new ProcessImageState(processor);

      /* Transitions have changed as well */
      FSM fsm = new FSM(stateSetup);
      fsm.Add(stateSetup, "OK", stateWaitScan);
      fsm.Add(stateWaitScan, "SCAN", stateProcessScan);

      if (camera.IsEnabled())
      {
         fsm.Add(stateProcessScan, "OK", stateTriggerCamera);
         fsm.Add(stateTriggerCamera, "OK", stateProcessImage);
         fsm.Add(stateProcessImage, "OK", stateCount);
      }
      else
      {
         fsm.Add(stateProcessScan, "OK", stateCount);
      }
      fsm.Add(stateCount, "CONTINUE", stateWaitScan);
      fsm.Add(stateCount, "COMPLETE", stateComplete);
      fsm.Add(stateComplete, "OK", stateSetup);
   }
}

// Create FSM for Product B
public class FSMCreatorForProductB implements IFSMCreator
{
   public FSM CreateFSM()
   {
      IScanner scanner = new Scanner_Brand1();
      IPrinter printer = new Printer_Brand1();
      
      /* Need new device now */
      ICamera camera = new Camera_Brand1(); 
      camera.SetEnabled(GetCameraEnabledSetting()); // Enable/disable based on some setting (GetCameraEnabledSetting() returns true or false)

      IPartsDatabase partsDB = new PartsDB_Oracle();
      IShipmentsDatabase inventoryDB = new InventoryDatabase_MySql();

      IParser parser = new Parser1ForProductB();
      IProductLogic productLogic = new ProductLogic1ForProductB(partsDB, inventoryDB);
      IShipmentLogic batchCompleteLogic = new BatchCompleteLogic2(printer);

      /* Need logic to do something with image */
      IProcessor processor = new ImageProcessorForProductB(partsDB)

      IState stateSetup = new SetupState(partsDB);
      IState stateWaitScan = new WaitScanState(scanner);
      IState stateProcessScan = new ProcessScanState(parser, productLogic);
      IState stateComplete = new CompleteState(batchCompleteLogic) 

      /* Added states */
      IState stateTriggerCamera = new TriggerCameraState(camera);
      IState stateProcessImage = new ProcessImageState(processor);

      /* Transitions have changed as well */
      FSM fsm = new FSM(stateSetup);
      fsm.Add(stateSetup, "OK", stateWaitScan);
      fsm.Add(stateWaitScan, "SCAN", stateProcessScan);

      if (camera.IsEnabled())
      {
         fsm.Add(stateProcessScan, "OK", stateTriggerCamera);
         fsm.Add(stateTriggerCamera, "OK", stateProcessImage);
         fsm.Add(stateProcessImage, "OK", stateCount);
      }
      else
      {
         fsm.Add(stateProcessScan, "OK", stateCount);
      }
      fsm.Add(stateCount, "CONTINUE", stateWaitScan);
      fsm.Add(stateCount, "COMPLETE", stateComplete);
      fsm.Add(stateComplete, "OK", stateSetup);
   }
}

// Create FSM for Product C
public class FSMCreatorForProductC implements IFSMCreator
{
   public FSM CreateFSM()
   {
      IScanner scanner = new Scanner_Brand2(); 

      /* Need new device now */
      ICamera camera = new Camera_Brand1(); 
      camera.SetEnabled(GetCameraEnabledSetting()); // Enable/disable based on some setting (GetCameraEnabledSetting() returns true or false)

      IPartsDatabase partsDB = new PartsDB_Access();

      IShipmentsDatabase inventoryDB = new InventoryDatabase_MySql();

      IParser parser1 = new Parser1ForProductC();
      IParser parser1 = new Parser2ForProductC();
      IProductLogic productLogic1 = new ProductLogic1ForProductC(partsDB);
      IProductLogic productLogic2 = new ProductLogic2ForProductC(partsDB);

      /* Need logic to do something with image */
      IProcessor processor = new ImageProcessorForProductC(partsDB)

      IState stateWaitScan1 = new WaitScanState(scanner);
      IState stateProcessScan1 = new ProcessScanState(parser1, productLogic1);
      IState stateWaitScan2 = new WaitScanState(scanner);
      IState stateProcessScan2 = new ProcessScanState(parser2, productLogic2);

      /* Added states */
      IState stateTriggerCamera = new TriggerCameraState(camera);
      IState stateProcessImage = new ProcessImageState(processor);

      /* Transitions have changed as well */
      FSM fsm = new FSM(stateWaitScan1);
      fsm.Add(stateWaitScan1, "SCAN", stateProcessScan1);
      fsm.Add(stateProcessScan1, "OK", stateWaitScan2);
      fsm.Add(stateWaitScan2, "SCAN", stateProcessScan2);

      if (camera.IsEnabled())
      {
         fsm.Add(stateProcessScan2, "OK", stateTriggerCamera);
         fsm.Add(stateTriggerCamera, "OK", stateProcessImage);
         fsm.Add(stateProcessImage, "OK", stateWaitScan1);
      }
      else 
      {
         fsm.Add(stateProcessScan2, "OK", stateWaitScan1);
      }
   }
}

您必须始终编辑代码,因为您的要求总是在变化。如果您坚持使用这种方法,看起来您将始终需要更改代码。

所以我们发现您的工作流程总是在变化。我们的目标是对代码进行最少的更改。

我们能做什么?我们可以将您的工作流转移到存储中,并根据这些数据,我们可以 运行 您的 FSM。 This is how Jira workflow works.。他们有很多用户,很难按工作流程编辑代码,而且这是不可能的。他们如何解决他们的问题? Jira 像存储数据一样存储工作流,他们编辑数据,而不是代码。

这是一个粗略的例子,不是一个完整的解决方案,但是它将显示如何编写适合 open closed principle 的解决方案的方向。

因此,您可以将工作流程存储在 json 文件中:

static string products = @"{
        ""products"":
        [
            {
                ""key"": ""A"",
                ""components"":
                {
                    ""scanners"": [""scannerBrand_1"", ""scannerBrand_2""],
                    ""printers"": [""printerBrand_1"", ""printerBrand_2""],
                    ""partsDb"": [""partsDbBrand_1"", ""partsDbBrand_2""],
                    ""inventoryDb"": [""mySql_1""],

                    ""parser"": [""parserProduct_A""],
                    ""producLogic"": [
                        { ""key"": ""A"", ""partsDb"": 0}],
                    ""batchCompleteLogic"": [
                        {""key"": ""batchCompleteLogic_1"", 
                           ""parameters"": [""inventoryDb"", ""printers""]
                        }
                    ],
                    ""states"": [
                        { ""key"": ""setupState"", 
                           ""parameters"": [{""key"": ""partsDb"", ""value"": 0}]}
                    ]
                }
            }
        ]
    }";
    

并且可以创建映射 classes based on your json:

public class Product
{
    public string Key { get; set; }
    public Components Components { get; set; }
}

public class SomeStateMachine
{
    public List<Product> Products { get; set; }
}

public class ProducLogic
{
    public string Key { get; set; }
    public int PartsDb { get; set; }
}

public class BatchCompleteLogic
{
    public string Key { get; set; }
    public List<string> Parameters { get; set; }
}

public class Parameter
{
    public string Key { get; set; }
    public object Value { get; set; }
}

public class State
{
    public string Key { get; set; }
    public List<Parameter> Parameters { get; set; }
}

public class Components
{
    public List<string> Scanners { get; set; }
    public List<string> Printers { get; set; }
    public List<string> PartsDb { get; set; }
    public List<string> InventoryDb { get; set; }
    public List<string> Parser { get; set; }
    public List<ProducLogic> ProducLogic { get; set; }
    public List<BatchCompleteLogic> BatchCompleteLogic { get; set; }
    public List<State> States { get; set; }
}

然后去序列化你的数据:

SomeStateMachine someStateMachine = JsonConvert.DeserializeObject<SomeStateMachine>(products);

然后根据您 SomeStateMachine 的数据,您可以创建所有组件的工厂,例如 ScannersPrintersPartsDb 然后 States:

public class ScannerFactory
{
    Dictionary<string, Scanner> GetInstance = new()
    {
        { "scannerBrand_1", new Scanner_A() }
    };
}

public abstract class Scanner
{ }

public class Scanner_A : Scanner
{ }

然后在 FSM class 中,您将遍历 States 并将实例添加到 FSM:

public void Add() 
{
    foreach (State state in States)
    {
        // all your complicated logic of whether it should be added or not can 
        // be extracted in separated class. E.g. if `camera.IsEnabled()`
        // fsm.Add(...);
    }
}

编辑:

您可以在 json 文件中创建一个部分并将其命名为“common”:

"common": 
{
    "state": ["fooState"]
}

然后编写一个方法来遍历所有产品并添加此状态。