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



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

我在标题0: BEFORE Requirements Change下有一个概念。这个概念展示了如何创建不同产品的FSM,以及FSM如何运行。在任何给定时间,只有一个产品的FSM可以在工作站/计算机上运行,但一个工作站可以允许多个产品(在不同时间(运行。就上下文而言,这是一个制造环境,有许多产品需要经过扫描过程。一些产品的流程有共性,如产品A和B(为产品设置批次-扫描一个零件-应用业务逻辑-重复多个零件,直到批次完成,打印标签-设置下一个批次…(。但其他产品有不同的流程,如产品C。产品的流程还可以要求/包括/排除不同的组件(不同的设备、数据库、业务逻辑(;这都显示在0: BEFORE Requirements Change下。

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

是否有更好/更干净的方式来组织架构或创建FSM,从而避免这种重复

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

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。这就是Jira的工作流程。。他们有很多用户,很难按照工作流编辑代码,这是不可能的。他们是如何解决问题的?Jira存储类似工作流的数据,它们编辑数据,而不是代码。

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

因此,您可以将工作流存储在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}]}
]
}
}
]
}";

并且可以基于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类中,您将遍历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": 
{
"state": ["fooState"]
}

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

最新更新