用于在单个应用程序中创建不同有限状态机的简洁架构
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
的数据,您可以创建所有组件的工厂,例如 Scanners
、Printers
、PartsDb
然后 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"]
}
然后编写一个方法来遍历所有产品并添加此状态。
在单个应用程序中创建不同的、灵活的 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
的数据,您可以创建所有组件的工厂,例如 Scanners
、Printers
、PartsDb
然后 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"]
}
然后编写一个方法来遍历所有产品并添加此状态。