状态机 - 无状态与传统的 if-else 代码,难以掌握好处
State-machine - Stateless vs. traditional if-else code, hard to grasp the benefit
我最近遇到了脏的 if-else 代码,所以我寻找了一个重构选项,并在 state-machine
上找到了推荐作为脏 if-else
代码的优雅替代品。
但有些事情我很难理解:作为客户,我似乎有责任将机器从一种状态移动到另一种状态。现在,如果有 2 个转换选项(取决于在当前状态下完成的工作的结果),我还需要使用 if-else 吗?如果是这样,该模式的主要好处是什么?从我的角度来看,机器可能会从起始状态自动进行转换
在提问之前我已经阅读了以下内容,它只会加强我的观点:
Auto advancing state machine with Stateless
How to encapsulate .NET Stateless state machine
在我的示例中,我有一个 MarketPriceEvent
需要存储在 Redis 中。在存储之前,它必须通过验证路径。验证路径状态为:
- 基本验证
- 比较
- 另一个比较
- 存储
- 审核错误
问题是我有很多决定要做。例如:仅当 BasicValidation
成功通过时我才想移动到 Comparison
。现在,如果 Comparison
成功,我想移动到 Storing
,否则移动到 ErrorAuditing
。
所以如果我们进入代码:
_machine.Configure(State.Validate).PermitIf(Trigger.Validated, State.Compare1, () => isValid);
_machine.Configure(State.Compare1).OnEntry(CompareWithResource1).
PermitIf(Trigger.Compared, State.Store, () => isValid)
.PermitIf(Trigger.Compared, State.Compare2, () => !isValid);
在我的 client/wrapper 代码中我会写:
//Stay at Validate state
var marketPriceProcessingMachine = new MarketPriceProcessingMachine();
if (marketPriceProcessingMachine.Permitted(Trigger.Validated))
marketPriceProcessingMachine.Fire(Trigger.Validated);
//else
// ...
简而言之,如果我需要使用if-else
,我从这种状态机概念中得到了什么好处?如果它是确定性的,为什么它不自动移动到下一个状态?如果我错了,那有什么问题?
使用状态机的一个好处是可以减少对象可以处于的状态数。我曾与一个人一起工作,他在一个 class 中有 22 个 bool 标志。有很多 if !(something && !somethingElse || !userClicked) …
这种代码难以阅读、难以调试、难以进行单元测试,而且几乎不可能推断出 class 的真实状态。 22 个布尔标志意味着 class 可以处于超过 400 万个状态。尝试为此进行单元测试...
状态机可以降低代码的复杂性,但在新项目开始时几乎总会使代码变得更加复杂。然而,从长远来看,我发现整体复杂性最终会降低。这是因为它很容易扩展和添加更多状态,因为已经定义的状态可以保持不变。
多年来我发现 OOP 和状态机通常是相同的两个方面。而且我还发现 OOP 很难,而且很难获得 'right'.
我认为状态机不应该对对象的外部可见,包括它的触发器。您很可能想要 public 只读状态 属性.
我把classes设计成调用者不能直接改变状态,或者让调用者直接调用Fire方法。相反,我使用的方法是作为动作的动词,例如 Validate()。
您的工作流程需要条件语句,但您可以自由选择将它们放在哪里。我建议将业务逻辑与状态机配置分开。我认为这使状态机更易于阅读。
这样的事情怎么样:
namespace ConsoleApp1
{
using Stateless;
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press Q to stop validating events");
ConsoleKeyInfo c;
do
{
var mpe = new MarketPriceEvent();
mpe.Validate();
c = Console.ReadKey();
} while (c.Key != ConsoleKey.Q);
}
}
public class MarketPriceEvent
{
public void Validate()
{
_machine.Fire(Trigger.Validate);
}
public enum State { Validate, Compare2, ErrorAuditing, Compare1, Storing }
private enum Trigger { Validate, CompareOneOk, CompareTwoOk, Error, }
private readonly StateMachine<State, Trigger> _machine;
public MarketPriceEvent()
{
_machine = new StateMachine<State, Trigger>(State.Validate);
_machine.Configure(State.Validate)
.Permit(Trigger.Validate, State.Compare1);
_machine.Configure(State.Compare1)
.OnEntry(DoEventValidation)
.Permit(Trigger.CompareOneOk, State.Compare2)
.Permit(Trigger.Error, State.ErrorAuditing);
_machine.Configure(State.Compare2)
.OnEntry(DoEventValidationAgainstResource2)
.Permit(Trigger.CompareTwoOk, State.Storing)
.Permit(Trigger.Error, State.ErrorAuditing);
_machine.Configure(State.Storing)
.OnEntry(HandleStoring);
_machine.Configure(State.ErrorAuditing)
.OnEntry(HandleError);
}
private void DoEventValidation()
{
// Business logic goes here
if (isValid())
_machine.Fire(Trigger.CompareOneOk);
else
_machine.Fire(Trigger.Error);
}
private void DoEventValidationAgainstResource2()
{
// Business logic goes here
if (isValid())
_machine.Fire(Trigger.CompareTwoOk);
else
_machine.Fire(Trigger.Error);
}
private bool isValid()
{
// Returns false every five seconds...
return (DateTime.UtcNow.Second % 5) != 0;
}
private void HandleStoring()
{
Console.WriteLine("Awesome, validation OK!");
}
private void HandleError()
{
Console.WriteLine("Oh noes, validation failed!");
}
}
}
我最近遇到了脏的 if-else 代码,所以我寻找了一个重构选项,并在 state-machine
上找到了推荐作为脏 if-else
代码的优雅替代品。
但有些事情我很难理解:作为客户,我似乎有责任将机器从一种状态移动到另一种状态。现在,如果有 2 个转换选项(取决于在当前状态下完成的工作的结果),我还需要使用 if-else 吗?如果是这样,该模式的主要好处是什么?从我的角度来看,机器可能会从起始状态自动进行转换
在提问之前我已经阅读了以下内容,它只会加强我的观点:
Auto advancing state machine with Stateless
How to encapsulate .NET Stateless state machine
在我的示例中,我有一个 MarketPriceEvent
需要存储在 Redis 中。在存储之前,它必须通过验证路径。验证路径状态为:
- 基本验证
- 比较
- 另一个比较
- 存储
- 审核错误
问题是我有很多决定要做。例如:仅当 BasicValidation
成功通过时我才想移动到 Comparison
。现在,如果 Comparison
成功,我想移动到 Storing
,否则移动到 ErrorAuditing
。
所以如果我们进入代码:
_machine.Configure(State.Validate).PermitIf(Trigger.Validated, State.Compare1, () => isValid);
_machine.Configure(State.Compare1).OnEntry(CompareWithResource1).
PermitIf(Trigger.Compared, State.Store, () => isValid)
.PermitIf(Trigger.Compared, State.Compare2, () => !isValid);
在我的 client/wrapper 代码中我会写:
//Stay at Validate state
var marketPriceProcessingMachine = new MarketPriceProcessingMachine();
if (marketPriceProcessingMachine.Permitted(Trigger.Validated))
marketPriceProcessingMachine.Fire(Trigger.Validated);
//else
// ...
简而言之,如果我需要使用if-else
,我从这种状态机概念中得到了什么好处?如果它是确定性的,为什么它不自动移动到下一个状态?如果我错了,那有什么问题?
使用状态机的一个好处是可以减少对象可以处于的状态数。我曾与一个人一起工作,他在一个 class 中有 22 个 bool 标志。有很多 if !(something && !somethingElse || !userClicked) …
这种代码难以阅读、难以调试、难以进行单元测试,而且几乎不可能推断出 class 的真实状态。 22 个布尔标志意味着 class 可以处于超过 400 万个状态。尝试为此进行单元测试...
状态机可以降低代码的复杂性,但在新项目开始时几乎总会使代码变得更加复杂。然而,从长远来看,我发现整体复杂性最终会降低。这是因为它很容易扩展和添加更多状态,因为已经定义的状态可以保持不变。
多年来我发现 OOP 和状态机通常是相同的两个方面。而且我还发现 OOP 很难,而且很难获得 'right'.
我认为状态机不应该对对象的外部可见,包括它的触发器。您很可能想要 public 只读状态 属性.
我把classes设计成调用者不能直接改变状态,或者让调用者直接调用Fire方法。相反,我使用的方法是作为动作的动词,例如 Validate()。
您的工作流程需要条件语句,但您可以自由选择将它们放在哪里。我建议将业务逻辑与状态机配置分开。我认为这使状态机更易于阅读。
这样的事情怎么样:
namespace ConsoleApp1
{
using Stateless;
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press Q to stop validating events");
ConsoleKeyInfo c;
do
{
var mpe = new MarketPriceEvent();
mpe.Validate();
c = Console.ReadKey();
} while (c.Key != ConsoleKey.Q);
}
}
public class MarketPriceEvent
{
public void Validate()
{
_machine.Fire(Trigger.Validate);
}
public enum State { Validate, Compare2, ErrorAuditing, Compare1, Storing }
private enum Trigger { Validate, CompareOneOk, CompareTwoOk, Error, }
private readonly StateMachine<State, Trigger> _machine;
public MarketPriceEvent()
{
_machine = new StateMachine<State, Trigger>(State.Validate);
_machine.Configure(State.Validate)
.Permit(Trigger.Validate, State.Compare1);
_machine.Configure(State.Compare1)
.OnEntry(DoEventValidation)
.Permit(Trigger.CompareOneOk, State.Compare2)
.Permit(Trigger.Error, State.ErrorAuditing);
_machine.Configure(State.Compare2)
.OnEntry(DoEventValidationAgainstResource2)
.Permit(Trigger.CompareTwoOk, State.Storing)
.Permit(Trigger.Error, State.ErrorAuditing);
_machine.Configure(State.Storing)
.OnEntry(HandleStoring);
_machine.Configure(State.ErrorAuditing)
.OnEntry(HandleError);
}
private void DoEventValidation()
{
// Business logic goes here
if (isValid())
_machine.Fire(Trigger.CompareOneOk);
else
_machine.Fire(Trigger.Error);
}
private void DoEventValidationAgainstResource2()
{
// Business logic goes here
if (isValid())
_machine.Fire(Trigger.CompareTwoOk);
else
_machine.Fire(Trigger.Error);
}
private bool isValid()
{
// Returns false every five seconds...
return (DateTime.UtcNow.Second % 5) != 0;
}
private void HandleStoring()
{
Console.WriteLine("Awesome, validation OK!");
}
private void HandleError()
{
Console.WriteLine("Oh noes, validation failed!");
}
}
}