65.9K
CodeProject 正在变化。 阅读更多。
Home

面向状态编程或“无 IF”编程

starIconstarIconstarIconstarIconstarIcon

5.00/5 (23投票s)

2021 年 12 月 24 日

CPOL

10分钟阅读

viewsIcon

30070

“无 If”编程,提高代码可测试性

引言

面向状态编程是一种将编程视为状态和行为而非过程流的思维方式。传统的编程,包括过程式、函数式和面向对象编程风格,代码中都会包含条件语句,通常以“if”语句的形式出现,可能还有case/switch语句,但它几乎肯定会包含某种逻辑来决定是否执行某个动作。条件代码的目的多种多样,但任何不包含条件语句的程序都会显得很奇怪。

然而,如果我们分析这些条件语句,它们实际表达的是“如果对象/代码处于此状态,则执行‘X’,否则执行‘Y’。”

面向状态编程试图(我稍后会谈到“试图”)通过将条件语句替换为函数调用来改变这种叙述方式,其中要调用的函数是在程序/对象状态改变时提前确定的。

因此,我们不再使用“If-Then-Else”子句(或类似的东西),而是使用“Execute_Function()”,其中执行的函数已更改以匹配对象状态。

例如,考虑模拟英国交通灯。

传统的 C# 代码块可能如下所示:

namespace TrafficLight
{
    public enum Switch { Off, On }
    public enum Signal { Stop = 0, ReadyGo, Go, ReadyStop };

    public class ClassicTrafficLight
    {
        private Signal currentSignal;
        public ClassicTrafficLight()
        {
            // Start with Red On.
            SwitchRed(Switch.On);
            currentSignal = Signal.Stop;
        }

        /*
         * Change Traffic Light from current state to next state
         */
        public void ChangeSignal(Signal currentSignal)
        {
            switch (currentSignal)
            {
                case Signal.Stop:
                    SwitchAmber(Switch.On);
                    currentSignal = Signal.ReadyGo;
                    break;
                case Signal.ReadyGo:
                    SwitchRed(Switch.Off);
                    SwitchAmber(Switch.Off);
                    SwitchGreen(Switch.On);
                    currentSignal = Signal.Go;
                    break;
                case Signal.Go:
                    SwitchGreen(Switch.Off);
                    SwitchAmber(Switch.On);
                    currentSignal = Signal.ReadyStop;
                    break;
                case Signal.ReadyStop:
                    SwitchAmber(Switch.Off);
                    SwitchRed(Switch.On);
                    currentSignal = Signal.Stop;
                    break;
                default:
                    throw new Exception("Unknown signal state");
            }
        }
    }
}

使用 SOP 方法,我们有:

using System;

namespace SOPTrafficLight
{
    public enum Switch { Off, On }
    public enum Signal { Stop = 0, ReadyGo, Go, ReadyStop };

    public class SOPTrafficLight
    {
        private Func <Signal>[] signalChanges;
        private Signal currentSignal;

        public SOPTrafficLight()
        {
            signalChanges =
                 new Func<Signal>[] { Stop, ReadyGo, Go, ReadyStop };
            currentSignal = Signal.Stop;
            SwitchRed(Switch.On);
        }

        public void ChangeSignal()
        {
            currentSignal = signalChanges[(int)currentSignal]();
        }

        private Signal Stop()
        {
            SwitchAmber(Switch.On);
            return Signal.ReadyGo;
        }

        private Signal ReadyGo()
        {
            SwitchRed(Switch.Off);
            SwitchAmber(Switch.Off);
            SwitchGreen(Switch.On);
            return Signal.Go;
        }

        private Signal Go()
        {
            SwitchGreen(Switch.Off);
            SwitchAmber(Switch.On);
            return Signal.ReadyStop;
        }

        private Signal ReadyStop()
        {
            SwitchAmber(Switch.Off);
            SwitchRed(Switch.On);
            return Signal.Stop;
        }
    }
}

首先要注意的是:检查 ChangeSignal 方法。使用 SOP,它是一个单行方法,并且整个代码不包含任何条件语句。没有“IF”语句,没有“Switch/Case”,根本没有任何条件语句!!!这多酷啊。

好吧——我知道你会说我所做的只是将代码封装在 **case** 子句中的函数中。在实际的函数代码行上,这(可能)是一个有效的观点。但你有没有想过可以在没有条件语句的情况下模拟一组交通灯?老实说,你没有,是吗?

我们所做的是将函数与状态绑定。交通灯的每一个状态变化都封装在一个状态变化函数中。它独立于所有其他可能的状态触发,最重要的一点是,将要执行的函数是在执行之前确定的

没有代码说“如果处于此状态,则执行此操作”。交通灯所处的状态决定了它将采取的行动,并且该行动是预先确定的。

现在交通灯场景的状态和状态转换数量有限。增加状态和转换的数量,我认为 SOP 方法会很快生成更容易维护、理解和最重要的是更容易测试的代码。

更复杂的状态图,S.O.P. 的更多理由

上面的交通灯是一个简单的顺序状态变化对象的例子。交通灯只执行 ChangeState 函数中的一行。我们指示对象所做的只是从当前状态移动到下一个状态。

然而,对象的状态很少如此简单。通常,一个对象会有一个状态矩阵。所以让我们来看一个稍微复杂一点的例子:一个计算器。

计算器

我之所以选择计算器是因为:

  1. 它是我们都熟悉的东西。
  2. 它是独立的。
  3. 我们可以从有限的功能开始,然后在此基础上扩展。

对于初始实现,我将功能限制为接受以下输入:

  • 数字:(数字 0-9)
  • 小数指示符:(.)
  • 二元运算符:(+,-,/,*)
  • 结果运算符(=)
  • 清空累加器(我知道——上图缺少“清空”按钮,我们假设它在侧面。)

即使输入集有限,我们仍然要处理许多状态问题:

  • 分数指示符(小数点)仅在某些情况下有效。
  • 分数指示符(小数点)决定下一个数字输入的有效值。
  • 二元运算符不能紧跟二元运算符。
  • 结果运算符不能紧跟二元运算符。
  • 二元运算符最早的执行时间是在第二个操作数输入完成后。
  • 完成计算后的数字输入意味着开始新的计算,而二元运算符输入意味着将当前结果作为第一个操作数。

计算器功能

计算器的初始条件是值 = 0。

如果在计算开始时按下的第一个键是运算符,则计算器值将用作第一个操作数的值。如果按下的第一个键是数字,则假定正在开始新的计算,并且任何现有值都将被丢弃。

计算器状态

如果我们将操作数的定义视为计算状态的主要焦点,而将非数字键(运算符、小数点、“=”和清除)视为状态转换触发器,可能还附带相关的转换动作,那么我们有九种可能的状态:

  1. 初始启动
  2. 定义第一个操作数的整数部分
  3. 定义第一个操作数的小数部分
  4. 定义第二个操作数的第一个整数部分
  5. 定义第二个操作数的后续整数部分。
  6. 定义第二个操作数的小数部分。
  7. 计算完成
  8. 重置(已清除)
  9. 出错

现在,虽然我们有九种可能的状态,但如果我们将初始状态视为结果为零的计算状态,并且重置和出错状态将我们带回初始状态,那么编号为 1、7、8 和 9 的状态实际上是相同的,这使得我们只有六种状态。(重命名为 State0 - State5 以匹配代码)

  • State0:计算开始
  • State1:定义第一个操作数的整数部分
  • State2:定义第一个操作数的小数部分
  • State3:定义第二个操作数的第一个整数部分
  • State4:定义第二个操作数的后续整数部分
  • State5:定义第二个操作数的小数部分

这六种状态具有以下可能的状态转换:

  • State0
    • 0-9 -> State1
    • 小数点 -> State1
    • 运算符 -> State3
    • 清除或 = -> State0
  • State1
    • 0-9 -> State1
    • 小数点 -> State2
    • 运算符 -> State3
    • 清除或 = -> State0
  • State2
    • 0-9 -> State2
    • 小数点 -> 错误,然后 State0
    • 运算符 -> State3
    • 清除或 = -> State0
  • State3
    • 0-9 -> State4
    • 小数点 -> State5
    • 运算符 -> 错误,然后 State0
    • 清除或 = -> State0
  • State4
    • 0-9 -> State4
    • 小数点 -> State5
    • 运算符 -> State3
    • 清除或 = -> State0
  • State5
    • 0-9 -> State5
    • 小数点 -> 错误,然后 State0
    • 运算符 -> State3
    • 清除或 = -> State0

计算器状态动作

我建议一个非常简单的实现,即对于六种状态中的每一种,我们为十七个可能的键中的每一个创建一个“动作和转换状态”字典。

  • 数字 (0 – 9)
  • 小数点
  • 运算符 (+ – * /)
  • 结果
  • Clear
            // Start of Calculation 
            state0 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(StartOfNewCalculation, States.State1) },
                { '1', new StateAction(StartOfNewCalculation, States.State1) },
                { '2', new StateAction(StartOfNewCalculation, States.State1) },
                { '3', new StateAction(StartOfNewCalculation, States.State1) },
                { '4', new StateAction(StartOfNewCalculation, States.State1) },
                { '5', new StateAction(StartOfNewCalculation, States.State1) },
                { '6', new StateAction(StartOfNewCalculation, States.State1) },
                { '7', new StateAction(StartOfNewCalculation, States.State1) },
                { '8', new StateAction(StartOfNewCalculation, States.State1) },
                { '9', new StateAction(StartOfNewCalculation, States.State1) },
                { '.', new StateAction(StartOfNewCalculation, States.State1) },
                { '+', new StateAction(Operator, States.State3) },
                { '-', new StateAction(Operator, States.State3) },
                { '*', new StateAction(Operator, States.State3) },
                { '/', new StateAction(Operator, States.State3) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining integral part of first operand State
            state1 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(FirstIntegralDigit, States.State1) },
                { '1', new StateAction(FirstIntegralDigit, States.State1) },
                { '2', new StateAction(FirstIntegralDigit, States.State1) },
                { '3', new StateAction(FirstIntegralDigit, States.State1) },
                { '4', new StateAction(FirstIntegralDigit, States.State1) },
                { '5', new StateAction(FirstIntegralDigit, States.State1) },
                { '6', new StateAction(FirstIntegralDigit, States.State1) },
                { '7', new StateAction(FirstIntegralDigit, States.State1) },
                { '8', new StateAction(FirstIntegralDigit, States.State1) },
                { '9', new StateAction(FirstIntegralDigit, States.State1) },
                { '.', new StateAction(DecimalPoint, States.State2) },
                { '+', new StateAction(Operator, States.State3) },
                { '-', new StateAction(Operator, States.State3) },
                { '*', new StateAction(Operator, States.State3) },
                { '/', new StateAction(Operator, States.State3) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining decimal part of first operand
            state2 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(FirstFractionalDigit, States.State2) },
                { '1', new StateAction(FirstFractionalDigit, States.State2) },
                { '2', new StateAction(FirstFractionalDigit, States.State2) },
                { '3', new StateAction(FirstFractionalDigit, States.State2) },
                { '4', new StateAction(FirstFractionalDigit, States.State2) },
                { '5', new StateAction(FirstFractionalDigit, States.State2) },
                { '6', new StateAction(FirstFractionalDigit, States.State2) },
                { '7', new StateAction(FirstFractionalDigit, States.State2) },
                { '8', new StateAction(FirstFractionalDigit, States.State2) },
                { '9', new StateAction(FirstFractionalDigit, States.State2) },
                { '.', new StateAction(DecimalPointNotAllowed, States.State0) },
                { '+', new StateAction(Operator, States.State3) },
                { '-', new StateAction(Operator, States.State3) },
                { '*', new StateAction(Operator, States.State3) },
                { '/', new StateAction(Operator, States.State3) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining first integral number of second operand
            state3 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(IntegralDigit, States.State4) },
                { '1', new StateAction(IntegralDigit, States.State4) },
                { '2', new StateAction(IntegralDigit, States.State4) },
                { '3', new StateAction(IntegralDigit, States.State4) },
                { '4', new StateAction(IntegralDigit, States.State4) },
                { '5', new StateAction(IntegralDigit, States.State4) },
                { '6', new StateAction(IntegralDigit, States.State4) },
                { '7', new StateAction(IntegralDigit, States.State4) },
                { '8', new StateAction(IntegralDigit, States.State4) },
                { '9', new StateAction(IntegralDigit, States.State4) },
                { '.', new StateAction(DecimalPoint, States.State5) },
                { '+', new StateAction(OperatorNotAllowed, States.State0) },
                { '-', new StateAction(OperatorNotAllowed, States.State0) },
                { '*', new StateAction(OperatorNotAllowed, States.State0) },
                { '/', new StateAction(OperatorNotAllowed, States.State0) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining integral part of second operand
            state4 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(IntegralDigit, States.State4) },
                { '1', new StateAction(IntegralDigit, States.State4) },
                { '2', new StateAction(IntegralDigit, States.State4) },
                { '3', new StateAction(IntegralDigit, States.State4) },
                { '4', new StateAction(IntegralDigit, States.State4) },
                { '5', new StateAction(IntegralDigit, States.State4) },
                { '6', new StateAction(IntegralDigit, States.State4) },
                { '7', new StateAction(IntegralDigit, States.State4) },
                { '8', new StateAction(IntegralDigit, States.State4) },
                { '9', new StateAction(IntegralDigit, States.State4) },
                { '.', new StateAction(DecimalPoint, States.State5) },
                { '+', new StateAction(CalcAndOperator, States.State3) },
                { '-', new StateAction(CalcAndOperator, States.State3) },
                { '*', new StateAction(CalcAndOperator, States.State3) },
                { '/', new StateAction(CalcAndOperator, States.State3) },
                { '=', new StateAction(CalcAndResult, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining decimal part of subsequent operand.
            state5 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(FractionalDigit, States.State5) },
                { '1', new StateAction(FractionalDigit, States.State5) },
                { '2', new StateAction(FractionalDigit, States.State5) },
                { '3', new StateAction(FractionalDigit, States.State5) },
                { '4', new StateAction(FractionalDigit, States.State5) },
                { '5', new StateAction(FractionalDigit, States.State5) },
                { '6', new StateAction(FractionalDigit, States.State5) },
                { '7', new StateAction(FractionalDigit, States.State5) },
                { '8', new StateAction(FractionalDigit, States.State5) },
                { '9', new StateAction(FractionalDigit, States.State5) },
                { '.', new StateAction(DecimalPointNotAllowed, States.State0) },
                { '+', new StateAction(CalcAndOperator, States.State3) },
                { '-', new StateAction(CalcAndOperator, States.State3) },
                { '*', new StateAction(CalcAndOperator, States.State3) },
                { '/', new StateAction(CalcAndOperator, States.State3) },
                { '=', new StateAction(CalcAndResult, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

为了能够拥有通用的运算符代码,并且能够为特定运算符执行特定功能,我们创建了一个运算符函数字典。

OperatorFunctions = new Dictionary<char, Func<double, double, double>> {
            { '+', PlusOperator},
            { '-', MinusOperator },
            { '*', MultiplicationOperator },
            { '/', DivisionOperator } };

加上转换函数——但它们是小型的自包含代码片段。

将所有内容整合在一起,包括执行运算符的函数和 StateAction 类,我们得到了一个功能齐全的 CalculatorEngine——没有任何条件语句。

using System;
using System.Collections.Generic;

namespace Calculator.Models
{
    public enum States { State0 = 0, State1, State2, State3, State4, State5 };

    public class CalculatorEngine
    {
        private char operatorInWaiting;

        private double currentOperand;
        private double fractionalDivisor;

        private double accumulator;

        private States state;
        private IDictionary<char, StateAction> state0;
        private IDictionary<char, StateAction> state1;
        private IDictionary<char, StateAction> state2;
        private IDictionary<char, StateAction> state3;
        private IDictionary<char, StateAction> state4;
        private IDictionary<char, StateAction> state5;

        private IDictionary<States, IDictionary<char, StateAction>> stateSet;

        private IDictionary<char, Func<double, double, double>> OperatorFunctions;

        public CalculatorEngine()
        {
            // Start of Calculation 
            state0 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(StartOfNewCalculation, States.State1) },
                { '1', new StateAction(StartOfNewCalculation, States.State1) },
                { '2', new StateAction(StartOfNewCalculation, States.State1) },
                { '3', new StateAction(StartOfNewCalculation, States.State1) },
                { '4', new StateAction(StartOfNewCalculation, States.State1) },
                { '5', new StateAction(StartOfNewCalculation, States.State1) },
                { '6', new StateAction(StartOfNewCalculation, States.State1) },
                { '7', new StateAction(StartOfNewCalculation, States.State1) },
                { '8', new StateAction(StartOfNewCalculation, States.State1) },
                { '9', new StateAction(StartOfNewCalculation, States.State1) },
                { '.', new StateAction(StartOfNewCalculation, States.State1) },
                { '+', new StateAction(Operator, States.State3) },
                { '-', new StateAction(Operator, States.State3) },
                { '*', new StateAction(Operator, States.State3) },
                { '/', new StateAction(Operator, States.State3) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining integral part of first operand State
            state1 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(FirstIntegralDigit, States.State1) },
                { '1', new StateAction(FirstIntegralDigit, States.State1) },
                { '2', new StateAction(FirstIntegralDigit, States.State1) },
                { '3', new StateAction(FirstIntegralDigit, States.State1) },
                { '4', new StateAction(FirstIntegralDigit, States.State1) },
                { '5', new StateAction(FirstIntegralDigit, States.State1) },
                { '6', new StateAction(FirstIntegralDigit, States.State1) },
                { '7', new StateAction(FirstIntegralDigit, States.State1) },
                { '8', new StateAction(FirstIntegralDigit, States.State1) },
                { '9', new StateAction(FirstIntegralDigit, States.State1) },
                { '.', new StateAction(DecimalPoint, States.State2) },
                { '+', new StateAction(Operator, States.State3) },
                { '-', new StateAction(Operator, States.State3) },
                { '*', new StateAction(Operator, States.State3) },
                { '/', new StateAction(Operator, States.State3) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining decimal part of first operand
            state2 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(FirstFractionalDigit, States.State2) },
                { '1', new StateAction(FirstFractionalDigit, States.State2) },
                { '2', new StateAction(FirstFractionalDigit, States.State2) },
                { '3', new StateAction(FirstFractionalDigit, States.State2) },
                { '4', new StateAction(FirstFractionalDigit, States.State2) },
                { '5', new StateAction(FirstFractionalDigit, States.State2) },
                { '6', new StateAction(FirstFractionalDigit, States.State2) },
                { '7', new StateAction(FirstFractionalDigit, States.State2) },
                { '8', new StateAction(FirstFractionalDigit, States.State2) },
                { '9', new StateAction(FirstFractionalDigit, States.State2) },
                { '.', new StateAction(DecimalPointNotAllowed, States.State0) },
                { '+', new StateAction(Operator, States.State3) },
                { '-', new StateAction(Operator, States.State3) },
                { '*', new StateAction(Operator, States.State3) },
                { '/', new StateAction(Operator, States.State3) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining first integral number of second operand
            state3 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(IntegralDigit, States.State4) },
                { '1', new StateAction(IntegralDigit, States.State4) },
                { '2', new StateAction(IntegralDigit, States.State4) },
                { '3', new StateAction(IntegralDigit, States.State4) },
                { '4', new StateAction(IntegralDigit, States.State4) },
                { '5', new StateAction(IntegralDigit, States.State4) },
                { '6', new StateAction(IntegralDigit, States.State4) },
                { '7', new StateAction(IntegralDigit, States.State4) },
                { '8', new StateAction(IntegralDigit, States.State4) },
                { '9', new StateAction(IntegralDigit, States.State4) },
                { '.', new StateAction(DecimalPoint, States.State5) },
                { '+', new StateAction(OperatorNotAllowed, States.State0) },
                { '-', new StateAction(OperatorNotAllowed, States.State0) },
                { '*', new StateAction(OperatorNotAllowed, States.State0) },
                { '/', new StateAction(OperatorNotAllowed, States.State0) },
                { '=', new StateAction(Result, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining integral part of second operand
            state4 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(IntegralDigit, States.State4) },
                { '1', new StateAction(IntegralDigit, States.State4) },
                { '2', new StateAction(IntegralDigit, States.State4) },
                { '3', new StateAction(IntegralDigit, States.State4) },
                { '4', new StateAction(IntegralDigit, States.State4) },
                { '5', new StateAction(IntegralDigit, States.State4) },
                { '6', new StateAction(IntegralDigit, States.State4) },
                { '7', new StateAction(IntegralDigit, States.State4) },
                { '8', new StateAction(IntegralDigit, States.State4) },
                { '9', new StateAction(IntegralDigit, States.State4) },
                { '.', new StateAction(DecimalPoint, States.State5) },
                { '+', new StateAction(CalcAndOperator, States.State3) },
                { '-', new StateAction(CalcAndOperator, States.State3) },
                { '*', new StateAction(CalcAndOperator, States.State3) },
                { '/', new StateAction(CalcAndOperator, States.State3) },
                { '=', new StateAction(CalcAndResult, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            // Defining decimal part of subsequent operand.
            state5 = new Dictionary<char, StateAction>
            {
                { '0', new StateAction(FractionalDigit, States.State5) },
                { '1', new StateAction(FractionalDigit, States.State5) },
                { '2', new StateAction(FractionalDigit, States.State5) },
                { '3', new StateAction(FractionalDigit, States.State5) },
                { '4', new StateAction(FractionalDigit, States.State5) },
                { '5', new StateAction(FractionalDigit, States.State5) },
                { '6', new StateAction(FractionalDigit, States.State5) },
                { '7', new StateAction(FractionalDigit, States.State5) },
                { '8', new StateAction(FractionalDigit, States.State5) },
                { '9', new StateAction(FractionalDigit, States.State5) },
                { '.', new StateAction(DecimalPointNotAllowed, States.State0) },
                { '+', new StateAction(CalcAndOperator, States.State3) },
                { '-', new StateAction(CalcAndOperator, States.State3) },
                { '*', new StateAction(CalcAndOperator, States.State3) },
                { '/', new StateAction(CalcAndOperator, States.State3) },
                { '=', new StateAction(CalcAndResult, States.State0) },
                { 'C', new StateAction(Clear, States.State0) }
            };

            OperatorFunctions = new Dictionary<char, Func<double, double, double>> {
            { '+', PlusOperator},
            { '-', MinusOperator },
            { '*', MultiplicationOperator },
            { '/', DivisionOperator } };

            stateSet = new Dictionary<States, IDictionary<char, StateAction>>()
            {
                {States.State0, state0 },
                {States.State1, state1 },
                {States.State2, state2 },
                {States.State3, state3 },
                {States.State4, state4 },
                {States.State5, state5 }
            };

            //
            ResetCalculator();
        }

        public double Calculate(char key)
        {
            stateSet[state][key].Action(key);
            state = stateSet[state][key].TransitionToState;
            return accumulator;
        }

        #region Key Functions
        private void StartOfNewCalculation(char key)
        {
            accumulator = 0.0;
            state = stateSet[state][key].TransitionToState;
            stateSet[state][key].Action(key);
        }

        private void FirstIntegralDigit(char key)
        {
            accumulator = accumulator * 10.0 + Char.GetNumericValue(key);
        }

        private void DecimalPoint(char key)
        {
            // No action required. Just a change of state
        }

        private void FirstFractionalDigit(char key)
        {
            fractionalDivisor *= 10.0;
            accumulator = accumulator + (Char.GetNumericValue(key) / fractionalDivisor);
        }

        private void Operator(char key)
        {
            ResetNumerics();
            operatorInWaiting = key;
        }

        private void IntegralDigit(char key)
        {

            currentOperand = currentOperand * 10.0 + Char.GetNumericValue(key);
        }

        private void FractionalDigit(char key)
        {
            fractionalDivisor *= 10.0;
            currentOperand = currentOperand + (Char.GetNumericValue(key) / fractionalDivisor);
        }

        private void CalcAndOperator(char key)
        {
            ExecuteStackedOperator();
            Operator(key);
        }

        private void DecimalPointNotAllowed(char key)
        {
            ResetCalculator();
            throw new Exception("Error: Decimal Point not valid");
        }

        private void OperatorNotAllowed(char key)
        {
            ResetCalculator();
            throw new Exception("Error: Operator not valid");
        }

        private void Result(<span class="token keyword">char</span> key)
        {
            ResetNumerics();
        }

        private void CalcAndResult(<span class="token keyword">char</span> key)
        {
            ExecuteStackedOperator();
            ResetNumerics();
        }

        private void Clear(<span class="token keyword">char</span> key)
        {
            ResetCalculator();
        }

        #endregion Key Functions
        #region Operator Functions
        private double PlusOperator(<double operand1, double operand2)
        {
            return operand1 + operand2;
        }
        private double MinusOperator(double operand1, double operand2)
        {
            return operand1 - operand2; ;
        }
        private double MultiplicationOperator(double operand1, double operand2)
        {
            return operand1 * operand2;
        }
        private double DivisionOperator(double operand1, double operand2)
        {
            return operand1 / operand2;
        }
        #endregion Operator Functions

        #region State transition and Operator execution
        private void TransitionState(States requiredState)
        {
            state = requiredState;
        }

        private void ExecuteStackedOperator()
        {
            accumulator = OperatorFunctions[operatorInWaiting](accumulator, currentOperand);
        }
        #endregion State transition and Operator execution

        private void ResetCalculator()
        {
            state = 0;
            accumulator = 0.0;
            ResetNumerics();
            TransitionState(States.State0);
        }

        private void ResetNumerics()
        {
            currentOperand = 0.0;
            fractionalDivisor = 1.0;
        }
    }
}
using System;

namespace Calculator.Models
{
    public class StateAction
    {
        public StateAction(Action<char> action, States transitionToState)
        {
            Action = action;
            TransitionToState = transitionToState;
        }
        public States TransitionToState { get; private set; }

        public Action<Char> Action { get; private set; }
    }
}

我已将 Calculator 的完整 C#/WPF 实现整合在一起。该实现中没有“if”语句或 switch 语句。我承认有两条 null 合并语句,可以认为是条件语句。不幸的是,C# 事件功能(顺便说一句,我认为它很出色)没有提供一种机制来知道事件何时被订阅/取消订阅。因此,在触发事件的委托之前,您确实必须测试它是否为 null。(即,是否未被订阅)。

完整代码可从 Github 下载:https://github.com/SteveD430/Calculator

优点与缺点

使用条件语句的优点

  • 它们与我们的思维过程相符。
  • 如果它们不太复杂,那么很容易理解正在测试什么以及如果测试为 true 将执行什么操作。
  • 如果测试的是局部条件,那么很容易确定条件将如何触发。

使用条件语句的缺点

使用条件语句的缺点实际上归结为最后两个优点所附带的警告——如果测试不太复杂,并且如果测试的是局部条件。条件语句的问题在于,被测试的条件可能与局部代码无关。如果被测试的是在代码的另一个部分设置的条件,可能与检查条件的代码完全无关,那么几乎不可能理解程序为何处于当前状态。这意味着,当出现问题时,要弄清楚它为什么会出错可能极其困难。您可能会发现自己处于那种经典的情况,唯一的可用信息是“计算机说不”。

我确信以下情况我们都曾经历过:在调试器中反复单步执行相同的代码,使用相同的起始条件,设置复杂的监视语句,徒劳地试图追踪导致被测试对象或对象处于引发问题的状态的事件链。当最终理解问题时(如果能理解的话),它很可能过于复杂而无法解决,或者在事件开始时解决它风险太大,或者它甚至可能是一个以前未曾考虑过的有效场景。无论原因是什么,解决方案通常是“再添加一个‘if’语句来捕获条件,从而解决此问题实例”。添加另一个“if”语句会使代码更加复杂,更难维护,将来更容易引起问题,并且很少是正确的解决方案。

面向状态编程试图颠覆“如果这样,那么那样”的过程。SOP 不再测试一个或多个对象的状态,然后有条件地执行某些行为,而是在一个或多个对象进入所需状态时设置所需的行为。然后,该行为可以在状态转换点(事件驱动范式)执行,或者注入到本应有“if”语句的代码中(过程式范式)。

SOP 的优点

  • 它让你在编码前就思考状态和状态转换。
  • 它通过删除“if”和其他条件语句来降低代码复杂性。
  • 它将行为变化与对象状态变化关联起来。状态变化和行为变化在代码层面紧密结合。
  • 它极大地提高了进行 TDD 的能力,因为你在编码之前就知道所有可能的状态以及如何触发它们。此外,代码测试更容易,因为更容易确保所有代码路径都已测试。

SOP 的缺点

  • 并非所有应用程序都适合 SOP 方法。例如,一个对象可能具有非常大甚至无限数量的状态(当然,在 PC 上能达到的接近无限)——例如,如果一个对象的双精度属性达到某个值时其状态会改变。通常,事件驱动程序(如基于 UI 的应用程序)非常适合 SOP 方法。算法和数据挖掘应用程序则不那么适合。
  • 语言并不总是有助于轻松实现 SOP 的构造。上面的 Calculator 示例大量使用了函数指针、字典、映射、事件和委托。如果没有这些特性,很难使用 SOP 方法。
  • 难以将概念注入现有代码中。

历史

  • 2021 年 12 月 24 日:初始版本
  • 2022 年 1 月 15 日:文章更新
© . All rights reserved.