Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

State Oriented Programming or 'IF' Free Programming

Rate me:
Please Sign up or sign in to vote.
5.00/5 (23 votes)
15 Jan 2022CPOL10 min read 23.9K   25   35
'If' free programming that improves testability of code
In this article, you will learn about a way of thinking about programming that reduces the number of conditional statements in your code and improves testability.

Introduction

State Oriented Programming is a way of thinking about programming not as process flow but as state and behaviour. With traditional programming, and by traditional, I include procedural, functional and OO programming styles, the code will contain conditional statements, usually in the form of ‘if’ statements, with possibly, case/switch statements, but it will almost certainly contain some form of logic to decide whether an action should be performed or not. The purpose of the conditional code will be varied, but it would be a strange program that did not contain any conditional statements.

However, if we analyze these Conditional Statements, what they are actually saying is ‘If object/code is in this state, then do ‘X’ if not then do ‘Y’.

State Oriented Programming tries (and I will come on to the ‘tries’ in a moment) to change that narrative by replacing the conditional statements with a call to a function, where the function that will be called is determined ahead of time, whenever the state of the program/object changes.

So, instead of the ‘If-Then-Else’ clause (or something similar), we have ‘Execute_Function()’, where the function being executed has changed to match the object state.

As an example, consider modelling UK traffic lights:

A traditional C# code block might look like the following:

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");
            }
        }
    }
}

Using an SOP approach, we have:

C#
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;
        }
    }
}

First thing to notice: Examine the ChangeSignal method. With S.O.P., it is a one line method, and the whole code contains no conditional statements. No ‘IF‘s, no ‘Switch/Case‘, no conditional statements at all!!! How cool is that.

OK – so I hear you say that all I have done is encapsulate the code in the case clauses within functions. On the actual lines of functioning code, that is (possibly) a valid viewpoint. But did you even think you could model a set of traffic lights without conditional statements. Be honest, you didn’t, did you?

What we have done is to tie functions to state. Every state change of the traffic light is encapsulated in a state change function. It is triggered in isolation from every other possible state, and, the most important point, the function that will be executed next is determined ahead of it being executed.

There is no code that is saying “if in this state, then do this”. The state that the traffic light is in dictates the action it will take, and that action is pre-determined.

Now the traffic light scenario has a limited number of states and state transitions. Increase the numbers of states and transitions, I would argue that an S.O.P. approach very quickly produces code that is easier to maintain, understand, and, crucially, Test.

More Complex State Maps, More reason for S.O.P.

The traffic light above is an example of a simple sequential state changing object. The traffic light only ever executes the one line in the ChangeState function. All we instruct the object to do is move from its current state to the next state.

However, objects’ states are rarely this simple, More often than not, an object will have a matrix of states. So let's examine a slightly more complex example: A Calculator.

Calculator

I have chosen a calculator because:

  1. It is something familiar to us all.
  2. It is self contained.
  3. We can start with a limited functionality and then expand on that.

For the initial implementation, I am restricting the functionality to accepting the following inputs:

  • Numerics: (Numbers 0-9)
  • Fraction indicator: (.)
  • Binary operators: (+, -, /, *)
  • Result Operator (=)
  • Clear accumulator (I know – the above image is missing a ‘Clear’ button, let's assume it is on the side.)

Even with this limited set of inputs, we already have a number of state issues to contend with:

  • The Fraction indicator (decimal point) is only valid in certain circumstances.
  • The Fraction indicator (decimal point) determines the effective value of the next numeric input.
  • A Binary Operator cannot follow a Binary Operator.
  • The Result operator cannot follow a Binary Operator.
  • The earliest the execution of Binary Operator can take place is when input of the second operand has been completed.
  • A Numeric input following a completed calculation means start a new calculation, whilst a Binary Operator input means take the current result as being the first operand.

Calculator Functionality

The Calculator will have as its initial condition a value = 0.

If the first key pressed at the start of a calculation is an Operator, then the Calculator value will be used as the value of the first Operand. If the first key pressed is Numeric, then it will be assumed that a new calculation is being started and any existing value will be discarded.

Calculator States

If we consider the defining of the operands as being the main focus of the calculation states, and the non numeric keys – Operators, Decimal Point, ‘=’ and Clear as state transition triggers, possibly with associated transition actions, then we have nine possible states:

  1. Initial Start up
  2. Defining integral numeric of first operand
  3. Defining decimal part of first operand
  4. Defining first integral numeric of second operand
  5. Defining subsequent integral numeric of second operand.
  6. Defining decimal part of second operand.
  7. Calculation Completed
  8. Reset (Cleared)
  9. Errored

Now whilst we have nine possible states, if we consider the initial state equal to a calculated state with a result of zero, and the Reset and Errored states taking us back to the Initial state, then the states numbered 1, 7, 8, and 9 are in effect identical, and that brings us down to six states. (Renamed State0 - State5 to match code)

  • State0: Start of Calculation
  • State1: Defining Integral part of first operand
  • State2: Defining Decimal part of first operand
  • State3: Defining First Integral of second operand
  • State4: Defining Subsequent Integral part of second operand
  • State5: Defining Decimal part of second operand

The six states have the following possible state transitions:

  • State0
    • 0-9 -> State1
    • Dec pt -> State1
    • Operator -> State3
    • Clear or = -> State0
  • State1
    • 0-9 -> State1
    • Dec pt -> State2
    • Operator -> State3
    • Clear or = -> State0
  • State2
    • 0-9 -> State2
    • Dec Pt -> Error, then State0
    • Operator -> State3
    • Clear or = -> State0
  • State3
    • 0-9 -> State4
    • Dec Pt -> State5
    • Operator -> Error, then State0
    • Clear or = -> State0
  • State4
    • 0-9 -> State4
    • Dec pt -> State5
    • Operator -> State3
    • Clear or = -> State0
  • State5
    • 0-9 -> State5
    • Dec Pt -> Error, then State0
    • Operator -> State3
    • Clear or = -> State0

Calculator State Actions

I am proposing a very simplistic implementation in which for each of the six states, we create a dictionary of Action and TransitionState for each of the seventeen possible keys:

  • Numeric (0 – 9)
  • Decimal point
  • Operators (+ – * /)
  • Result
  • Clear
C#
// 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) }
};

To enable us to have common operator code, and yet execute specific functions for specific operators, we create a dictionary of Operator Functions.

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

Plus the transition functions – but they are small self-contained code snippets.

Putting the whole thing together, including the functions to execute the operators and the StateAction class, we have the following as a fully functioning CalculatorEngine – with NO conditionals.

C#
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;
        }
    }
}
C#
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; }
    }
}

I have put together a complete C#/WPF implementation of the Calculator. There are no ‘if’ statements or switch statements in the implementation. I will admit that there are two null coalescing statements, which can be argued are conditionals. Unfortunately, the C# Event functionality (which btw, I think is brilliant) does not provide a mechanism to know when an Event has been subscribed to/ unsubscribed from. And so before triggering the Event’s delegates, you do have to test if it is null. (i.e., whether or not it has not been subscribed to).

The complete code is available to download from Github at https://github.com/SteveD430/Calculator.

Pros & Cons

Pros to Using Conditional Statements

  • They match our thought processes.
  • If they are not too complex, then it is easy to understand what is being tested and what action will be executed if the test is true.
  • If what is being tested is local, then it is easy to determine how the condition will be triggered.

Cons to Using Conditional Statements

The cons to using conditional statements are really summed up in the caveats attached to the last two pros – If the tests are not too complex and if what is being tested is local. The problem with conditionals is that the condition being tested may not be related to the local code. If what is being tested are conditions set-up in another section of code, possibly completely unrelated with code checking the condition, then it can be nigh impossible to understand why the program is in the state that it is. Which means, when something goes wrong, it can be extremely difficult to work out why it went wrong. You can find yourself in that classic situation where the only information is “The Computer – It say No”.

I am sure the following is a scenario we all recognize: Repeatedly, in the debugger, stepping through the same code, using same start conditions, setting up complex watch statements in a vain attempt to track down the chain of events that lead to the object or objects under test being set to the state that caused the problem. And when the problem is finally understood, assuming it ever is, it could well be that it is too complex to resolve, or resolving it at the start of the events too risky, or it may even be a valid scenario that had not previously been considered. Whatever the reason, the solution, all to often, is to “Add in another ‘if’ statement to trap the condition, thereby solving this instance of the problem“. Adding another ‘if’ statement makes the code more complex, less maintainable, more likely to cause problems in the future, and is rarely the correct solution.

State Oriented Programming attempts to reverse the ‘if this then that’ processes. Instead of testing the state of an object (or objects) and then conditionally executing some behaviour, SOP sets the behaviour that is needed at the point where the object or objects move(s) into the required state, The behaviour can then be executed either at the point of state transition (event driven paradigm) or injected into the code where the ‘if’ statement would have been (procedural paradigm).

Pros to SOP

  • It makes you think about state and state transitions before coding.
  • It reduces code complexity by removing ‘if’ and other conditional statements.
  • It associates the behaviour change with the object state change. The state change and behaviour change are tied together code wise.
  • It dramatically improves the ability to undertake TDD, because you know all possible states and how to trigger them before any coding has taken place. Plus the testing of the code is easier, because it is easier to ensure all code paths are tested.

Cons to SOP

  • Not all applications lend themselves to the SOP approach. For instance, it is possible for an object to have a very large or even an infinite number of states (well as near infinite as you can get with a PC) – for example, if an objects state changes when a double property hits a certain value. Generally, Event driven programs (such as UI based apps) lend themselves nicely to the SOP approach. Algorithmic and Data mining apps far less so.
  • Languages do not always have the constructs that make implementing SOP easy. The Calculator example above makes extensive use of pointers to functions, dictionaries, maps, events and delegates. It would be difficult to use an SOP approach without these features.
  • Difficult to inject the concept into existing code.

History

  • 24th December, 2021: Initial version
  • 15th January, 2022: Article updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
kentgorrell20-Feb-22 12:27
professionalkentgorrell20-Feb-22 12:27 
GeneralMy vote of 5 Pin
Prasad Khandekar9-Feb-22 23:15
professionalPrasad Khandekar9-Feb-22 23:15 
GeneralRe: My vote of 5 Pin
Steve_Hemlocks11-Feb-22 6:22
Steve_Hemlocks11-Feb-22 6:22 
QuestionSOP is faster Pin
Robert Pfeffer18-Jan-22 2:21
professionalRobert Pfeffer18-Jan-22 2:21 
AnswerRe: SOP is faster Pin
Steve_Hemlocks18-Jan-22 3:58
Steve_Hemlocks18-Jan-22 3:58 
GeneralMy vote of 5 Pin
pierrecoach17-Jan-22 5:55
professionalpierrecoach17-Jan-22 5:55 
GeneralRe: My vote of 5 Pin
Steve_Hemlocks18-Jan-22 3:39
Steve_Hemlocks18-Jan-22 3:39 
GeneralIfs and States should be combined Pin
Paulo Zemek12-Jan-22 16:18
mvaPaulo Zemek12-Jan-22 16:18 
GeneralRe: Ifs and States should be combined Pin
Steve_Hemlocks12-Jan-22 21:39
Steve_Hemlocks12-Jan-22 21:39 
QuestionCheck out HSM Pin
Stocker.D31-Dec-21 10:22
Stocker.D31-Dec-21 10:22 
AnswerRe: Check out HSM Pin
Steve_Hemlocks3-Jan-22 5:09
Steve_Hemlocks3-Jan-22 5:09 
GeneralRe: Check out HSM Pin
Stocker.D3-Jan-22 11:01
Stocker.D3-Jan-22 11:01 
QuestionFYI Pin
Gary R. Wheeler31-Dec-21 7:21
Gary R. Wheeler31-Dec-21 7:21 
Generalminor correction Pin
Nelek31-Dec-21 0:27
protectorNelek31-Dec-21 0:27 
GeneralRe: minor correction Pin
Steve_Hemlocks31-Dec-21 1:40
Steve_Hemlocks31-Dec-21 1:40 
GeneralRe: minor correction Pin
Nelek31-Dec-21 7:11
protectorNelek31-Dec-21 7:11 
GeneralMy vote of 5 Pin
raddevus28-Dec-21 8:44
mvaraddevus28-Dec-21 8:44 
GeneralRe: My vote of 5 Pin
Steve_Hemlocks28-Dec-21 9:15
Steve_Hemlocks28-Dec-21 9:15 
GeneralNow I have a name for it Pin
Chad Rotella27-Dec-21 17:09
professionalChad Rotella27-Dec-21 17:09 
GeneralRe: Now I have a name for it Pin
Steve_Hemlocks28-Dec-21 10:40
Steve_Hemlocks28-Dec-21 10:40 
QuestionChallenge Accepted Pin
George Swan26-Dec-21 13:47
mveGeorge Swan26-Dec-21 13:47 
AnswerRe: Challenge Accepted Pin
Steve_Hemlocks27-Dec-21 6:11
Steve_Hemlocks27-Dec-21 6:11 
GeneralRe: Challenge Accepted Pin
George Swan27-Dec-21 9:37
mveGeorge Swan27-Dec-21 9:37 
GeneralRe: Challenge Accepted Pin
Steve_Hemlocks27-Dec-21 13:14
Steve_Hemlocks27-Dec-21 13:14 
QuestionThere is missing state Amber blinking. Pin
intfmiro25-Dec-21 19:46
intfmiro25-Dec-21 19:46 
Could you update your code with amber blinking? This state of semaphore lights can be activeted from all others states with event turn off control of crossroad. Event should be avctivated in midnight, for example. I want to see if your code is able to hold open close principle.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.